From 992e0de0239a7d938d91ecbc7ac66bcf80b01143 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 10 Sep 2026 18:16:50 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=BD=20Sync=202026-09-10=2018:16:50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../luci-static/resources/view/wifidogx.css | 515 ------------- .../luci-static/resources/view/wifidogx.js | 457 +++++------- .../resources/view/wifidogx/display.js | 650 ---------------- .../resources/view/wifidogx/status.js | 6 +- .../luci-static/resources/view/xdpi/l7.js | 695 ------------------ .../po/zh_Hans/apfree-wifidog.po | 57 ++ .../uci-defaults/luci-app-apfree-wifidog.sh | 6 - .../root/usr/libexec/rpcd/luci.wifidogx | 2 +- .../luci/menu.d/luci-app-apfree-wifidog.json | 41 +- .../rpcd/acl.d/luci-app-apfree-wifidog.json | 8 +- luci-app-passwall/Makefile | 2 +- .../root/usr/share/passwall/app.sh | 56 +- .../root/usr/share/passwall/iptables.sh | 2 +- .../root/usr/share/passwall/nftables.sh | 2 +- luci-app-ss-redir/Makefile | 14 - luci-app-ss-redir/README.md | 42 -- .../luci-static/resources/view/ss-redir.js | 157 ---- luci-app-ss-redir/po/templates/ss-redir.pot | 0 luci-app-ss-redir/po/zh-cn | 1 - luci-app-ss-redir/po/zh_Hans/ss-redir.po | 51 -- .../share/luci/menu.d/luci-app-ss-redir.json | 12 - .../share/rpcd/acl.d/luci-app-ss-redir.json | 14 - luci-app-yt-dlp/Makefile | 13 - luci-app-yt-dlp/README.md | 42 -- .../luci-static/resources/view/yt-dlp.js | 171 ----- luci-app-yt-dlp/luci-app-yt-dlp_intro.png | Bin 49083 -> 0 bytes luci-app-yt-dlp/po/zh-cn | 1 - luci-app-yt-dlp/po/zh_Hans/yt-dlp.po | 29 - luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp | 30 - .../share/luci/menu.d/luci-app-yt-dlp.json | 12 - .../usr/share/rpcd/acl.d/luci-app-yt-dlp.json | 26 - luci-theme-footstrap/Makefile | 2 +- .../htdocs/luci-static/resources/fs-fit.js | 33 +- ss-redir/Makefile | 50 -- ss-redir/files/firewall4.include | 4 - ss-redir/files/ss-redir.conf | 6 - ss-redir/files/ss-redir.defaults.nftables | 11 - ss-redir/files/ss-redir.init | 138 ---- torrserver/Makefile | 6 +- 39 files changed, 334 insertions(+), 3030 deletions(-) delete mode 100644 luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css delete mode 100644 luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js delete mode 100644 luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js delete mode 100644 luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh delete mode 100644 luci-app-ss-redir/Makefile delete mode 100644 luci-app-ss-redir/README.md delete mode 100644 luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js delete mode 100644 luci-app-ss-redir/po/templates/ss-redir.pot delete mode 120000 luci-app-ss-redir/po/zh-cn delete mode 100644 luci-app-ss-redir/po/zh_Hans/ss-redir.po delete mode 100644 luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json delete mode 100644 luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json delete mode 100644 luci-app-yt-dlp/Makefile delete mode 100644 luci-app-yt-dlp/README.md delete mode 100644 luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js delete mode 100644 luci-app-yt-dlp/luci-app-yt-dlp_intro.png delete mode 120000 luci-app-yt-dlp/po/zh-cn delete mode 100644 luci-app-yt-dlp/po/zh_Hans/yt-dlp.po delete mode 100755 luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp delete mode 100644 luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json delete mode 100644 luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json delete mode 100644 ss-redir/Makefile delete mode 100644 ss-redir/files/firewall4.include delete mode 100644 ss-redir/files/ss-redir.conf delete mode 100644 ss-redir/files/ss-redir.defaults.nftables delete mode 100644 ss-redir/files/ss-redir.init diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css deleted file mode 100644 index a99733e18..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css +++ /dev/null @@ -1,515 +0,0 @@ -#chartjs-tooltip { - opacity: 0; - position: absolute; - background: rgba(0, 0, 0, .7); - color: white; - padding: 3px; - border-radius: 3px; - transition: all .1s ease; - pointer-events: none; - transform: translate(-50%, 0); - z-index: 20000; -} - -#chartjs-tooltip.above { - transform: translate(-50%, -100%); -} - -#chartjs-tooltip.above:before { - border: solid; - border-color: #111 transparent; - border-color: rgba(0, 0, 0, .8) transparent; - border-width: 8px 8px 0 8px; - bottom: 1em; - content: ""; - display: block; - left: 50%; - top: 100%; - position: absolute; - z-index: 99; - transform: translate(-50%, 0); -} - -.pie label { - font-weight: bold; - font-size: 14px; - display: block; - margin-bottom: 10px; - text-align: center; -} - -.kpi ul { - list-style: none; -} - -.kpi li { - margin: 10px; - display: none; -} - -.kpi big { - font-weight: bold; -} - -.head { - text-align: center; - position: relative; - display: flex; - flex-wrap: wrap; - white-space: normal; -} - -.head .pie { - /* min-width: 200px; */ - padding: 5px; - flex: 1 1 30%; -} - -.cbi-tooltip .head .pie { - min-width: 100px; -} - -.head .kpi { - padding: 5px; - font-size: smaller; - text-align: left; - align-self: center; - flex: 1 0 33%; - min-width: 150px; - display: flex; - justify-content: center; -} - -.head .kpi ul { - margin: 0; -} - -.td.double > span { - display: block; -} - -.cbi-tooltip { - box-shadow: 0 0 5px #000; -} - -@media screen and (max-width: 992px) { - .td.hide-xs { - display: none; - } - - .td.double:not(.hide-xs) > span { - white-space: nowrap; - text-align: left; - } - - .td.double:not(.hide-xs) > span:first-child::before { - content: "IPv4: "; - font-weight: bold; - } - - .td.double:not(.hide-xs) > span:last-child::before { - content: "IPv6: "; - font-weight: bold; - } -} - -.form-group { - display: flex; - align-items: center; /* 垂直居中 */ - margin-bottom: 15px; /* 项间距 */ - gap: 10px; /* Label 和 Input 间距 */ -} - -/* 标签固定最小宽度 + 右对齐 */ -.form-label { - flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */ - text-align: right; - font-weight: bold; - color: #333; -} - -.th-sort-asc::after { - content: " ▲"; -} - -.th-sort-desc::after { - content: " ▼"; -} - -.table .th { - cursor: pointer; -} - -.table .th:hover { - background-color: #f0f0f0; -} - -/* L7 View Specific Styles */ -.l7-view-container #l7-error-message { color: red; background-color: #ffefef; border: 1px solid red; padding: 10px; margin-bottom: 10px; display: none; } -.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } -.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; } -.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } -.l7-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; } -.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; } -.l7-view-container .kpi-card-label { font-size: 0.9em; color: #666; } -.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } -.l7-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; } -.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; } -.l7-view-container .l7-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 20px; - padding: 15px 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); - color: white; -} -.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right { - display: flex; - align-items: center; - gap: 15px; -} - -/* Display View Specific Styles */ -.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } -.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } -.display-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; } -.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; } -.display-view-container .kpi-card-label { font-size: 0.9em; color: #666; } -.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } -.display-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; } -.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; } -.display-view-container .display-controls { - display: flex; - justify-content: space-between; - align-items: center; - padding: 15px 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); - margin-top: 20px; -} - -/* Control Groups */ -.control-group { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background-color: rgba(255, 255, 255, 0.15); - border-radius: 6px; - backdrop-filter: blur(10px); -} - -.control-icon { - font-size: 1.2em; - display: inline-block; -} - -.control-label { - font-weight: 500; - color: white; - margin: 0; - white-space: nowrap; -} - -.control-input { - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.3); - background-color: rgba(255, 255, 255, 0.9); - padding: 6px 12px; - transition: all 0.3s ease; -} - -.control-input:focus { - background-color: white; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); - outline: none; -} - -.control-buttons { - display: flex; - align-items: center; - gap: 10px; -} - -.status-group { - background-color: rgba(255, 255, 255, 0.2); -} - -.last-updated-text { - color: rgba(255, 255, 255, 0.95); - font-size: 0.9em; - font-weight: 500; -} - -/* Enhanced Select Styling */ -.cbi-input-select { - padding: 6px 30px 6px 12px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.3); - background-color: rgba(255, 255, 255, 0.9); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 12px; - appearance: none; - cursor: pointer; - transition: all 0.3s ease; - font-weight: 500; -} - -.cbi-input-select:hover { - background-color: white; - border-color: #667eea; -} - -.cbi-input-select:focus { - background-color: white; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); - outline: none; -} - -/* Button Enhancements for Controls */ -.l7-controls .cbi-button, -.display-controls .cbi-button { - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - transition: all 0.3s ease; - border: 2px solid transparent; -} - -.cbi-button-action { - background: rgba(255, 193, 7, 0.9); - color: #333; - border-color: rgba(255, 193, 7, 0.3); -} - -.cbi-button-action:hover { - background: #ffc107; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4); -} - -.cbi-button-positive { - background: rgba(40, 167, 69, 0.9); - color: white; - border-color: rgba(40, 167, 69, 0.3); -} - -.cbi-button-positive:hover { - background: #28a745; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4); -} - -/* Responsive Controls */ -@media screen and (max-width: 768px) { - .l7-controls, - .display-controls { - flex-direction: column; - gap: 15px; - } - - .l7-controls-left, - .l7-controls-right, - .control-buttons { - width: 100%; - justify-content: center; - } - - .control-group { - justify-content: center; - } -} - -/* Table Cell Icon Styles */ -.activity-indicator { - font-size: 12px; - display: inline-block; - margin-right: 2px; -} - -.icon, .btn-icon, .th-icon { - display: inline-block; - font-style: normal; - margin-right: 4px; -} - -.th-icon { - opacity: 0.7; -} - -/* Cell Specific Styles */ -.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.speed-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - font-weight: 500; - min-width: 100px; -} - -.speed-cell.download { - color: #28a745; -} - -.speed-cell.upload { - color: #007bff; -} - -.volume-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - min-width: 90px; -} - -.volume-cell.download { - color: #17a2b8; -} - -.volume-cell.upload { - color: #6610f2; -} - -.packet-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - color: #6c757d; - min-width: 80px; -} - -/* Data value with monospace font for better alignment */ -.data-value { - font-family: 'Courier New', Consolas, monospace; - font-size: 0.95em; - letter-spacing: 0.5px; -} - -/* Table header enhancements */ -.table .th { - font-weight: 600; - padding: 10px 8px; - white-space: nowrap; -} - -.table .td { - padding: 8px; - vertical-align: middle; -} - -.protocol-icon.l7::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: #5470c6; - margin-right: 4px; -} - -.protocol-icon.domain::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: #91cc75; - margin-right: 4px; -} - -/* Button Enhancements */ -.cbi-button { - display: inline-flex; - align-items: center; - gap: 4px; - transition: all 0.2s ease; -} - -.cbi-button:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.cbi-button-edit { - background-color: #ffc107; - border-color: #ffc107; -} - -.cbi-button-edit:hover { - background-color: #e0a800; - border-color: #e0a800; -} - -.cbi-button-remove { - background-color: #dc3545; - border-color: #dc3545; - color: white; -} - -.cbi-button-remove:hover { - background-color: #c82333; - border-color: #bd2130; -} - -.cbi-button-add { - background-color: #28a745; - border-color: #28a745; - color: white; -} - -.cbi-button-add:hover:not(:disabled) { - background-color: #218838; - border-color: #1e7e34; -} - -.cbi-button-add:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Table Row Hover Effects */ -.table .tr:not(.table-titles):not(.placeholder):hover { - background-color: #f8f9fa; - transition: background-color 0.2s ease; -} - -/* Active/Inactive Status Styles */ -.activity-indicator[title*="Active"] { - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* Responsive Icon Handling */ -@media screen and (max-width: 768px) { - .icon, .btn-icon { - margin-right: 2px; - } - - .btn-icon + span { - display: none; - } - - .th-icon { - margin-right: 0; - } -} diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js index 426085294..1ac0fa375 100644 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js +++ b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js @@ -30,219 +30,6 @@ var callGetWanMac = rpc.declare({ expect: { result: {} } }); -// Generate Device ID: AW + 7 random digits + 12-char MAC (total 21 chars) -function generateDeviceId(macAddress) { - var macPart = macAddress.replace(/-/g, ''); // Remove hyphens from MAC - var randomPart = ''; - for (var i = 0; i < 7; i++) { - randomPart += Math.floor(Math.random() * 10); - } - return 'AW' + randomPart + macPart; -} - -// Generate Location ID: 14 digits (AAAAAABBBCCCCC) -// AAAAAA: Administrative division code (GB/T 2260) - using Beijing as default (110101) -// BBB: Service type (100 for commercial internet service locations, positions 7-9) -// CCCCC: 5-digit random sequence number (positions 10-14) -function generateLocationId() { - var adminCode = '110101'; // Default to Beijing Dongcheng District (positions 1-6) - var serviceType = '100'; // Commercial internet service location (positions 7-9) - var sequenceNum = ''; - - // Generate 5-digit random sequence number (positions 10-14) - for (var i = 0; i < 5; i++) { - sequenceNum += Math.floor(Math.random() * 10); - } - - return adminCode + serviceType + sequenceNum; // 6 + 3 + 5 = 14 digits -} - -// Auto-fill helper functions -function showAutoFillModal() { - ui.showModal(_('Auto Fill'), [ - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-section-descr' }, _('Getting device information and location data, please wait...')), - E('div', { 'id': 'auto-fill-progress' }, [ - E('ul', { 'style': 'margin: 10px 0;' }, [ - E('li', { 'id': 'mac-status' }, _('Getting WAN MAC address...')), - E('li', { 'id': 'device-id-status' }, _('Generating Device ID...')), - E('li', { 'id': 'location-id-status' }, _('Generating Location ID...')), - E('li', { 'id': 'location-status' }, _('Getting location coordinates...')) - ]) - ]) - ]) - ]); -} - -function updateMacStatus(macAddr, isSuccess, errorMsg, isFallback) { - var statusElement = document.getElementById('mac-status'); - if (isSuccess) { - var prefix = isFallback ? _('✓ WAN MAC address obtained (fallback): ') : _('✓ WAN MAC address obtained: '); - var suffix = isFallback ? '' : _(' (interface: ') + arguments[4] + _(')'); - statusElement.innerHTML = prefix + macAddr + suffix; - } else { - statusElement.innerHTML = _('✗ Failed to get WAN MAC address: ') + (errorMsg || _('Unknown error')); - } -} - -function generateAndUpdateIds(macAddr) { - var deviceId = generateDeviceId(macAddr); - var locationId = generateLocationId(); - - document.getElementById('device-id-status').innerHTML = _('✓ Device ID generated: ') + deviceId; - document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + locationId; - - return { deviceId: deviceId, locationId: locationId }; -} - -function getMacAddressPromise(results) { - return callGetWanMac().then(function (response) { - if (response && response.status === 'success') { - var macAddr = response.mac.toUpperCase().replace(/:/g, '-'); - results.macAddress = macAddr; - updateMacStatus(macAddr, true, null, false, response.interface); - - var ids = generateAndUpdateIds(macAddr); - results.deviceId = ids.deviceId; - results.locationId = ids.locationId; - } else { - return getMacAddressFallback(results); - } - }).catch(function (error) { - return getMacAddressFallback(results); - }); -} - -function getMacAddressFallback(results) { - document.getElementById('mac-status').innerHTML = _('RPC failed, trying fallback...'); - - return fs.exec('/bin/sh', ['-c', 'ip route | grep default | awk \'{print $5}\' | head -n1 | xargs -I {} cat /sys/class/net/{}/address 2>/dev/null || echo "failed"']).then(function (response) { - if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') { - var macAddr = response.stdout.trim().toUpperCase().replace(/:/g, '-'); - results.macAddress = macAddr; - updateMacStatus(macAddr, true, null, true); - - var ids = generateAndUpdateIds(macAddr); - results.deviceId = ids.deviceId; - results.locationId = ids.locationId; - } else { - updateMacStatus(null, false, _('Unknown error')); - document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID without MAC address'); - } - }).catch(function (error) { - updateMacStatus(null, false, error.message); - document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID due to MAC error'); - }); -} - -function formatCoordinate(coord) { - var num = parseFloat(coord).toFixed(6); - if (num >= 0) { - return num.padStart(10, '0'); - } else { - return '-' + Math.abs(num).toFixed(6).padStart(9, '0'); - } -} - -function getLocationPromise(results) { - return callGetLocation().then(function (response) { - if (response && response.status === 'success') { - var lat = formatCoordinate(response.lat); - var lon = formatCoordinate(response.lon); - - results.longitude = lon; - results.latitude = lat; - document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + lat + ', ' + lon; - } else { - var errorMsg = (response && response.message) ? response.message : _('Unknown error'); - document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg; - } - }).catch(function (error) { - return getLocationFallback(results); - }); -} - -function getLocationFallback(results) { - document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...'); - - return fs.exec('/bin/sh', ['-c', 'curl -s --connect-timeout 10 --max-time 30 "https://ipapi.co/json" 2>/dev/null || curl -s --connect-timeout 10 --max-time 30 "http://ipinfo.io/json" 2>/dev/null || echo "failed"']).then(function (response) { - if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') { - try { - var data = JSON.parse(response.stdout); - var lat, lon; - - if (data.latitude && data.longitude) { - lat = formatCoordinate(data.latitude); - lon = formatCoordinate(data.longitude); - } else if (data.loc) { - var coords = data.loc.split(','); - lat = formatCoordinate(coords[0]); - lon = formatCoordinate(coords[1]); - } - - if (lat && lon) { - results.longitude = lon; - results.latitude = lat; - document.getElementById('location-status').innerHTML = _('✓ Location obtained (fallback): ') + lat + ', ' + lon; - } else { - document.getElementById('location-status').innerHTML = _('✗ No valid coordinates found'); - } - } catch (e) { - document.getElementById('location-status').innerHTML = _('✗ Failed to parse location data'); - } - } else { - document.getElementById('location-status').innerHTML = _('✗ Failed to get location data'); - } - }).catch(function (fallbackError) { - document.getElementById('location-status').innerHTML = _('✗ Error getting location: ') + fallbackError.message; - }); -} - -function fillFormFields(results) { - var successCount = 0; - var totalFields = 0; - var messages = []; - var fieldMappings = [ - { result: 'macAddress', selector: 'ap_mac_address', label: _('MAC Address: ') }, - { result: 'deviceId', selector: 'ap_device_id', label: _('Device ID: ') }, - { result: 'locationId', selector: 'location_id', label: _('Location ID: ') }, - { result: 'longitude', selector: 'ap_longitude', label: _('Longitude: ') }, - { result: 'latitude', selector: 'ap_latitude', label: _('Latitude: ') } - ]; - - fieldMappings.forEach(function (mapping) { - if (results[mapping.result]) { - var field = document.querySelector('input[data-name="' + mapping.selector + '"]') || - document.querySelector('input[name*="' + mapping.selector + '"]') || - document.querySelector('input[id*="' + mapping.selector + '"]') || - document.querySelector('#cbid\\.wifidogx\\.default\\.' + mapping.selector); - - if (field) { - field.value = results[mapping.result]; - field.dispatchEvent(new Event('input', { bubbles: true })); - field.dispatchEvent(new Event('change', { bubbles: true })); - successCount++; - messages.push(mapping.label + results[mapping.result]); - } - totalFields++; - } - }); - - return { successCount: successCount, totalFields: totalFields, messages: messages }; -} - -function showAutoFillResult(stats) { - ui.hideModal(); - - if (stats.successCount === stats.totalFields && stats.totalFields > 0) { - ui.addNotification(null, E('p', _('Auto fill completed successfully! All fields have been filled.') + '
' + stats.messages.join('
')), 'info'); - } else if (stats.successCount > 0) { - ui.addNotification(null, E('p', _('Auto fill partially completed.') + ' ' + stats.successCount + '/' + stats.totalFields + ' ' + _('fields filled successfully.') + '
' + stats.messages.join('
')), 'warning'); - } else { - ui.addNotification(null, E('p', _('Auto fill failed. No fields could be filled. Please check your network connection and try again.')), 'error'); - } -} - function getServiceStatus() { return L.resolveDefault(callServiceList('wifidogx'), {}).then(function (res) { var isRunning = false; @@ -253,6 +40,25 @@ function getServiceStatus() { }); } +function generateDeviceId(macAddress) { + var macPart = macAddress.replace(/-/g, ''); + var randomPart = ''; + for (var i = 0; i < 7; i++) { + randomPart += Math.floor(Math.random() * 10); + } + return 'AW' + randomPart + macPart; +} + +function generateLocationId() { + var adminCode = '110101'; + var serviceType = '100'; + var sequenceNum = ''; + for (var i = 0; i < 5; i++) { + sequenceNum += Math.floor(Math.random() * 10); + } + return adminCode + serviceType + sequenceNum; +} + function renderStatus(isRunning) { var renderHTML = ""; var spanTemp = '%s %s'; @@ -284,7 +90,6 @@ return view.extend({ s.tab('gateway', _('Gateway Settings')); s.tab('advanced', _('Advanced Settings')); s.tab('rule', _('Rule Settings')); - s.tab('qos', _('QoS Settings')); s.tab('location', _('Authentication Location Settings')); // basic settings @@ -368,7 +173,7 @@ return view.extend({ o = s.taboption('basic', form.Flag, 'disable_portal_auth', _('Disable Portal Authentication'), _('When enabled, users can access the internet without portal authentication. Firewall redirect rules will not be created. Use this mode for pure traffic statistics without captive portal.')); o.rmempty = false; - o.default = '1'; + o.default = '0'; o = s.taboption('basic', form.ListValue, 'log_level', _('Log Level'), _('The log level of the apfree-wifidog.')); @@ -380,7 +185,7 @@ return view.extend({ o.value(2, _('Critical')); o.value(1, _('Alert')); o.value(0, _('Emergency')); - o.defaulValue = 0; + o.default = 0; o.optional = false; // gateway settings @@ -392,7 +197,7 @@ return view.extend({ o = ss.option(form.Flag, 'gateway_auth_enabled', _('Auth Enabled'), _('Enable the authentication of the gateway.')); o.rmempty = false; - o.defaulValue = true; + o.default = true; o = ss.option(widgets.DeviceSelect, 'gateway_name', _('Gateway Name')); o.rmempty = false; @@ -425,14 +230,14 @@ return view.extend({ o.datatype = 'uinteger'; o.rmempty = false; o.optional = false; - o.defaulValue = 60; + o.default = 60; o = s.taboption('advanced', form.Value, 'client_timeout', _('Client Timeout'), _('The timeout of the client.')); o.datatype = 'uinteger'; o.rmempty = false; o.optional = false; - o.defaulValue = 5; + o.default = 5; o = s.taboption('advanced', form.Flag, 'wired_passed', _('Wired Passed'), _('Wired users do not need to authenticate to access the internet.')); @@ -441,23 +246,30 @@ return view.extend({ o = s.taboption('advanced', form.Flag, 'apple_cna', _('Apple CNA'), _('Enable Apple Captive Network Assistant.')); o.rmempty = false; - o.defaulValue = false; + o.default = false; o = s.taboption('advanced', form.Flag, 'js_filter', _('JS Filter'), _('Enable JS redirect.')); o.rmempty = false; - o.defaulValue = true; + o.default = true; o = s.taboption('advanced', form.Flag, 'enable_anti_nat', _('Enable Anti NAT'), _('Enable Anti NAT devices.')); o.rmempty = false; - o.defaulValue = false; + o.default = false; + + o = s.taboption('advanced', form.Value, 'privileged_ops_secret', _('Privileged Ops Secret'), + _('Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices.')); + o.password = true; + o.rmempty = false; + o.optional = false; + o.default = 'chawrt@2026'; o = s.taboption('advanced', form.Value, 'ttl_value', _('TTL Value'), _('The TTL value of the gateway support.')); o.datatype = 'string'; o.rmempty = false; - o.defaulValue = '64,128'; + o.default = '64,128'; o.depends('enable_anti_nat', '1'); o = s.taboption('advanced', form.Value, 'anti_nat_permit_macs', _('Anti NAT Permit MAC'), @@ -466,17 +278,87 @@ return view.extend({ o.rmempty = true; o.depends('enable_anti_nat', '1'); - o = s.taboption('advanced', form.Flag, 'enable_event_log', _('Enable Internet Access Log'), - _('Enable logging of internet access events and user activities.')); - o.rmempty = false; - o.defaulValue = false; - // Auth Server Settings o = s.taboption('auth', form.SectionValue, '_auth', form.GridSection, 'auth'); ss = o.subsection; ss.addremove = true; ss.nodescriptions = true; + ss.handleAdd = function (ev, name) { + var self = this; + if (name) { + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + } + return this.super('handleAdd', [ev, name]); + }; + + ss.handleRename = function (section_id, ev) { + var self = this; + var oldName = section_id; + var nameEl = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'auth-rename-input', + 'value': oldName, + 'style': 'width: 100%;' + }); + ui.showModal(_('Rename Authentication Server'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this authentication server profile.')), + E('div', { 'class': 'cbi-section-table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, _('Name')), + E('div', { 'class': 'td' }, [nameEl]) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')), + E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () { + var newName = nameEl.value.trim(); + if (!newName) { + ui.addNotification(null, E('p', _('Please enter a name.')), 'error'); + return; + } + if (newName === oldName) { + ui.hideModal(); + return; + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === newName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === newName && authSections[i]['.name'] !== oldName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + ui.hideModal(); + uci.rename('wifidogx', section_id, newName); + self.map.save(null, true); + }}, _('Rename')) + ]) + ]); + }; + o = ss.option(form.Value, 'auth_server_hostname', _('Auth Server Hostname'), _('The domain or IP address of the authentication server.')); o.rmempty = false; @@ -499,29 +381,6 @@ return view.extend({ o.optional = false; o.default = '/wifidog/'; - - // QoS settings - o = s.taboption('qos', form.Flag, 'enable_qos', _('Enable Global QoS'), - _('Enable Global QoS.')); - o.rmempty = false; - o.defaulValue = false; - - o = s.taboption('qos', form.Value, 'qos_up', _('Global QoS Up'), - _('The global QoS up value(Mbps).')); - o.datatype = 'uinteger'; - o.rmempty = true; - o.optional = true; - o.defaulValue = 0; - o.depends('enable_qos', '1'); - - o = s.taboption('qos', form.Value, 'qos_down', _('Global QoS Down'), - _('The global QoS down value(Mbps).')); - o.datatype = 'uinteger'; - o.rmempty = true; - o.optional = true; - o.defaulValue = 0; - o.depends('enable_qos', '1'); - // Authentication Location Settings // Add auto-fill button o = s.taboption('location', form.Button, '_auto_fill', _('Auto Fill All Fields'), @@ -544,11 +403,8 @@ return view.extend({ getLocationPromise(results) ]; - // Wait for all promises to complete and fill form fields Promise.all(promises).then(function () { - setTimeout(function () { - fillFormFields(results); - }, 2000); + fillFormFields(results); }); }; @@ -583,10 +439,11 @@ return view.extend({ results.locationId = generateLocationId(); document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + results.locationId; } else { + document.getElementById('mac-status').innerHTML = _('RPC returned error, trying fallback...'); return getMacAddressFallback(results); } }).catch(function (error) { - document.getElementById('mac-status').innerHTML = _('✗ RPC call failed, trying fallback...'); + document.getElementById('mac-status').innerHTML = _('RPC call failed, trying fallback...'); return getMacAddressFallback(results); }); } @@ -625,8 +482,8 @@ return view.extend({ formatAndSetCoordinates(results, lat, lon); document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + results.latitude + ', ' + results.longitude; } else { - var errorMsg = (response && response.message) ? response.message : 'Unknown error'; - document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg; + document.getElementById('location-status').innerHTML = _('RPC returned error, trying fallback...'); + return getLocationFallback(results); } }).catch(function (error) { document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...'); @@ -741,8 +598,6 @@ return view.extend({ o.rmempty = false; o.datatype = 'string'; o.optional = false; - o.depends('auth_server_mode', 'cloud'); - o.depends('auth_server_mode', 'bypass'); o.placeholder = '110101100123456'; o.validate = function (section_id, value) { if (!value || value === '') @@ -766,7 +621,8 @@ return view.extend({ // Validate service type code (7-9 digits) var serviceType = value.substring(6, 9); - if (serviceType !== '100' && serviceType !== '2' + value.charAt(7) + value.charAt(8) && serviceType !== '3' + value.charAt(7) + value.charAt(8)) { + var firstDigit = serviceType.charAt(0); + if (serviceType !== '100' && firstDigit !== '2' && firstDigit !== '3') { return _('Service type code (7-9 digits) must be "100" for commercial internet service locations, "2XX" for non-commercial locations, or "3XX" for WiFi wireless collection terminals'); } @@ -936,6 +792,81 @@ return view.extend({ ss.addremove = true; ss.nodescriptions = true; + ss.handleAdd = function (ev, name) { + var self = this; + if (name) { + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + } + return this.super('handleAdd', [ev, name]); + }; + + ss.handleRename = function (section_id, ev) { + var self = this; + var oldName = section_id; + var nameEl = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'longconn-rename-input', + 'value': oldName, + 'style': 'width: 100%;' + }); + ui.showModal(_('Rename Long Connection Profile'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this long connection profile.')), + E('div', { 'class': 'cbi-section-table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, _('Name')), + E('div', { 'class': 'td' }, [nameEl]) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')), + E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () { + var newName = nameEl.value.trim(); + if (!newName) { + ui.addNotification(null, E('p', _('Please enter a name.')), 'error'); + return; + } + if (newName === oldName) { + ui.hideModal(); + return; + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === newName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === newName && longconnSections[i]['.name'] !== oldName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + ui.hideModal(); + uci.rename('wifidogx', section_id, newName); + self.map.save(null, true); + }}, _('Rename')) + ]) + ]); + }; + o = ss.option(form.ListValue, 'long_conn_mode', _('Connection Mode'), _('The type of persistent connection to the remote management server.')); o.value('ws', _('WebSocket (ws://)')); @@ -1011,7 +942,7 @@ return view.extend({ // rule settings o = s.taboption('rule', form.DynamicList, 'trusted_wildcard_domains', _('Trusted Wildcard Domains'), - _('The trusted wildcard domains of the gateway')); + _('Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf.')); o.rmempty = true; o.optional = true; o.datatype = 'wildcard'; @@ -1025,7 +956,7 @@ return view.extend({ o.placeholder = 'www.example.com'; o = s.taboption('rule', form.DynamicList, 'trusted_macs', _('Trusted MACs'), - _('The trusted wildcard domains of the gateway.')); + _('The trusted MAC addresses of the gateway.')); o.rmempty = true; o.optional = true; o.datatype = 'macaddr'; diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js deleted file mode 100644 index 644dc872a..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js +++ /dev/null @@ -1,650 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; -'require poll'; -'require rpc'; -'require dom'; -'require uci'; - -// Global variables from original display.js -var chartRegistry = {}; -var hostNames = {}; // mac => hostname -var hostInfo = {}; // ip => mac -var hostNameMacSectionId = ""; -var isPaused = false; -var lastUpdated = null; - -// Line chart variables (from l7.js) -var downloadLineChart = {}, uploadLineChart = {}; -var lineCategories = { ipv4: [], ipv6: [], mac: [] }; -var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; -var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; - -// Color palette for chart series -var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; - -var resizeListenerAdded = false; - -// Pre-fill with 60 empty points for a smooth start -['ipv4', 'ipv6', 'mac'].forEach(function(type) { - for (var i = 0; i < 60; i++) { - lineCategories[type].push(''); - } -}); - -// Helper to convert hex to rgba (from l7.js) -function hexToRgba(hex, opacity) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : - null; -}; - -return view.extend({ - // --- Core Data Logic from display.js --- - - loadHostNames: async function() { - try { - await uci.sections('hostnames', "hostname", function (params) { - hostNameMacSectionId = params['.name']; - for (var key in params) { - if (key.startsWith('.')) continue; - var macAddr = key.split('_').join(':'); - hostNames[macAddr] = params[key]; - } - }); - - const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text'); - dhcpLeases.split('\n').forEach(function(line) { - if (line === '') return; - const [mac, ip, hostname] = line.split(' '); - if (!hostNames.hasOwnProperty(mac)) { - hostNames[mac] = hostname; - } - }); - - const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text'); - arp.split('\n').forEach(function(line, i) { - if (i === 0 || line === '') return; - const [ip, mac] = line.split(' '); - hostInfo[ip] = mac; - }); - - } catch (e) { - console.error('Error getting host names:', e); - } - }, - - loadHostSpeedData: async function() { - var self = this; - try { - const results = await Promise.all([ - fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'), - fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'), - fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json') - ]); - - const defaultData = {status: "success", data: []}; - const ipv4Data = results[0] || defaultData; - const ipv6Data = results[1] || defaultData; - const macData = results[2] || defaultData; - - ipv4Data.data.forEach(function(item) { - const mac = hostInfo[item.ip]; - if (mac) { - item.mac = mac; - item.hostname = hostNames[mac]; - } - }); - macData.data.forEach(function(item) { - const mac = item.mac; - if (mac) { - item.hostname = hostNames[mac]; - } - }); - - self.renderHostSpeed(ipv4Data, "ipv4"); - self.renderHostSpeed(ipv6Data, "ipv6"); - self.renderHostSpeed(macData, "mac"); - - lastUpdated = new Date(); - var timestampEl = document.getElementById('display-last-updated'); - if (timestampEl) { - timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); - } - - } catch (e) { - console.error('Error polling data:', e); - } - }, - - pollData: function() { - poll.add(L.bind(async function() { - if (isPaused) return; - await this.loadHostNames(); - await this.loadHostSpeedData(); - }, this), 5); - }, - - // --- UI Rendering and Interaction (New structure based on l7.js) --- - - pie: function(id, data, valueFormatter) { - var total = data.reduce(function(n, d) { return n + d.value; }, 0); - data.sort(function(a, b) { return b.value - a.value; }); - - if (total === 0) { - data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; - } - - data.forEach(function(d, i) { - if (!d.color) { - var hue = (i * 137.508) % 360; - d.color = 'hsl(' + hue + ', 75%, 55%)'; - } - }); - - var option = { - tooltip: { - trigger: 'item', - formatter: function(params) { - if (valueFormatter) { - // 将 ECharts params 对象转换为自定义格式 - return valueFormatter({ - name: params.name, - value: params.value, - percent: params.percent.toFixed(2) - }); - } - return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; - } - }, - series: [{ - type: 'pie', - radius: ['25%', '80%'], - avoidLabelOverlap: false, - padAngle: 10, - itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, - label: { show: false, position: 'center' }, - emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, - labelLine: { show: false }, - data: data.map(function(d) { - return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; - }) - }] - }; - - var dom = typeof id === 'string' ? document.getElementById(id) : id; - if (!chartRegistry[id]) { - chartRegistry[id] = echarts.init(dom); - } - chartRegistry[id].setOption(option, true); - return chartRegistry[id]; - }, - - updateStackedLineCharts: function(type, perHostDownload, perHostUpload) { - var now = new Date().toLocaleTimeString(); - lineCategories[type].push(now); - lineCategories[type].shift(); - - var processChartData = function(seriesData, perHostData) { - var allHosts = Object.keys(seriesData); - Object.keys(perHostData).forEach(function(host) { - if (allHosts.indexOf(host) === -1) { - allHosts.push(host); - } - }); - - allHosts.forEach(function(host) { - if (!seriesData[host]) { - seriesData[host] = Array(59).fill(0); - } - var rate = perHostData[host] || 0; - seriesData[host].push(rate); - seriesData[host].shift(); - }); - - return Object.keys(seriesData).map(function(host, index) { - var color = colorPalette[index % colorPalette.length]; - return { - name: host, - type: 'line', - stack: 'Total', - smooth: true, - lineStyle: { width: 1, color: color }, - showSymbol: false, - itemStyle: { color: color }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: hexToRgba(color, 0.5) }, - { offset: 1, color: hexToRgba(color, 0) } - ]) - }, - data: seriesData[host] - }; - }); - }; - - var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload); - var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload); - - var legendData = downloadChartSeries.map(function(s) { return s.name; }); - - if (downloadLineChart[type]) { - downloadLineChart[type].setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: downloadChartSeries, - xAxis: { data: lineCategories[type] } - }); - } - - if (uploadLineChart[type]) { - uploadLineChart[type].setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: uploadChartSeries, - xAxis: { data: lineCategories[type] } - }); - } - }, - - renderHostSpeed: function(data, type) { - if (!data || data.status !== "success" || !Array.isArray(data.data)) return; - - var rows = []; - var txRateData = [], rxRateData = []; - var txVolumeData = [], rxVolumeData = []; - var tx_rate_total = 0, rx_rate_total = 0; - var tx_bytes_total = 0, rx_bytes_total = 0; - var perHostTxRate = {}; - var perHostRxRate = {}; - - data.data.forEach(item => { - if (!item || !item.incoming || !item.outgoing) return; - - var host = item.ip || item.mac || ''; - var hostname = item.hostname || hostNames[item.mac] || ''; - var displayName = hostname || host; - - // 判断连接是否活跃 - var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; - var activityIcon = isActive ? '🟢' : '⚪'; - - rows.push([ - E('span', { 'class': 'host-cell' }, [ - E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), - E('span', {}, ' ' + host) - ]), - E('span', { 'class': 'hostname-cell' }, [ - E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'), - E('span', {}, ' ' + (hostname || _('Unknown'))) - ]), - E('span', { 'class': 'speed-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate)) - ]), - E('span', { 'class': 'volume-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) - ]), - E('span', { 'class': 'packet-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) - ]), - E('span', { 'class': 'speed-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate)) - ]), - E('span', { 'class': 'volume-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) - ]), - E('span', { 'class': 'packet-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) - ]), - E('div', { 'class': 'button-container' }, [ - E('button', { - 'class': 'btn cbi-button cbi-button-edit', - 'style': 'margin-right: 5px;', - 'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type)) - }, [ - E('span', { 'class': 'btn-icon' }, '✏️'), - E('span', {}, ' ' + _('Edit')) - ]), - E('button', { - 'class': 'btn cbi-button cbi-button-remove', - 'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type)) - }, [ - E('span', { 'class': 'btn-icon' }, '🗑️'), - E('span', {}, ' ' + _('Delete')) - ]) - ]) - ]); - rx_rate_total += item.outgoing.rate; - tx_rate_total += item.incoming.rate; - rx_bytes_total += item.outgoing.total_bytes; - tx_bytes_total += item.incoming.total_bytes; - - rxRateData.push({ value: item.outgoing.rate, label: displayName }); - txRateData.push({ value: item.incoming.rate, label: displayName }); - rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName }); - txVolumeData.push({ value: item.incoming.total_bytes, label: displayName }); - - perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate; - perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate; - }); - - this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate); - - var table = document.getElementById(type + '-speed-data'); - cbi_update_table(table, rows, E('em', _('No data recorded yet.'))); - - this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); - - var hostEl = document.getElementById(type + '-host-val'); - if (hostEl) hostEl.textContent = data.data.length; - - var txRateEl = document.getElementById(type + '-tx-rate-val'); - if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total); - - var rxRateEl = document.getElementById(type + '-rx-rate-val'); - if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total); - - var txVolEl = document.getElementById(type + '-tx-volume-val'); - if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); - - var rxVolEl = document.getElementById(type + '-rx-volume-val'); - if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); - }, - - // --- Interaction Handlers from display.js --- - - handleDeleteHost: function(host, type) { - ui.showModal(_('Delete Host'), [ - E('p', _('Are you sure you want to delete this host?')), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), - E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => { - try { - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text'); - this.loadHostSpeedData(); - ui.hideModal(); - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - ui.hideModal(); - } - })}, _('Delete')) - ]) - ]); - }, - - handleEditSpeed: function(host, mac, hostname, type) { - fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => { - let rate_limit_dl = 0, rate_limit_ul = 0; - if (res && res.status === 'success' && Array.isArray(res.data)) { - const item = res.data.find(d => (d.ip === host || d.mac === host)); - if (item) { - rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024; - rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024; - } - } - this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul); - }, this)).catch(e => { - console.error('Error getting speed limit:', e); - this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0); - }); - }, - - displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) { - const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac }); - - ui.showModal(_('Edit Speed Limit'), [ - E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]), - E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]), - E('div', { 'class': 'form-group' }, [ - E('label', { 'class': 'form-label' }, _('Download Limit')), - E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }), - E('span',{}, " Mbps") - ]), - E('div', { 'class': 'form-group' }, [ - E('label', { 'class': 'form-label' }, _('Upload Limit')), - E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }), - E('span',{}, " Mbps") - ]), - E('div', { 'class': 'cbi-page-actions right' }, [ - E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')), - E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => { - const dl_val = document.getElementById('dl-rate').value; - const ul_val = document.getElementById('ul-rate').value; - const newName = document.getElementById('host-name').value; - try { - if (mac && newName !== hostname) { - hostNames[mac] = newName; - await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName); - await uci.save('hostnames'); - await uci.apply('hostnames'); - } - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']); - this.loadHostSpeedData(); - ui.addNotification(null, E('p',_('Speed limit updated'))); - ui.hideModal(); - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - } - })}, _('Save')) - ]) - ]); - }, - - validateData: function(value, type) { - if (typeof value !== 'string') return false; - const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; - const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i; - const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i; - return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value); - }, - - createAddControls: function(type, placeholder) { - const input = E('input', { - type: 'text', - class: 'cbi-input-text control-input', - style: (type === 'ipv6') ? 'width:320px' : 'width:180px', - placeholder: _(placeholder) - }); - const addBtn = E('button', { - class: 'btn cbi-button cbi-button-add', - disabled: true - }, [ - E('span', { 'class': 'btn-icon' }, '➕'), - E('span', {}, ' ' + _('Add')) - ]); - const refreshBtn = E('button', { - class: 'btn cbi-button cbi-button-action', - click: () => this.loadHostSpeedData() - }, [ - E('span', { 'class': 'btn-icon' }, '🔄'), - E('span', {}, ' ' + _('Refresh')) - ]); - - input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); }); - addBtn.addEventListener('click', ui.createHandlerFn(this, async () => { - const value = input.value.trim(); - if (!this.validateData(value, type)) { - return ui.addNotification(null, E('p', _('Data format error'))); - } - try { - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]); - this.loadHostSpeedData(); - ui.addNotification(null, E('p',_('Updated successfully!'))); - input.value = ''; - addBtn.disabled = true; - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - } - })); - - return E('div', { 'class': 'display-controls' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '🖥️'), - E('label', { 'class': 'control-label' }, _('Add Host:')), - input - ]), - E('div', { 'class': 'control-buttons' }, [ - addBtn, - refreshBtn, - E('div', { 'class': 'control-group status-group' }, [ - E('span', { 'class': 'control-icon' }, '🕐'), - E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready')) - ]) - ]) - ]); - }, - - initializeUI: function() { - if (window.echarts) { - var self = this; - ['ipv4', 'ipv6', 'mac'].forEach(function(type) { - var dlChartEl = document.getElementById(type + '-download-speed-line-chart'); - var ulChartEl = document.getElementById(type + '-upload-speed-line-chart'); - if (!dlChartEl || !ulChartEl) return; - - var baseChartOption = { - tooltip: { - trigger: 'axis', - formatter: function (params) { - if (!params || params.length === 0) { - return null; - } - var tooltipContent = params[0].axisValueLabel + '
'; - params.sort(function(a, b) { return b.value - a.value; }); - params.forEach(function(item) { - if (item.value > 0) { - tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '
'; - } - }); - return tooltipContent; - } - }, - grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, - xAxis: { type: 'category', boundaryGap: false, data: lineCategories[type] }, - yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); } } }, - series: [] - }; - - downloadLineChart[type] = echarts.init(dlChartEl); - downloadLineChart[type].setOption(baseChartOption); - - uploadLineChart[type] = echarts.init(ulChartEl); - uploadLineChart[type].setOption(baseChartOption); - }); - - // 添加窗口大小变化监听器,使图表能够响应式调整 - if (!resizeListenerAdded) { - var resizeTimer = null; - var resizeHandler = function() { - // 使用防抖,避免频繁触发 resize - if (resizeTimer) { - clearTimeout(resizeTimer); - } - resizeTimer = setTimeout(function() { - // 调整折线图大小 - ['ipv4', 'ipv6', 'mac'].forEach(function(type) { - if (downloadLineChart[type]) { - downloadLineChart[type].resize(); - } - if (uploadLineChart[type]) { - uploadLineChart[type].resize(); - } - }); - // 调整饼图大小 - Object.keys(chartRegistry).forEach(function(chartId) { - if (chartRegistry[chartId]) { - chartRegistry[chartId].resize(); - } - }); - }, 200); - }; - - window.addEventListener('resize', resizeHandler); - resizeListenerAdded = true; - } - - this.pollData(); - } else { - setTimeout(this.initializeUI.bind(this), 50); - } - }, - - // --- Main Render Function (New) --- - - render: function() { - var self = this; - - const createTab = (type, title, placeholder) => { - return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [ - E('div', { 'class': 'dashboard-container' }, [ - E('div', { 'class': 'line-chart-row' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Download Speed')]), - E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Upload Speed')]), - E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]) - ]), - E('div', { 'class': 'kpi-row' }, [ - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) - ]), - E('div', { 'class': 'chart-grid' }, [ - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ]) - ]) - ]), - E('table', { 'class': 'table', 'id': type + '-speed-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]), - E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ]) - ]), - self.createAddControls(type, placeholder) - ]); - }; - - var tabContainer = E('div', {}, [ - createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'), - createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'), - createTab('mac', 'MAC', 'Please enter a valid MAC address') - ]); - - var node = E([], [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }), - E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), - E('div', { 'class': 'l7-view-container' }, [ - E('h2', [ _('Auth User Speed Monitor') ]), - tabContainer - ]) - ]); - - ui.tabs.initTabGroup(tabContainer.childNodes); - - setTimeout(() => this.initializeUI(), 0); - - return node; - }, - - handleSave: null, - handleSaveApply: null, - handleReset: null -}); diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js index 6be79ea1a..445daf4bc 100644 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js +++ b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js @@ -266,14 +266,14 @@ return view.extend({ } } + // Initial update + updateStatus(); + // Poll status every 5 seconds L.Poll.add(function() { return updateStatus(); }, 5); - // Initial update - updateStatus(); - return container; }, diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js deleted file mode 100644 index 781382bb5..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js +++ /dev/null @@ -1,695 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; -'require poll'; -'require rpc'; -'require dom'; - -var chartRegistry = {}; -var downloadLineChart, uploadLineChart; - -// Data structures for stacked line charts -var lineCategories = []; -var downloadSeriesData = {}; -var uploadSeriesData = {}; - -// Color palette for chart series -var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; - -var currentSortInfo = { - table: null, - column: null, - reverse: false -}; -var sidLookupTable = {}; -var isPaused = false; -var lastUpdated = null; -var pollActive = false; -var lastSIDData = null; -var resizeListenerAdded = false; -var resizeTimer = null; - -// Pre-fill with 60 empty points for a smooth start -for (var i = 0; i < 60; i++) { - lineCategories.push(''); -} - -// Helper to convert hex to rgba -function hexToRgba(hex, opacity) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : - null; -}; - -return view.extend({ - load: function() { - return Promise.all([ - this.loadSIDData(), - this.loadL7ProtoData() - ]); - }, - - showError: function(message) { - var errorEl = document.getElementById('l7-error-message'); - if (errorEl) { - errorEl.textContent = message; - errorEl.style.display = 'block'; - } - }, - - hideError: function() { - var errorEl = document.getElementById('l7-error-message'); - if (errorEl) { - errorEl.style.display = 'none'; - } - }, - - loadSIDData: function() { - var self = this; - return fs.exec_direct('/usr/bin/aw-bpfctl', ['sid', 'json'], 'json').then(function(result) { - self.hideError(); - lastSIDData = result; - return result; - }).catch(function(error) { - console.error('Error loading SID data:', error); - self.showError(_('Error loading SID data: %s').format(error.message)); - return { status: 'error', data: [] }; - }); - }, - - loadL7ProtoData: function() { - var self = this; - return fs.exec_direct('/usr/bin/aw-bpfctl', ['l7', 'json'], 'json').then(function(result) { - self.hideError(); - return result; - }).catch(function(error) { - console.error('Error loading L7 protocol data:', error); - self.showError(_('Error loading L7 protocol data: %s').format(error.message)); - return { status: 'error', data: [] }; - }); - }, - - updateStackedLineCharts: function(perServiceDownload, perServiceUpload) { - var now = new Date().toLocaleTimeString(); - lineCategories.push(now); - lineCategories.shift(); - - var processChartData = function(seriesData, perServiceData) { - var allServices = Object.keys(seriesData); - Object.keys(perServiceData).forEach(function(service) { - if (allServices.indexOf(service) === -1) { - allServices.push(service); - } - }); - - allServices.forEach(function(service) { - if (!seriesData[service]) { - seriesData[service] = Array(59).fill(0); - } - var rate = perServiceData[service] || 0; - seriesData[service].push(rate); - seriesData[service].shift(); - }); - - return Object.keys(seriesData).map(function(service, index) { - var color = colorPalette[index % colorPalette.length]; - return { - name: service, - type: 'line', - stack: 'Total', - smooth: true, - lineStyle: { width: 1, color: color }, - showSymbol: false, - itemStyle: { color: color }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: hexToRgba(color, 0.5) }, - { offset: 1, color: hexToRgba(color, 0) } - ]) - }, - data: seriesData[service] - }; - }); - }; - - var downloadChartSeries = processChartData(downloadSeriesData, perServiceDownload); - var uploadChartSeries = processChartData(uploadSeriesData, perServiceUpload); - - var legendData = downloadChartSeries.map(function(s) { return s.name; }); - - if (downloadLineChart) { - downloadLineChart.setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: downloadChartSeries, - xAxis: { data: lineCategories } - }); - } - - if (uploadLineChart) { - uploadLineChart.setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: uploadChartSeries, - xAxis: { data: lineCategories } - }); - } - }, - - pie: function(id, data, valueFormatter) { - var total = data.reduce(function(n, d) { return n + d.value; }, 0); - - data.sort(function(a, b) { return b.value - a.value; }); - - if (total === 0) { - data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; - } - - data.forEach(function(d, i) { - if (!d.color) { - var hue = (i * 137.508) % 360; - d.color = 'hsl(' + hue + ', 75%, 55%)'; - } - }); - - var option = { - tooltip: { - trigger: 'item', - formatter: function(params) { - if (valueFormatter) { - // 将 ECharts params 对象转换为自定义格式 - return valueFormatter({ - name: params.name, - value: params.value, - percent: params.percent.toFixed(2) - }); - } - return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; - } - }, - series: [{ - type: 'pie', - radius: ['25%', '80%'], - avoidLabelOverlap: false, - padAngle: 10, - itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, - label: { show: false, position: 'center' }, - emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, - labelLine: { show: false }, - data: data.map(function(d) { - return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; - }) - }] - }; - - var dom = typeof id === 'string' ? document.getElementById(id) : id; - - if (!chartRegistry[id]) { - chartRegistry[id] = echarts.init(dom); - } - - chartRegistry[id].setOption(option, true); - - return chartRegistry[id]; - }, - - sortTable: function(table, column) { - var tbody = table.querySelector('tbody'); - if (!tbody) return; - var rows = Array.from(tbody.querySelectorAll('tr:not(.table-titles):not(.placeholder)')); - var reverse = (currentSortInfo.table === table && currentSortInfo.column === column) ? !currentSortInfo.reverse : false; - - table.querySelectorAll('th').forEach(function(th) { - th.classList.remove('th-sort-asc', 'th-sort-desc'); - }); - - var th = table.querySelector('th:nth-child(' + (column + 1) + ')'); - th.classList.add(reverse ? 'th-sort-desc' : 'th-sort-asc'); - - rows.sort(function(row1, row2) { - var a = row1.cells[column].getAttribute('data-value') || row1.cells[column].textContent; - var b = row2.cells[column].getAttribute('data-value') || row2.cells[column].textContent; - - if (!isNaN(a) && !isNaN(b)) { a = Number(a); b = Number(b); } - - if (a < b) return reverse ? 1 : -1; - if (a > b) return reverse ? -1 : 1; - return 0; - }); - - currentSortInfo.table = table; - currentSortInfo.column = column; - currentSortInfo.reverse = reverse; - - rows.forEach(function(row) { tbody.removeChild(row); }); - rows.forEach(function(row) { tbody.appendChild(row); }); - }, - - formatMbps: function(bits) { - if (typeof bits !== 'number') return '0.00 Mbps'; - return (bits / 1024 / 1024).toFixed(2) + ' Mbps'; - }, - - formatMB: function(bytes) { - if (typeof bytes !== 'number') return '0.00 MB'; - return (bytes / 1024 / 1024).toFixed(2) + ' MB'; - }, - - renderSIDData: function(data) { - var rows = []; - var txRateData = [], rxRateData = []; - var txVolumeData = [], rxVolumeData = []; - var tx_rate_total = 0, rx_rate_total = 0; - var tx_bytes_total = 0, rx_bytes_total = 0; - var perServiceTxRate = {}; - var perServiceRxRate = {}; - var self = this; - var allItems = []; - - if (data && data.status === 'success' && Array.isArray(data.data)) { - allItems = data.data; - var listSizeEl = document.getElementById('sid-size-select'); - var listSize = listSizeEl ? parseInt(listSizeEl.value, 10) : 10; - - var activeConnections = allItems.filter(function(item) { return item.incoming.rate > 0 || item.outgoing.rate > 0; }); - var inactiveConnections = allItems.filter(function(item) { return item.incoming.rate === 0 && item.outgoing.rate === 0; }); - - activeConnections.sort(function(a, b) { return (b.incoming.rate + b.outgoing.rate) - (a.incoming.rate + a.outgoing.rate); }); - inactiveConnections.sort(function(a, b) { return b.incoming.total_bytes - a.incoming.total_bytes; }); - - var displayData = activeConnections; - if (displayData.length < listSize) { - displayData = displayData.concat(inactiveConnections.slice(0, listSize - displayData.length)); - } - - if (displayData.length > listSize) { - displayData = displayData.slice(0, listSize); - } - - displayData.forEach(function(item) { - var domainOrL7Proto = 'unknown'; - var lookupInfo = sidLookupTable[item.sid]; - - if (lookupInfo) { - domainOrL7Proto = lookupInfo.name; - } else if (item.sid_type === 'Domain' && item.domain && item.domain !== 'unknown') { - domainOrL7Proto = item.domain; - } else if (item.sid_type === 'L7' && item.l7_proto_desc && item.l7_proto_desc !== 'unknown') { - domainOrL7Proto = item.l7_proto_desc; - } - - // 判断连接是否活跃 - var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; - var activityIcon = isActive ? '🟢' : '⚪'; - - rows.push([ - E('span', { 'class': 'sid-cell' }, [ - E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), - E('span', {}, ' ' + item.sid) - ]), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon' }, '🌐'), - E('span', {}, ' ' + domainOrL7Proto) - ]), - [ item.incoming.rate, E('span', { 'class': 'speed-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.incoming.rate)) - ])], - [ item.incoming.total_bytes, E('span', { 'class': 'volume-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) - ])], - [ item.incoming.total_packets, E('span', { 'class': 'packet-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) - ])], - [ item.outgoing.rate, E('span', { 'class': 'speed-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.outgoing.rate)) - ])], - [ item.outgoing.total_bytes, E('span', { 'class': 'volume-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) - ])], - [ item.outgoing.total_packets, E('span', { 'class': 'packet-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) - ])] - ]); - - txRateData.push({ value: item.incoming.rate, label: domainOrL7Proto }); - rxRateData.push({ value: item.outgoing.rate, label: domainOrL7Proto }); - txVolumeData.push({ value: item.incoming.total_bytes, label: domainOrL7Proto }); - rxVolumeData.push({ value: item.outgoing.total_bytes, label: domainOrL7Proto }); - - perServiceTxRate[domainOrL7Proto] = (perServiceTxRate[domainOrL7Proto] || 0) + item.incoming.rate; - perServiceRxRate[domainOrL7Proto] = (perServiceRxRate[domainOrL7Proto] || 0) + item.outgoing.rate; - }); - - allItems.forEach(function(item) { - tx_rate_total += item.incoming.rate; - rx_rate_total += item.outgoing.rate; - tx_bytes_total += item.incoming.total_bytes; - rx_bytes_total += item.outgoing.total_bytes; - }); - } - - this.updateStackedLineCharts(perServiceTxRate, perServiceRxRate); - - var table = document.getElementById('sid-data'); - cbi_update_table('#sid-data', rows, E('em', _('No data recorded yet.'))); - - var headers = table.querySelectorAll('th'); - - if (!table.hasAttribute('data-sort-initialized')) { - headers.forEach(function(header, index) { - header.style.cursor = 'pointer'; - header.addEventListener('click', function() { self.sortTable(table, index); }); - }); - table.setAttribute('data-sort-initialized', 'true'); - } - - table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) { - if (!rows[rowIndex]) return; - Array.from(row.cells).forEach(function(cell, cellIndex) { - if (Array.isArray(rows[rowIndex][cellIndex])) { - cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]); - } - }); - }); - - this.pie('sid-tx-rate-pie', txRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-rx-rate-pie', rxRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-tx-volume-pie', txVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-rx-volume-pie', rxVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); - - var sidTotalEl = document.getElementById('sid-total-val'); - if(sidTotalEl) sidTotalEl.textContent = allItems.length; - - var txRateEl = document.getElementById('sid-tx-rate-val'); - if(txRateEl) txRateEl.textContent = '%1024.2mbps'.format(tx_rate_total); - - var rxRateEl = document.getElementById('sid-rx-rate-val'); - if(rxRateEl) rxRateEl.textContent = '%1024.2mbps'.format(rx_rate_total); - - var txVolEl = document.getElementById('sid-tx-volume-val'); - if(txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); - - var rxVolEl = document.getElementById('sid-rx-volume-val'); - if(rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); - - lastUpdated = new Date(); - var timestampEl = document.getElementById('last-updated'); - if (timestampEl) { - timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); - } - }, - - renderL7ProtoData: function(data) { - var rows = []; - var self = this; - - sidLookupTable = {}; - - if (data && data.status === 'success' && data.data) { - if (Array.isArray(data.data.protocols)) { - data.data.protocols.forEach(function(item) { - sidLookupTable[item.sid] = { type: 'protocol', name: item.protocol }; - rows.push([ - E('span', { 'class': 'id-cell' }, item.id), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon l7' }, '🔌'), - E('span', {}, ' ' + item.protocol) - ]), - E('span', { 'class': 'sid-cell' }, item.sid) - ]); - }); - } - - if (Array.isArray(data.data.domains)) { - data.data.domains.forEach(function(item) { - sidLookupTable[item.sid] = { type: 'domain', name: item.domain }; - rows.push([ - E('span', { 'class': 'id-cell' }, item.id), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon domain' }, '🌍'), - E('span', {}, ' ' + item.domain) - ]), - E('span', { 'class': 'sid-cell' }, item.sid) - ]); - }); - } - } - - var table = document.getElementById('l7proto-data'); - var headers = table.querySelectorAll('th'); - - if (!table.hasAttribute('data-sort-initialized')) { - headers.forEach(function(header, index) { - header.style.cursor = 'pointer'; - header.addEventListener('click', function() { self.sortTable(table, index); }); - }); - table.setAttribute('data-sort-initialized', 'true'); - } - - cbi_update_table('#l7proto-data', rows, E('em', _('No data recorded yet.'))); - }, - - pollL7Data: function() { - if (pollActive) return; - - var self = this; - pollActive = true; - - self.loadL7ProtoData().then(function(l7data) { - self.renderL7ProtoData(l7data); - return self.loadSIDData(); - }).then(function(sidData){ - self.renderSIDData(sidData); - }); - - poll.add(function() { - if (isPaused) return Promise.resolve(); - - return self.loadL7ProtoData().then(function(data) { - self.renderL7ProtoData(data); - }).then(function() { - return self.loadSIDData().then(function(data) { - self.renderSIDData(data); - }); - }); - }, 5); - }, - - initializeUI: function() { - if (window.echarts) { - var dlChartEl = document.getElementById('download-speed-line-chart'); - var ulChartEl = document.getElementById('upload-speed-line-chart'); - if (!dlChartEl || !ulChartEl) return; - - var baseChartOption = { - tooltip: { - trigger: 'axis', - formatter: function (params) { - if (!params || params.length === 0) { - return null; - } - var tooltipContent = params[0].axisValueLabel + '
'; - params.sort(function(a, b) { return b.value - a.value; }); - params.forEach(function(item) { - if (item.value > 0) { - tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mbps'.format(item.value) + '
'; - } - }); - return tooltipContent; - } - }, - grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, - xAxis: { type: 'category', boundaryGap: false, data: lineCategories }, - yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mbps'.format(val); } } }, - series: [] - }; - - - downloadLineChart = echarts.init(dlChartEl); - downloadLineChart.setOption(baseChartOption); - - uploadLineChart = echarts.init(ulChartEl); - uploadLineChart.setOption(baseChartOption); - - // 添加窗口大小变化监听器,使图表能够响应式调整 - if (!resizeListenerAdded) { - var resizeTimer = null; - var resizeHandler = function() { - // 使用防抖,避免频繁触发 resize - if (resizeTimer) { - clearTimeout(resizeTimer); - } - resizeTimer = setTimeout(function() { - // 调整折线图大小 - if (downloadLineChart) { - downloadLineChart.resize(); - } - if (uploadLineChart) { - uploadLineChart.resize(); - } - // 调整饼图大小 - Object.keys(chartRegistry).forEach(function(chartId) { - if (chartRegistry[chartId]) { - chartRegistry[chartId].resize(); - } - }); - }, 200); - }; - - window.addEventListener('resize', resizeHandler); - resizeListenerAdded = true; - } - - this.pollL7Data(); - } else { - setTimeout(this.initializeUI.bind(this), 50); - } -}, render: function() { - var self = this; - - var controls = E('div', { 'class': 'l7-controls' }, [ - E('div', { 'class': 'l7-controls-left' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '📊'), - E('label', { 'for': 'sid-size-select', 'class': 'control-label' }, _('Show entries:')), - E('select', { - 'id': 'sid-size-select', - 'class': 'cbi-input-select', - 'change': ui.createHandlerFn(this, function() { - if (lastSIDData) { - self.renderSIDData(lastSIDData); - } - }) - }, [ - E('option', { 'value': '10' }, '10'), - E('option', { 'value': '15' }, '15'), - E('option', { 'value': '20' }, '20'), - E('option', { 'value': '25' }, '25'), - E('option', { 'value': '50' }, '50') - ]) - ]) - ]), - E('div', { 'class': 'l7-controls-right' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '🕐'), - E('span', { 'id': 'last-updated', 'class': 'last-updated-text' }, _('Last updated: never')) - ]), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'id': 'pause-resume-btn', - 'click': function(ev) { - isPaused = !isPaused; - var btn = ev.target; - if (isPaused) { - btn.innerHTML = '▶️ ' + _('Resume'); - btn.classList.remove('cbi-button-action'); - btn.classList.add('cbi-button-positive'); - } else { - btn.innerHTML = '⏸️ ' + _('Pause'); - btn.classList.remove('cbi-button-positive'); - btn.classList.add('cbi-button-action'); - } - } - }, [ - E('span', { 'class': 'btn-icon' }, '⏸️'), - E('span', {}, ' ' + _('Pause')) - ]) - ]) - ]); - - var tabContainer = E('div', {}, [ - E('div', { 'class': 'cbi-section', 'data-tab': 'sid', 'data-tab-title': _('L7 SID Data') }, [ - E('div', { 'class': 'dashboard-container' }, [ - E('div', { 'class': 'line-chart-row' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Download Speed')]), - E('div', { id: 'download-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Upload Speed')]), - E('div', { id: 'upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]) - ]), - E('div', { 'class': 'kpi-row' }, [ - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-total-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('L7 Protocol Data')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) - ]), - E('div', { 'class': 'chart-grid' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Download Speed / SID')]), - E('div', { id: 'sid-tx-rate-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Upload Speed / SID')]), - E('div', { id: 'sid-rx-rate-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Download Total')]), - E('div', { id: 'sid-tx-volume-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Upload Total')]), - E('div', { id: 'sid-rx-volume-pie', style: 'width: 100%; height: 300px;' }) - ]) - ]) - ]), - E('table', { 'class': 'table', 'id': 'sid-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🆔'), ' ', _('SID') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed (Bit/s)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download (Bytes)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download (Packets)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed (Bit/s)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload (Bytes)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload (Packets)') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ - E('td', { 'class': 'td', 'colspan': '8' }, [ - E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) - ]) - ]) - ]), - controls - ]), - E('div', { 'class': 'cbi-section', 'data-tab': 'l7proto', 'data-tab-title': _('L7 Protocol Data') }, [ - E('table', { 'class': 'table', 'id': 'l7proto-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ - E('td', { 'class': 'td', 'colspan': '3' }, [ - E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) - ]) - ]) - ]) - ]) - ]); - - var node = E([], [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }), - E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), - - E('div', { 'class': 'l7-view-container' }, [ - E('h2', [ _('L7 Data Monitor') ]), - E('div', { 'id': 'l7-error-message' }), - tabContainer - ]) - ]); - - ui.tabs.initTabGroup(tabContainer.childNodes); - - setTimeout(this.initializeUI.bind(this), 0); - - return node; - }, - - handleSave: null, - handleSaveApply: null, - handleReset: null -}); \ No newline at end of file diff --git a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po index 868c0241d..d44cd5fec 100644 --- a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po +++ b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po @@ -15,6 +15,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Settings" +msgstr "设置" + msgid "apfree-wifidog" msgstr "门户认证" @@ -165,6 +168,9 @@ msgstr "信任的通配符域名" msgid "The trusted wildcard domains of the gateway." msgstr "网关的信任通配符域名" +msgid "Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf." +msgstr "依赖 aw-bpf 的 DNS XDP(dns_ringbuf_portal)。格式如 .example.com;解析到的 IP 写入门户防火墙 inet wifidogx,不会写入 inet awbpf。" + msgid "App White List" msgstr "应用白名单" @@ -228,6 +234,12 @@ msgstr "启用防NAT" msgid "Enable Anti NAT devices." msgstr "启用防NAT设备" +msgid "Privileged Ops Secret" +msgstr "特权管控秘钥" + +msgid "Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices." +msgstr "AWAS云端管理平台动态授权远程OTA升级、重启及系统配置变更所需的本地安全秘钥。生产环境请修改默认值。" + msgid "TTL Value" msgstr "TTL值" @@ -1093,3 +1105,48 @@ msgstr "MQTT认证的用户名。" msgid "The password for MQTT authentication." msgstr "MQTT认证的密码。" +msgid "Add Authentication Server" +msgstr "添加认证服务器" + +msgid "Enter a unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的唯一名称。" + +msgid "Add Long Connection Profile" +msgstr "添加长连接配置" + +msgid "Enter a unique name for this long connection profile." +msgstr "请输入长连接配置文件的唯一名称。" + +msgid "Rename Authentication Server" +msgstr "重命名认证服务器" + +msgid "Enter a new unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的新名称。" + +msgid "Rename Long Connection Profile" +msgstr "重命名长连接配置" + +msgid "Enter a new unique name for this long connection profile." +msgstr "请输入长连接配置文件的新名称。" + +msgid "Please enter a name." +msgstr "请输入名称。" + +msgid "The name \"%s\" already exists in Auth Server profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于认证服务器配置中,请选择其他名称。" + +msgid "The name \"%s\" already exists in Long Connection profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于长连接配置中,请选择其他名称。" + +msgid "Created new authentication server profile \"%s\"." +msgstr "已创建新的认证服务器配置\"%s\"。" + +msgid "Created new long connection profile \"%s\"." +msgstr "已创建新的长连接配置\"%s\"。" + +msgid "Renamed authentication server profile from \"%s\" to \"%s\"." +msgstr "已将认证服务器配置从\"%s\"重命名为\"%s\"。" + +msgid "Renamed long connection profile from \"%s\" to \"%s\"." +msgstr "已将长连接配置从\"%s\"重命名为\"%s\"。" + diff --git a/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh b/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh deleted file mode 100644 index cf3a7cbdd..000000000 --- a/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -[ -f "/etc/config/hostnames" ] || { - echo 'config hostname' > /etc/config/hostnames -} - -exit 0 diff --git a/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx b/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx index ff8d2e55c..9c4794010 100755 --- a/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx +++ b/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx @@ -70,7 +70,7 @@ case "$1" in json_init json_add_object "result" json_add_string "status" "fail" - json_add_string "message" "WAN interface not found. Available interfaces: $(ls /sys/class/net/ | tr '\n' ' ')" + json_add_string "message" "WAN interface not found" json_close_object json_dump fi diff --git a/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json b/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json index ba3b75c70..2add1a4d8 100644 --- a/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json +++ b/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json @@ -3,56 +3,27 @@ "title": "apfree-wifidog", "order": 1, "action": { - "type": "view", - "path": "wifidogx" + "type": "firstchild", + "recurse": true }, "depends": { "acl": [ "luci-app-apfree-wifidog" ] } }, - "admin/QoS": { - "title": "QoS", - "order": 20, - "action": { - "type": "firstchild", - "recurse": true - } - }, - "admin/QoS/wifidogx": { - "title": "Auth User", - "order": 2, - "action": { - "type": "alias", - "path": "admin/QoS/wifidogx/display" - }, - "depends": { - "acl": [ - "luci-app-apfree-wifidog" - ] - } - }, - "admin/QoS/wifidogx/display": { - "title": "Display", + "admin/services/wifidogx/config": { + "title": "Settings", "order": 10, "action": { "type": "view", - "path": "wifidogx/display" + "path": "wifidogx" } }, - "admin/QoS/wifidogx/status": { + "admin/services/wifidogx/status": { "title": "Status", "order": 20, "action": { "type": "view", "path": "wifidogx/status" } - }, - "admin/QoS/xdpi": { - "title": "L7 application", - "order": 30, - "action": { - "type": "view", - "path": "xdpi/l7" - } } } diff --git a/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json b/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json index 78975c66c..115acc1ae 100644 --- a/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json +++ b/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json @@ -8,14 +8,12 @@ }, "file": { "/etc/init.d/wifidogx": [ "exec" ], - "/usr/bin/wdctlx": [ "exec" ], - "/usr/bin/aw-bpfctl": [ "exec" ], - "/usr/bin/awk": [ "exec" ] + "/usr/bin/wdctlx": [ "exec" ] }, - "uci": ["wifidogx", "hostnames", "network"] + "uci": ["wifidogx", "network"] }, "write": { - "uci": ["wifidogx", "hostnames"], + "uci": ["wifidogx"], "file": { "/etc/wifidogx/*": [ "write" ] } diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index b70561826..961c0b53d 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.9.9 -PKG_RELEASE:=263 +PKG_RELEASE:=264 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index ab5bce439..76df7aa55 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1483,23 +1483,21 @@ acl_app() { esac } } - dns_cache_key="${dns_mode}_${remote_dns}_${v2ray_dns_mode:-none}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}_${remote_rewrite_ttl:-30}" - ([ "$v2ray_dns_mode" = "doh" ] || [ "$v2ray_dns_mode" = "http3" ]) && { - dns_cache_key="${dns_mode}_${remote_dns_doh:-https://1.1.1.1/dns-query}_${v2ray_dns_mode:-doh}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}_${remote_rewrite_ttl:-30}" - } + + local dns_cache_str="${dns_mode}_${v2ray_dns_mode}_${remote_dns}_${remote_dns_doh}_${remote_dns_client_ip}_${remote_fakedns}_${remote_rewrite_ttl}" + dns_cache_key="$(echo -n "${dns_cache_str}" | md5sum | cut -d " " -f1)" if [ "$remote_fakedns" = "1" ] || ([ "$protocol" = "_shunt" ] && [ "$(config_n_get $node fakedns)" = "1" ]); then use_fakedns=1 fi run_dns() { - local _dns_port - [ -n $1 ] && _dns_port=$1 - [ -z ${_dns_port} ] && { + local dns_fwd_port=$1 + [ -z "$dns_fwd_port" ] && { dns_port=$(get_new_port $(expr $dns_port + 1)) - _dns_port=$dns_port + dns_fwd_port=$dns_port if [ "$dns_mode" = "dns2socks" ]; then - run_dns2socks flag=acl_${sid} socks_address=127.0.0.1 socks_port=$socks_port listen_address=0.0.0.0 listen_port=${_dns_port} dns=$remote_dns cache=1 + run_dns2socks flag=acl_${sid} socks_address=127.0.0.1 socks_port=$socks_port listen_address=0.0.0.0 listen_port=$dns_fwd_port dns=$remote_dns cache=1 elif [ "$dns_mode" = "sing-box" ] || [ "$dns_mode" = "xray" ]; then config_file=$TMP_ACL_PATH/${node}_SOCKS_${socks_port}_DNS.json remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query} @@ -1507,10 +1505,15 @@ acl_app() { [ "${dns_mode}" = "sing-box" ] && type="singbox" dnsmasq_filter_proxy_ipv6=0 remote_dns_query_strategy="UseIP" - [ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4" - run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file + [ "$filter_proxy_ipv6" = "1" ] && { + remote_dns_query_strategy="UseIPv4" + set_cache_var "node_${node}_${dns_cache_key}_ipv6" "0" + } + run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=$dns_fwd_port \ + remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" \ + remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file fi - set_cache_var "node_${node}_$(echo -n "${dns_cache_key}" | md5sum | cut -d " " -f1)" "${_dns_port}" + set_cache_var "node_${node}_${dns_cache_key}" "$dns_fwd_port" } [ "$dns_shunt" = "chinadns-ng" ] && [ -n "$(first_type chinadns-ng)" ] && { @@ -1539,7 +1542,7 @@ acl_app() { _flag="$sid" \ _listen_port=${chinadns_port} \ _dns_local=${_chinadns_local_dns} \ - _dns_trust=127.0.0.1#${_dns_port} \ + _dns_trust=127.0.0.1#${dns_fwd_port} \ _no_ipv6_trust=${filter_proxy_ipv6} \ _use_direct_list=${use_direct_list} \ _use_proxy_list=${use_proxy_list} \ @@ -1556,33 +1559,33 @@ acl_app() { use_default_dns="chinadns_ng" } + ([ "$filter_proxy_ipv6" = "1" ] && [ "$dnsmasq_filter_proxy_ipv6" = "1" ]) && { + dnsmasq_filter_proxy_ipv6=$(get_cache_var "node_${node}_${dns_cache_key}_ipv6") + dnsmasq_filter_proxy_ipv6=${dnsmasq_filter_proxy_ipv6:-1} + } + dnsmasq_port=$(get_new_port $(expr $dnsmasq_port + 1)) local dnsmasq_conf=${acl_path}/dnsmasq.conf local dnsmasq_conf_path=${acl_path}/dnsmasq.d lua $APP_PATH/helper_dnsmasq.lua add_rule -FLAG ${sid} -TMP_DNSMASQ_PATH ${dnsmasq_conf_path} -DNSMASQ_CONF_FILE ${dnsmasq_conf} \ -LISTEN_PORT ${dnsmasq_port} -DEFAULT_DNS ${DEFAULT_DNS} -LOCAL_DNS $LOCAL_DNS \ -USE_DIRECT_LIST "${use_direct_list}" -USE_PROXY_LIST "${use_proxy_list}" -USE_BLOCK_LIST "${use_block_list}" -USE_GFW_LIST "${use_gfw_list}" -CHN_LIST "${chn_list}" \ - -TUN_DNS "127.0.0.1#${_dns_port}" -USE_DEFAULT_DNS "${use_default_dns:-direct}" -CHINADNS_DNS ${_china_ng_listen:-0} \ + -TUN_DNS "127.0.0.1#${dns_fwd_port}" -USE_DEFAULT_DNS "${use_default_dns:-direct}" -CHINADNS_DNS ${_china_ng_listen:-0} \ -NODE $node -DEFAULT_PROXY_MODE ${tcp_proxy_mode} -NO_PROXY_IPV6 ${dnsmasq_filter_proxy_ipv6:-0} -NFTFLAG ${nftflag:-0} \ -NO_LOGIC_LOG 1 ln_run "$(first_type dnsmasq)" "dnsmasq_${sid}" "/dev/null" -C ${dnsmasq_conf} -x ${acl_path}/dnsmasq.pid set_cache_var "ACL_${sid}_dns_port" "${dnsmasq_port}" - set_cache_var "node_${node}_$(echo -n "${tcp_proxy_mode}_${dns_cache_key}" | md5sum | cut -d " " -f1)" "${dnsmasq_port}" #dhcp.leases to hosts $APP_PATH/lease2hosts.sh > /dev/null 2>&1 & } - _redir_port=$(get_cache_var "node_${node}_redir_port") - _socks_port=$(get_cache_var "node_${node}_socks_port") + local _redir_port=$(get_cache_var "node_${node}_redir_port") + local _socks_port=$(get_cache_var "node_${node}_socks_port") + local _dns_port if [ -n "${_socks_port}" ] && [ -n "${_redir_port}" ]; then socks_port=${_socks_port} node_port=${_redir_port} - _dnsmasq_port=$(get_cache_var "node_${node}_$(echo -n "${tcp_proxy_mode}_${dns_cache_key}" | md5sum | cut -d " " -f1)") - if [ -z "${_dnsmasq_port}" ]; then - _dns_port=$(get_cache_var "node_${node}_$(echo -n "${dns_cache_key}" | md5sum | cut -d " " -f1)") - run_dns ${_dns_port} - else - [ -n "${_dnsmasq_port}" ] && set_cache_var "ACL_${sid}_dns_port" "${_dnsmasq_port}" - fi + _dns_port=$(get_cache_var "node_${node}_${dns_cache_key}") + run_dns ${_dns_port} else socks_port=$(get_new_port $(expr $socks_port + 1)) set_cache_var "node_${node}_socks_port" "${socks_port}" @@ -1600,7 +1603,10 @@ acl_app() { _dns_port=$dns_port dnsmasq_filter_proxy_ipv6=0 remote_dns_query_strategy="UseIP" - [ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4" + [ "$filter_proxy_ipv6" = "1" ] && { + remote_dns_query_strategy="UseIPv4" + set_cache_var "node_${node}_${dns_cache_key}_ipv6" "0" + } remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query} _extra_param="${_extra_param} dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns}" _extra_param="${_extra_param} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_fakedns=${remote_fakedns:-0} remote_dns_client_ip=${remote_dns_client_ip}" diff --git a/luci-app-passwall/root/usr/share/passwall/iptables.sh b/luci-app-passwall/root/usr/share/passwall/iptables.sh index 452f922fd..511f48b5f 100755 --- a/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -417,7 +417,7 @@ load_acl() { [ "$_ipv4" != "1" ] && $ip6t_n -A PSW_DNS $(comment "$remarks") -p udp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} 2>/dev/null $ipt_n -A PSW_DNS $(comment "$remarks") -p tcp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} [ "$_ipv4" != "1" ] && $ip6t_n -A PSW_DNS $(comment "$remarks") -p tcp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} 2>/dev/null - [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}使用与全局配置不相同节点,已将DNS强制重定向到专用 DNS 服务器。" + [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}与全局配置不同节点,DNS 重定向到专用 DNS 服务器 [${dns_redirect}]。" fi [ -n "$tcp_port" ] || [ -n "$udp_port" ] && { diff --git a/luci-app-passwall/root/usr/share/passwall/nftables.sh b/luci-app-passwall/root/usr/share/passwall/nftables.sh index 8065b8feb..e0234ee24 100755 --- a/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -478,7 +478,7 @@ load_acl() { #nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" - [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}使用与全局配置不相同节点,已将DNS强制重定向到专用 DNS 服务器。" + [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}与全局配置不同节点,DNS 重定向到专用 DNS 服务器 [${dns_redirect}]。" fi [ -n "$tcp_port" ] || [ -n "$udp_port" ] && { diff --git a/luci-app-ss-redir/Makefile b/luci-app-ss-redir/Makefile deleted file mode 100644 index c2a8c17cf..000000000 --- a/luci-app-ss-redir/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# This is free software, licensed under the Apache License, Version 2.0 . -# - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=LuCI Support for ss-redir -LUCI_DEPENDS:=+luci-base +ss-redir - -PKG_LICENSE:=Apache-2.0 -PKG_MAINTAINER:=Dengfeng Liu - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-ss-redir/README.md b/luci-app-ss-redir/README.md deleted file mode 100644 index b1a434d11..000000000 --- a/luci-app-ss-redir/README.md +++ /dev/null @@ -1,42 +0,0 @@ -## luci-app-ss-redir - -该项目为ss-redir的luci配置界面 - -### 视频演示 - -
- - luci-app-ss-redir_intro_video - -
- -## 如何集成到openwrt中编译 - -1. 复制仓库中的文件到如下目录,并执行安装 - -``` -feeds/luci/applications/luci-app-ss-redir/ -./scripts/feeds install luci -a -``` - -2. 选择路径 - -`make menuconfig` - -LuCI > 3. Applications > luci-app-ss-redir - -3. 编译openwrt固件 - -``` -make -j4 -``` - -4. 单独编译 - -``` -make package/luci-app-ss-redir/compile -``` - -## 联系方式 - -QQ群:331230369 diff --git a/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js b/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js deleted file mode 100644 index 34fd7643f..000000000 --- a/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; -'require view'; -'require ui'; -'require form'; -'require rpc'; -'require uci'; -'require tools.widgets as widgets'; -'require tools.github as github'; - -var methods = [ - // aead - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305', - 'xchacha20-ietf-poly1305', - // stream - 'table', - 'rc4', - 'rc4-md5', - 'aes-128-cfb', - 'aes-192-cfb', - 'aes-256-cfb', - 'aes-128-ctr', - 'aes-192-ctr', - 'aes-256-ctr', - 'bf-cfb', - 'camellia-128-cfb', - 'camellia-192-cfb', - 'camellia-256-cfb', - 'salsa20', - 'chacha20', - 'chacha20-ietf', -]; - -var callServiceList = rpc.declare({ - object: 'service', - method: 'list', - params: ['name'], - expect: { '': {} } -}); - -function getServiceStatus() { - return L.resolveDefault(callServiceList('ss-redir'), {}).then(function (res) { - var isRunning = false; - try { - isRunning = res['ss-redir']['instances']['instance1']['running']; - } catch (e) { } - return isRunning; - }); -} - -function renderStatus(isRunning) { - var renderHTML = ""; - var spanTemp = '%s %s'; - - if (isRunning) { - renderHTML += String.format(spanTemp, 'green', _("ss-redir "), _("running...")); - } else { - renderHTML += String.format(spanTemp, 'red', _("ss-redir "), _("not running...")); - } - - return renderHTML; -} - -return view.extend({ - load: function() { - return uci.load('kcptun'); - }, - render: function() { - var m, s, o; - - m = new form.Map('ss-redir', _('ss-redir')); - m.description = github.luci_desc('ss-redir redirect tcp service to kcptun client process.', 'liudf0716', 'ss-redir'); - - // add kcptun-client status section and option - s = m.section(form.NamedSection, '_status'); - s.anonymous = true; - s.render = function (section_id) { - L.Poll.add(function () { - return L.resolveDefault(getServiceStatus()).then(function(res) { - var view = document.getElementById("service_status"); - view.innerHTML = renderStatus(res); - }); - }); - - return E('div', { class: 'cbi-map' }, - E('fieldset', { class: 'cbi-section'}, [ - E('p', { id: 'service_status' }, - _('Collecting data ...')) - ]) - ); - } - - s = m.section(form.NamedSection, 'server', 'ss_redir'); - s.dynamic = true; - - // add two tabs - s.tab('ss-redir', _('ss-redir Settings')); - s.tab('server', _('Shadowsocks Server Settings')); - - // ss-redir settings - o = s.taboption('ss-redir', form.Flag, "enabled", _("Enable"), - _("Enable ss-redir service")); - o.rmempty = false; - - // server settings - o = s.taboption('ss-redir', form.Value, "server", _("Server Address"), - _("The address of kcptun client listening on. default: 127.0.0.1")); - o.datatype = "host"; - o.rmempty = false; - o.titleref = L.url('admin', 'vpn', 'kcptun'); - - o = s.taboption('ss-redir', form.Value, "server_port", _("Server Port"), - _("The port of kcptun client listening on. default: 12948")); - o.datatype = "port"; - o.rmempty = false; - - o = s.taboption('ss-redir', form.Value, "password", _("Password"), - _("The password of shadowsocks server")); - o.password = true; - o.rmempty = false; - - o = s.taboption('ss-redir', form.ListValue, "encrypt", _("Encryption Method"), - _("The encryption method of shadowsocks server")); - methods.forEach(function (method) { - o.value(method, method); - }); - o.rmempty = false; - - // add a textarea to show the config file content - o = s.taboption('server', form.DummyValue, "_config", _("Server Command"), - _("Copy the command to run ss-server on the server side")); - o.rawhtml = true; - o.cfgvalue = function(section_id) { - var server = uci.get('ss-redir', section_id, 'server'); - var password = uci.get('ss-redir', section_id, 'password'); - var encrypt = uci.get('ss-redir', section_id, 'encrypt'); - var kcp_port = uci.get('ss-redir', section_id, 'kcp_port') || 6441; - - var config = "ss-server -s " + server + " -p " + kcp_port + " -k " + password + " -m " + encrypt; - return ""; - } - // add a button to copy the command - o = s.taboption('server', form.Button, "_copy", _("Copy Command")); - o.inputstyle = 'apply'; - o.inputtitle = _("Copy the command to clipboard"); - o.onclick = function(ev) { - var textarea = document.querySelector('textarea'); - textarea.select(); - document.execCommand('copy'); - ui.addNotification(null, E('p', _('Copied to clipboard')), 'info'); - }; - - return m.render(); - } -}); diff --git a/luci-app-ss-redir/po/templates/ss-redir.pot b/luci-app-ss-redir/po/templates/ss-redir.pot deleted file mode 100644 index e69de29bb..000000000 diff --git a/luci-app-ss-redir/po/zh-cn b/luci-app-ss-redir/po/zh-cn deleted file mode 120000 index 8d69574dd..000000000 --- a/luci-app-ss-redir/po/zh-cn +++ /dev/null @@ -1 +0,0 @@ -zh_Hans \ No newline at end of file diff --git a/luci-app-ss-redir/po/zh_Hans/ss-redir.po b/luci-app-ss-redir/po/zh_Hans/ss-redir.po deleted file mode 100644 index 1d57c1fad..000000000 --- a/luci-app-ss-redir/po/zh_Hans/ss-redir.po +++ /dev/null @@ -1,51 +0,0 @@ -#: ss-redir package -msgid "ss-redir" -msgstr "SS转发服务" - -msgid "ss-redir redirect tcp service to kcptun client process." -msgstr "SS转发服务,将TCP流量重定向到本地KCP隧道,要结合本地KCP隧道服务一起使用。" - -msgid "Shadowsocks Server Settings" -msgstr "Shadowsocks服务器设置" - -msgid "ss-redir Settings" -msgstr "SS转发服务设置" - -msgid "The address of kcptun client listening on. default: 127.0.0.1" -msgstr "KCP隧道客户端监听地址,默认: 127.0.0.1" - -msgid "Server Address" -msgstr "服务器地址" - -msgid "The port of kcptun client listening on. default: 12948" -msgstr "KCP隧道客户端监听端口,默认: 12948" - -msgid "The password of shadowsocks server" -msgstr "Shadowsocks服务器密码" - -msgid "Server Port" -msgstr "服务器端口" - -msgid "Encryption Method" -msgstr "加密方式" - -msgid "The encryption method of shadowsocks server" -msgstr "Shadowsocks服务器的加密方式" - -msgid "Enable ss-redir service" -msgstr "启用SS转发服务" - -msgid "Server Command" -msgstr "服务器端命令" - -msgid "Copy the command to run ss-server on the server side" -msgstr "复制在服务器端运行ss-server的命令" - -msgid "Copy Command" -msgstr "复制命令" - -msgid "Compy the command to clipboard" -msgstr "复制服务器端命令到剪贴板" - -msgid "Copied to clipboard" -msgstr "已复制到剪贴板" \ No newline at end of file diff --git a/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json b/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json deleted file mode 100644 index a4c12b893..000000000 --- a/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin/vpn/ss-redir": { - "title": "ss-redir", - "action": { - "type": "view", - "path": "ss-redir" - }, - "depends": { - "uci": { "ss-redir": true } - } - } -} \ No newline at end of file diff --git a/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json b/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json deleted file mode 100644 index 8ec797424..000000000 --- a/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "luci-app-ss-redir": { - "description": "Grant access to LuCI app ss-redir", - "read": { - "ubus": { - "service": [ "list" ] - }, - "uci": ["ss-redir"] - }, - "write": { - "uci": ["ss-redir"] - } - } -} diff --git a/luci-app-yt-dlp/Makefile b/luci-app-yt-dlp/Makefile deleted file mode 100644 index e190d9a36..000000000 --- a/luci-app-yt-dlp/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -# This is free software, licensed under the Apache License, Version 2.0 - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=LuCI Support for yt-dlp -LUCI_DEPENDS:=+yt-dlp - -PKG_LICENSE:=Apache-2.0 -PKG_MAINTAINER:=Dengfeng Liu - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-yt-dlp/README.md b/luci-app-yt-dlp/README.md deleted file mode 100644 index 0d98e1362..000000000 --- a/luci-app-yt-dlp/README.md +++ /dev/null @@ -1,42 +0,0 @@ -## luci-app-yt-dlp -该项目是yt-dlp的luci界面,通过该界面可以下载youtube、Twitter、B站等yt-dlp支持下载的视频网站。 - -### 视频演示 - -
- - luci-app-yt-dlp_intro_video - -
- - -## 如何集成到openwrt中编译 - -1. 复制仓库中的文件到如下目录,并执行安装 - -``` -feeds/luci/applications/luci-app-yt-dlp/ -./scripts/feeds install luci -a -``` - -2. 选择路径 - -`make menuconfig` - -LuCI > 3. Applications > luci-app-yt-dlp - -3. 编译openwrt固件 - -``` -make -j4 -``` - -4. 单独编译 - -``` -make package/luci-app-yt-dlp/compile -``` - -## 联系方式 - -QQ群:331230369 diff --git a/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js b/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js deleted file mode 100644 index f1b88c035..000000000 --- a/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require fs'; -'require ui'; -'require uci'; -'require network'; -'require rpc'; -'require tools.github as github'; - -return view.extend({ - load: function() { - return L.resolveDefault(fs.list('/tmp/yt-dlp/'), null); - }, - - updateTable: function(table, files) { - var rows = []; - - if (files == null) - return; - - for(var i = 0; i < files.length; i++) { - var file = files[i]; - - rows.push([ - file.name, - file.size, - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action important', - 'click': ui.createHandlerFn(this, 'handleDownloadVideo', file.name) - }, _('Download')), - E('button', { - 'class': 'cbi-button cbi-button-action cbi-button-negative', - 'click': ui.createHandlerFn(this, 'handleDeleteVideo', file.name) - }, _('Delete')) - ]), - ]); - } - - cbi_update_table(table, rows, E('em', _('No information available'))); - }, - - handleYtdlp: function(ev) { - var exec = '/usr/bin/yt-dlp', - addr = ev.currentTarget.parentNode.previousSibling.value, - args = ['-o', "/tmp/yt-dlp/%(title)s.%(ext)s", addr]; - - if (addr == '') { - ui.addNotification(null, E('p', _('Please enter a valid URL'))); - return; - } - - var buttons = document.querySelectorAll('.diag-action > .cbi-button'); - - for (var i = 0; i < buttons.length; i++) - buttons[i].setAttribute('disabled', 'true'); - - return fs.exec(exec, args).then(L.bind(function(res) { - var out = document.querySelector('.command-output'); - out.style.display = ''; - - dom.content(out, [ res.stdout || '', res.stderr || '' ]); - if (res.stdout) { - L.resolveDefault(fs.list('/tmp/yt-dlp/'), null).then(L.bind(function(files) { - var table = document.getElementById('videos'); - this.updateTable('#videos', files); - }, this)); - } - }, this)).catch(function(err) { - ui.addNotification(null, E('p', [ err ])); - return null - }).finally(function() { - for (var i = 0; i < buttons.length; i++) - buttons[i].removeAttribute('disabled'); - }); - }, - - handleDeleteVideo: function(filename, ev) { - var file = '/tmp/yt-dlp/' + filename; - return fs.remove(file).then(L.bind(function(res) { - L.resolveDefault(fs.list('/tmp/yt-dlp/'), null).then(L.bind(function(files) { - var table = document.getElementById('videos'); - this.updateTable("#videos", files); - }, this)); - }, this)).catch(function(err) { - ui.addNotification(null, E('p', [ err ])); - return null - }); - }, - - - handleDownloadVideo: function(filename, ev) { - var file = '/tmp/yt-dlp/' + filename; - var form = E('form', { - 'method': 'post', - 'action': L.env.cgi_base + '/cgi-download', - 'enctype': 'application/x-www-form-urlencoded' - }, [ - E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }), - E('input', { 'type': 'hidden', 'name': 'path', 'value': file }), - E('input', { 'type': 'hidden', 'name': 'filename', 'value': filename }) - ]); - - ev.currentTarget.parentNode.appendChild(form); - - form.submit(); - form.parentNode.removeChild(form); - }, - - clearInput: function(ev) { - document.getElementById('video_url').value = ''; - }, - - render: function(files) { - var video_url; - var tbl_videos = E('table', { 'class': 'table', 'id': 'videos' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th', 'style': 'width:60%' }, _('Name')), - E('th', { 'class': 'th', 'style': 'width;20%' }, _('Size')), - E('th', { 'class': 'th', 'style': 'width:20%' }, _('Action')), - ]) - ]); - var description = github.luci_desc('Download video from Youtube, Facebook, Twitter, etc.', 'liudf0716', 'yt-dlp') - - var v = E([], [ - E('h2', {}, [ _('Download Video') ]), - E('div', { 'class': 'cbi-section-descr' }, description ), - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td left' }, [ - E('input', { - 'style': 'margin:5px 0; width: 50% !important;', - 'type': 'text', - 'id': 'video_url', - 'value': video_url, - 'placeholder': _('Enter video URL') - }), - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': ui.createHandlerFn(this, 'handleYtdlp') - }, [ _('Yt-dlp Online') ]) - ]), - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action cbi-button-negative', - 'click': ui.createHandlerFn(this, 'clearInput') - }, [ _('Clear Input') ]) - ]),] - ), - ]) - ]), - E('pre', { 'class': 'command-output', 'style': 'display:none' }), - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'left' }, [ - E('h3', _('Download Video Details')), - tbl_videos - ]) - ]) - ]); - - this.updateTable(tbl_videos, files); - - return v; - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/luci-app-yt-dlp/luci-app-yt-dlp_intro.png b/luci-app-yt-dlp/luci-app-yt-dlp_intro.png deleted file mode 100644 index 76a8771437db7e445e1fe629619a45b381665fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49083 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

