luci-app-homeproxy: sync upstream

last commit: 5c226ff344
This commit is contained in:
gitea-action 2025-01-11 01:00:22 +08:00
parent 93974a7fed
commit 2e7fe92932
12 changed files with 250 additions and 225 deletions

View File

@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-2.0-only * SPDX-License-Identifier: GPL-2.0-only
* *
* Copyright (C) 2022-2023 ImmortalWrt.org * Copyright (C) 2022-2025 ImmortalWrt.org
*/ */
'use strict'; 'use strict';
@ -73,7 +73,7 @@ return baseclass.extend({
} }
}), }),
calcStringMD5: function(e) { calcStringMD5(e) {
/* Thanks to https://stackoverflow.com/a/41602636 */ /* Thanks to https://stackoverflow.com/a/41602636 */
function h(a, b) { function h(a, b) {
var c, d, e, f, g; var c, d, e, f, g;
@ -154,7 +154,7 @@ return baseclass.extend({
return (p(a) + p(b) + p(c) + p(d)).toLowerCase(); return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
}, },
decodeBase64Str: function(str) { decodeBase64Str(str) {
if (!str) if (!str)
return null; return null;
@ -169,7 +169,7 @@ return baseclass.extend({
).join('')); ).join(''));
}, },
getBuiltinFeatures: function() { getBuiltinFeatures() {
const callGetSingBoxFeatures = rpc.declare({ const callGetSingBoxFeatures = rpc.declare({
object: 'luci.homeproxy', object: 'luci.homeproxy',
method: 'singbox_get_features', method: 'singbox_get_features',
@ -179,7 +179,7 @@ return baseclass.extend({
return L.resolveDefault(callGetSingBoxFeatures(), {}); return L.resolveDefault(callGetSingBoxFeatures(), {});
}, },
generateRand: function(type, length) { generateRand(type, length) {
var byteArr; var byteArr;
if (['base64', 'hex'].includes(type)) if (['base64', 'hex'].includes(type))
byteArr = crypto.getRandomValues(new Uint8Array(length)); byteArr = crypto.getRandomValues(new Uint8Array(length));
@ -201,7 +201,7 @@ return baseclass.extend({
}; };
}, },
loadDefaultLabel: function(uciconfig, ucisection) { loadDefaultLabel(uciconfig, ucisection) {
var label = uci.get(uciconfig, ucisection, 'label'); var label = uci.get(uciconfig, ucisection, 'label');
if (label) { if (label) {
return label; return label;
@ -211,12 +211,12 @@ return baseclass.extend({
} }
}, },
loadModalTitle: function(title, addtitle, uciconfig, ucisection) { loadModalTitle(title, addtitle, uciconfig, ucisection) {
var label = uci.get(uciconfig, ucisection, 'label'); var label = uci.get(uciconfig, ucisection, 'label');
return label ? title + ' » ' + label : addtitle; return label ? title + ' » ' + label : addtitle;
}, },
renderSectionAdd: function(section, extra_class) { renderSectionAdd(section, extra_class) {
var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]), var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
nameEl = el.querySelector('.cbi-section-create-name'); nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, (v) => { ui.addValidator(nameEl, 'uciname', true, (v) => {
@ -238,7 +238,7 @@ return baseclass.extend({
return el; return el;
}, },
uploadCertificate: function(option, type, filename, ev) { uploadCertificate(_option, type, filename, ev) {
const callWriteCertificate = rpc.declare({ const callWriteCertificate = rpc.declare({
object: 'luci.homeproxy', object: 'luci.homeproxy',
method: 'certificate_write', method: 'certificate_write',
@ -247,7 +247,7 @@ return baseclass.extend({
}); });
return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target) return ui.uploadFile('/tmp/homeproxy_certificate.tmp', ev.target)
.then(L.bind((btn, res) => { .then(L.bind((_btn, res) => {
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => { return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
if (ret.result === true) if (ret.result === true)
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size))); ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
@ -258,7 +258,7 @@ return baseclass.extend({
.catch((e) => { ui.addNotification(null, E('p', e.message)) }); .catch((e) => { ui.addNotification(null, E('p', e.message)) });
}, },
validateBase64Key: function(length, section_id, value) { validateBase64Key(length, section_id, value) {
/* Thanks to luci-proto-wireguard */ /* Thanks to luci-proto-wireguard */
if (section_id && value) if (section_id && value)
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
@ -267,7 +267,7 @@ return baseclass.extend({
return true; return true;
}, },
validateCertificatePath: function(section_id, value) { validateCertificatePath(section_id, value) {
if (section_id && value) if (section_id && value)
if (!value.match(/^(\/etc\/homeproxy\/certs\/|\/etc\/acme\/|\/etc\/ssl\/).+$/)) if (!value.match(/^(\/etc\/homeproxy\/certs\/|\/etc\/acme\/|\/etc\/ssl\/).+$/))
return _('Expecting: %s').format(_('/etc/homeproxy/certs/..., /etc/acme/..., /etc/ssl/...')); return _('Expecting: %s').format(_('/etc/homeproxy/certs/..., /etc/acme/..., /etc/ssl/...'));
@ -275,7 +275,7 @@ return baseclass.extend({
return true; return true;
}, },
validateUniqueValue: function(uciconfig, ucisection, ucioption, section_id, value) { validateUniqueValue(uciconfig, ucisection, ucioption, section_id, value) {
if (section_id) { if (section_id) {
if (!value) if (!value)
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));
@ -295,7 +295,7 @@ return baseclass.extend({
return true; return true;
}, },
validateUUID: function(section_id, value) { validateUUID(section_id, value) {
if (section_id) { if (section_id) {
if (!value) if (!value)
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));

View File

@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-2.0-only * SPDX-License-Identifier: GPL-2.0-only
* *
* Copyright (C) 2022-2023 ImmortalWrt.org * Copyright (C) 2022-2025 ImmortalWrt.org
*/ */
'use strict'; 'use strict';
@ -40,7 +40,7 @@ const callWriteDomainList = rpc.declare({
function getServiceStatus() { function getServiceStatus() {
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => { return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
var isRunning = false; let isRunning = false;
try { try {
isRunning = res['homeproxy']['instances']['sing-box-c']['running']; isRunning = res['homeproxy']['instances']['sing-box-c']['running'];
} catch (e) { } } catch (e) { }
@ -49,8 +49,8 @@ function getServiceStatus() {
} }
function renderStatus(isRunning, version) { function renderStatus(isRunning, version) {
var spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>'; let spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>';
var renderHTML; let renderHTML;
if (isRunning) if (isRunning)
renderHTML = spanTemp.format('green', _('HomeProxy'), version, _('RUNNING')); renderHTML = spanTemp.format('green', _('HomeProxy'), version, _('RUNNING'));
else else
@ -78,21 +78,21 @@ function validatePortRange(section_id, value) {
return true; return true;
} }
var stubValidator = { let stubValidator = {
factory: validation, factory: validation,
apply: function(type, value, args) { apply(type, value, args) {
if (value != null) if (value != null)
this.value = value; this.value = value;
return validation.types[type].apply(this, args); return validation.types[type].apply(this, args);
}, },
assert: function(condition) { assert(condition) {
return !!condition; return !!condition;
} }
}; };
return view.extend({ return view.extend({
load: function() { load() {
return Promise.all([ return Promise.all([
uci.load('homeproxy'), uci.load('homeproxy'),
hp.getBuiltinFeatures(), hp.getBuiltinFeatures(),
@ -100,16 +100,16 @@ return view.extend({
]); ]);
}, },
render: function(data) { render(data) {
let m, s, o, ss, so; let m, s, o, ss, so;
var features = data[1], let features = data[1],
hosts = data[2]?.hosts; hosts = data[2]?.hosts;
/* Cache all configured proxy nodes, they will be called multiple times */ /* Cache all configured proxy nodes, they will be called multiple times */
var proxy_nodes = {}; let proxy_nodes = {};
uci.sections(data[0], 'node', (res) => { uci.sections(data[0], 'node', (res) => {
var nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '', let nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '',
nodeport = ((res.type === 'direct') ? res.override_port : res.port) || ''; nodeport = ((res.type === 'direct') ? res.override_port : res.port) || '';
proxy_nodes[res['.name']] = proxy_nodes[res['.name']] =
@ -124,7 +124,7 @@ return view.extend({
s.render = function () { s.render = function () {
poll.add(function () { poll.add(function () {
return L.resolveDefault(getServiceStatus()).then((res) => { return L.resolveDefault(getServiceStatus()).then((res) => {
var view = document.getElementById('service_status'); let view = document.getElementById('service_status');
view.innerHTML = renderStatus(res, features.version); view.innerHTML = renderStatus(res, features.version);
}); });
}); });
@ -140,7 +140,7 @@ return view.extend({
o = s.taboption('routing', form.ListValue, 'main_node', _('Main node')); o = s.taboption('routing', form.ListValue, 'main_node', _('Main node'));
o.value('nil', _('Disable')); o.value('nil', _('Disable'));
for (var i in proxy_nodes) for (let i in proxy_nodes)
o.value(i, proxy_nodes[i]); o.value(i, proxy_nodes[i]);
o.default = 'nil'; o.default = 'nil';
o.depends({'routing_mode': 'custom', '!reverse': true}); o.depends({'routing_mode': 'custom', '!reverse': true});
@ -149,7 +149,7 @@ return view.extend({
o = s.taboption('routing', form.ListValue, 'main_udp_node', _('Main UDP node')); o = s.taboption('routing', form.ListValue, 'main_udp_node', _('Main UDP node'));
o.value('nil', _('Disable')); o.value('nil', _('Disable'));
o.value('same', _('Same as main node')); o.value('same', _('Same as main node'));
for (var i in proxy_nodes) for (let i in proxy_nodes)
o.value(i, proxy_nodes[i]); o.value(i, proxy_nodes[i]);
o.default = 'nil'; o.default = 'nil';
o.depends({'routing_mode': /^((?!custom).)+$/, 'proxy_mode': /^((?!redirect$).)+$/}); o.depends({'routing_mode': /^((?!custom).)+$/, 'proxy_mode': /^((?!redirect$).)+$/});
@ -173,9 +173,9 @@ return view.extend({
if (!value) if (!value)
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));
var ipv6_support = this.map.lookupOption('ipv6_support', section_id)[0].formvalue(section_id); let ipv6_support = this.map.lookupOption('ipv6_support', section_id)[0].formvalue(section_id);
try { try {
var url = new URL(value); let url = new URL(value);
if (stubValidator.apply('hostname', url.hostname)) if (stubValidator.apply('hostname', url.hostname))
return true; return true;
else if (stubValidator.apply('ip4addr', url.hostname)) else if (stubValidator.apply('ip4addr', url.hostname))
@ -209,7 +209,7 @@ return view.extend({
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));
try { try {
var url = new URL(value); let url = new URL(value);
if (stubValidator.apply('hostname', url.hostname)) if (stubValidator.apply('hostname', url.hostname))
return true; return true;
else if (stubValidator.apply('ip4addr', url.hostname)) else if (stubValidator.apply('ip4addr', url.hostname))
@ -247,8 +247,8 @@ return view.extend({
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (section_id && value && value !== 'common') { if (section_id && value && value !== 'common') {
var ports = []; let ports = [];
for (var i of value.split(',')) { for (let i of value.split(',')) {
if (!stubValidator.apply('port', i) && !stubValidator.apply('portrange', i)) if (!stubValidator.apply('port', i) && !stubValidator.apply('portrange', i))
return _('Expecting: %s').format(_('valid port value')); return _('Expecting: %s').format(_('valid port value'));
if (ports.includes(i)) if (ports.includes(i))
@ -301,7 +301,7 @@ return view.extend({
so.depends('homeproxy.config.proxy_mode', 'tun'); so.depends('homeproxy.config.proxy_mode', 'tun');
so.rmempty = false; so.rmempty = false;
so.onchange = function(ev, section_id, value) { so.onchange = function(ev, section_id, value) {
var desc = ev.target.nextElementSibling; let desc = ev.target.nextElementSibling;
if (value === 'mixed') if (value === 'mixed')
desc.innerHTML = _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.') desc.innerHTML = _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.')
else if (value === 'gvisor') else if (value === 'gvisor')
@ -333,7 +333,7 @@ return view.extend({
so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'), so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
_('If set, the requested domain name will be resolved to IP before routing.')); _('If set, the requested domain name will be resolved to IP before routing.'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]) so.value(i, hp.dns_strategy[i])
so = ss.option(form.Flag, 'sniff_override', _('Override destination'), so = ss.option(form.Flag, 'sniff_override', _('Override destination'),
@ -387,14 +387,14 @@ return view.extend({
so = ss.option(form.ListValue, 'node', _('Node'), so = ss.option(form.ListValue, 'node', _('Node'),
_('Outbound node')); _('Outbound node'));
so.value('urltest', _('URLTest')); so.value('urltest', _('URLTest'));
for (var i in proxy_nodes) for (let i in proxy_nodes)
so.value(i, proxy_nodes[i]); so.value(i, proxy_nodes[i]);
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'node'); so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'node');
so.editable = true; so.editable = true;
so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'), so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
_('If set, the server domain name will be resolved to IP before connecting.<br/>')); _('If set, the server domain name will be resolved to IP before connecting.<br/>'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]); so.value(i, hp.dns_strategy[i]);
so.depends({'node': 'urltest', '!reverse': true}); so.depends({'node': 'urltest', '!reverse': true});
so.modalonly = true; so.modalonly = true;
@ -422,9 +422,9 @@ return view.extend({
} }
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) { if (section_id && value) {
var node = this.map.lookupOption('node', section_id)[0].formvalue(section_id); let node = this.map.lookupOption('node', section_id)[0].formvalue(section_id);
var conflict = false; let conflict = false;
uci.sections(data[0], 'routing_node', (res) => { uci.sections(data[0], 'routing_node', (res) => {
if (res['.name'] !== section_id) { if (res['.name'] !== section_id) {
if (res.outbound === section_id && res['.name'] == value) if (res.outbound === section_id && res['.name'] == value)
@ -444,7 +444,7 @@ return view.extend({
so = ss.option(hp.CBIStaticList, 'urltest_nodes', _('URLTest nodes'), so = ss.option(hp.CBIStaticList, 'urltest_nodes', _('URLTest nodes'),
_('List of nodes to test.')); _('List of nodes to test.'));
for (var i in proxy_nodes) for (let i in proxy_nodes)
so.value(i, proxy_nodes[i]); so.value(i, proxy_nodes[i]);
so.depends('node', 'urltest'); so.depends('node', 'urltest');
so.modalonly = true; so.modalonly = true;
@ -454,7 +454,7 @@ return view.extend({
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) { if (section_id && value) {
try { try {
var url = new URL(value); let url = new URL(value);
if (!url.hostname) if (!url.hostname)
return _('Expecting: %s').format(_('valid URL')); return _('Expecting: %s').format(_('valid URL'));
} }
@ -473,7 +473,7 @@ return view.extend({
so.datatype = 'uinteger'; so.datatype = 'uinteger';
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) { if (section_id && value) {
var idle_timeout = this.map.lookupOption('urltest_idle_timeout', section_id)[0].formvalue(section_id) || '1800'; let idle_timeout = this.map.lookupOption('urltest_idle_timeout', section_id)[0].formvalue(section_id) || '1800';
if (parseInt(value) > parseInt(idle_timeout)) if (parseInt(value) > parseInt(idle_timeout))
return _('Test interval must be less or equal than idle timeout.'); return _('Test interval must be less or equal than idle timeout.');
} }
@ -706,7 +706,7 @@ return view.extend({
ss = o.subsection; ss = o.subsection;
so = ss.option(form.ListValue, 'default_strategy', _('Default DNS strategy'), so = ss.option(form.ListValue, 'default_strategy', _('Default DNS strategy'),
_('The DNS strategy for resolving the domain name in the address.')); _('The DNS strategy for resolving the domain name in the address.'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]); so.value(i, hp.dns_strategy[i]);
so = ss.option(form.ListValue, 'default_server', _('Default DNS server')); so = ss.option(form.ListValue, 'default_server', _('Default DNS server'));
@ -788,7 +788,7 @@ return view.extend({
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));
try { try {
var url = new URL(value); let url = new URL(value);
if (stubValidator.apply('hostname', url.hostname)) if (stubValidator.apply('hostname', url.hostname))
return true; return true;
else if (stubValidator.apply('ip4addr', url.hostname)) else if (stubValidator.apply('ip4addr', url.hostname))
@ -824,7 +824,7 @@ return view.extend({
} }
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) { if (section_id && value) {
var conflict = false; let conflict = false;
uci.sections(data[0], 'dns_server', (res) => { uci.sections(data[0], 'dns_server', (res) => {
if (res['.name'] !== section_id) if (res['.name'] !== section_id)
if (res.address_resolver === section_id && res['.name'] == value) if (res.address_resolver === section_id && res['.name'] == value)
@ -840,13 +840,13 @@ return view.extend({
so = ss.option(form.ListValue, 'address_strategy', _('Address strategy'), so = ss.option(form.ListValue, 'address_strategy', _('Address strategy'),
_('The domain strategy for resolving the domain name in the address.')); _('The domain strategy for resolving the domain name in the address.'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]); so.value(i, hp.dns_strategy[i]);
so.modalonly = true; so.modalonly = true;
so = ss.option(form.ListValue, 'resolve_strategy', _('Resolve strategy'), so = ss.option(form.ListValue, 'resolve_strategy', _('Resolve strategy'),
_('Default domain strategy for resolving the domain names.')); _('Default domain strategy for resolving the domain names.'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
so.value(i, hp.dns_strategy[i]); so.value(i, hp.dns_strategy[i]);
so.editable = true; so.editable = true;
@ -1150,7 +1150,7 @@ return view.extend({
return _('Expecting: %s').format(_('non-empty value')); return _('Expecting: %s').format(_('non-empty value'));
try { try {
var url = new URL(value); let url = new URL(value);
if (!url.hostname) if (!url.hostname)
return _('Expecting: %s').format(_('valid URL')); return _('Expecting: %s').format(_('valid URL'));
} }
@ -1278,20 +1278,24 @@ return view.extend({
so.monospace = true; so.monospace = true;
so.datatype = 'hostname'; so.datatype = 'hostname';
so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true}); so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true});
so.load = function(section_id) { so.load = function(/* ... */) {
return L.resolveDefault(callReadDomainList('proxy_list')).then((res) => { return L.resolveDefault(callReadDomainList('proxy_list')).then((res) => {
return res.content; return res.content;
}, {}); }, {});
} }
so.write = function(section_id, value) { so.write = function(_section_id, value) {
return callWriteDomainList('proxy_list', value); return callWriteDomainList('proxy_list', value);
} }
so.remove = function(section_id, value) { so.remove = function(/* ... */) {
return callWriteDomainList('proxy_list', ''); let routing_mode = this.map.lookupOption('routing_mode', 'config')[0].formvalue('config');
if (routing_mode !== 'custom')
return callWriteDomainList('proxy_list', '');
return true;
} }
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) if (section_id && value)
for (var i of value.split('\n')) for (let i of value.split('\n'))
if (i && !stubValidator.apply('hostname', i)) if (i && !stubValidator.apply('hostname', i))
return _('Expecting: %s').format(_('valid hostname')); return _('Expecting: %s').format(_('valid hostname'));
@ -1307,20 +1311,24 @@ return view.extend({
so.monospace = true; so.monospace = true;
so.datatype = 'hostname'; so.datatype = 'hostname';
so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true}); so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true});
so.load = function(section_id) { so.load = function(/* ... */) {
return L.resolveDefault(callReadDomainList('direct_list')).then((res) => { return L.resolveDefault(callReadDomainList('direct_list')).then((res) => {
return res.content; return res.content;
}, {}); }, {});
} }
so.write = function(section_id, value) { so.write = function(_section_id, value) {
return callWriteDomainList('direct_list', value); return callWriteDomainList('direct_list', value);
} }
so.remove = function(section_id, value) { so.remove = function(/* ... */) {
return callWriteDomainList('direct_list', ''); let routing_mode = this.map.lookupOption('routing_mode', 'config')[0].formvalue('config');
if (routing_mode !== 'custom')
return callWriteDomainList('direct_list', '');
return true;
} }
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
if (section_id && value) if (section_id && value)
for (var i of value.split('\n')) for (let i of value.split('\n'))
if (i && !stubValidator.apply('hostname', i)) if (i && !stubValidator.apply('hostname', i))
return _('Expecting: %s').format(_('valid hostname')); return _('Expecting: %s').format(_('valid hostname'));

View File

@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-2.0-only * SPDX-License-Identifier: GPL-2.0-only
* *
* Copyright (C) 2022-2024 ImmortalWrt.org * Copyright (C) 2022-2025 ImmortalWrt.org
*/ */
'use strict'; 'use strict';
@ -14,20 +14,20 @@
'require homeproxy as hp'; 'require homeproxy as hp';
'require tools.widgets as widgets'; 'require tools.widgets as widgets';
function allowInsecureConfirm(ev, section_id, value) { function allowInsecureConfirm(ev, _section_id, value) {
if (value === '1' && !confirm(_('Are you sure to allow insecure?'))) if (value === '1' && !confirm(_('Are you sure to allow insecure?')))
ev.target.firstElementChild.checked = null; ev.target.firstElementChild.checked = null;
} }
function parseShareLink(uri, features) { function parseShareLink(uri, features) {
var config; let config, url, params;
uri = uri.split('://'); uri = uri.split('://');
if (uri[0] && uri[1]) { if (uri[0] && uri[1]) {
switch (uri[0]) { switch (uri[0]) {
case 'http': case 'http':
case 'https': case 'https':
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
config = { config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null, label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
@ -42,8 +42,8 @@ function parseShareLink(uri, features) {
break; break;
case 'hysteria': case 'hysteria':
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */ /* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var params = url.searchParams; params = url.searchParams;
/* WeChat-Video / FakeTCP are unsupported by sing-box currently */ /* WeChat-Video / FakeTCP are unsupported by sing-box currently */
if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp')) if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp'))
@ -70,8 +70,8 @@ function parseShareLink(uri, features) {
case 'hysteria2': case 'hysteria2':
case 'hy2': case 'hy2':
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */ /* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var params = url.searchParams; params = url.searchParams;
if (!features.with_quic) if (!features.with_quic)
return null; return null;
@ -97,7 +97,7 @@ function parseShareLink(uri, features) {
case 'socks4a': case 'socks4a':
case 'socsk5': case 'socsk5':
case 'socks5h': case 'socks5h':
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
config = { config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null, label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
@ -114,7 +114,7 @@ function parseShareLink(uri, features) {
try { try {
/* "Lovely" Shadowrocket format */ /* "Lovely" Shadowrocket format */
try { try {
var suri = uri[1].split('#'), slabel = ''; let suri = uri[1].split('#'), slabel = '';
if (suri.length <= 2) { if (suri.length <= 2) {
if (suri.length === 2) if (suri.length === 2)
slabel = '#' + suri[1]; slabel = '#' + suri[1];
@ -123,9 +123,9 @@ function parseShareLink(uri, features) {
} catch(e) { } } catch(e) { }
/* SIP002 format https://shadowsocks.org/guide/sip002.html */ /* SIP002 format https://shadowsocks.org/guide/sip002.html */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var userinfo; let userinfo;
if (url.username && url.password) if (url.username && url.password)
/* User info encoded with URIComponent */ /* User info encoded with URIComponent */
userinfo = [url.username, decodeURIComponent(url.password)]; userinfo = [url.username, decodeURIComponent(url.password)];
@ -136,9 +136,9 @@ function parseShareLink(uri, features) {
if (!hp.shadowsocks_encrypt_methods.includes(userinfo[0])) if (!hp.shadowsocks_encrypt_methods.includes(userinfo[0]))
return null; return null;
var plugin, plugin_opts; let plugin, plugin_opts;
if (url.search && url.searchParams.get('plugin')) { if (url.search && url.searchParams.get('plugin')) {
var plugin_info = url.searchParams.get('plugin').split(';'); let plugin_info = url.searchParams.get('plugin').split(';');
plugin = plugin_info[0]; plugin = plugin_info[0];
plugin_opts = plugin_info.slice(1) ? plugin_info.slice(1).join(';') : null; plugin_opts = plugin_info.slice(1) ? plugin_info.slice(1).join(';') : null;
} }
@ -173,8 +173,8 @@ function parseShareLink(uri, features) {
break; break;
case 'trojan': case 'trojan':
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */ /* https://p4gefau1t.github.io/trojan-go/developer/url/ */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var params = url.searchParams; params = url.searchParams;
/* Check if password exists */ /* Check if password exists */
if (!url.username) if (!url.username)
@ -208,8 +208,8 @@ function parseShareLink(uri, features) {
break; break;
case 'tuic': case 'tuic':
/* https://github.com/daeuniverse/dae/discussions/182 */ /* https://github.com/daeuniverse/dae/discussions/182 */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var params = url.searchParams; params = url.searchParams;
/* Check if uuid exists */ /* Check if uuid exists */
if (!url.username) if (!url.username)
@ -232,8 +232,8 @@ function parseShareLink(uri, features) {
break; break;
case 'vless': case 'vless':
/* https://github.com/XTLS/Xray-core/discussions/716 */ /* https://github.com/XTLS/Xray-core/discussions/716 */
var url = new URL('http://' + uri[1]); url = new URL('http://' + uri[1]);
var params = url.searchParams; params = url.searchParams;
/* Unsupported protocol */ /* Unsupported protocol */
if (params.get('type') === 'kcp') if (params.get('type') === 'kcp')
@ -356,7 +356,7 @@ function parseShareLink(uri, features) {
} }
function renderNodeSettings(section, data, features, main_node, routing_mode) { function renderNodeSettings(section, data, features, main_node, routing_mode) {
var s = section, o; let s = section, o;
s.rowcolors = true; s.rowcolors = true;
s.sortable = true; s.sortable = true;
s.nodescriptions = true; s.nodescriptions = true;
@ -439,12 +439,12 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends({'type': 'socks', 'socks_version': '5'}); o.depends({'type': 'socks', 'socks_version': '5'});
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (section_id) { if (section_id) {
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
var required_type = [ 'shadowsocks', 'shadowtls', 'trojan' ]; let required_type = [ 'shadowsocks', 'shadowtls', 'trojan' ];
if (required_type.includes(type)) { if (required_type.includes(type)) {
if (type === 'shadowsocks') { if (type === 'shadowsocks') {
var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id); let encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
if (encmode === 'none') if (encmode === 'none')
return true; return true;
} }
@ -546,7 +546,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
/* Shadowsocks config start */ /* Shadowsocks config start */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method')); o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (var i of hp.shadowsocks_encrypt_methods) for (let i of hp.shadowsocks_encrypt_methods)
o.value(i); o.value(i);
/* Stream ciphers */ /* Stream ciphers */
o.value('aes-128-ctr'); o.value('aes-128-ctr');
@ -720,7 +720,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('type', 'vless'); o.depends('type', 'vless');
o.depends('type', 'vmess'); o.depends('type', 'vmess');
o.onchange = function(ev, section_id, value) { o.onchange = function(ev, section_id, value) {
var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling; let desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
if (value === 'http') if (value === 'http')
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.'); desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
else if (value === 'quic') else if (value === 'quic')
@ -728,7 +728,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
else else
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.'); desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
if ((value === 'http' && tls.checked) || (value === 'grpc' && !features.with_grpc)) { if ((value === 'http' && tls.checked) || (value === 'grpc' && !features.with_grpc)) {
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML = this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
_('Specifies the period of time (in seconds) after which a health check will be performed using a ping frame if no frames have been received on the connection.<br/>' + _('Specifies the period of time (in seconds) after which a health check will be performed using a ping frame if no frames have been received on the connection.<br/>' +
@ -951,10 +951,10 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o.depends('type', 'tuic'); o.depends('type', 'tuic');
o.depends('type', 'vless'); o.depends('type', 'vless');
o.depends('type', 'vmess'); o.depends('type', 'vmess');
o.validate = function(section_id, value) { o.validate = function(section_id, _value) {
if (section_id) { if (section_id) {
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
if (['hysteria', 'hysteria2', 'shadowtls', 'tuic'].includes(type)) { if (['hysteria', 'hysteria2', 'shadowtls', 'tuic'].includes(type)) {
tls.checked = true; tls.checked = true;
@ -990,7 +990,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'), o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
_('The minimum TLS version that is acceptable.')); _('The minimum TLS version that is acceptable.'));
o.value('', _('default')); o.value('', _('default'));
for (var i of hp.tls_versions) for (let i of hp.tls_versions)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.modalonly = true; o.modalonly = true;
@ -998,14 +998,14 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'), o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
_('The maximum TLS version that is acceptable.')); _('The maximum TLS version that is acceptable.'));
o.value('', _('default')); o.value('', _('default'));
for (var i of hp.tls_versions) for (let i of hp.tls_versions)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.modalonly = true; o.modalonly = true;
o = s.option(hp.CBIStaticList, 'tls_cipher_suites', _('Cipher suites'), 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.')); _('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
for (var i of hp.tls_cipher_suites) for (let i of hp.tls_cipher_suites)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.optional = true; o.optional = true;
@ -1144,22 +1144,22 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
} }
return view.extend({ return view.extend({
load: function() { load() {
return Promise.all([ return Promise.all([
uci.load('homeproxy'), uci.load('homeproxy'),
hp.getBuiltinFeatures() hp.getBuiltinFeatures()
]); ]);
}, },
render: function(data) { render(data) {
let m, s, o, ss, so; let m, s, o, ss, so;
var main_node = uci.get(data[0], 'config', 'main_node'); let main_node = uci.get(data[0], 'config', 'main_node');
var routing_mode = uci.get(data[0], 'config', 'routing_mode'); let routing_mode = uci.get(data[0], 'config', 'routing_mode');
var features = data[1]; let features = data[1];
/* Cache subscription information, it will be called multiple times */ /* Cache subscription information, it will be called multiple times */
var subinfo = []; let subinfo = [];
for (var suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) { for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
const url = new URL(suburl); const url = new URL(suburl);
const urlhash = hp.calcStringMD5(suburl.replace(/#.*$/, '')); const urlhash = hp.calcStringMD5(suburl.replace(/#.*$/, ''));
const title = url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname; const title = url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname;
@ -1177,7 +1177,7 @@ return view.extend({
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode); ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
ss.addremove = true; ss.addremove = true;
ss.filter = function(section_id) { ss.filter = function(section_id) {
for (var info of subinfo) for (let info of subinfo)
if (info.hash === uci.get(data[0], section_id, 'grouphash')) if (info.hash === uci.get(data[0], section_id, 'grouphash'))
return false; return false;
@ -1186,7 +1186,7 @@ return view.extend({
/* Import subscription links start */ /* Import subscription links start */
/* Thanks to luci-app-shadowsocks-libev */ /* Thanks to luci-app-shadowsocks-libev */
ss.handleLinkImport = function() { ss.handleLinkImport = function() {
var textarea = new ui.Textarea(); let textarea = new ui.Textarea();
ui.showModal(_('Import share links'), [ ui.showModal(_('Import share links'), [
E('p', _('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.')), E('p', _('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.')),
textarea.render(), textarea.render(),
@ -1199,25 +1199,25 @@ return view.extend({
E('button', { E('button', {
class: 'btn cbi-button-action', class: 'btn cbi-button-action',
click: ui.createHandlerFn(this, function() { click: ui.createHandlerFn(this, function() {
var input_links = textarea.getValue().trim().split('\n'); let input_links = textarea.getValue().trim().split('\n');
if (input_links && input_links[0]) { if (input_links && input_links[0]) {
/* Remove duplicate lines */ /* Remove duplicate lines */
input_links = input_links.reduce((pre, cur) => input_links = input_links.reduce((pre, cur) =>
(!pre.includes(cur) && pre.push(cur), pre), []); (!pre.includes(cur) && pre.push(cur), pre), []);
var allow_insecure = uci.get(data[0], 'subscription', 'allow_insecure'); let allow_insecure = uci.get(data[0], 'subscription', 'allow_insecure');
var packet_encoding = uci.get(data[0], 'subscription', 'packet_encoding'); let packet_encoding = uci.get(data[0], 'subscription', 'packet_encoding');
var imported_node = 0; let imported_node = 0;
input_links.forEach((l) => { input_links.forEach((l) => {
var config = parseShareLink(l, features); let config = parseShareLink(l, features);
if (config) { if (config) {
if (config.tls === '1' && allow_insecure === '1') if (config.tls === '1' && allow_insecure === '1')
config.tls_insecure = '1' config.tls_insecure = '1'
if (['vless', 'vmess'].includes(config.type)) if (['vless', 'vmess'].includes(config.type))
config.packet_encoding = packet_encoding config.packet_encoding = packet_encoding
var nameHash = hp.calcStringMD5(config.label); let nameHash = hp.calcStringMD5(config.label);
var sid = uci.add(data[0], 'node', nameHash); let sid = uci.add(data[0], 'node', nameHash);
Object.keys(config).forEach((k) => { Object.keys(config).forEach((k) => {
uci.set(data[0], sid, k, config[k]); uci.set(data[0], sid, k, config[k]);
}); });
@ -1245,12 +1245,12 @@ return view.extend({
]) ])
} }
ss.renderSectionAdd = function(/* ... */) { ss.renderSectionAdd = function(/* ... */) {
var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments), let el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments),
nameEl = el.querySelector('.cbi-section-create-name'); nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, (v) => { ui.addValidator(nameEl, 'uciname', true, (v) => {
var button = el.querySelector('.cbi-section-create > .cbi-button-add'); let button = el.querySelector('.cbi-section-create > .cbi-button-add');
var uciconfig = this.uciconfig || this.map.config; let uciconfig = this.uciconfig || this.map.config;
if (!v) { if (!v) {
button.disabled = true; button.disabled = true;
@ -1296,7 +1296,7 @@ return view.extend({
o.rmempty = false; o.rmempty = false;
o = s.taboption('subscription', form.ListValue, 'auto_update_time', _('Update time')); o = s.taboption('subscription', form.ListValue, 'auto_update_time', _('Update time'));
for (var i = 0; i < 24; i++) for (let i = 0; i < 24; i++)
o.value(i, i + ':00'); o.value(i, i + ':00');
o.default = '2'; o.default = '2';
o.depends('auto_update', '1'); o.depends('auto_update', '1');
@ -1311,7 +1311,7 @@ return view.extend({
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (section_id && value) { if (section_id && value) {
try { try {
var url = new URL(value); let url = new URL(value);
if (!url.hostname) if (!url.hostname)
return _('Expecting: %s').format(_('valid URL')); return _('Expecting: %s').format(_('valid URL'));
} }
@ -1364,7 +1364,7 @@ return view.extend({
o = s.taboption('subscription', form.Button, '_update_subscriptions', _('Update nodes from subscriptions')); o = s.taboption('subscription', form.Button, '_update_subscriptions', _('Update nodes from subscriptions'));
o.inputstyle = 'apply'; o.inputstyle = 'apply';
o.inputtitle = function(section_id) { o.inputtitle = function(section_id) {
var sublist = uci.get(data[0], section_id, 'subscription_url') || []; let sublist = uci.get(data[0], section_id, 'subscription_url') || [];
if (sublist.length > 0) { if (sublist.length > 0) {
return _('Update %s subscriptions').format(sublist.length); return _('Update %s subscriptions').format(sublist.length);
} else { } else {
@ -1384,7 +1384,7 @@ return view.extend({
o = s.taboption('subscription', form.Button, '_remove_subscriptions', _('Remove all nodes from subscriptions')); o = s.taboption('subscription', form.Button, '_remove_subscriptions', _('Remove all nodes from subscriptions'));
o.inputstyle = 'reset'; o.inputstyle = 'reset';
o.inputtitle = function() { o.inputtitle = function() {
var subnodes = []; let subnodes = [];
uci.sections(data[0], 'node', (res) => { uci.sections(data[0], 'node', (res) => {
if (res.grouphash) if (res.grouphash)
subnodes = subnodes.concat(res['.name']) subnodes = subnodes.concat(res['.name'])
@ -1398,13 +1398,13 @@ return view.extend({
} }
} }
o.onclick = function() { o.onclick = function() {
var subnodes = []; let subnodes = [];
uci.sections(data[0], 'node', (res) => { uci.sections(data[0], 'node', (res) => {
if (res.grouphash) if (res.grouphash)
subnodes = subnodes.concat(res['.name']) subnodes = subnodes.concat(res['.name'])
}); });
for (var i in subnodes) for (let i in subnodes)
uci.remove(data[0], subnodes[i]); uci.remove(data[0], subnodes[i]);
if (subnodes.includes(uci.get(data[0], 'config', 'main_node'))) if (subnodes.includes(uci.get(data[0], 'config', 'main_node')))

View File

@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-2.0-only * SPDX-License-Identifier: GPL-2.0-only
* *
* Copyright (C) 2022-2023 ImmortalWrt.org * Copyright (C) 2022-2025 ImmortalWrt.org
*/ */
'use strict'; 'use strict';
@ -23,7 +23,7 @@ const callServiceList = rpc.declare({
function getServiceStatus() { function getServiceStatus() {
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => { return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
var isRunning = false; let isRunning = false;
try { try {
isRunning = res['homeproxy']['instances']['sing-box-s']['running']; isRunning = res['homeproxy']['instances']['sing-box-s']['running'];
} catch (e) { } } catch (e) { }
@ -32,8 +32,8 @@ function getServiceStatus() {
} }
function renderStatus(isRunning, version) { function renderStatus(isRunning, version) {
var spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>'; let spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>';
var renderHTML; let renderHTML;
if (isRunning) if (isRunning)
renderHTML = spanTemp.format('green', _('HomeProxy Server'), version, _('RUNNING')); renderHTML = spanTemp.format('green', _('HomeProxy Server'), version, _('RUNNING'));
else else
@ -43,10 +43,10 @@ function renderStatus(isRunning, version) {
} }
function handleGenKey(option) { function handleGenKey(option) {
var section_id = this.section.section; let section_id = this.section.section;
var type = this.section.getOption('type').formvalue(section_id); let type = this.section.getOption('type').formvalue(section_id);
var widget = this.map.findElement('id', 'widget.cbid.homeproxy.%s.%s'.format(section_id, option)); let widget = this.map.findElement('id', 'widget.cbid.homeproxy.%s.%s'.format(section_id, option));
var password, required_method; let password, required_method;
if (option === 'uuid') if (option === 'uuid')
required_method = 'uuid'; required_method = 'uuid';
@ -83,16 +83,16 @@ function handleGenKey(option) {
} }
return view.extend({ return view.extend({
load: function() { load() {
return Promise.all([ return Promise.all([
uci.load('homeproxy'), uci.load('homeproxy'),
hp.getBuiltinFeatures() hp.getBuiltinFeatures()
]); ]);
}, },
render: function(data) { render(data) {
let m, s, o; let m, s, o;
var features = data[1]; let features = data[1];
m = new form.Map('homeproxy', _('HomeProxy Server'), m = new form.Map('homeproxy', _('HomeProxy Server'),
_('The modern OpenWrt proxy platform for ARM64/AMD64.')); _('The modern OpenWrt proxy platform for ARM64/AMD64.'));
@ -101,7 +101,7 @@ return view.extend({
s.render = function () { s.render = function () {
poll.add(function () { poll.add(function () {
return L.resolveDefault(getServiceStatus()).then((res) => { return L.resolveDefault(getServiceStatus()).then((res) => {
var view = document.getElementById('service_status'); let view = document.getElementById('service_status');
view.innerHTML = renderStatus(res, features.version); view.innerHTML = renderStatus(res, features.version);
}); });
}); });
@ -183,7 +183,7 @@ return view.extend({
o.depends('type', 'trojan'); o.depends('type', 'trojan');
o.depends('type', 'tuic'); o.depends('type', 'tuic');
o.renderWidget = function() { o.renderWidget = function() {
var node = form.Value.prototype.renderWidget.apply(this, arguments); let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', { (node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply', 'class': 'cbi-button cbi-button-apply',
@ -195,12 +195,12 @@ return view.extend({
} }
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (section_id) { if (section_id) {
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
var required_type = [ 'http', 'mixed', 'naive', 'socks', 'shadowsocks' ]; let required_type = [ 'http', 'mixed', 'naive', 'socks', 'shadowsocks' ];
if (required_type.includes(type)) { if (required_type.includes(type)) {
if (type === 'shadowsocks') { if (type === 'shadowsocks') {
var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id); let encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
if (encmode === 'none') if (encmode === 'none')
return true; return true;
else if (encmode === '2022-blake3-aes-128-gcm') else if (encmode === '2022-blake3-aes-128-gcm')
@ -266,7 +266,7 @@ return view.extend({
o.depends('type', 'hysteria'); o.depends('type', 'hysteria');
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/}); o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
o.renderWidget = function() { o.renderWidget = function() {
var node = form.Value.prototype.renderWidget.apply(this, arguments); let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', { (node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply', 'class': 'cbi-button cbi-button-apply',
@ -319,7 +319,7 @@ return view.extend({
/* Shadowsocks config */ /* Shadowsocks config */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method')); o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (var i of hp.shadowsocks_encrypt_methods) for (let i of hp.shadowsocks_encrypt_methods)
o.value(i); o.value(i);
o.default = 'aes-128-gcm'; o.default = 'aes-128-gcm';
o.depends('type', 'shadowsocks'); o.depends('type', 'shadowsocks');
@ -331,7 +331,7 @@ return view.extend({
o.depends('type', 'vless'); o.depends('type', 'vless');
o.depends('type', 'vmess'); o.depends('type', 'vmess');
o.renderWidget = function() { o.renderWidget = function() {
var node = form.Value.prototype.renderWidget.apply(this, arguments); let node = form.Value.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', { (node.querySelector('.control-group') || node).appendChild(E('button', {
'class': 'cbi-button cbi-button-apply', 'class': 'cbi-button cbi-button-apply',
@ -402,7 +402,7 @@ return view.extend({
o.depends('type', 'vless'); o.depends('type', 'vless');
o.depends('type', 'vmess'); o.depends('type', 'vmess');
o.onchange = function(ev, section_id, value) { o.onchange = function(ev, section_id, value) {
var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling; let desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
if (value === 'http') if (value === 'http')
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.'); desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
else if (value === 'quic') else if (value === 'quic')
@ -410,7 +410,7 @@ return view.extend({
else else
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.'); desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
var tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; 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)) 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 = 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.'); _('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
@ -539,8 +539,8 @@ return view.extend({
o.rmempty = false; o.rmempty = false;
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (section_id) { if (section_id) {
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id); let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild; let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
if (['hysteria', 'hysteria2', 'tuic'].includes(type)) { if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
tls.checked = true; tls.checked = true;
@ -567,7 +567,7 @@ return view.extend({
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'), o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
_('The minimum TLS version that is acceptable.')); _('The minimum TLS version that is acceptable.'));
o.value('', _('default')); o.value('', _('default'));
for (var i of hp.tls_versions) for (let i of hp.tls_versions)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.modalonly = true; o.modalonly = true;
@ -575,14 +575,14 @@ return view.extend({
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'), o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
_('The maximum TLS version that is acceptable.')); _('The maximum TLS version that is acceptable.'));
o.value('', _('default')); o.value('', _('default'));
for (var i of hp.tls_versions) for (let i of hp.tls_versions)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.modalonly = true; o.modalonly = true;
o = s.option(hp.CBIStaticList, 'tls_cipher_suites', _('Cipher suites'), 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.')); _('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
for (var i of hp.tls_cipher_suites) for (let i of hp.tls_cipher_suites)
o.value(i); o.value(i);
o.depends('tls', '1'); o.depends('tls', '1');
o.optional = true; o.optional = true;
@ -808,7 +808,7 @@ return view.extend({
o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'), o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
_('If set, the requested domain name will be resolved to IP before routing.')); _('If set, the requested domain name will be resolved to IP before routing.'));
for (var i in hp.dns_strategy) for (let i in hp.dns_strategy)
o.value(i, hp.dns_strategy[i]) o.value(i, hp.dns_strategy[i])
o.modalonly = true; o.modalonly = true;

View File

@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-2.0-only * SPDX-License-Identifier: GPL-2.0-only
* *
* Copyright (C) 2022-2023 ImmortalWrt.org * Copyright (C) 2022-2025 ImmortalWrt.org
*/ */
'use strict'; 'use strict';
@ -15,7 +15,7 @@
'require view'; 'require view';
/* Thanks to luci-app-aria2 */ /* Thanks to luci-app-aria2 */
var css = ' \ const css = ' \
#log_textarea { \ #log_textarea { \
padding: 10px; \ padding: 10px; \
text-align: left; \ text-align: left; \
@ -29,7 +29,7 @@ var css = ' \
background-color: #33ccff; \ background-color: #33ccff; \
}'; }';
var hp_dir = '/var/run/homeproxy'; const hp_dir = '/var/run/homeproxy';
function getConnStat(self, site) { function getConnStat(self, site) {
const callConnStat = rpc.declare({ const callConnStat = rpc.declare({
@ -44,7 +44,7 @@ function getConnStat(self, site) {
'class': 'btn cbi-button cbi-button-action', 'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() { 'click': ui.createHandlerFn(this, function() {
return L.resolveDefault(callConnStat(site), {}).then((ret) => { return L.resolveDefault(callConnStat(site), {}).then((ret) => {
var ele = self.default.firstElementChild.nextElementSibling; let ele = self.default.firstElementChild.nextElementSibling;
if (ret.result) { if (ret.result) {
ele.style.setProperty('color', 'green'); ele.style.setProperty('color', 'green');
ele.innerHTML = _('passed'); ele.innerHTML = _('passed');
@ -76,7 +76,7 @@ function getResVersion(self, type) {
}); });
return L.resolveDefault(callResVersion(type), {}).then((res) => { return L.resolveDefault(callResVersion(type), {}).then((res) => {
var spanTemp = E('div', { 'style': 'cbi-value-field' }, [ let spanTemp = E('div', { 'style': 'cbi-value-field' }, [
E('button', { E('button', {
'class': 'btn cbi-button cbi-button-action', 'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() { 'click': ui.createHandlerFn(this, function() {
@ -121,7 +121,7 @@ function getRuntimeLog(name, filename) {
expect: { '': {} } expect: { '': {} }
}); });
var log_textarea = E('div', { 'id': 'log_textarea' }, let log_textarea = E('div', { 'id': 'log_textarea' },
E('img', { E('img', {
'src': L.resource('icons/loading.gif'), 'src': L.resource('icons/loading.gif'),
'alt': _('Loading'), 'alt': _('Loading'),
@ -129,7 +129,7 @@ function getRuntimeLog(name, filename) {
}, _('Collecting data...')) }, _('Collecting data...'))
); );
var log; let log;
poll.add(L.bind(function() { poll.add(L.bind(function() {
return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text') return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
.then(function(res) { .then(function(res) {
@ -176,15 +176,8 @@ function getRuntimeLog(name, filename) {
} }
return view.extend({ return view.extend({
load: function() { render() {
return Promise.all([ let m, s, o;
uci.load('homeproxy')
]);
},
render: function(data) {
var m, s, o;
var routing_mode = uci.get(data[0], 'config', 'routing_mode') || 'bypass_mainland_china';
m = new form.Map('homeproxy'); m = new form.Map('homeproxy');

View File

@ -209,7 +209,7 @@ msgstr ""
msgid "BBR" msgid "BBR"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:194 #: htdocs/luci-static/resources/view/homeproxy/status.js:187
msgid "BaiDu" msgid "BaiDu"
msgstr "" msgstr ""
@ -309,15 +309,15 @@ msgstr ""
msgid "China DNS server" msgid "China DNS server"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:204 #: htdocs/luci-static/resources/view/homeproxy/status.js:197
msgid "China IPv4 list version" msgid "China IPv4 list version"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:208 #: htdocs/luci-static/resources/view/homeproxy/status.js:201
msgid "China IPv6 list version" msgid "China IPv6 list version"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:212 #: htdocs/luci-static/resources/view/homeproxy/status.js:205
msgid "China list version" msgid "China list version"
msgstr "" msgstr ""
@ -373,7 +373,7 @@ msgstr ""
msgid "Congestion control algorithm" msgid "Congestion control algorithm"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:191 #: htdocs/luci-static/resources/view/homeproxy/status.js:184
msgid "Connection check" msgid "Connection check"
msgstr "" msgstr ""
@ -467,7 +467,7 @@ msgstr ""
msgid "Direct" msgid "Direct"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1303 #: htdocs/luci-static/resources/view/homeproxy/client.js:1307
msgid "Direct Domain List" msgid "Direct Domain List"
msgstr "" msgstr ""
@ -737,8 +737,8 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1150 #: htdocs/luci-static/resources/view/homeproxy/client.js:1150
#: htdocs/luci-static/resources/view/homeproxy/client.js:1155 #: htdocs/luci-static/resources/view/homeproxy/client.js:1155
#: htdocs/luci-static/resources/view/homeproxy/client.js:1158 #: htdocs/luci-static/resources/view/homeproxy/client.js:1158
#: htdocs/luci-static/resources/view/homeproxy/client.js:1296 #: htdocs/luci-static/resources/view/homeproxy/client.js:1300
#: htdocs/luci-static/resources/view/homeproxy/client.js:1325 #: htdocs/luci-static/resources/view/homeproxy/client.js:1333
#: htdocs/luci-static/resources/view/homeproxy/node.js:452 #: htdocs/luci-static/resources/view/homeproxy/node.js:452
#: htdocs/luci-static/resources/view/homeproxy/node.js:1087 #: htdocs/luci-static/resources/view/homeproxy/node.js:1087
#: htdocs/luci-static/resources/view/homeproxy/node.js:1260 #: htdocs/luci-static/resources/view/homeproxy/node.js:1260
@ -791,7 +791,7 @@ msgstr ""
msgid "GET" msgid "GET"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:216 #: htdocs/luci-static/resources/view/homeproxy/status.js:209
msgid "GFW list version" msgid "GFW list version"
msgstr "" msgstr ""
@ -849,7 +849,7 @@ msgstr ""
msgid "Global settings" msgid "Global settings"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:197 #: htdocs/luci-static/resources/view/homeproxy/status.js:190
msgid "Google" msgid "Google"
msgstr "" msgstr ""
@ -897,7 +897,7 @@ msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:55 #: htdocs/luci-static/resources/view/homeproxy/client.js:55
#: htdocs/luci-static/resources/view/homeproxy/client.js:57 #: htdocs/luci-static/resources/view/homeproxy/client.js:57
#: htdocs/luci-static/resources/view/homeproxy/client.js:120 #: htdocs/luci-static/resources/view/homeproxy/client.js:120
#: htdocs/luci-static/resources/view/homeproxy/status.js:224 #: htdocs/luci-static/resources/view/homeproxy/status.js:217
#: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3 #: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3
msgid "HomeProxy" msgid "HomeProxy"
msgstr "" msgstr ""
@ -1787,7 +1787,7 @@ msgstr ""
msgid "Resolve strategy" msgid "Resolve strategy"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:201 #: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Resources management" msgid "Resources management"
msgstr "" msgstr ""
@ -2653,11 +2653,11 @@ msgstr ""
msgid "quic-go / uquic chrome" msgid "quic-go / uquic chrome"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:227 #: htdocs/luci-static/resources/view/homeproxy/status.js:220
msgid "sing-box client" msgid "sing-box client"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/status.js:230 #: htdocs/luci-static/resources/view/homeproxy/status.js:223
msgid "sing-box server" msgid "sing-box server"
msgstr "" msgstr ""
@ -2722,8 +2722,8 @@ msgstr ""
msgid "valid base64 key with %d characters" msgid "valid base64 key with %d characters"
msgstr "" msgstr ""
#: htdocs/luci-static/resources/view/homeproxy/client.js:1296 #: htdocs/luci-static/resources/view/homeproxy/client.js:1300
#: htdocs/luci-static/resources/view/homeproxy/client.js:1325 #: htdocs/luci-static/resources/view/homeproxy/client.js:1333
msgid "valid hostname" msgid "valid hostname"
msgstr "" msgstr ""

View File

@ -218,7 +218,7 @@ msgstr "自动更新订阅和地理数据。"
msgid "BBR" msgid "BBR"
msgstr "BBR" msgstr "BBR"
#: htdocs/luci-static/resources/view/homeproxy/status.js:194 #: htdocs/luci-static/resources/view/homeproxy/status.js:187
msgid "BaiDu" msgid "BaiDu"
msgstr "百度" msgstr "百度"
@ -318,15 +318,15 @@ msgstr "检查更新"
msgid "China DNS server" msgid "China DNS server"
msgstr "国内 DNS 服务器" msgstr "国内 DNS 服务器"
#: htdocs/luci-static/resources/view/homeproxy/status.js:204 #: htdocs/luci-static/resources/view/homeproxy/status.js:197
msgid "China IPv4 list version" msgid "China IPv4 list version"
msgstr "国内 IPv4 库版本" msgstr "国内 IPv4 库版本"
#: htdocs/luci-static/resources/view/homeproxy/status.js:208 #: htdocs/luci-static/resources/view/homeproxy/status.js:201
msgid "China IPv6 list version" msgid "China IPv6 list version"
msgstr "国内 IPv6 库版本" msgstr "国内 IPv6 库版本"
#: htdocs/luci-static/resources/view/homeproxy/status.js:212 #: htdocs/luci-static/resources/view/homeproxy/status.js:205
msgid "China list version" msgid "China list version"
msgstr "国内域名列表版本" msgstr "国内域名列表版本"
@ -382,7 +382,7 @@ msgstr "仅常用端口(绕过 P2P 流量)"
msgid "Congestion control algorithm" msgid "Congestion control algorithm"
msgstr "拥塞控制算法" msgstr "拥塞控制算法"
#: htdocs/luci-static/resources/view/homeproxy/status.js:191 #: htdocs/luci-static/resources/view/homeproxy/status.js:184
msgid "Connection check" msgid "Connection check"
msgstr "连接检查" msgstr "连接检查"
@ -476,7 +476,7 @@ msgstr "默认服务器名称"
msgid "Direct" msgid "Direct"
msgstr "直连" msgstr "直连"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1303 #: htdocs/luci-static/resources/view/homeproxy/client.js:1307
msgid "Direct Domain List" msgid "Direct Domain List"
msgstr "直连域名列表" msgstr "直连域名列表"
@ -758,8 +758,8 @@ msgstr "加密方式"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1150 #: htdocs/luci-static/resources/view/homeproxy/client.js:1150
#: htdocs/luci-static/resources/view/homeproxy/client.js:1155 #: htdocs/luci-static/resources/view/homeproxy/client.js:1155
#: htdocs/luci-static/resources/view/homeproxy/client.js:1158 #: htdocs/luci-static/resources/view/homeproxy/client.js:1158
#: htdocs/luci-static/resources/view/homeproxy/client.js:1296 #: htdocs/luci-static/resources/view/homeproxy/client.js:1300
#: htdocs/luci-static/resources/view/homeproxy/client.js:1325 #: htdocs/luci-static/resources/view/homeproxy/client.js:1333
#: htdocs/luci-static/resources/view/homeproxy/node.js:452 #: htdocs/luci-static/resources/view/homeproxy/node.js:452
#: htdocs/luci-static/resources/view/homeproxy/node.js:1087 #: htdocs/luci-static/resources/view/homeproxy/node.js:1087
#: htdocs/luci-static/resources/view/homeproxy/node.js:1260 #: htdocs/luci-static/resources/view/homeproxy/node.js:1260
@ -812,7 +812,7 @@ msgstr "格式"
msgid "GET" msgid "GET"
msgstr "GET" msgstr "GET"
#: htdocs/luci-static/resources/view/homeproxy/status.js:216 #: htdocs/luci-static/resources/view/homeproxy/status.js:209
msgid "GFW list version" msgid "GFW list version"
msgstr "GFW 域名列表版本" msgstr "GFW 域名列表版本"
@ -870,7 +870,7 @@ msgstr "全局代理 MAC 地址"
msgid "Global settings" msgid "Global settings"
msgstr "全局设置" msgstr "全局设置"
#: htdocs/luci-static/resources/view/homeproxy/status.js:197 #: htdocs/luci-static/resources/view/homeproxy/status.js:190
msgid "Google" msgid "Google"
msgstr "谷歌" msgstr "谷歌"
@ -918,7 +918,7 @@ msgstr "心跳间隔"
#: htdocs/luci-static/resources/view/homeproxy/client.js:55 #: htdocs/luci-static/resources/view/homeproxy/client.js:55
#: htdocs/luci-static/resources/view/homeproxy/client.js:57 #: htdocs/luci-static/resources/view/homeproxy/client.js:57
#: htdocs/luci-static/resources/view/homeproxy/client.js:120 #: htdocs/luci-static/resources/view/homeproxy/client.js:120
#: htdocs/luci-static/resources/view/homeproxy/status.js:224 #: htdocs/luci-static/resources/view/homeproxy/status.js:217
#: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3 #: root/usr/share/luci/menu.d/luci-app-homeproxy.json:3
msgid "HomeProxy" msgid "HomeProxy"
msgstr "HomeProxy" msgstr "HomeProxy"
@ -1814,7 +1814,7 @@ msgstr "保留字段字节"
msgid "Resolve strategy" msgid "Resolve strategy"
msgstr "解析策略" msgstr "解析策略"
#: htdocs/luci-static/resources/view/homeproxy/status.js:201 #: htdocs/luci-static/resources/view/homeproxy/status.js:194
msgid "Resources management" msgid "Resources management"
msgstr "资源管理" msgstr "资源管理"
@ -2720,11 +2720,11 @@ msgstr "私钥"
msgid "quic-go / uquic chrome" msgid "quic-go / uquic chrome"
msgstr "quic-go / uquic chrome" msgstr "quic-go / uquic chrome"
#: htdocs/luci-static/resources/view/homeproxy/status.js:227 #: htdocs/luci-static/resources/view/homeproxy/status.js:220
msgid "sing-box client" msgid "sing-box client"
msgstr "sing-box 客户端" msgstr "sing-box 客户端"
#: htdocs/luci-static/resources/view/homeproxy/status.js:230 #: htdocs/luci-static/resources/view/homeproxy/status.js:223
msgid "sing-box server" msgid "sing-box server"
msgstr "sing-box 服务端" msgstr "sing-box 服务端"
@ -2790,8 +2790,8 @@ msgstr "有效网址"
msgid "valid base64 key with %d characters" msgid "valid base64 key with %d characters"
msgstr "包含 %d 个字符的有效 base64 密钥" msgstr "包含 %d 个字符的有效 base64 密钥"
#: htdocs/luci-static/resources/view/homeproxy/client.js:1296 #: htdocs/luci-static/resources/view/homeproxy/client.js:1300
#: htdocs/luci-static/resources/view/homeproxy/client.js:1325 #: htdocs/luci-static/resources/view/homeproxy/client.js:1333
msgid "valid hostname" msgid "valid hostname"
msgstr "有效主机名" msgstr "有效主机名"

View File

@ -13,8 +13,8 @@ import { connect } from 'ubus';
import { cursor } from 'uci'; import { cursor } from 'uci';
import { import {
executeCommand, isEmpty, strToBool, strToInt, isEmpty, strToBool, strToInt,
removeBlankAttrs, validateHostname, validation, removeBlankAttrs, validation,
HP_DIR, RUN_DIR HP_DIR, RUN_DIR
} from 'homeproxy'; } from 'homeproxy';
@ -373,20 +373,15 @@ config.dns = {
if (!isEmpty(main_node)) { if (!isEmpty(main_node)) {
/* Main DNS */ /* Main DNS */
let default_final_dns = 'default-dns'; push(config.dns.servers, {
if (dns_server !== wan_dns) { tag: 'main-dns',
push(config.dns.servers, { address: !match(dns_server, /:\/\//) ? 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server) : dns_server,
tag: 'main-dns', strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
address: !match(dns_server, /:\/\//) ? 'tcp://' + (validation('ip6addr', dns_server) ? `[${dns_server}]` : dns_server) : dns_server, address_resolver: 'default-dns',
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null, address_strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
address_resolver: 'default-dns', detour: 'main-out'
address_strategy: (ipv6_support !== '1') ? 'ipv4_only' : null, });
detour: 'main-out' config.dns.final = 'main-dns';
});
default_final_dns = 'main-dns';
}
config.dns.final = default_final_dns;
/* Avoid DNS loop */ /* Avoid DNS loop */
push(config.dns.rules, { push(config.dns.rules, {
@ -394,16 +389,16 @@ if (!isEmpty(main_node)) {
server: 'default-dns' server: 'default-dns'
}); });
if (direct_domain_list) if (length(direct_domain_list))
push(config.dns.rules, { push(config.dns.rules, {
domain_keyword: direct_domain_list, rule_set: 'direct-domain',
server: 'default-dns' server: (routing_mode === 'bypass_mainland_china' ) ? 'china-dns' : 'default-dns'
}); });
/* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */ /* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */
if (routing_mode === 'gfwlist' || proxy_domain_list) if (routing_mode === 'gfwlist' || length(proxy_domain_list))
push(config.dns.rules, { push(config.dns.rules, {
domain_keyword: (routing_mode !== 'gfwlist') ? proxy_domain_list : null, rule_set: (routing_mode !== 'gfwlist') ? 'proxy-domain' : null,
query_type: [64, 65], query_type: [64, 65],
server: 'block-dns' server: 'block-dns'
}); });
@ -415,10 +410,10 @@ if (!isEmpty(main_node)) {
detour: 'direct-out' detour: 'direct-out'
}); });
if (proxy_domain_list) if (length(proxy_domain_list))
push(config.dns.rules, { push(config.dns.rules, {
domain_keyword: proxy_domain_list, rule_set: 'proxy-domain',
server: default_final_dns server: 'main-dns'
}); });
push(config.dns.rules, { push(config.dns.rules, {
@ -440,6 +435,7 @@ if (!isEmpty(main_node)) {
server: 'china-dns' server: 'china-dns'
}); });
} }
} else if (!isEmpty(default_outbound)) { } else if (!isEmpty(default_outbound)) {
/* DNS servers */ /* DNS servers */
uci.foreach(uciconfig, ucidnsserver, (cfg) => { uci.foreach(uciconfig, ucidnsserver, (cfg) => {
@ -590,11 +586,13 @@ config.outbounds = [
if (!isEmpty(main_node)) { if (!isEmpty(main_node)) {
const main_node_cfg = uci.get_all(uciconfig, main_node) || {}; const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
push(config.outbounds, generate_outbound(main_node_cfg)); push(config.outbounds, generate_outbound(main_node_cfg));
config.outbounds[length(config.outbounds)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.outbounds[length(config.outbounds)-1].tag = 'main-out'; config.outbounds[length(config.outbounds)-1].tag = 'main-out';
if (dedicated_udp_node) { if (dedicated_udp_node) {
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {}; const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
push(config.outbounds, generate_outbound(main_udp_node_cfg)); push(config.outbounds, generate_outbound(main_udp_node_cfg));
config.outbounds[length(config.outbounds)-1].domain_strategy = (ipv6_support !== '1') ? 'prefer_ipv4' : null;
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out'; config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
} }
} else if (!isEmpty(default_outbound)) { } else if (!isEmpty(default_outbound)) {
@ -651,7 +649,7 @@ if (!isEmpty(main_node)) {
/* Direct list */ /* Direct list */
if (length(direct_domain_list)) if (length(direct_domain_list))
push(config.route.rules, { push(config.route.rules, {
domain_keyword: direct_domain_list, rule_set: 'direct-domain',
outbound: 'direct-out' outbound: 'direct-out'
}); });
@ -665,6 +663,30 @@ if (!isEmpty(main_node)) {
config.route.final = 'main-out'; config.route.final = 'main-out';
/* Rule set */ /* Rule set */
/* Direct list */
if (length(direct_domain_list))
push(config.route.rule_set, {
type: 'inline',
tag: 'direct-domain',
rules: [
{
domain_keyword: direct_domain_list,
}
]
});
/* Proxy list */
if (length(proxy_domain_list))
push(config.route.rule_set, {
type: 'inline',
tag: 'proxy-domain',
rules: [
{
domain_keyword: proxy_domain_list,
}
]
});
if (routing_mode === 'bypass_mainland_china') { if (routing_mode === 'bypass_mainland_china') {
push(config.route.rule_set, { push(config.route.rule_set, {
type: 'remote', type: 'remote',
@ -688,6 +710,9 @@ if (!isEmpty(main_node)) {
download_detour: 'main-out' download_detour: 'main-out'
}); });
} }
if (isEmpty(config.route.rule_set))
config.route.rule_set = null;
} else if (!isEmpty(default_outbound)) { } else if (!isEmpty(default_outbound)) {
uci.foreach(uciconfig, uciroutingrule, (cfg) => { uci.foreach(uciconfig, uciroutingrule, (cfg) => {
if (cfg.enabled !== '1') if (cfg.enabled !== '1')

View File

@ -7,13 +7,12 @@
'use strict'; 'use strict';
import { readfile, writefile } from 'fs'; import { writefile } from 'fs';
import { cursor } from 'uci'; import { cursor } from 'uci';
import { import {
executeCommand, isEmpty, strToBool, strToInt, isEmpty, strToBool, strToInt,
removeBlankAttrs, validateHostname, validation, removeBlankAttrs, HP_DIR, RUN_DIR
HP_DIR, RUN_DIR
} from 'homeproxy'; } from 'homeproxy';
/* UCI config start */ /* UCI config start */

View File

@ -5,7 +5,7 @@
*/ */
import { mkstemp } from 'fs'; import { mkstemp } from 'fs';
import { urldecode, urldecode_params } from 'luci.http'; import { urldecode_params } from 'luci.http';
/* Global variables start */ /* Global variables start */
export const HP_DIR = '/etc/homeproxy'; export const HP_DIR = '/etc/homeproxy';

View File

@ -11,11 +11,11 @@ import { open } from 'fs';
import { connect } from 'ubus'; import { connect } from 'ubus';
import { cursor } from 'uci'; import { cursor } from 'uci';
import { urldecode, urlencode, urldecode_params } from 'luci.http'; import { urldecode, urlencode } from 'luci.http';
import { init_action } from 'luci.sys'; import { init_action } from 'luci.sys';
import { import {
calcStringMD5, wGET, executeCommand, decodeBase64Str, calcStringMD5, wGET, decodeBase64Str,
getTime, isEmpty, parseURL, validation, getTime, isEmpty, parseURL, validation,
HP_DIR, RUN_DIR HP_DIR, RUN_DIR
} from 'homeproxy'; } from 'homeproxy';

View File

@ -7,7 +7,7 @@
'use strict'; 'use strict';
import { access, error, lstat, mkstemp, popen, readfile, writefile } from 'fs'; import { access, error, lstat, popen, readfile, writefile } from 'fs';
/* Kanged from ucode/luci */ /* Kanged from ucode/luci */
function shellquote(s) { function shellquote(s) {