mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 08:33:56 +08:00
🎁 Sync 2026-07-31 00:21:49
This commit is contained in:
1 parent
f6c3fbbbbe
commit
c71b32c9fb
204 files changed
+187145
-10038
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,571 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require homeproxy as hp';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const css = '\
|
||||
.diag-section { margin-bottom: 1.5em; }\
|
||||
.diag-row { display:flex; align-items:baseline; margin:.35em 0; gap:.6em; }\
|
||||
.diag-label { min-width:11em; color:#666; font-size:.9em; flex-shrink:0; }\
|
||||
.diag-ok { color: #2a2; font-weight: bold; }\
|
||||
.diag-fail { color: #c33; font-weight: bold; }\
|
||||
.diag-warn { color: #c80; font-weight: bold; }\
|
||||
.diag-gray { color: #888; }\
|
||||
.diag-pre { font-family:monospace; font-size:.82em; background:#f5f5f5; border:1px solid #ddd;\
|
||||
padding:.5em .8em; white-space:pre-wrap; word-break:break-all; border-radius:3px;\
|
||||
margin:.3em 0 0; max-height:14em; overflow-y:auto; }\
|
||||
.diag-input { padding:.25em .4em; border:1px solid #ccc; border-radius:3px;\
|
||||
font-size:.9em; width:14em; }\
|
||||
.diag-btn { margin-right:.4em; }\
|
||||
.diag-port-line { font-family:monospace; font-size:.82em; margin:.1em 0; }\
|
||||
#diag-report-area { width:100%; height:22em; font-family:monospace; font-size:.8em;\
|
||||
white-space:pre; overflow:auto; background:#f5f5f5; border:1px solid #ddd; padding:.5em; }\
|
||||
';
|
||||
|
||||
/* ── RPC declarations ─────────────────────────────────────────────────── */
|
||||
|
||||
const callCoreCheck = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_core_check',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callConfigCheck = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_config_check',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callDnsRu = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_dns_ru',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callNftables = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_nftables',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
|
||||
const callReport = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_report',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callServiceRestart = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'diag_service_restart',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callConnCheck = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callIpInfo = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_ip_info',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callActiveNode = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_active_node',
|
||||
params: ['tag'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
/* Resolve a sing-box outbound tag (cfg-<section>-out) to its UCI label. */
|
||||
function resolveTag(tag) {
|
||||
const m = tag && tag.match(/^cfg-(.+)-out$/);
|
||||
if (m) {
|
||||
const label = uci.get('homeproxy', m[1], 'label');
|
||||
if (label) return label;
|
||||
}
|
||||
/* Fixed aggregate tags → name the configured node behind them. */
|
||||
const specials = { 'main-out': 'main_node', 'main-udp-out': 'main_udp_node' };
|
||||
if (specials[tag]) {
|
||||
const v = uci.get('homeproxy', 'config', specials[tag]);
|
||||
if (v === 'byedpi-out') return 'ByeDPI';
|
||||
if (v === 'zapret-out') return 'Zapret';
|
||||
if (v && !['urltest', 'same', 'nil', ''].includes(v)) {
|
||||
const label = uci.get('homeproxy', v, 'label');
|
||||
if (label) return label;
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/* ── DOM helpers ──────────────────────────────────────────────────────── */
|
||||
|
||||
function statusBadge(ok, text) {
|
||||
/* ucode serialises booleans as integers 0/1 — use loose checks so both
|
||||
JS booleans and integers work. null/undefined → "uncertain" (orange ?). */
|
||||
const isOk = !!ok;
|
||||
const isWarn = ok == null; /* catches null AND undefined */
|
||||
const icon = isOk ? '✓ ' : isWarn ? '? ' : '✗ ';
|
||||
const cls = isOk ? 'diag-ok' : isWarn ? 'diag-warn' : 'diag-fail';
|
||||
const label = text || (isOk ? _('OK') : isWarn ? '?' : _('FAIL'));
|
||||
return E('strong', { 'class': cls }, icon + label);
|
||||
}
|
||||
|
||||
function row(label, value) {
|
||||
return E('div', { 'class': 'diag-row' }, [
|
||||
E('span', { 'class': 'diag-label' }, label),
|
||||
E('span', {}, value)
|
||||
]);
|
||||
}
|
||||
|
||||
function pre(text) {
|
||||
return E('div', { 'class': 'diag-pre' }, text || _('(empty)'));
|
||||
}
|
||||
|
||||
function sectionCard(title, id, rows) {
|
||||
return E('div', { 'class': 'cbi-section diag-section' }, [
|
||||
E('h3', {}, title),
|
||||
E('div', { 'id': id }, rows)
|
||||
]);
|
||||
}
|
||||
|
||||
function spinner(el, label) {
|
||||
dom.content(el, E('em', { 'class': 'diag-gray' }, label || _('Testing…')));
|
||||
}
|
||||
|
||||
/* ── Section builders ─────────────────────────────────────────────────── */
|
||||
|
||||
function buildConnectivitySection(view, coreType) {
|
||||
const items = []; /* manual checks; the section's run() fires them together */
|
||||
|
||||
function siteRow(label, site) {
|
||||
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
|
||||
function run() {
|
||||
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
|
||||
return L.resolveDefault(callConnCheck(site), {}).then(function(ret) {
|
||||
dom.content(resultEl, statusBadge(!!ret.result, ret.result ? _('passed') : _('failed')));
|
||||
});
|
||||
}
|
||||
items.push(run);
|
||||
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
|
||||
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
|
||||
}
|
||||
|
||||
/* Exit IP / geo — only hiddify-core's Clash API reports it. */
|
||||
function ipRow(label, type) {
|
||||
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
|
||||
function run() {
|
||||
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
|
||||
return L.resolveDefault(callIpInfo(), {}).then(function(ret) {
|
||||
if (ret.error) { dom.content(resultEl, E('span', { 'class': 'diag-fail' }, ret.error)); return; }
|
||||
const entry = ret[type];
|
||||
if (!entry || !entry.ip) { dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No data'))); return; }
|
||||
const meta = [entry.country, entry.org].filter(Boolean).join(', ');
|
||||
const delay = (entry.delay && entry.delay !== 65535) ? ' — ' + entry.delay + ' ms' : '';
|
||||
const node = (type === 'proxy' && entry.node) ? resolveTag(entry.node) + ': ' : '';
|
||||
dom.content(resultEl, E('strong', { 'class': 'diag-ok' },
|
||||
node + entry.ip + (meta ? ' (' + meta + ')' : '') + delay));
|
||||
});
|
||||
}
|
||||
items.push(run);
|
||||
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
|
||||
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
|
||||
}
|
||||
|
||||
/* Active node — live status (which node sing-box has selected), auto-updating;
|
||||
* sing-box can't report exit IP, so this stands in for the IP rows. */
|
||||
/* Live active-node row. `tag` selects which outbound group to resolve:
|
||||
undefined → the main (TCP) node via GLOBAL; 'main-udp-out' → the dedicated UDP node. */
|
||||
function activeNodeRow(labelText, tag) {
|
||||
const resultEl = E('span', { 'class': 'diag-gray' }, '—');
|
||||
poll.add(L.bind(function() {
|
||||
return L.resolveDefault(callActiveNode(tag), {}).then(function(ret) {
|
||||
if (ret && !ret.error && ret.node) {
|
||||
const type = ret.type ? ' (' + ret.type + ')' : '';
|
||||
/* Same 4-colour scheme as the status/client pages: 65535 ms is the
|
||||
URLTest timeout sentinel (confirmed dead → red); >=3000 ms is
|
||||
working-but-slow (orange); a real low latency is green; no delay
|
||||
is unmeasured (gray, no number). */
|
||||
let cls, dStr = '';
|
||||
if (ret.delay === 65535) { cls = 'diag-fail'; dStr = ' — ' + _('timeout'); }
|
||||
else if (ret.delay >= 3000) { cls = 'diag-warn'; dStr = ' — ' + ret.delay + ' ms'; }
|
||||
else if (ret.delay) { cls = 'diag-ok'; dStr = ' — ' + ret.delay + ' ms'; }
|
||||
else cls = 'diag-gray';
|
||||
dom.content(resultEl, E('strong', { 'class': cls }, resolveTag(ret.node) + type + dStr));
|
||||
} else {
|
||||
dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No active node')));
|
||||
}
|
||||
});
|
||||
}));
|
||||
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, labelText), resultEl ]);
|
||||
}
|
||||
|
||||
const rows = [
|
||||
siteRow(_('Baidu'), 'baidu'),
|
||||
siteRow(_('Google'), 'google'),
|
||||
siteRow(_('YouTube'), 'youtube'),
|
||||
siteRow(_('Yandex'), 'yandex'),
|
||||
siteRow(_('Speedtest'), 'speedtest')
|
||||
];
|
||||
|
||||
/* hiddify → exit-IP rows; sing-box → live Active Node (it can't report exit IP). */
|
||||
if (coreType === 'hiddify') {
|
||||
rows.push(ipRow(_('Direct IP'), 'direct'));
|
||||
rows.push(ipRow(_('Proxy IP'), 'proxy'));
|
||||
} else if (coreType === 'singbox') {
|
||||
rows.push(activeNodeRow(_('Active Node')));
|
||||
}
|
||||
|
||||
/* Dedicated UDP node — only the modes that expose "Main UDP node" in the UI set it
|
||||
(Global + the bypass_cn/bypass_ir reverse modes; proxy_banned_ru/custom don't, and
|
||||
'redirect' proxy mode has no UDP path). Show which node currently carries UDP. */
|
||||
const udpNode = uci.get('homeproxy', 'config', 'main_udp_node');
|
||||
const proxyMode = uci.get('homeproxy', 'config', 'proxy_mode');
|
||||
if (udpNode && !['nil', 'same', ''].includes(udpNode) && proxyMode !== 'redirect')
|
||||
rows.push(activeNodeRow(_('Active UDP Node'), 'main-udp-out'));
|
||||
|
||||
return {
|
||||
el: sectionCard(_('Connectivity'), 'diag-connectivity', rows),
|
||||
run: function() { return Promise.all(items.map(function(fn) { return fn(); })); }
|
||||
};
|
||||
}
|
||||
|
||||
function buildCoreSection(view) {
|
||||
const resultsEl = E('div', {});
|
||||
const restartMsgEl = E('span', { 'class': 'diag-gray', 'style': 'font-size:.9em' });
|
||||
|
||||
function run() {
|
||||
spinner(resultsEl, _('Checking core…'));
|
||||
return L.resolveDefault(callCoreCheck(), {}).then(function(ret) {
|
||||
if (!ret || ret.error) {
|
||||
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
|
||||
return;
|
||||
}
|
||||
|
||||
const portLines = (ret.listen_ports && ret.listen_ports.length)
|
||||
? ret.listen_ports.map(function(l) { return E('div', { 'class': 'diag-port-line' }, l); })
|
||||
: [ E('em', { 'class': 'diag-gray' }, _('none detected')) ];
|
||||
|
||||
dom.content(resultsEl, [
|
||||
row(_('Hiddify-core'), statusBadge(ret.hiddify_installed, ret.hiddify_installed ? _('installed') : _('not found'))),
|
||||
row(_('sing-box'), statusBadge(ret.singbox_installed, ret.singbox_installed ? _('installed') : _('not found'))),
|
||||
row(_('ByeDPI'), statusBadge(ret.byedpi_installed, ret.byedpi_installed ? _('installed') : _('not found'))),
|
||||
row(_('Zapret 2'), statusBadge(ret.zapret_installed, ret.zapret_installed ? _('installed') : _('not found'))),
|
||||
ret.binary ? row(_('Active binary'), E('code', {}, ret.binary)) : null,
|
||||
ret.version ? row(_('Version'), E('span', { 'class': 'diag-pre', 'style': 'max-height:5em' }, ret.version)) : null,
|
||||
row(_('Running'), statusBadge(ret.running, ret.running ? _('yes') + (ret.pid ? ' (PID ' + ret.pid + ')' : '') : _('no'))),
|
||||
ret.byedpi_installed ? row(_('ByeDPI running'), statusBadge(ret.byedpi_running, ret.byedpi_running ? _('yes') + (ret.byedpi_pid ? ' (PID ' + ret.byedpi_pid + ')' : '') : _('no'))) : null,
|
||||
ret.zapret_installed ? row(_('Zapret 2 running'), statusBadge(ret.zapret_running, ret.zapret_running ? _('yes') + (ret.zapret_pid ? ' (PID ' + ret.zapret_pid + ')' : '') : _('no'))) : null,
|
||||
row(_('Listening ports'), E('div', {}, portLines))
|
||||
].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
function restart() {
|
||||
dom.content(restartMsgEl, E('em', { 'class': 'diag-gray' }, _('Restarting… (may take ~5 s)')));
|
||||
return L.resolveDefault(callServiceRestart(), {}).then(function(ret) {
|
||||
if (ret && ret.result) {
|
||||
dom.content(restartMsgEl, statusBadge(true, _('Service restarted')));
|
||||
/* Refresh core status after restart */
|
||||
return run();
|
||||
} else {
|
||||
dom.content(restartMsgEl, statusBadge(false,
|
||||
_('Restart failed') + (ret && ret.exit_code != null ? ' (exit ' + ret.exit_code + ')' : '')));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
el: sectionCard(_('Core & System'), 'diag-core', [
|
||||
E('div', { 'class': 'diag-row' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run)
|
||||
}, _('Check')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative diag-btn',
|
||||
'click': ui.createHandlerFn(view, restart)
|
||||
}, _('Restart Service')),
|
||||
restartMsgEl
|
||||
]),
|
||||
resultsEl
|
||||
]),
|
||||
run: run
|
||||
};
|
||||
}
|
||||
|
||||
function buildConfigSection(view) {
|
||||
const resultsEl = E('div', {});
|
||||
|
||||
function run() {
|
||||
spinner(resultsEl, _('Checking config…'));
|
||||
return L.resolveDefault(callConfigCheck(), {}).then(function(ret) {
|
||||
if (!ret || ret.error) {
|
||||
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
|
||||
return;
|
||||
}
|
||||
|
||||
dom.content(resultsEl, [
|
||||
row(_('Valid'), statusBadge(ret.valid)),
|
||||
ret.size_bytes != null ? row(_('Size'), (ret.size_bytes / 1024).toFixed(1) + ' KB') : null,
|
||||
ret.stats ? row(_('Inbounds'), String(ret.stats.inbounds)) : null,
|
||||
ret.stats ? row(_('Outbounds'), String(ret.stats.outbounds)) : null,
|
||||
ret.stats ? row(_('Rules'), String(ret.stats.rules)) : null,
|
||||
ret.stats ? row(_('DNS servers'), String(ret.stats.dns_servers)) : null,
|
||||
ret.check_output ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, _('Check output')),
|
||||
pre(ret.check_output)
|
||||
]) : null
|
||||
].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
el: sectionCard(_('Configuration'), 'diag-config', [
|
||||
E('div', { 'class': 'diag-row' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run)
|
||||
}, _('Check'))
|
||||
]),
|
||||
resultsEl
|
||||
]),
|
||||
run: run
|
||||
};
|
||||
}
|
||||
|
||||
function buildDnsSection(view) {
|
||||
const resultsEl = E('div', {});
|
||||
|
||||
const cardEl = sectionCard(_('DNS Tests'), 'diag-dns', [
|
||||
E('div', { 'class': 'diag-row' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run)
|
||||
}, _('Test'))
|
||||
]),
|
||||
resultsEl
|
||||
]);
|
||||
cardEl.style.display = 'none';
|
||||
|
||||
function run() {
|
||||
spinner(resultsEl, _('Testing DNS…'));
|
||||
return L.resolveDefault(callDnsRu(), {}).then(function(ret) {
|
||||
if (ret.skip) {
|
||||
cardEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
cardEl.style.display = '';
|
||||
if (ret.error) {
|
||||
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
|
||||
return;
|
||||
}
|
||||
|
||||
const regionDomain = ret.region_domain || '?';
|
||||
dom.content(resultsEl, [
|
||||
E('div', { 'style': 'font-weight:bold; margin:.4em 0 .2em' },
|
||||
(ret.region_label ? ret.region_label + ' ' : '') + _('DNS (direct)')),
|
||||
row(_('Server'), E('code', {}, ret.region_server || '?')),
|
||||
row(regionDomain, statusBadge(ret.region_ok,
|
||||
ret.region_ok ? _('Resolved') : _('No answer — check server address'))),
|
||||
ret.region_output ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, 'nslookup ' + regionDomain),
|
||||
pre(ret.region_output)
|
||||
]) : null,
|
||||
|
||||
E('div', { 'style': 'font-weight:bold; margin:.8em 0 .2em' }, _('Secure DNS')),
|
||||
ret.secure_server ? row(_('Server'), E('code', {}, ret.secure_server)) : null,
|
||||
row(_('Bootstrap'), E('code', {}, ret.bootstrap || '?')),
|
||||
row('andrevi.ch', statusBadge(ret.secure_ok,
|
||||
ret.secure_ok ? _('Resolved via proxy') : _('No answer — check proxy and proxy list'))),
|
||||
ret.secure_output ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, 'nslookup andrevi.ch'),
|
||||
pre(ret.secure_output)
|
||||
]) : null
|
||||
].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
return { el: cardEl, run: run };
|
||||
}
|
||||
|
||||
function buildNftSection(view) {
|
||||
const resultsEl = E('div', {});
|
||||
|
||||
function run() {
|
||||
spinner(resultsEl, _('Reading nftables…'));
|
||||
return L.resolveDefault(callNftables(), {}).then(function(ret) {
|
||||
if (ret.error) {
|
||||
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
|
||||
return;
|
||||
}
|
||||
|
||||
dom.content(resultsEl, [
|
||||
row(_('Re:HomeProxy chains'), statusBadge(ret.nft_present,
|
||||
ret.nft_present ? _('found') : _('not found — firewall rules may be missing'))),
|
||||
ret.nft_rules ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, _('Matched lines in fw4')),
|
||||
pre(ret.nft_rules)
|
||||
]) : null,
|
||||
ret.udp_tproxy ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, _('UDP TPROXY counters (UDP into HomeProxy)')),
|
||||
pre(ret.udp_tproxy)
|
||||
]) : null,
|
||||
ret.uci_firewall ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, _('UCI firewall settings')),
|
||||
pre(ret.uci_firewall)
|
||||
]) : null,
|
||||
ret.zapret_enabled ? row(_('Zapret (nfqws2)'), statusBadge(ret.zapret_running,
|
||||
ret.zapret_running ? _('running') : _('enabled, but nfqws2 not running'))) : null,
|
||||
(ret.zapret_enabled && ret.zapret_queue) ? E('div', {}, [
|
||||
E('div', { 'class': 'diag-label' }, _('Zapret NFQUEUE counters (mark 110 → queue)')),
|
||||
pre(ret.zapret_queue)
|
||||
]) : null
|
||||
].filter(Boolean));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
el: sectionCard(_('Network Intercept'), 'diag-nft', [
|
||||
E('div', { 'class': 'diag-row' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run)
|
||||
}, _('Check'))
|
||||
]),
|
||||
resultsEl
|
||||
]),
|
||||
run: run
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function buildReportSection(view) {
|
||||
const textArea = E('textarea', {
|
||||
'id': 'diag-report-area',
|
||||
'readonly': true,
|
||||
'placeholder': _('Click Generate to collect diagnostics…')
|
||||
});
|
||||
|
||||
function run() {
|
||||
textArea.value = _('Collecting diagnostics — this may take a few seconds…');
|
||||
return L.resolveDefault(callReport(), {}).then(function(ret) {
|
||||
textArea.value = ret.report || _('(empty)');
|
||||
});
|
||||
}
|
||||
|
||||
function copy() {
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
}
|
||||
|
||||
function download() {
|
||||
const text = textArea.value;
|
||||
if (!text || text === _('Click Generate to collect diagnostics…')) return;
|
||||
const now = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const stamp = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) +
|
||||
'_' + pad(now.getHours()) + '-' + pad(now.getMinutes()) + '-' + pad(now.getSeconds());
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = E('a', { href: url, download: 'homeproxy-diagnostics-' + stamp + '.txt' });
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
return {
|
||||
el: sectionCard(_('Diagnostics Report'), 'diag-report', [
|
||||
E('div', { 'class': 'diag-row' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action diag-btn',
|
||||
'click': ui.createHandlerFn(view, run)
|
||||
}, _('Generate')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button diag-btn',
|
||||
'click': ui.createHandlerFn(view, copy)
|
||||
}, _('Copy')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button diag-btn',
|
||||
'click': ui.createHandlerFn(view, download)
|
||||
}, _('Download')),
|
||||
E('em', { 'class': 'diag-gray', 'style': 'font-size:.85em' },
|
||||
_('Sensitive values (passwords, keys, UUIDs) are redacted.'))
|
||||
]),
|
||||
textArea
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
/* ── View ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(hp.getBuiltinFeatures(), {}),
|
||||
uci.load('homeproxy')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
const features = (data && data[0]) || {};
|
||||
document.head.appendChild(E('style', {}, [ css ]));
|
||||
|
||||
const core = buildCoreSection(this);
|
||||
const config = buildConfigSection(this);
|
||||
const dns = buildDnsSection(this);
|
||||
const nft = buildNftSection(this);
|
||||
const conn = buildConnectivitySection(this, features.core_type || null);
|
||||
const report = buildReportSection(this);
|
||||
|
||||
dns.run();
|
||||
|
||||
const runAll = ui.createHandlerFn(this, function() {
|
||||
return Promise.all([
|
||||
core.run(),
|
||||
config.run(),
|
||||
nft.run(),
|
||||
conn.run()
|
||||
]);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('Re:HomeProxy Diagnostics')),
|
||||
E('div', { 'class': 'cbi-section', 'style': 'padding-bottom:.5em' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action important',
|
||||
'click': runAll
|
||||
}, _('Run All Tests')),
|
||||
E('em', { 'class': 'diag-gray', 'style': 'margin-left:.8em; font-size:.9em' },
|
||||
_('Runs Core, Config, Network, and Connectivity checks simultaneously.'))
|
||||
]),
|
||||
core.el,
|
||||
config.el,
|
||||
dns.el,
|
||||
nft.el,
|
||||
conn.el,
|
||||
report.el
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,936 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const CBIGenValue = form.Value.extend({
|
||||
__name__: 'CBI.GenValue',
|
||||
|
||||
renderWidget(/* ... */) {
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
if (!this.password)
|
||||
node.classList.add('control-group');
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, this.hp_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
|
||||
let isRunning = false;
|
||||
try {
|
||||
isRunning = res['homeproxy']['instances']['hiddify-c']['running'];
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning, features) {
|
||||
let coreName = features.core_type === 'singbox' ? 'sing-box' :
|
||||
features.core_type === 'hiddify' ? 'hiddify-core' : null;
|
||||
let verStr = features.version ? 'v' + features.version : _('unknown');
|
||||
let coreStr = coreName ? ('%s %s').format(coreName, verStr) : _('no core installed');
|
||||
let spanTemp = '<em><span style="color:%s"><strong>%s (%s) %s</strong></span></em>';
|
||||
let renderHTML;
|
||||
if (isRunning)
|
||||
renderHTML = spanTemp.format('green', _('Re:HomeProxy Server'), coreStr, _('RUNNING'));
|
||||
else
|
||||
renderHTML = spanTemp.format('red', _('Re:HomeProxy Server'), coreStr, _('NOT RUNNING'));
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
function handleGenKey(option) {
|
||||
let section_id = this.section.section;
|
||||
let type = this.section.getOption('type')?.formvalue(section_id);
|
||||
let widget = L.bind((option) => {
|
||||
return this.map.findElement('id', 'widget.' + this.cbid(section_id).replace(/\.[^\.]+$/, '.') + option);
|
||||
}, this);
|
||||
|
||||
const callSingBoxGenerator = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'singbox_generator',
|
||||
params: ['type', 'params'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
if (typeof option === 'object') {
|
||||
return callSingBoxGenerator(option.type, option.params).then((res) => {
|
||||
if (res.result)
|
||||
option.callback.call(this, res.result).forEach(([k, v]) => {
|
||||
widget(k).value = v ?? '';
|
||||
});
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to generate %s, error: %s.').format(type, res.error)));
|
||||
});
|
||||
} else {
|
||||
let password, required_method;
|
||||
|
||||
if (option === 'uuid')
|
||||
required_method = 'uuid';
|
||||
else if (type === 'shadowsocks')
|
||||
required_method = this.section.getOption('shadowsocks_encrypt_method')?.formvalue(section_id);
|
||||
|
||||
switch (required_method) {
|
||||
case 'none':
|
||||
password = '';
|
||||
break;
|
||||
case 'uuid':
|
||||
password = hp.generateRand('uuid');
|
||||
break;
|
||||
default:
|
||||
password = hp.generateRand('hex', 16);
|
||||
break;
|
||||
}
|
||||
/* AEAD */
|
||||
((length) => {
|
||||
if (length && length > 0)
|
||||
password = hp.generateRand('base64', length);
|
||||
})(hp.shadowsocks_encrypt_length[required_method]);
|
||||
|
||||
return widget(option).value = password;
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy'),
|
||||
hp.getBuiltinFeatures()
|
||||
]);
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o;
|
||||
let features = data[1];
|
||||
|
||||
m = new form.Map('homeproxy', _('Re:HomeProxy Server'),
|
||||
_('The modern multi-core proxy platform. Fork of ImmortalWrt.'));
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.render = function() {
|
||||
poll.add(() => {
|
||||
return L.resolveDefault(getServiceStatus()).then((res) => {
|
||||
let view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(res, features);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.GridSection, 'server', _('Server settings'));
|
||||
s.addremove = true;
|
||||
s.rowcolors = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]);
|
||||
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s);
|
||||
|
||||
o = s.option(form.Value, 'label', _('Label'));
|
||||
o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'label');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.Flag, 'firewall', _('Firewall'),
|
||||
_('Allow access from the Internet.'));
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'type', _('Type'));
|
||||
o.value('anytls', _('AnyTLS'));
|
||||
o.value('http', _('HTTP'));
|
||||
if (features.with_quic) {
|
||||
o.value('hysteria', _('Hysteria'));
|
||||
o.value('hysteria2', _('Hysteria2'));
|
||||
o.value('naive', _('NaïveProxy'));
|
||||
}
|
||||
o.value('mixed', _('Mixed'));
|
||||
if (features.core_type === 'singbox')
|
||||
o.value('mtproxy', _('MTProxy'));
|
||||
o.value('shadowsocks', _('Shadowsocks'));
|
||||
o.value('socks', _('Socks'));
|
||||
o.value('trojan', _('Trojan'));
|
||||
if (features.with_quic)
|
||||
o.value('tuic', _('Tuic'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
o.placeholder = '::';
|
||||
o.datatype = 'ipaddr';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Listen port'),
|
||||
_('The port must be unique.'));
|
||||
o.datatype = 'port';
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port');
|
||||
|
||||
o = s.option(form.Value, 'username', _('Username'));
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'mixed');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'socks');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends('type', 'anytls');
|
||||
o.depends({'type': /^(http|mixed|naive|socks)$/, 'username': /[\s\S]/});
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'tuic');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
let type = this.section.formvalue(section_id, 'type');
|
||||
let required_type = [ 'anytls', 'http', 'mixed', 'naive', 'socks', 'shadowsocks', 'trojan' ];
|
||||
|
||||
if (required_type.includes(type)) {
|
||||
if (type === 'shadowsocks') {
|
||||
let encmode = this.section.formvalue(section_id, 'shadowsocks_encrypt_method');
|
||||
if (encmode === 'none')
|
||||
return true;
|
||||
else if (encmode === '2022-blake3-aes-128-gcm')
|
||||
return hp.validateBase64Key(24, section_id, value);
|
||||
else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode))
|
||||
return hp.validateBase64Key(44, section_id, value);
|
||||
}
|
||||
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* AnyTLS config */
|
||||
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
|
||||
_('AnyTLS padding scheme in array.'));
|
||||
o.depends('type', 'anytls');
|
||||
o.modalonly = true;
|
||||
|
||||
/* MTProxy config start */
|
||||
o = s.option(form.DynamicList, 'mtproxy_secrets', _('Secrets'),
|
||||
_('List of MTProxy secrets. Each secret is a 32-character hex string (16 bytes). One secret per user.'));
|
||||
o.depends('type', 'mtproxy');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id && value && !/^[0-9a-fA-F]{32}$/.test(value))
|
||||
return _('Expecting: %s').format(_('32 hex characters'));
|
||||
return true;
|
||||
};
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'domain_fronting_host', _('Domain fronting host'),
|
||||
_('Target hostname for FakeTLS domain fronting. Clients will see TLS to this domain.'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('type', 'mtproxy');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'domain_fronting_port', _('Domain fronting port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '443';
|
||||
o.depends({'type': 'mtproxy', 'domain_fronting_host': /[\s\S]/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'mtproxy_idle_timeout', _('Idle timeout'),
|
||||
_('Close idle connections after this duration (e.g. 5m, 30s).'));
|
||||
o.placeholder = '5m';
|
||||
o.depends('type', 'mtproxy');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'mtproxy_handshake_timeout', _('Handshake timeout'),
|
||||
_('Maximum time to complete handshake (e.g. 10s).'));
|
||||
o.placeholder = '10s';
|
||||
o.depends('type', 'mtproxy');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'mtproxy_concurrency', _('Concurrency'),
|
||||
_('Worker pool size. Leave empty for default.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'mtproxy');
|
||||
o.modalonly = true;
|
||||
/* MTProxy config end */
|
||||
|
||||
/* Hysteria (2) config start */
|
||||
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
||||
o.value('udp');
|
||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
||||
o.value('wechat-video');
|
||||
o.value('faketcp');
|
||||
*/
|
||||
o.default = 'udp';
|
||||
o.depends('type', 'hysteria');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
|
||||
_('Max download speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
|
||||
_('Max upload speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('base64', _('Base64'));
|
||||
o.value('string', _('String'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
|
||||
o.password = true;
|
||||
o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('salamander', _('Salamander'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'hysteria_obfs_password', _('Obfuscate password'));
|
||||
o.password = true;
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
|
||||
_('The QUIC stream-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '67108864';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'),
|
||||
_('The QUIC connection-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '15728640';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'),
|
||||
_('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1024';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'),
|
||||
_('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
|
||||
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
|
||||
o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
|
||||
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
/* Hysteria (2) config end */
|
||||
|
||||
/* Shadowsocks config */
|
||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||
for (let i of hp.shadowsocks_encrypt_methods)
|
||||
o.value(i);
|
||||
o.default = 'aes-128-gcm';
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Tuic config start */
|
||||
o = s.option(CBIGenValue, 'uuid', _('UUID'));
|
||||
o.password = true;
|
||||
o.depends('type', 'tuic');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.validate = hp.validateUUID;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'),
|
||||
_('QUIC congestion control algorithm.'));
|
||||
o.value('cubic');
|
||||
o.value('new_reno');
|
||||
o.value('bbr');
|
||||
o.default = 'cubic';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tuic_auth_timeout', _('Auth timeout'),
|
||||
_('How long the server should wait for the client to send the authentication command (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'),
|
||||
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
|
||||
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'),
|
||||
_('Interval for sending heartbeat packets for keeping the connection alive (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '10';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
/* Tuic config end */
|
||||
|
||||
/* VLESS / VMess config start */
|
||||
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
||||
o.value('', _('None'));
|
||||
o.value('xtls-rprx-vision');
|
||||
o.depends('type', 'vless');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
|
||||
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
/* VMess config end */
|
||||
|
||||
/* Transport config start */
|
||||
o = s.option(form.ListValue, 'transport', _('Transport'),
|
||||
_('No TCP transport, plain HTTP is merged into the HTTP transport.'));
|
||||
o.value('', _('None'));
|
||||
o.value('grpc', _('gRPC'));
|
||||
o.value('http', _('HTTP'));
|
||||
o.value('httpupgrade', _('HTTPUpgrade'));
|
||||
o.value('quic', _('QUIC'));
|
||||
o.value('ws', _('WebSocket'));
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
let desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
|
||||
if (value === 'http')
|
||||
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
|
||||
else if (value === 'quic')
|
||||
desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.');
|
||||
else
|
||||
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
|
||||
|
||||
let tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc))
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
|
||||
else if (value === 'grpc' && features.with_grpc)
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.');
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config start */
|
||||
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config end */
|
||||
|
||||
/* HTTP(Upgrade) config start */
|
||||
o = s.option(form.DynamicList, 'http_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'httpupgrade_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_path', _('Path'));
|
||||
o.depends('transport', 'http');
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_method', _('Method'));
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.depends({'transport': 'http', 'tls': '1'});
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_grpc) {
|
||||
o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
|
||||
_('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* HTTP config end */
|
||||
|
||||
/* WebSocket config start */
|
||||
o = s.option(form.Value, 'ws_host', _('Host'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'ws_path', _('Path'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data', _('Early data'),
|
||||
_('Allowed payload size is in the request.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.value('2048');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
|
||||
_('Early data is sent in path instead of header by default.') +
|
||||
'<br/>' +
|
||||
_('To be compatible with Xray-core, set this to <code>Sec-WebSocket-Protocol</code>.'));
|
||||
o.value('Sec-WebSocket-Protocol');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
/* WebSocket config end */
|
||||
|
||||
/* Transport config end */
|
||||
|
||||
/* Mux config start */
|
||||
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.hp_has_tcp_brutal) {
|
||||
o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
|
||||
_('Enable TCP Brutal congestion control algorithm'));
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_down', _('Download bandwidth'),
|
||||
_('Download bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_up', _('Upload bandwidth'),
|
||||
_('Upload bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* Mux config end */
|
||||
|
||||
/* TLS config start */
|
||||
o = s.option(form.Flag, 'tls', _('TLS'));
|
||||
o.depends('type', 'anytls');
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'tuic');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
|
||||
let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
|
||||
if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
|
||||
tls.checked = true;
|
||||
tls.disabled = true;
|
||||
} else {
|
||||
tls.disabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_sni', _('TLS SNI'),
|
||||
_('Used to verify the hostname on the returned certificates unless insecure is given.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
||||
_('List of supported application level protocols, in order of preference.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
|
||||
_('The minimum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (let i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
|
||||
_('The maximum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (let i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(hp.CBIStaticList, 'tls_cipher_suites', _('Cipher suites'),
|
||||
_('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
|
||||
for (let i of hp.tls_cipher_suites)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.optional = true;
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_acme) {
|
||||
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
||||
_('Use ACME TLS certificate issuer.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
|
||||
_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_email', _('Email'),
|
||||
_('The email address to use when creating or selecting an existing ACME server account.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format('non-empty value');
|
||||
else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
|
||||
return _('Expecting: %s').format('valid email address');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
|
||||
_('The ACME CA provider to use.'));
|
||||
o.value('letsencrypt', _('Let\'s Encrypt'));
|
||||
o.value('zerossl', _('ZeroSSL'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_dns01_challenge', _('DNS01 challenge'))
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('Alibaba Cloud DNS'));
|
||||
o.value('cloudflare', _('Cloudflare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cloudflare';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'cloudflare');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
|
||||
_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
|
||||
_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
|
||||
_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
|
||||
'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
|
||||
o.password = true;
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
|
||||
o.password = true;
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'type': /^(anytls|vless)$/});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'type': /^(anytls|vless)$/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'tls_reality_private_key', _('REALITY private key'));
|
||||
o.password = true;
|
||||
o.hp_options = {
|
||||
type: 'reality-keypair',
|
||||
params: '',
|
||||
callback: function(result) {
|
||||
return [
|
||||
[this.option, result.private_key],
|
||||
['tls_reality_public_key', result.public_key]
|
||||
]
|
||||
}
|
||||
}
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_public_key', _('REALITY public key'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
|
||||
_('The maximum time difference between the server and the client.'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
|
||||
_('The server public key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_publickey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.validate = hp.validateCertificatePath;
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_key_path', _('Key path'),
|
||||
_('The server private key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_privatekey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.validate = hp.validateCertificatePath;
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_key', _('Upload key'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.TextValue, 'tls_ech_key', _('ECH key'));
|
||||
o.placeholder = '-----BEGIN ECH KEYS-----\nACBE2+piYBLrOywCbRYU+ZpEkk8keeBlUXbKqLRmQ/68FwBL/g0ARwAAIAAgn8HI\n93RfdV/LaDk+LC9H4h+4WhVBFmWKdhiT3vvpGi8ACAABAAEAAQADABRvdXRlci1z\nbmkuYW55LmRvbWFpbgAA\n-----END ECH KEYS-----';
|
||||
o.monospace = true;
|
||||
o.cols = 30
|
||||
o.rows = 3;
|
||||
o.hp_options = {
|
||||
type: 'ech-keypair',
|
||||
params: '',
|
||||
callback: function(result) {
|
||||
return [
|
||||
[this.option, result.ech_key],
|
||||
['tls_ech_config', result.ech_cfg]
|
||||
]
|
||||
}
|
||||
}
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
let node = form.TextValue.prototype.renderWidget.apply(this, arguments);
|
||||
const cbid = this.cbid(section_id) + '._outer_sni';
|
||||
|
||||
node.appendChild(E('div', { 'class': 'control-group' }, [
|
||||
E('input', {
|
||||
id: cbid,
|
||||
class: 'cbi-input-text',
|
||||
style: 'width: 10em',
|
||||
placeholder: 'outer-sni.any.domain'
|
||||
}),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hp_options.params = document.getElementById(cbid).value;
|
||||
|
||||
return handleGenKey.call(this, this.hp_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.TextValue, 'tls_ech_config', _('ECH config'));
|
||||
o.placeholder = '-----BEGIN ECH CONFIGS-----\nAEv+DQBHAAAgACCfwcj3dF91X8toOT4sL0fiH7haFUEWZYp2GJPe++kaLwAIAAEA\nAQABAAMAFG91dGVyLXNuaS5hbnkuZG9tYWluAAA=\n-----END ECH CONFIGS-----';
|
||||
o.monospace = true;
|
||||
o.cols = 30
|
||||
o.rows = 3;
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
/* TLS config end */
|
||||
|
||||
/* Extra settings start */
|
||||
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
||||
_('Enable tcp fast open for listener.'));
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
|
||||
_('Enable UDP fragmentation.'));
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'udp_timeout', _('UDP NAT expiration time'),
|
||||
_('In seconds.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '300';
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'network', _('Network'));
|
||||
o.value('tcp', _('TCP'));
|
||||
o.value('udp', _('UDP'));
|
||||
o.value('', _('Both'));
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
||||
_('The network interface to bind to.'));
|
||||
o.multiple = false;
|
||||
o.noaliases = true;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
|
||||
_('Reuse listener address.'));
|
||||
o.modalonly = true;
|
||||
/* Extra settings end */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,926 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require homeproxy as hp';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
const css = ' \
|
||||
#log_textarea { \
|
||||
padding: 10px; \
|
||||
text-align: left; \
|
||||
} \
|
||||
#log_textarea pre { \
|
||||
padding: .5rem; \
|
||||
word-break: break-all; \
|
||||
margin: 0; \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
}';
|
||||
|
||||
const hp_dir = '/var/run/homeproxy';
|
||||
|
||||
/* Map a sing-box outbound tag to its human-readable UCI label */
|
||||
function resolveTag(tag) {
|
||||
const m = tag && tag.match(/^cfg-(.+)-out$/);
|
||||
if (m) {
|
||||
const label = uci.get('homeproxy', m[1], 'label');
|
||||
if (label) return label;
|
||||
}
|
||||
/* Fixed tags: look up the backing UCI section via the 'main' config section */
|
||||
const specials = { 'main-out': 'main_node', 'main-udp-out': 'main_udp_node' };
|
||||
if (specials[tag]) {
|
||||
const section = uci.get('homeproxy', 'main', specials[tag]);
|
||||
if (section) {
|
||||
const label = uci.get('homeproxy', section, 'label');
|
||||
if (label) return label;
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function getIPInfo(o, type) {
|
||||
const callIPInfo = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_ip_info',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const resultEl = E('strong', { 'style': 'color:gray' }, _('unchecked'));
|
||||
|
||||
const formatIPInfo = (entry, nodeTag) => {
|
||||
if (!entry)
|
||||
return E('strong', { 'style': 'color:red' }, _('No data'));
|
||||
|
||||
const lines = [];
|
||||
if (nodeTag)
|
||||
lines.push(E('span', {}, [ _('Node') + ': ', E('strong', {}, resolveTag(nodeTag)) ]));
|
||||
if (entry.ip) {
|
||||
const delayStr = (entry.delay && entry.delay !== 65535) ? ` — ${entry.delay} ms` : '';
|
||||
const meta = [entry.country, entry.org].filter(Boolean).join(', ');
|
||||
const label = entry.ip + (meta ? ` (${meta})` : '') + delayStr;
|
||||
lines.push(E('span', {}, [ 'IP: ', E('strong', { 'style': 'color:green' }, label) ]));
|
||||
}
|
||||
|
||||
return E('span', {}, lines.map((l) => E('div', {}, [ l ])));
|
||||
};
|
||||
|
||||
o.default = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callIPInfo(), {}).then((ret) => {
|
||||
const el = o.default.firstElementChild.nextElementSibling;
|
||||
if (ret.error) {
|
||||
dom.content(el, E('span', { 'style': 'color:red' }, ret.error));
|
||||
return;
|
||||
}
|
||||
const entry = ret[type];
|
||||
dom.content(el, formatIPInfo(entry, type === 'proxy' ? entry?.node : null));
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ]),
|
||||
' ',
|
||||
resultEl
|
||||
]);
|
||||
}
|
||||
|
||||
function getConnStat(o, site) {
|
||||
const callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
o.default = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
if (ret.result) {
|
||||
ele.style.setProperty('color', 'green');
|
||||
ele.innerHTML = _('passed');
|
||||
} else {
|
||||
ele.style.setProperty('color', 'red');
|
||||
ele.innerHTML = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
}, [ _('Check') ]),
|
||||
' ',
|
||||
E('strong', { 'style': 'color:gray' }, _('unchecked')),
|
||||
]);
|
||||
}
|
||||
|
||||
function getRuntimeLog(o, name, _option_index, section_id, _in_table) {
|
||||
const filename = o.option.split('_')[1];
|
||||
|
||||
let section, log_level_el;
|
||||
switch (filename) {
|
||||
case 'homeproxy':
|
||||
section = null;
|
||||
break;
|
||||
case 'hiddify-c':
|
||||
section = 'config';
|
||||
break;
|
||||
}
|
||||
|
||||
if (section) {
|
||||
const selected = uci.get('homeproxy', section, 'log_level') || 'warn';
|
||||
const choices = {
|
||||
trace: _('Trace'),
|
||||
debug: _('Debug'),
|
||||
info: _('Info'),
|
||||
warn: _('Warn'),
|
||||
error: _('Error'),
|
||||
fatal: _('Fatal'),
|
||||
panic: _('Panic')
|
||||
};
|
||||
|
||||
log_level_el = E('select', {
|
||||
'id': o.cbid(section_id),
|
||||
'class': 'cbi-input-select',
|
||||
'style': 'margin-left: 4px; width: 6em;',
|
||||
'change': ui.createHandlerFn(this, (ev) => {
|
||||
uci.set('homeproxy', section, 'log_level', ev.target.value);
|
||||
return o.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
Object.keys(choices).forEach((v) => {
|
||||
log_level_el.appendChild(E('option', {
|
||||
'value': v,
|
||||
'selected': (v === selected) ? '' : null
|
||||
}, [ choices[v] ]));
|
||||
});
|
||||
}
|
||||
|
||||
const callLogClean = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'log_clean',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
E('img', {
|
||||
'src': L.resource('icons/loading.svg'),
|
||||
'alt': _('Loading'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
let log;
|
||||
poll.add(L.bind(() => {
|
||||
return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
|
||||
.then((res) => {
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
res.trim() || _('Log is empty.')
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
}).catch((err) => {
|
||||
if (err.toString().includes('NotFoundError'))
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Log file does not exist.')
|
||||
]);
|
||||
else
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Unknown error: %s').format(err)
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
});
|
||||
}));
|
||||
|
||||
return E([
|
||||
E('style', [ css ]),
|
||||
E('div', {'class': 'cbi-map'}, [
|
||||
E('h3', {'name': 'content', 'style': 'align-items: center; display: flex;'}, [
|
||||
_('%s log').format(name),
|
||||
log_level_el || '',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-left: 4px;',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callLogClean(filename), {});
|
||||
})
|
||||
}, [ _('Clean log') ])
|
||||
]),
|
||||
E('div', {'class': 'cbi-section'}, [
|
||||
log_textarea,
|
||||
E('div', {'style': 'text-align:right'},
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
const CORE_MGMT = '/usr/share/homeproxy/scripts/core_mgmt.uc';
|
||||
|
||||
const callCurlStatus = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'curl_status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callCurlInstall = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'curl_install',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callCurlRemove = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'curl_remove',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callByeDPIStatus = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'byedpi_status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callByeDPIPrepareInstall = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'byedpi_prepare_install',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callByeDPIInstallPkg = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'byedpi_install_pkg',
|
||||
params: ['tmp_path', 'pkg_manager'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callByeDPIRemove = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'byedpi_remove',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callZapretStatus = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'zapret_status',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callZapretPrepareInstall = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'zapret_prepare_install',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callZapretInstallPkg = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'zapret_install_pkg',
|
||||
params: ['tmp_path', 'pkg_manager'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callZapretRemove = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'zapret_remove',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function buildByeDPICard(byedpi, isMainNode) {
|
||||
let installed = byedpi?.installed || false;
|
||||
let version = byedpi?.version || null;
|
||||
const running = byedpi?.running || false;
|
||||
const pkgMgr = byedpi?.pkg_manager || null;
|
||||
const canInstall = !!pkgMgr;
|
||||
|
||||
const statusEl = E('strong', {
|
||||
style: installed ? 'color:green' : 'color:gray'
|
||||
}, installed ? (version ? 'v' + version : _('Installed')) : _('Not installed'));
|
||||
|
||||
const runEl = E('span', {
|
||||
style: 'margin-left:6px; font-size:0.9em; color:' + (running ? 'green' : 'gray')
|
||||
}, running ? _('running') : _('stopped'));
|
||||
|
||||
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
|
||||
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
|
||||
|
||||
const installBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !canInstall || null,
|
||||
title: canInstall ? '' : _('No supported package manager detected'),
|
||||
click: async function() {
|
||||
const prevInstalled = installed;
|
||||
const prevVersion = version;
|
||||
installBtn.disabled = true;
|
||||
removeBtn.disabled = true;
|
||||
statusEl.textContent = _('Installing...');
|
||||
statusEl.style.color = 'gray';
|
||||
|
||||
const fail = (msg) => {
|
||||
installed = prevInstalled;
|
||||
version = prevVersion;
|
||||
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Not installed');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = !installed;
|
||||
setMsg(msg, 'red');
|
||||
};
|
||||
|
||||
setMsg(_('Checking requirements...'), 'gray');
|
||||
const prep = await L.resolveDefault(callByeDPIPrepareInstall(), {});
|
||||
if (prep.error) return fail(prep.error);
|
||||
|
||||
setMsg(_('Downloading...'), 'gray');
|
||||
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
|
||||
if (!dl.result) return fail(dl.error || _('Download failed'));
|
||||
|
||||
setMsg(_('Installing package...'), 'gray');
|
||||
const inst = await L.resolveDefault(callByeDPIInstallPkg(prep.tmp_path, prep.pkg_manager), {});
|
||||
if (!inst.result) return fail(inst.error || _('Installation failed'));
|
||||
|
||||
const fresh = await L.resolveDefault(callByeDPIStatus(), {});
|
||||
installed = fresh.installed || false;
|
||||
version = fresh.version || null;
|
||||
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Unknown');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.textContent = _('Update');
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = false;
|
||||
setMsg(_('Installed successfully'), 'green');
|
||||
}
|
||||
}, [ installed ? _('Update') : _('Install') ]);
|
||||
|
||||
const removeBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-negative',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !installed || isMainNode || null,
|
||||
title: isMainNode ? _('Cannot remove: ByeDPI is selected as Main Node. Change Main Node first.') : '',
|
||||
click: async function() {
|
||||
removeBtn.disabled = true;
|
||||
installBtn.disabled = true;
|
||||
setMsg(_('Removing...'), 'gray');
|
||||
const ret = await L.resolveDefault(callByeDPIRemove(), {});
|
||||
installBtn.disabled = false;
|
||||
if (ret.result) {
|
||||
installed = false;
|
||||
version = null;
|
||||
statusEl.textContent = _('Not installed');
|
||||
statusEl.style.color = 'gray';
|
||||
installBtn.textContent = _('Install');
|
||||
setMsg(_('Removed successfully'), 'green');
|
||||
} else {
|
||||
removeBtn.disabled = false;
|
||||
setMsg(ret.error || _('Removal failed'), 'red');
|
||||
}
|
||||
}
|
||||
}, [ _('Remove') ]);
|
||||
|
||||
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
|
||||
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
|
||||
E('strong', {}, 'ciadpi (ByeDPI)'),
|
||||
statusEl,
|
||||
runEl,
|
||||
installBtn,
|
||||
removeBtn,
|
||||
msgEl
|
||||
]),
|
||||
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
|
||||
_('Local SOCKS5 DPI bypass proxy by <a href="https://github.com/hufrea/byedpi" target="_blank">hufrea</a>. ' +
|
||||
'Packages by <a href="https://github.com/1andrevich/ByeDPI-OpenWrt" target="_blank">1andrevich/ByeDPI-OpenWrt</a>. ' +
|
||||
'Configure in the Client → ByeDPI tab.'))
|
||||
]);
|
||||
}
|
||||
|
||||
function buildZapretCard(zapret) {
|
||||
let installed = zapret?.installed || false;
|
||||
let version = zapret?.version || null;
|
||||
const running = zapret?.running || false;
|
||||
const pkgMgr = zapret?.pkg_manager || null;
|
||||
let kmodOk = (zapret?.kmod_ok != null) ? zapret.kmod_ok : true;
|
||||
const canInstall = !!pkgMgr;
|
||||
|
||||
const statusEl = E('strong', {
|
||||
style: installed ? 'color:green' : 'color:gray'
|
||||
}, installed ? (version ? 'v' + version : _('Installed')) : _('Not installed'));
|
||||
|
||||
const runEl = E('span', {
|
||||
style: 'margin-left:6px; font-size:0.9em; color:' + (running ? 'green' : 'gray')
|
||||
}, running ? _('running') : _('stopped'));
|
||||
|
||||
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
|
||||
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
|
||||
|
||||
const installBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !canInstall || null,
|
||||
title: canInstall ? '' : _('No supported package manager detected'),
|
||||
click: async function() {
|
||||
const prevInstalled = installed;
|
||||
const prevVersion = version;
|
||||
installBtn.disabled = true;
|
||||
removeBtn.disabled = true;
|
||||
statusEl.textContent = _('Installing...');
|
||||
statusEl.style.color = 'gray';
|
||||
|
||||
const fail = (msg) => {
|
||||
installed = prevInstalled;
|
||||
version = prevVersion;
|
||||
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Not installed');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = !installed;
|
||||
setMsg(msg, 'red');
|
||||
};
|
||||
|
||||
setMsg(_('Checking requirements...'), 'gray');
|
||||
const prep = await L.resolveDefault(callZapretPrepareInstall(), {});
|
||||
if (prep.error) return fail(prep.error);
|
||||
|
||||
setMsg(_('Downloading...'), 'gray');
|
||||
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
|
||||
if (!dl.result) return fail(dl.error || _('Download failed'));
|
||||
|
||||
setMsg(_('Installing package...'), 'gray');
|
||||
const inst = await L.resolveDefault(callZapretInstallPkg(prep.tmp_path, prep.pkg_manager), {});
|
||||
if (!inst.result) return fail(inst.error || _('Installation failed'));
|
||||
|
||||
const fresh = await L.resolveDefault(callZapretStatus(), {});
|
||||
installed = fresh.installed || false;
|
||||
version = fresh.version || null;
|
||||
kmodOk = (fresh.kmod_ok != null) ? fresh.kmod_ok : true;
|
||||
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Unknown');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.textContent = _('Update');
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = false;
|
||||
if (installed && !kmodOk)
|
||||
setMsg(_('Installed, but kmod-nft-queue is missing — Zapret cannot intercept traffic without it.'), 'red');
|
||||
else
|
||||
setMsg(_('Installed successfully'), 'green');
|
||||
}
|
||||
}, [ installed ? _('Update') : _('Install') ]);
|
||||
|
||||
const removeBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-negative',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !installed || null,
|
||||
click: async function() {
|
||||
removeBtn.disabled = true;
|
||||
installBtn.disabled = true;
|
||||
setMsg(_('Removing...'), 'gray');
|
||||
const ret = await L.resolveDefault(callZapretRemove(), {});
|
||||
installBtn.disabled = false;
|
||||
if (ret.result) {
|
||||
installed = false;
|
||||
version = null;
|
||||
statusEl.textContent = _('Not installed');
|
||||
statusEl.style.color = 'gray';
|
||||
installBtn.textContent = _('Install');
|
||||
setMsg(_('Removed successfully'), 'green');
|
||||
} else {
|
||||
removeBtn.disabled = false;
|
||||
setMsg(ret.error || _('Removal failed'), 'red');
|
||||
}
|
||||
}
|
||||
}, [ _('Remove') ]);
|
||||
|
||||
/* nfqws2's NFQUEUE rule needs kmod-nft-queue; warn up-front if it's missing. */
|
||||
if (installed && !kmodOk)
|
||||
setMsg(_('Warning: kmod-nft-queue is not installed — Zapret cannot intercept traffic without it.'), 'red');
|
||||
|
||||
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
|
||||
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
|
||||
E('strong', {}, 'nfqws2 (Zapret 2)'),
|
||||
statusEl,
|
||||
runEl,
|
||||
installBtn,
|
||||
removeBtn,
|
||||
msgEl
|
||||
]),
|
||||
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
|
||||
_('Packet-level (NFQUEUE) DPI bypass by <a href="https://github.com/bol-van/zapret2" target="_blank">bol-van</a> (nfqws2). ' +
|
||||
'Packages by <a href="https://github.com/1andrevich/zapret2-openwrt" target="_blank">1andrevich/zapret2-openwrt</a>. ' +
|
||||
'Configure in the Node Settings → Zapret tab.'))
|
||||
]);
|
||||
}
|
||||
|
||||
function buildCurlCard(curl) {
|
||||
let installed = curl?.installed || false;
|
||||
const pkgMgr = curl?.pkg_manager || null;
|
||||
const canInstall = !!pkgMgr;
|
||||
|
||||
const statusEl = E('strong', {
|
||||
style: installed ? 'color:green' : 'color:gray'
|
||||
}, installed ? _('Installed') : _('Not installed'));
|
||||
|
||||
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
|
||||
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
|
||||
|
||||
const installBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !canInstall || null,
|
||||
title: canInstall ? '' : _('No supported package manager detected'),
|
||||
click: async function() {
|
||||
installBtn.disabled = true;
|
||||
removeBtn.disabled = true;
|
||||
statusEl.textContent = _('Installing...');
|
||||
statusEl.style.color = 'gray';
|
||||
setMsg('', 'gray');
|
||||
const ret = await L.resolveDefault(callCurlInstall(), {});
|
||||
if (ret.result) {
|
||||
installed = true;
|
||||
statusEl.textContent = _('Installed');
|
||||
statusEl.style.color = 'green';
|
||||
installBtn.textContent = _('Reinstall');
|
||||
removeBtn.disabled = false;
|
||||
setMsg(_('Installed successfully'), 'green');
|
||||
} else {
|
||||
statusEl.textContent = _('Not installed');
|
||||
statusEl.style.color = 'gray';
|
||||
installBtn.disabled = false;
|
||||
setMsg(ret.error || _('Installation failed'), 'red');
|
||||
}
|
||||
}
|
||||
}, [ _('Install') ]);
|
||||
|
||||
const removeBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-negative',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !installed || null,
|
||||
click: async function() {
|
||||
removeBtn.disabled = true;
|
||||
installBtn.disabled = true;
|
||||
setMsg(_('Removing...'), 'gray');
|
||||
const ret = await L.resolveDefault(callCurlRemove(), {});
|
||||
installBtn.disabled = false;
|
||||
if (ret.result) {
|
||||
installed = false;
|
||||
statusEl.textContent = _('Not installed');
|
||||
statusEl.style.color = 'gray';
|
||||
installBtn.textContent = _('Install');
|
||||
setMsg(_('Removed successfully'), 'green');
|
||||
} else {
|
||||
removeBtn.disabled = false;
|
||||
setMsg(ret.error || _('Removal failed'), 'red');
|
||||
}
|
||||
}
|
||||
}, [ _('Remove') ]);
|
||||
|
||||
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
|
||||
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
|
||||
E('strong', {}, 'curl'),
|
||||
statusEl,
|
||||
installBtn,
|
||||
removeBtn,
|
||||
msgEl
|
||||
]),
|
||||
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
|
||||
_('Enables real HTTP-based ByeDPI strategy testing. Required for the "Test all strategies" feature in Client → ByeDPI tab.'))
|
||||
]);
|
||||
}
|
||||
|
||||
function callCoreInfo() {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'info'], 'json');
|
||||
}
|
||||
|
||||
function callCoreCheckRemote(core) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'check_remote', core], 'json');
|
||||
}
|
||||
|
||||
function callCorePrepare(core, variant) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'prepare_install', core, variant || ''], 'json');
|
||||
}
|
||||
|
||||
function callCoreDownload(url, tmpPath) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'download_pkg', url, tmpPath], 'json');
|
||||
}
|
||||
|
||||
function callCoreInstallPkg(core, tmpPath, pkgManager) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'install_pkg', core, tmpPath, pkgManager], 'json');
|
||||
}
|
||||
|
||||
function callCoreInstallKmods(pkgManager) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'install_kmods', pkgManager], 'json');
|
||||
}
|
||||
|
||||
function callCoreRemove(core) {
|
||||
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'remove', core], 'json');
|
||||
}
|
||||
|
||||
function buildCoreCard(core, coreInfo) {
|
||||
const isHiddify = core === 'hiddify';
|
||||
const name = isHiddify ? 'hiddify-core' : 'sing-box-extended';
|
||||
const pkgMgr = coreInfo.pkg_manager;
|
||||
const coreData = (isHiddify ? coreInfo.hiddify : coreInfo.singbox) || {};
|
||||
const canInstall = !!pkgMgr;
|
||||
|
||||
const desc = isHiddify
|
||||
? _('hiddify-core with sing-box syntax compatibility. Supports Hiddify App protocols and advanced features. Best compatibility with Hiddify Manager protocols. Does not support AmneziaWG.')
|
||||
: _('Extended sing-box with additional protocols including AmneziaWG and TrustTunnel support. Created by shtorm-7.');
|
||||
|
||||
let installed = coreData.installed || false;
|
||||
let version = coreData.version || null;
|
||||
|
||||
const statusEl = E('strong', {
|
||||
style: installed ? 'color:green' : 'color:gray'
|
||||
}, installed ? 'v' + version : _('Not installed'));
|
||||
|
||||
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
|
||||
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
|
||||
|
||||
const remoteEl = E('span', { style: 'font-size:0.9em; color:gray' }, '');
|
||||
|
||||
const checkBtn = E('button', {
|
||||
class: 'btn cbi-button',
|
||||
click: async function() {
|
||||
checkBtn.disabled = true;
|
||||
remoteEl.textContent = _('Checking...');
|
||||
remoteEl.style.color = 'gray';
|
||||
const ret = await L.resolveDefault(callCoreCheckRemote(core), {});
|
||||
checkBtn.disabled = false;
|
||||
if (ret.error) {
|
||||
remoteEl.textContent = ret.error;
|
||||
remoteEl.style.color = 'red';
|
||||
} else {
|
||||
remoteEl.textContent = _('Latest') + ': v' + ret.version;
|
||||
remoteEl.style.color = installed && version === ret.version ? 'green' : 'darkorange';
|
||||
}
|
||||
}
|
||||
}, [ _('Check update') ]);
|
||||
|
||||
/* One smart Install: the backend auto-picks the build that fits this device's flash
|
||||
* (and RAM for the compact one) — the user never sees "UPX" or has to choose. */
|
||||
const doInstall = async () => {
|
||||
const prevInstalled = installed;
|
||||
const prevVersion = version;
|
||||
installBtn.disabled = true;
|
||||
removeBtn.disabled = true;
|
||||
statusEl.textContent = _('Installing...');
|
||||
statusEl.style.color = 'gray';
|
||||
|
||||
const fail = (msg) => {
|
||||
installed = prevInstalled;
|
||||
version = prevVersion;
|
||||
statusEl.textContent = installed ? 'v' + (version || '?') : _('Not installed');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = !installed;
|
||||
setMsg(msg, 'red');
|
||||
};
|
||||
|
||||
setMsg(_('Checking requirements...'), 'gray');
|
||||
const prep = await L.resolveDefault(callCorePrepare(core, ''), {});
|
||||
if (prep.error) return fail(prep.error);
|
||||
|
||||
const compact = (prep.variant === 'upx');
|
||||
setMsg(prep.note || _('Downloading...'), prep.note ? 'darkorange' : 'gray');
|
||||
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
|
||||
if (!dl.result) return fail(dl.error || _('Download failed'));
|
||||
|
||||
setMsg(_('Installing package...'), 'gray');
|
||||
const inst = await L.resolveDefault(callCoreInstallPkg(core, prep.tmp_path, prep.pkg_manager), {});
|
||||
if (!inst.result) return fail(inst.error || _('Installation failed'));
|
||||
|
||||
setMsg(_('Installing kernel modules...'), 'gray');
|
||||
await L.resolveDefault(callCoreInstallKmods(prep.pkg_manager), {});
|
||||
|
||||
const fresh = await L.resolveDefault(callCoreInfo(), {});
|
||||
const fd = (isHiddify ? fresh.hiddify : fresh.singbox) || {};
|
||||
installed = fd.installed || false;
|
||||
version = fd.version || null;
|
||||
statusEl.textContent = installed ? 'v' + version : _('Unknown');
|
||||
statusEl.style.color = installed ? 'green' : 'gray';
|
||||
installBtn.textContent = _('Update');
|
||||
installBtn.disabled = false;
|
||||
removeBtn.disabled = false;
|
||||
setMsg(compact ? _('Installed successfully (compact build)') : _('Installed successfully'), 'green');
|
||||
};
|
||||
|
||||
const installBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !canInstall || null,
|
||||
title: canInstall ? '' : _('No supported package manager detected'),
|
||||
click: function() { return doInstall(); }
|
||||
}, [ installed ? _('Update') : _('Install') ]);
|
||||
|
||||
const removeBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-negative',
|
||||
style: 'margin-left:4px',
|
||||
disabled: !installed || null,
|
||||
click: async function() {
|
||||
removeBtn.disabled = true;
|
||||
installBtn.disabled = true;
|
||||
setMsg(_('Removing...'), 'gray');
|
||||
|
||||
const ret = await L.resolveDefault(callCoreRemove(core), {});
|
||||
|
||||
installBtn.disabled = false;
|
||||
if (ret.result) {
|
||||
installed = false;
|
||||
version = null;
|
||||
statusEl.textContent = _('Not installed');
|
||||
statusEl.style.color = 'gray';
|
||||
installBtn.textContent = _('Install');
|
||||
setMsg(_('Removed successfully'), 'green');
|
||||
} else {
|
||||
removeBtn.disabled = false;
|
||||
setMsg(ret.error || _('Removal failed'), 'red');
|
||||
}
|
||||
}
|
||||
}, [ _('Remove') ]);
|
||||
|
||||
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
|
||||
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
|
||||
E('strong', {}, name),
|
||||
statusEl,
|
||||
checkBtn,
|
||||
remoteEl,
|
||||
installBtn,
|
||||
removeBtn,
|
||||
msgEl
|
||||
]),
|
||||
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' }, desc)
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
hp.getBuiltinFeatures(),
|
||||
L.resolveDefault(callCoreInfo(), {}),
|
||||
uci.load('homeproxy'),
|
||||
L.resolveDefault(callByeDPIStatus(), {}),
|
||||
L.resolveDefault(callCurlStatus(), {}),
|
||||
L.resolveDefault(callZapretStatus(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render([features, coreInfo, _uci, byedpiStatus, curlStatus, zapretStatus]) {
|
||||
const routingMode = uci.get('homeproxy', 'config', 'routing_mode') || '';
|
||||
const isRuMode = routingMode === 'proxy_banned_ru';
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_active_core', _('Active core'));
|
||||
const coreName = features.core_type === 'hiddify' ? 'hiddify-core' :
|
||||
features.core_type === 'singbox' ? 'sing-box' : null;
|
||||
const coreVer = features.version ? ' v' + features.version : '';
|
||||
const coreCustomSuffix = features.core_custom ? ' (custom)' : '';
|
||||
|
||||
if (!features.core_type) {
|
||||
const callDetectCustomCore = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'detect_custom_core',
|
||||
params: ['path'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const savedPath = uci.get('homeproxy', 'config', 'custom_core_path') || '';
|
||||
const pathInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'value': savedPath,
|
||||
'placeholder': '/path/to/hiddify-core',
|
||||
'style': 'width:260px; margin-right:4px'
|
||||
});
|
||||
const detectMsg = E('span', { 'style': 'margin-left:8px; font-size:0.9em; color:gray' }, '');
|
||||
const detectBtn = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': async function() {
|
||||
const path = pathInput.value.trim();
|
||||
if (!path) return;
|
||||
detectBtn.disabled = true;
|
||||
detectMsg.textContent = _('Detecting...');
|
||||
detectMsg.style.color = 'gray';
|
||||
const ret = await L.resolveDefault(callDetectCustomCore(path), {});
|
||||
detectBtn.disabled = false;
|
||||
if (ret.result) {
|
||||
const typeName = ret.type === 'hiddify' ? 'hiddify-core' : 'sing-box';
|
||||
detectMsg.textContent = _('Detected') + ': ' + typeName + (ret.version ? ' v' + ret.version : '') + ' — ' + _('reload page to apply');
|
||||
detectMsg.style.color = 'green';
|
||||
} else {
|
||||
detectMsg.textContent = ret.error || _('Detection failed');
|
||||
detectMsg.style.color = 'red';
|
||||
}
|
||||
}
|
||||
}, [ _('Detect') ]);
|
||||
|
||||
o.default = E('div', {}, [
|
||||
E('strong', { 'style': 'color:red' }, _('No core installed')),
|
||||
E('details', { 'style': 'margin-top:6px' }, [
|
||||
E('summary', { 'style': 'cursor:pointer; color:#666; font-size:0.9em' }, _('I have a custom core path')),
|
||||
E('div', { 'style': 'margin-top:6px' }, [ pathInput, detectBtn, detectMsg ])
|
||||
])
|
||||
]);
|
||||
} else {
|
||||
o.default = E('strong', { 'style': 'color:green' }, coreName + coreVer + coreCustomSuffix);
|
||||
}
|
||||
|
||||
/* Region rule-sets (geosite/geoip .srs) are versioned and refreshed by the core itself,
|
||||
* so there are no local list-version files to display here. */
|
||||
|
||||
|
||||
if (!isRuMode) {
|
||||
o = s.option(form.Value, 'github_token', _('GitHub token'));
|
||||
o.description = _('Used to check for ByeDPI and Zapret updates. Without a token, GitHub limits anonymous requests to 60/hour. Create at github.com → Settings → Developer settings → Personal access tokens (no scopes needed).');
|
||||
o.password = true;
|
||||
o.renderWidget = function() {
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'title': _('Save'),
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return this.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
});
|
||||
}, this.option)
|
||||
}, [ _('Save') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Core management'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_core_env');
|
||||
const tmpMB = coreInfo.tmp_free_kb != null ? Math.round(coreInfo.tmp_free_kb / 1024) : '?';
|
||||
const overlayMB = coreInfo.overlay_free_kb != null ? Math.round(coreInfo.overlay_free_kb / 1024) : '?';
|
||||
o.default = E('div', { style: 'font-size:0.9em; color:#555; padding:2px 0 6px' }, [
|
||||
_('Package manager') + ': ',
|
||||
E('strong', {}, coreInfo.pkg_manager || _('none detected')),
|
||||
E('span', { style: 'margin:0 8px' }, '|'),
|
||||
_('Architecture') + ': ',
|
||||
E('strong', {}, coreInfo.arch || '?'),
|
||||
E('span', { style: 'margin:0 8px' }, '|'),
|
||||
_('Free /tmp') + ': ',
|
||||
E('strong', {
|
||||
style: (coreInfo.tmp_free_kb != null && coreInfo.tmp_free_kb < 30720) ? 'color:red' : 'color:green'
|
||||
}, tmpMB + ' MB'),
|
||||
E('span', { style: 'margin:0 8px' }, '|'),
|
||||
_('Free overlay') + ': ',
|
||||
E('strong', {
|
||||
style: (coreInfo.overlay_free_kb != null && coreInfo.overlay_free_kb < 30720) ? 'color:red' : 'color:green'
|
||||
}, overlayMB + ' MB')
|
||||
]);
|
||||
|
||||
o = s.option(form.DummyValue, '_core_hiddify');
|
||||
o.default = buildCoreCard('hiddify', coreInfo);
|
||||
|
||||
o = s.option(form.DummyValue, '_core_singbox');
|
||||
o.default = buildCoreCard('singbox', coreInfo);
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('AntiDPI'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_byedpi_card');
|
||||
o.default = buildByeDPICard(byedpiStatus, uci.get('homeproxy', 'config', 'main_node') === 'byedpi-out');
|
||||
|
||||
o = s.option(form.DummyValue, '_curl_card', _('ByeDPI strategy tester'));
|
||||
o.default = buildCurlCard(curlStatus);
|
||||
|
||||
o = s.option(form.DummyValue, '_zapret_card');
|
||||
o.default = buildZapretCard(zapretStatus);
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_homeproxy_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, o, _('Re:HomeProxy'));
|
||||
|
||||
o = s.option(form.DummyValue, '_hiddify-c_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, o, _('core client'));
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
Reference in new issue
Block a user