mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 12:05:13 +08:00
🗽 Sync 2026-09-10 18:16:50
This commit is contained in:
1 parent
b220b5a2f9
commit
992e0de023
39 files changed
+334
-3030
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.') + '<br>' + stats.messages.join('<br>')), 'info');
|
||||
} else if (stats.successCount > 0) {
|
||||
ui.addNotification(null, E('p', _('Auto fill partially completed.') + ' ' + stats.successCount + '/' + stats.totalFields + ' ' + _('fields filled successfully.') + '<br>' + stats.messages.join('<br>')), '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 = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
@@ -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';
|
||||
|
||||
@@ -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 + '<br/>';
|
||||
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) + '<br/>';
|
||||
}
|
||||
});
|
||||
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
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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 + '<br/>';
|
||||
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) + '<br/>';
|
||||
}
|
||||
});
|
||||
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 = '<span class="btn-icon">▶️</span> ' + _('Resume');
|
||||
btn.classList.remove('cbi-button-action');
|
||||
btn.classList.add('cbi-button-positive');
|
||||
} else {
|
||||
btn.innerHTML = '<span class="btn-icon">⏸️</span> ' + _('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
|
||||
});
|
||||
@@ -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\"。"
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
[ -f "/etc/config/hostnames" ] || {
|
||||
echo 'config hostname' > /etc/config/hostnames
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" ]
|
||||
}
|
||||
|
||||
@@ -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:= \
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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" ] && {
|
||||
|
||||
@@ -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" ] && {
|
||||
|
||||
@@ -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 <liu_df@qq.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -1,42 +0,0 @@
|
||||
## luci-app-ss-redir
|
||||
|
||||
该项目为ss-redir的luci配置界面
|
||||
|
||||
### 视频演示
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.bilibili.com/video/BV18m411d7Yj/?vd_source=b303f6e8e0ed18809d8752d41ab1de7d">
|
||||
<img width="972" alt="luci-app-ss-redir_intro_video" src="luci-app-ss-redir_intro.png">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 如何集成到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
|
||||
@@ -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 = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
|
||||
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 "<textarea style='width:100%;height:50px;'>" + config + "</textarea>";
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
Whitespace-only changes.
@@ -1 +0,0 @@
|
||||
zh_Hans
|
||||
@@ -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 "已复制到剪贴板"
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"admin/vpn/ss-redir": {
|
||||
"title": "ss-redir",
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "ss-redir"
|
||||
},
|
||||
"depends": {
|
||||
"uci": { "ss-redir": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <liudf0716@gmail.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -1,42 +0,0 @@
|
||||
## luci-app-yt-dlp
|
||||
该项目是yt-dlp的luci界面,通过该界面可以下载youtube、Twitter、B站等yt-dlp支持下载的视频网站。
|
||||
|
||||
### 视频演示
|
||||
|
||||
<div align="center">
|
||||
<a href="https://www.bilibili.com/video/BV1jx421U7h4/?vd_source=b303f6e8e0ed18809d8752d41ab1de7d">
|
||||
<img width="972" alt="luci-app-yt-dlp_intro_video" src="luci-app-yt-dlp_intro.png">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
## 如何集成到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
|
||||
@@ -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
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
@@ -1 +0,0 @@
|
||||
zh_Hans
|
||||
@@ -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"
|
||||
@@ -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 "$@"
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"admin/services/yt-dlp": {
|
||||
"title": "yt-dlp",
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "yt-dlp"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-yt-dlp" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <liudf0716@gmail.com>
|
||||
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))
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
/etc/init.d/ss-redir enabled && /etc/init.d/ss-redir restart
|
||||
exit 0
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# Copyright (C) 2023 Dengfeng Liu <liudf0716@gmail.com>
|
||||
#
|
||||
# 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"
|
||||
}
|
||||
+3
-3
@@ -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 <shevlakov@132lan.ru>
|
||||
|
||||
Reference in new issue
Block a user