F{rba}jy#UU>IUWI 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 diff --git a/luci-app-yt-dlp/po/zh-cn b/luci-app-yt-dlp/po/zh-cn deleted file mode 120000 index 8d69574dd..000000000 --- a/luci-app-yt-dlp/po/zh-cn +++ /dev/null @@ -1 +0,0 @@ -zh_Hans \ No newline at end of file diff --git a/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po b/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po deleted file mode 100644 index 22ff3225b..000000000 --- a/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po +++ /dev/null @@ -1,29 +0,0 @@ -msgid "Download Video" -msgstr "短视频下载" - -msgid "Download Video Details" -msgstr "下载视频详情" - -msgid "Download video from Youtube, Facebook, Twitter, etc." -msgstr "从各大网站下载视频,支持B站,Youtube, Twitter等" - -msgid "Yt-dlp Online" -msgstr "下载视频" - -msgid "Please enter a valid URL" -msgstr "请输入有效的URL" - -msgid "yt-dlp" -msgstr "短视频下载" - -msgid "Action" -msgstr "操作" - -msgid "Download" -msgstr "下载到本地" - -msgid "Clear Input" -msgstr "清空输入" - -msgid "Enter video URL" -msgstr "输入视频URL" \ No newline at end of file diff --git a/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp b/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp deleted file mode 100755 index ee1f3b4eb..000000000 --- a/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -ytdlp() { - url=$1 - output=$2 - /usr/bin/yt-dlp $url -o $output -} - -handleRequest() { - action=$1 - - if [ "$action" = "list" ]; then - echo "{\"download\":{\"description\":\"Download video\",\"params\":{\"url\":{\"type\":\"string\",\"description\":\"Video URL\"}}}}" - elif [ "$action" = "call" ]; then - method=$2 - - if [ "$method" = "download" ]; then - output="/tmp/yt-dlp-output.mp4" - read url - ytdlp "$url" "$output" - result="{\"result\":\"ok\",\"data\":{\"url\":\"$url\",\"output\":\"$output\"}}" - echo "$result" - else - errorResult="{\"result\":\"error\",\"message\":\"Unknown method\"}" - echo "$errorResult" - fi - fi -} - -handleRequest "$@" diff --git a/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json b/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json deleted file mode 100644 index 2997954ea..000000000 --- a/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin/services/yt-dlp": { - "title": "yt-dlp", - "action": { - "type": "view", - "path": "yt-dlp" - }, - "depends": { - "acl": [ "luci-app-yt-dlp" ] - } - } -} diff --git a/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json b/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json deleted file mode 100644 index 7a42309e7..000000000 --- a/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "luci-app-yt-dlp": { - "description": "Grant access to LuCI app yt-dlp", - "read": { - "ubus": { - "file": [ "exec", "read", "stat" ], - "luci": [ "isFfmpegOrWhispercppRunning" ] - }, - "file": { - "/tmp/yt-dlp/*": [ "read" ] - }, - "cgi-io": [ "download" ] - }, - "write": { - "cgi-io": [ "upload"], - "file": { - "/usr/bin/yt-dlp": ["exec", "write" ], - "/tmp/yt-dlp/*": [ "write", "remove" ] - }, - "ubus": { - "file": [ "exec", "remove", "write" ], - "yt-dlp": [ "download" ] - } - } - } -} diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 24f086470..e822d80b5 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=65 +PKG_RELEASE:=66 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index ae11e3dc2..4ab69bf81 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -885,13 +885,44 @@ function lateDrift(ref, grow, floorShrink) { /* fresh distrust starts the recovery count at 0 too — a streak from a PREVIOUS * spell of distrust proves nothing about this one */ if (++_lateMisses >= LATE_MISS_LIMIT) { _engineTrusted = false; _lateHits = 0; } + /* MIRROR OF task refill2's WRITE-PATH FIX, ON THE NO-WRITE PATH — task nine. + * A miss here means the OFFSET did not fully move; it does not mean this tick's + * geometry is unknown. Leaving `_rest` as `run()`'s own mid-transition capture (the + * DOM already changed, nothing had compensated yet) makes THAT stale snapshot the + * `ref.at`/`was` the NEXT tick measures against, same as the write path used to + * before `rememberRest(true)` was added there. `seen` and `el`'s rect, just read, + * ARE the true current position — uncorrected, but real — so the next comparison + * should start from here, not from before this tick began. Measured, `../tmp/ + * task-nine/`: without this, `firefox owrt2410 @390 top overview` counted a SECOND + * phantom miss off the stale baseline and tripped `_engineTrusted` false while + * REPEAT's own mark never moved (misses [true,true,false]) — e0b6db4's fault on the + * other side of the same comparison. */ + rememberRest(true); return; } /* else: within table-row rounding — drift is still whatever it was (< 1 per the guard * above), so the plain `drift < 1` return two lines down is what fires, unwritten and * uncounted: the engine did the job. */ } - if (Math.abs(drift) < 1) return; /* the engine put it back */ + if (Math.abs(drift) < 1) { + /* SAME MIRROR AS ABOVE, FOR THE "engine already got it right" EXIT — task nine. + * Gated on `grow` OR `floorShrink`, not unconditional: a tick where this floored box + * neither grew nor shrank pays nothing extra here (P5), and `run()`'s own synchronous + * reference is only ever wrong relative to what THIS tick's mutation did. `grow` alone + * is not enough — REPEAT's own SHRINK leg (the pad it removes between refills) is a + * real box change `grow` reads as <=1 (growth witness is deliberately one-sided, + * task detector), so gating on `grow > 1` alone left the SAME hole one level down: the + * shrink between refill 1 and refill 2 left `_rest` at run()'s mid-transition capture, + * and refill 2 read ITS drift against that stale baseline. Where it did change (either + * way) and the engine handled it without a write, `run()`'s snapshot is still what the + * NEXT tick's `lateDrift()` would measure against — masking a real residual as "small" + * until it surfaces as a flat, un-recovered offset. Measured, `../tmp/task-nine/`: + * gating on `grow > 1` alone left `/admin/network/dhcp @390` at 47-59px off on the + * second of three back-to-back refills, unchanged — chromium/firefox/webkit alike, + * `_engineTrusted` true throughout. */ + if (grow > 1 || floorShrink > 1) rememberRest(true); + return; /* the engine put it back */ + } if (Math.abs(drift) > (window.innerHeight || 800)) return; const sc = scroller(); const at = sc ? sc.scrollTop : window.scrollY; diff --git a/ss-redir/Makefile b/ss-redir/Makefile deleted file mode 100644 index 578216a0e..000000000 --- a/ss-redir/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright (C) 2022 Dengfeng Liu -# -# This is free software, licensed under the GNU General Public License v3. -# See /LICENSE for more information. -# - -include $(TOPDIR)/rules.mk - -PKG_NAME:=ss-redir -PKG_VERSION:=0.1.0 -PKG_RELEASE:=1 - -PKG_MAINTAINER:=Dengfeng Liu -PKG_LICENSE:=GPL-3.0-or-later -PKG_LICENSE_FILES:=COPYING - -include $(INCLUDE_DIR)/package.mk - -define Package/ss-redir - SUBMENU:=Web Servers/Proxies - SECTION:=net - CATEGORY:=Network - TITLE:= simplify ss-redir usage - DEPENDS:=+shadowsocks-libev-ss-redir -endef - -define Package/ss-redir/description - simplify ss-redir usage -endef - -define Build/Compile -endef - -define Package/ss-redir/conffiles - /etc/config/ss-redir -endef - -define Package/ss-redir/install - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_DIR) $(1)/etc/uci-defaults - $(INSTALL_DIR) $(1)/usr/share/ss-redir - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_BIN) ./files/ss-redir.init $(1)/etc/init.d/ss-redir - $(INSTALL_BIN) ./files/ss-redir.conf $(1)/etc/config/ss-redir - $(INSTALL_DATA) ./files/firewall4.include $(1)/usr/share/ss-redir/firewall.include - $(INSTALL_BIN) ./files/ss-redir.defaults.nftables $(1)/etc/uci-defaults/99-ss-redir -endef - -$(eval $(call BuildPackage,ss-redir)) diff --git a/ss-redir/files/firewall4.include b/ss-redir/files/firewall4.include deleted file mode 100644 index 916b43913..000000000 --- a/ss-redir/files/firewall4.include +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -/etc/init.d/ss-redir enabled && /etc/init.d/ss-redir restart -exit 0 diff --git a/ss-redir/files/ss-redir.conf b/ss-redir/files/ss-redir.conf deleted file mode 100644 index 716f801dd..000000000 --- a/ss-redir/files/ss-redir.conf +++ /dev/null @@ -1,6 +0,0 @@ -config ss_redir 'server' - option enabled '0' - option server '127.0.0.1' - option server_port '12948' - option password 'password' - option encrypt 'aes-256-cfb' diff --git a/ss-redir/files/ss-redir.defaults.nftables b/ss-redir/files/ss-redir.defaults.nftables deleted file mode 100644 index ff6ab60f8..000000000 --- a/ss-redir/files/ss-redir.defaults.nftables +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -uci -q batch <<-EOT - delete firewall.ss_redir - set firewall.ss_redir=include - set firewall.ss_redir.type=script - set firewall.ss_redir.path=/usr/share/ss-redir/firewall.include - commit firewall -EOT - -exit 0 diff --git a/ss-redir/files/ss-redir.init b/ss-redir/files/ss-redir.init deleted file mode 100644 index 8c7487550..000000000 --- a/ss-redir/files/ss-redir.init +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/sh /etc/rc.common -# -# Copyright (C) 2023 Dengfeng Liu -# -# This is free software, licensed under the GNU General Public License v3. -# See /LICENSE for more information. -# - -USE_PROCD=1 -START=99 - -CONF="ss-redir" -PROG="/usr/bin/ss-redir" -GO_SS2="/usr/bin/go-ss2" -CONFILE="/tmp/ss-redir.json" - -mk_ss_redir_conf() { - local stream_methods='"table", "rc4", "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", "salsa20", "chacha20", "chacha20-ietf"' - local aead_methods='"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305"' - - uci_validate_section ss-redir ss_redir "server" \ - 'server:host:127.0.0.1' \ - 'server_port:port:12948' \ - 'password:string' \ - "encrypt:or($stream_methods, $aead_methods)" \ - 'local_address:host:0.0.0.0' \ - 'local_port:port:1088' \ - 'fast_open:bool:1' \ - 'ipv6_first:bool:0' \ - 'no_delay:bool:0' \ - 'reuse_port:bool:1' \ - 'mode:or("tcp_only", "udp_only", "tcp_and_udp"):tcp_only' \ - 'mtu:uinteger:1350' \ - 'timeout:uinteger:60' - - json_init - json_add_string server $server - json_add_int server_port $server_port - json_add_string method $encrypt - json_add_string password $password - json_add_string local_address $local_address - json_add_int local_port $local_port - json_add_string mode $mode - json_add_int mtu $mtu - json_add_int timeout $timeout - json_add_boolean fast_open $fast_open - json_add_boolean ipv6_first $ipv6_first - json_add_boolean no_delay $no_delay - json_add_boolean reuse_port $reuse_port - json_dump -i >"$CONFILE" -} - -add_ss_redir_rule() { - nft add set inet fw4 ss-redir_set { type ipv4_addr\; } - nft add set inet fw4 ss-redir_set6 { type ipv6_addr\; } - nft add set inet fw4 all_proxy_set { type ipv4_addr\; } - nft add set inet fw4 all_proxy_set6 { type ipv6_addr\; } - nft add chain inet fw4 ss-redir_chain - nft add rule inet fw4 dstnat iifname br-lan jump ss-redir_chain - nft add rule inet fw4 ss-redir_chain ip saddr @all_proxy_set meta l4proto tcp redirect to 1080 - nft add rule inet fw4 ss-redir_chain ip6 saddr @all_proxy_set6 meta l4proto tcp redirect to 1080 - nft add rule inet fw4 ss-redir_chain ip daddr @ss-redir_set meta l4proto tcp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip daddr @ss-redir_set meta l4proto udp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip6 daddr @ss-redir_set6 meta l4proto udp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip6 daddr @ss-redir_set6 meta l4proto tcp redirect to 1088 - -} - -clear_ss_redir_rule() { - if [ -z "$(nft -a list chain inet fw4 dstnat | grep ss-redir_chain)" ]; then - echo "no ss-redir rule" - return - fi - # get the handle of the ss-redir_chain in dstnat - handle_num=$(nft -a list chain inet fw4 dstnat | grep ss-redir_chain | awk '{print $7}') - if [ -z "$handle_num" ]; then - echo "cant get the handle of ss-redir_chain in dstnat" - return - fi - nft delete rule inet fw4 dstnat handle $handle_num - nft flush chain inet fw4 ss-redir_chain - nft delete chain inet fw4 ss-redir_chain - nft delete set inet fw4 ss-redir_set - nft delete set inet fw4 ss-redir_set6 - nft delete set inet fw4 all_proxy_set - nft delete set inet fw4 all_proxy_set6 -} - -ss_redir_rule() { - clear_ss_redir_rule - add_ss_redir_rule -} - -get_proxy_command() { - if [ -x "$GO_SS2" ]; then - echo "$GO_SS2 -c 'ss://$encrypt:$password@$server:$server_port' -redir :$local_port" - else - echo "$PROG -c $CONFILE" - fi -} - -start_service() { - config_load "$CONF" - local enabled - - config_get_bool enabled "server" "enabled" "0" - [ "$enabled" -eq "1" ] || { - echo "service does not enabled" - clear_ss_redir_rule - return 1 - } - - mk_ss_redir_conf - [ ! -f $CONFILE ] && return 1 - - - procd_open_instance - if [ -x "$GO_SS2" ]; then - procd_set_param command "$GO_SS2" -c "ss://$encrypt:$password@$server:$server_port" -verbose -redir ":$local_port" - procd_set_param stdout 1 - procd_set_param stderr 1 - else - procd_set_param command "$PROG" -c "$CONFILE" - fi - procd_set_param respawn - procd_close_instance - - ss_redir_rule -} - -reload_service() { - stop - start -} - -service_triggers() { - procd_add_reload_trigger "$CONF" -} \ No newline at end of file diff --git a/torrserver/Makefile b/torrserver/Makefile index dc887fabe..665fa777a 100644 --- a/torrserver/Makefile +++ b/torrserver/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=torrserver -PKG_VERSION:=MatriX.144.1 -PKG_RELEASE:=19 +PKG_VERSION:=MatriX.144 +PKG_RELEASE:=20 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git -PKG_SOURCE_VERSION:=309a5917c12764c82791b555fb61a60f8e2fee44 +PKG_SOURCE_VERSION:=c416a8f2b89b244c779f03f1f86e2f0f690c2668 PKG_MIRROR_HASH:=skip PKG_MAINTAINER:=Konstantine Shevlakov