diff --git a/htdocs/luci-static/resources/view/homeproxy/client.js b/htdocs/luci-static/resources/view/homeproxy/client.js index a7a3927f..1d90b3b2 100644 --- a/htdocs/luci-static/resources/view/homeproxy/client.js +++ b/htdocs/luci-static/resources/view/homeproxy/client.js @@ -378,22 +378,23 @@ return view.extend({ so = ss.option(form.ListValue, 'node', _('Node'), _('Outbound node')); + so.value('urltest', _('URLTest')); for (var i in proxy_nodes) so.value(i, proxy_nodes[i]); - so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'node'); so.editable = true; so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'), _('If set, the server domain name will be resolved to IP before connecting.
')); for (var i in hp.dns_strategy) so.value(i, hp.dns_strategy[i]); + so.depends({'node': 'urltest', '!reverse': true}); so.modalonly = true; so = ss.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'), _('The network interface to bind to.')); so.multiple = false; so.noaliases = true; - so.depends('outbound', ''); + so.depends({'outbound': '', 'node': /^((?!urltest$).)+$/}); so.modalonly = true; so = ss.option(form.ListValue, 'outbound', _('Outbound'), @@ -416,9 +417,12 @@ return view.extend({ var conflict = false; 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) conflict = true; + else if (res?.urltest_nodes?.includes(node) && res['.name'] == value) + conflict = true; + } }); if (conflict) return _('Recursive outbound detected!'); @@ -426,6 +430,66 @@ return view.extend({ return true; } + so.depends({'node': 'urltest', '!reverse': true}); + + so = ss.option(hp.CBIStaticList, 'urltest_nodes', _('URLTest nodes'), + _('List of nodes to test.')); + for (var i in proxy_nodes) + so.value(i, proxy_nodes[i]); + so.depends('node', 'urltest'); + so.modalonly = true; + + so = ss.option(form.Value, 'urltest_url', _('Test URL'), + _('The URL to test. https://www.gstatic.com/generate_204 will be used if empty.')); + so.validate = function(section_id, value) { + if (section_id && value) { + try { + var url = new URL(value); + if (!url.hostname) + return _('Expecting: %s').format(_('valid URL')); + } + catch(e) { + return _('Expecting: %s').format(_('valid URL')); + } + } + + return true; + } + so.depends('node', 'urltest'); + so.modalonly = true; + + so = ss.option(form.Value, 'urltest_interval', _('Test interval'), + _('The test interval in seconds. 180 will be used if empty.')); + so.datatype = 'uinteger'; + so.validate = function(section_id, value) { + if (section_id && value) { + var idle_timeout = this.map.lookupOption('urltest_idle_timeout', section_id)[0].formvalue(section_id) || '1800'; + if (parseInt(value) > parseInt(idle_timeout)) + return _('Test interval must be less or equal than idle timeout.'); + } + + return true; + } + so.depends('node', 'urltest'); + so.modalonly = true; + + so = ss.option(form.Value, 'urltest_tolerance', _('Test tolerance'), + _('The test tolerance in milliseconds. 50 will be used if empty.')); + so.datatype = 'uinteger'; + so.depends('node', 'urltest'); + so.modalonly = true; + + so = ss.option(form.Value, 'urltest_idle_timeout', _('Idle timeout'), + _('The idle timeout in seconds. 1800 will be used if empty.')); + so.datatype = 'uinteger'; + so.depends('node', 'urltest'); + so.modalonly = true; + + so = ss.option(form.Flag, 'urltest_interrupt_exist_connections', _('Interrupt existing connections'), + _('Interrupt existing connections when the selected outbound has changed.')); + so.default = so.disabled; + so.depends('node', 'urltest'); + so.modalonly = true; /* Routing nodes end */ /* Routing rules start */ @@ -580,13 +644,12 @@ return view.extend({ _('Match user name.')); so.modalonly = true; - so = ss.taboption('field_other', form.MultiValue, 'rule_set', _('Rule set'), + so = ss.taboption('field_other', hp.CBIStaticList, 'rule_set', _('Rule set'), _('Match rule set.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; - this.value('', _('-- Please choose --')); uci.sections(data[0], 'ruleset', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); @@ -920,13 +983,12 @@ return view.extend({ _('Match user name.')); so.modalonly = true; - so = ss.taboption('field_other', form.MultiValue, 'rule_set', _('Rule set'), + so = ss.taboption('field_other', hp.CBIStaticList, 'rule_set', _('Rule set'), _('Match rule set.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; - this.value('', _('-- Please choose --')); uci.sections(data[0], 'ruleset', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); diff --git a/root/etc/homeproxy/scripts/generate_client.uc b/root/etc/homeproxy/scripts/generate_client.uc index dce37e7d..3716a0d8 100755 --- a/root/etc/homeproxy/scripts/generate_client.uc +++ b/root/etc/homeproxy/scripts/generate_client.uc @@ -300,6 +300,8 @@ function get_outbound(cfg) { const node = uci.get(uciconfig, cfg, 'node'); if (isEmpty(node)) die(sprintf("%s's node is missing, please check your configuration.", cfg)); + else if (node === 'urltest') + return 'cfg-' + cfg + '-out'; else return 'cfg-' + node + '-out'; } @@ -571,17 +573,39 @@ if (!isEmpty(main_node)) { push(config.outbounds, generate_outbound(main_udp_node_cfg)); config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out'; } -} else if (!isEmpty(default_outbound)) +} else if (!isEmpty(default_outbound)) { + let urltest_nodes = [], + routing_nodes = []; + uci.foreach(uciconfig, uciroutingnode, (cfg) => { if (cfg.enabled !== '1') return; - const outbound = uci.get_all(uciconfig, cfg.node) || {}; - push(config.outbounds, generate_outbound(outbound)); - config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy; - config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface; - config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound); + if (cfg.node === 'urltest') { + push(config.outbounds, { + type: 'urltest', + tag: 'cfg-' + cfg['.name'] + '-out', + outbounds: map(cfg.urltest_nodes, (k) => `cfg-${k}-out`), + url: cfg.urltest_url, + interval: cfg.urltest_interval ? (cfg.urltest_interval + 's') : null, + tolerance: strToInt(cfg.urltest_tolerance), + idle_timeout: cfg.urltest_idle_timeout ? (cfg.urltest_idle_timeout + 's') : null, + interrupt_exist_connections: (cfg.urltest_interrupt_exist_connections === '1') + }); + urltest_nodes = [...urltest_nodes, ...filter(cfg.urltest_nodes, ((l) => !~index(urltest_nodes, l)))]; + } else { + const outbound = uci.get_all(uciconfig, cfg.node) || {}; + push(config.outbounds, generate_outbound(outbound)); + config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy; + config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface; + config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound); + push(routing_nodes, cfg.node); + } }); + + for (let i in filter(urltest_nodes, ((l) => !~index(routing_nodes, l)))) + push(config.outbounds, generate_outbound(uci.get_all(uciconfig, i))); +} /* Outbound end */ /* Routing rules start */ @@ -593,7 +617,6 @@ config.route = { outbound: 'dns-out' } ], - rule_set: [], auto_detect_interface: isEmpty(default_interface) ? true : null, default_interface: default_interface }; @@ -654,6 +677,7 @@ if (!isEmpty(main_node)) { /* Rule set */ if (routing_mode === 'custom') { + config.route.rule_set = []; uci.foreach(uciconfig, uciruleset, (cfg) => { if (cfg.enabled !== '1') return null;