/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022 ImmortalWrt.org
*/
'use strict';
'require form';
'require fs';
'require uci';
'require ui';
'require view';
'require homeproxy as hp';
'require tools.widgets as widgets';
function allowInsecureConfirm(ev, section_id, value) {
if (value === '1' && !confirm(_('Are you sure to allow insecure?')))
ev.target.firstElementChild.checked = null;
}
function parseShareLink(uri, features) {
var config;
uri = uri.split('://');
if (uri[0] && uri[1]) {
switch (uri[0]) {
case 'http':
case 'https':
var url = new URL('http://' + uri[1]);
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'http',
address: url.hostname,
port: url.port || '80',
username: url.username ? decodeURIComponent(url.username) : null,
password: url.password ? decodeURIComponent(url.password) : null,
tls: (uri[0] === 'https') ? '1' : '0'
};
break;
case 'hysteria':
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
var url = new URL('http://' + uri[1]);
var params = url.searchParams;
/* WeChat-Video / FakeTCP are unsupported by sing-box currently */
if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp'))
return null;
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'hysteria',
address: url.hostname,
port: url.port || '80',
hysteria_protocol: params.get('protocol') || 'udp',
hysteria_auth_type: params.get('auth') ? 'string' : null,
hysteria_auth_payload: params.get('auth'),
hysteria_obfs_password: params.get('obfsParam'),
hysteria_down_mbps: params.get('downmbps'),
hysteria_up_mbps: params.get('upmbps'),
tls: '1',
tls_sni: params.get('peer'),
tls_alpn: params.get('alpn'),
tls_insecure: params.get('insecure') ? '1' : '0'
};
break;
case 'hysteria2':
case 'hy2':
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
var url = new URL('http://' + uri[1]);
var params = url.searchParams;
if (!features.with_quic)
return null;
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'hysteria2',
address: url.hostname,
port: url.port || '80',
password: url.username ? (
decodeURIComponent(url.username + (url.password ? (':' + url.password) : ''))
) : null,
hysteria_obfs_type: params.get('obfs'),
hysteria_obfs_password: params.get('obfs-password'),
tls: '1',
tls_sni: params.get('sni'),
tls_insecure: params.get('insecure') ? '1' : '0'
};
break;
case 'socks':
case 'socks4':
case 'socks4a':
case 'socsk5':
case 'socks5h':
var url = new URL('http://' + uri[1]);
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'socks',
address: url.hostname,
port: url.port || '80',
username: url.username ? decodeURIComponent(url.username) : null,
password: url.password ? decodeURIComponent(url.password) : null,
socks_version: (uri[0].includes('4')) ? '4' : '5'
};
break;
case 'ss':
try {
/* "Lovely" Shadowrocket format */
try {
var suri = uri[1].split('#'), slabel = '';
if (suri.length <= 2) {
if (suri.length === 2)
slabel = '#' + suri[1];
uri[1] = hp.decodeBase64Str(suri[0]) + slabel;
}
} catch(e) { }
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
var url = new URL('http://' + uri[1]);
var userinfo;
if (url.username && url.password)
/* User info encoded with URIComponent */
userinfo = [url.username, decodeURIComponent(url.password)];
else if (url.username)
/* User info encoded with base64 */
userinfo = hp.decodeBase64Str(decodeURIComponent(url.username)).split(':');
if (!hp.shadowsocks_encrypt_methods.includes(userinfo[0]))
return null;
var plugin, plugin_opts;
if (url.search && url.searchParams.get('plugin')) {
var plugin_info = url.searchParams.get('plugin').split(';');
plugin = plugin_info[0];
plugin_opts = plugin_info.slice(1) ? plugin_info.slice(1).join(';') : null;
}
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'shadowsocks',
address: url.hostname,
port: url.port || '80',
shadowsocks_encrypt_method: userinfo[0],
password: userinfo[1],
shadowsocks_plugin: plugin,
shadowsocks_plugin_opts: plugin_opts
};
} catch(e) {
/* Legacy format https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
uri = uri[1].split('@');
if (uri.length < 2)
return null;
else if (uri.length > 2)
uri = [ uri.slice(0, -1).join('@'), uri.slice(-1).toString() ];
config = {
type: 'shadowsocks',
address: uri[1].split(':')[0],
port: uri[1].split(':')[1],
shadowsocks_encrypt_method: uri[0].split(':')[0],
password: uri[0].split(':').slice(1).join(':')
};
}
break;
case 'trojan':
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
var url = new URL('http://' + uri[1]);
var params = url.searchParams;
/* Check if password exists */
if (!url.username)
return null;
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'trojan',
address: url.hostname,
port: url.port || '80',
password: decodeURIComponent(url.username),
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
tls: '1',
tls_sni: params.get('sni')
};
switch (params.get('type')) {
case 'grpc':
config.grpc_servicename = params.get('serviceName');
break;
case 'ws':
config.ws_host = params.get('host') ? decodeURIComponent(params.get('host')) : null;
config.ws_path = params.get('path') ? decodeURIComponent(params.get('path')) : null;
if (config.ws_path && config.ws_path.includes('?ed=')) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = config.ws_path.split('?ed=')[1];
config.ws_path = config.ws_path.split('?ed=')[0];
}
break;
}
break;
case 'tuic':
/* https://github.com/daeuniverse/dae/discussions/182 */
var url = new URL('http://' + uri[1]);
var params = url.searchParams;
/* Check if uuid exists */
if (!url.username)
return null;
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'tuic',
address: url.hostname,
port: url.port || '80',
uuid: url.username,
password: url.password ? decodeURIComponent(url.password) : null,
tuic_congestion_control: params.get('congestion_control'),
tuic_udp_relay_mode: params.get('udp_relay_mode'),
tls: '1',
tls_sni: params.get('sni'),
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null
};
break;
case 'vless':
/* https://github.com/XTLS/Xray-core/discussions/716 */
var url = new URL('http://' + uri[1]);
var params = url.searchParams;
/* Unsupported protocol */
if (params.get('type') === 'kcp')
return null;
else if (params.get('type') === 'quic' && ((params.get('quicSecurity') && params.get('quicSecurity') !== 'none') || !features.with_quic))
return null;
/* Check if uuid and type exist */
if (!url.username || !params.get('type'))
return null;
config = {
label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null,
type: 'vless',
address: url.hostname,
port: url.port || '80',
uuid: url.username,
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
tls: ['tls', 'xtls', 'reality'].includes(params.get('security')) ? '1' : '0',
tls_sni: params.get('sni'),
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null,
tls_reality: (params.get('security') === 'reality') ? '1' : '0',
tls_reality_public_key: params.get('pbk') ? decodeURIComponent(params.get('pbk')) : null,
tls_reality_short_id: params.get('sid'),
tls_utls: features.with_utls ? params.get('fp') : null,
vless_flow: ['tls', 'reality'].includes(params.get('security')) ? params.get('flow') : null
};
switch (params.get('type')) {
case 'grpc':
config.grpc_servicename = params.get('serviceName');
break;
case 'http':
case 'tcp':
if (config.transport === 'http' || params.get('headerType') === 'http') {
config.http_host = params.get('host') ? decodeURIComponent(params.get('host')).split(',') : null;
config.http_path = params.get('path') ? decodeURIComponent(params.get('path')) : null;
}
break;
case 'ws':
config.ws_host = params.get('host') ? decodeURIComponent(params.get('host')) : null;
config.ws_path = params.get('path') ? decodeURIComponent(params.get('path')) : null;
if (config.ws_path && config.ws_path.includes('?ed=')) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = config.ws_path.split('?ed=')[1];
config.ws_path = config.ws_path.split('?ed=')[0];
}
break;
}
break;
case 'vmess':
/* "Lovely" shadowrocket format */
if (uri.includes('&'))
return null;
/* https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) */
uri = JSON.parse(hp.decodeBase64Str(uri[1]));
if (uri.v != '2')
return null;
/* Unsupported protocols */
else if (uri.net === 'kcp')
return null;
else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || !features.with_quic))
return null;
/* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
* else if (uri.aid && parseInt(uri.aid) !== 0)
* return null;
*/
config = {
label: uri.ps,
type: 'vmess',
address: uri.add,
port: uri.port,
uuid: uri.id,
vmess_alterid: uri.aid,
vmess_encrypt: uri.scy || 'auto',
transport: (uri.net !== 'tcp') ? uri.net : null,
tls: uri.tls === 'tls' ? '1' : '0',
tls_sni: uri.sni || uri.host,
tls_alpn: uri.alpn ? uri.alpn.split(',') : null
};
switch (uri.net) {
case 'grpc':
config.grpc_servicename = uri.path;
break;
case 'h2':
case 'tcp':
if (uri.net === 'h2' || uri.type === 'http') {
config.transport = 'http';
config.http_host = uri.host ? uri.host.split(',') : null;
config.http_path = uri.path;
}
break;
case 'ws':
config.ws_host = uri.host;
config.ws_path = uri.path;
if (config.ws_path && config.ws_path.includes('?ed=')) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = config.ws_path.split('?ed=')[1];
config.ws_path = config.ws_path.split('?ed=')[0];
}
break;
}
break;
}
}
if (config) {
if (!config.address || !config.port)
return null;
else if (!config.label)
config.label = config.address + ':' + config.port;
config.address = config.address.replace(/\[|\]/g, '');
}
return config;
}
function renderNodeSettings(section, data, features, main_node, routing_mode) {
var s = section, o;
s.rowcolors = true;
s.sortable = true;
s.nodescriptions = true;
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Node'), _('Add a node'), data[0]);
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
if (routing_mode !== 'custom') {
o = s.option(form.Button, '_apply', _('Apply'));
o.editable = true;
o.modalonly = false;
o.inputstyle = 'apply';
o.inputtitle = function(section_id) {
if (main_node == section_id) {
this.readonly = true;
return _('Applied');
} else {
this.readonly = false;
return _('Apply');
}
}
o.onclick = function(ev, section_id) {
uci.set(data[0], 'config', 'main_node', section_id);
ui.changes.apply(true);
return this.map.save(null, true);
}
}
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], 'node', 'label');
o.modalonly = true;
o = s.option(form.ListValue, 'type', _('Type'));
o.value('direct', _('Direct'));
o.value('http', _('HTTP'));
if (features.with_quic) {
o.value('hysteria', _('Hysteria'));
o.value('hysteria2', _('Hysteria2'));
}
o.value('shadowsocks', _('Shadowsocks'));
o.value('shadowtls', _('ShadowTLS'));
o.value('socks', _('Socks'));
o.value('ssh', _('SSH'));
o.value('trojan', _('Trojan'));
if (features.with_quic)
o.value('tuic', _('Tuic'));
if (features.with_wireguard)
o.value('wireguard', _('WireGuard'));
o.value('vless', _('VLESS'));
o.value('vmess', _('VMess'));
o.rmempty = false;
o = s.option(form.Value, 'address', _('Address'));
o.datatype = 'host';
o.depends({'type': 'direct', '!reverse': true});
o.rmempty = false;
o = s.option(form.Value, 'port', _('Port'));
o.datatype = 'port';
o.depends({'type': 'direct', '!reverse': true});
o.rmempty = false;
o = s.option(form.Value, 'username', _('Username'));
o.depends('type', 'http');
o.depends('type', 'socks');
o.depends('type', 'ssh');
o.modalonly = true;
o = s.option(form.Value, 'password', _('Password'));
o.password = true;
o.depends('type', 'http');
o.depends('type', 'hysteria2');
o.depends('type', 'shadowsocks');
o.depends('type', 'ssh');
o.depends('type', 'trojan');
o.depends('type', 'tuic');
o.depends({'type': 'shadowtls', 'shadowtls_version': '2'});
o.depends({'type': 'shadowtls', 'shadowtls_version': '3'});
o.depends({'type': 'socks', 'socks_version': '5'});
o.validate = function(section_id, value) {
if (section_id) {
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
var required_type = [ 'shadowsocks', 'shadowtls', 'trojan' ];
if (required_type.includes(type)) {
if (type === 'shadowsocks') {
var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
if (encmode === 'none')
return true;
}
if (!value)
return _('Expecting: %s').format(_('non-empty value'));
}
}
return true;
}
o.modalonly = true;
/* Direct config */
o = s.option(form.Value, 'override_address', _('Override address'),
_('Override the connection destination address.'));
o.datatype = 'host';
o.depends('type', 'direct');
o = s.option(form.Value, 'override_port', _('Override port'),
_('Override the connection destination port.'));
o.datatype = 'port';
o.depends('type', 'direct');
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
_('Write proxy protocol in the connection header.'));
o.value('', _('Disable'));
o.value('1', _('v1'));
o.value('2', _('v2'));
o.depends('type', 'direct');
o.modalonly = true;
/* 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.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.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(form.Value, 'hysteria_obfs_password', _('Obfuscate password'));
o.depends('type', 'hysteria');
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
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.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
_('The QUIC stream-level flow control window for receiving data.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria');
o.modalonly = true;
o = s.option(form.Value, 'hysteria_revc_window', _('QUIC connection receive window'),
_('The QUIC connection-level flow control window for receiving data.'));
o.datatype = 'uinteger';
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.default = o.disabled;
o.depends('type', 'hysteria');
o.modalonly = true;
/* Hysteria (2) config end */
/* Shadowsocks config start */
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
for (var i of hp.shadowsocks_encrypt_methods)
o.value(i);
/* Stream ciphers */
o.value('aes-128-ctr');
o.value('aes-192-ctr');
o.value('aes-256-ctr');
o.value('aes-128-cfb');
o.value('aes-192-cfb');
o.value('aes-256-cfb');
o.value('chacha20');
o.value('chacha20-ietf');
o.value('rc4-md5');
o.default = 'aes-128-gcm';
o.depends('type', 'shadowsocks');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.ListValue, 'shadowsocks_plugin', _('Plugin'));
o.value('', _('none'));
o.value('obfs-local');
o.value('v2ray-plugin');
o.depends('type', 'shadowsocks');
o.modalonly = true;
o = s.option(form.Value, 'shadowsocks_plugin_opts', _('Plugin opts'));
o.depends('shadowsocks_plugin', 'obfs-local');
o.depends('shadowsocks_plugin', 'v2ray-plugin');
o.modalonly = true;
/* Shadowsocks config end */
/* ShadowTLS config */
o = s.option(form.ListValue, 'shadowtls_version', _('ShadowTLS version'));
o.value('1', _('v1'));
o.value('2', _('v2'));
o.value('3', _('v3'));
o.default = '1';
o.depends('type', 'shadowtls');
o.rmempty = false;
o.modalonly = true;
/* Socks config */
o = s.option(form.ListValue, 'socks_version', _('Socks version'));
o.value('4', _('Socks4'));
o.value('4a', _('Socks4A'));
o.value('5', _('Socks5'));
o.default = '5';
o.depends('type', 'socks');
o.rmempty = false;
o.modalonly = true;
/* SSH config start */
o = s.option(form.Value, 'ssh_client_version', _('Client version'),
_('Random version will be used if empty.'));
o.depends('type', 'ssh');
o.modalonly = true;
o = s.option(form.DynamicList, 'ssh_host_key', _('Host key'),
_('Accept any if empty.'));
o.depends('type', 'ssh');
o.modalonly = true;
o = s.option(form.DynamicList, 'ssh_host_key_algo', _('Host key algorithms'))
o.depends('type', 'ssh');
o.modalonly = true;
o = s.option(form.Value, 'ssh_priv_key', _('Private key'));
o.password = true;
o.depends('type', 'ssh');
o.modalonly = true;
o = s.option(form.Value, 'ssh_priv_key_pp', _('Private key passphrase'));
o.password = true;
o.depends('type', 'ssh');
o.modalonly = true;
/* SSH config end */
/* TUIC config start */
o = s.option(form.Value, 'uuid', _('UUID'));
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', _('CUBIC'));
o.value('new_reno', _('New Reno'));
o.value('bbr', _('BBR'));
o.default = 'cubic';
o.depends('type', 'tuic');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.ListValue, 'tuic_udp_relay_mode', _('UDP relay mode'),
_('UDP packet relay mode.'));
o.value('', _('Default'));
o.value('native', _('Native'));
o.value('quic', _('QUIC'));
o.depends('type', 'tuic');
o.modalonly = true;
o = s.option(form.Flag, 'tuic_udp_over_stream', _('UDP over stream'),
_('This is the TUIC port of the UDP over TCP protocol, designed to provide a QUIC stream based UDP relay mode that TUIC does not provide.'));
o.default = o.disabled;
o.depends({'type': 'tuic','tuic_udp_relay_mode': ''});
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.
' +
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
o.default = o.disabled;
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 */
/* VMess / VLESS 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;
o = s.option(form.ListValue, 'vmess_encrypt', _('Encrypt method'));
o.value('auto');
o.value('none');
o.value('zero');
o.value('aes-128-gcm');
o.value('chacha20-poly1305');
o.default = 'auto';
o.depends('type', 'vmess');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'vmess_global_padding', _('Global padding'),
_('Protocol parameter. Will waste traffic randomly if enabled (enabled by default in v2ray and cannot be disabled).'));
o.default = o.enabled;
o.depends('type', 'vmess');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'vmess_authenticated_length', _('Authenticated length'),
_('Protocol parameter. Enable length block encryption.'));
o.default = o.disabled;
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) {
var 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.');
var tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
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 =
_('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.
' +
'Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval.');
this.map.findElement('id', 'cbid.homeproxy.%s.http_ping_timeout'.format(section_id)).nextElementSibling.innerHTML =
_('Specifies the timeout duration (in seconds) after sending a PING frame, within which a response must be received.
' +
'If a response to the PING frame is not received within the specified timeout duration, the connection will be closed.');
} 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.');
this.map.findElement('id', 'cbid.homeproxy.%s.http_ping_timeout'.format(section_id)).nextElementSibling.innerHTML =
_('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.modalonly = true;
/* gRPC config start */
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
o.depends('transport', 'grpc');
o.modalonly = true;
if (features.with_grpc) {
o = s.option(form.Flag, 'grpc_permit_without_stream', _('gRPC permit without stream'),
_('If enabled, the client transport sends keepalive pings even with no active connections.'));
o.default = o.disabled;
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.value('GET', _('GET'));
o.value('PUT', _('PUT'));
o.depends('transport', 'http');
o.modalonly = true;
o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
_('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.
' +
'Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval.'));
o.datatype = 'uinteger';
o.depends('transport', 'grpc');
o.depends({'transport': 'http', 'tls': '1'});
o.modalonly = true;
o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
_('Specifies the timeout duration (in seconds) after sending a PING frame, within which a response must be received.
' +
'If a response to the PING frame is not received within the specified timeout duration, the connection will be closed.'));
o.datatype = 'uinteger';
o.depends('transport', 'grpc');
o.depends({'transport': 'http', 'tls': '1'});
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'));
o.value('Sec-WebSocket-Protocol');
o.depends('transport', 'ws');
o.modalonly = true;
/* WebSocket config end */
o = s.option(form.ListValue, 'packet_encoding', _('Packet encoding'));
o.value('', _('none'));
o.value('packetaddr', _('packet addr (v2ray-core v5+)'));
o.value('xudp', _('Xudp (Xray-core)'));
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.modalonly = true;
/* Transport config end */
/* Wireguard config start */
o = s.option(form.Flag, 'wireguard_gso', _('Generic segmentation offload'));
o.default = o.disabled;
o.depends('type', 'wireguard');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.DynamicList, 'wireguard_local_address', _('Local address'),
_('List of IP (v4 or v6) addresses prefixes to be assigned to the interface.'));
o.datatype = 'cidr';
o.depends('type', 'wireguard');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'wireguard_private_key', _('Private key'),
_('WireGuard requires base64-encoded private keys.'));
o.password = true;
o.depends('type', 'wireguard');
o.validate = L.bind(hp.validateBase64Key, this, 44);
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'wireguard_peer_public_key', _('Peer pubkic key'),
_('WireGuard peer public key.'));
o.depends('type', 'wireguard');
o.validate = L.bind(hp.validateBase64Key, this, 44);
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'wireguard_pre_shared_key', _('Pre-shared key'),
_('WireGuard pre-shared key.'));
o.password = true;
o.depends('type', 'wireguard');
o.validate = L.bind(hp.validateBase64Key, this, 44);
o.modalonly = true;
o = s.option(form.DynamicList, 'wireguard_reserved', _('Reserved field bytes'));
o.datatype = 'integer';
o.depends('type', 'wireguard');
o.modalonly = true;
o = s.option(form.Value, 'wireguard_mtu', _('MTU'));
o.datatype = 'range(0,9000)';
o.default = '1408';
o.depends('type', 'wireguard');
o.rmempty = false;
o.modalonly = true;
/* Wireguard config end */
/* Mux config start */
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
o.default = o.disabled;
o.depends('type', 'shadowsocks');
o.depends('type', 'trojan');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.modalonly = true;
o = s.option(form.ListValue, 'multiplex_protocol', _('Protocol'),
_('Multiplex protocol.'));
o.value('h2mux');
o.value('smux');
o.value('yamux');
o.default = 'h2mux';
o.depends('multiplex', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'multiplex_max_connections', _('Maximum connections'));
o.datatype = 'uinteger';
o.depends('multiplex', '1');
o.modalonly = true;
o = s.option(form.Value, 'multiplex_min_streams', _('Minimum streams'),
_('Minimum multiplexed streams in a connection before opening a new connection.'));
o.datatype = 'uinteger';
o.depends('multiplex', '1');
o.modalonly = true;
o = s.option(form.Value, 'multiplex_max_streams', _('Maximum streams'),
_('Maximum multiplexed streams in a connection before opening a new connection.
' +
'Conflict with Maximum connections
and Minimum streams
.'));
o.datatype = 'uinteger';
o.depends({'multiplex': '1', 'multiplex_max_connections': '', 'multiplex_min_streams': ''});
o.modalonly = true;
o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
o.default = o.disabled;
o.depends('multiplex', '1');
o.modalonly = true;
o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
_('Enable TCP Brutal congestion control algorithm'));
o.default = o.disabled;
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.default = o.disabled;
o.depends('type', 'http');
o.depends('type', 'hysteria');
o.depends('type', 'hysteria2');
o.depends('type', 'shadowtls');
o.depends('type', 'trojan');
o.depends('type', 'tuic');
o.depends('type', 'vless');
o.depends('type', 'vmess');
o.validate = function(section_id, value) {
if (section_id) {
var 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;
if (['hysteria', 'hysteria2', 'shadowtls', '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.Flag, 'tls_insecure', _('Allow insecure'),
_('Allow insecure connection at TLS client.') +
'
' +
_('This is DANGEROUS, your traffic is almost like PLAIN TEXT! Use at your own risk!'));
o.default = o.disabled;
o.depends('tls', '1');
o.onchange = allowInsecureConfirm;
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 (var 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 (var i of hp.tls_versions)
o.value(i);
o.depends('tls', '1');
o.modalonly = true;
o = s.option(form.MultiValue, '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 (var i of hp.tls_cipher_suites)
o.value(i);
o.depends('tls', '1');
o.optional = true;
o.modalonly = true;
o = s.option(form.Flag, 'tls_self_sign', _('Append self-signed certificate'),
_('If you have the root certificate, use this option instead of allowing insecure.'));
o.default = o.disabled;
o.depends('tls_insecure', '0');
o.modalonly = true;
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
_('The path to the server certificate, in PEM format.'));
o.value('/etc/homeproxy/certs/client_ca.pem');
o.depends('tls_self_sign', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
_('Save your configuration before uploading files!'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({'tls_self_sign': '1', 'tls_cert_path': '/etc/homeproxy/certs/client_ca.pem'});
o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'client_ca');
o.modalonly = true;
if (features.with_ech) {
o = s.option(form.Flag, 'tls_ech', _('Enable ECH'),
_('ECH (Encrypted Client Hello) is a TLS extension that allows a client to encrypt the first part of its ClientHello message.'));
o.depends('tls', '1');
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.Flag, 'tls_ech_tls_disable_drs', _('Disable dynamic record sizing'));
o.depends('tls_ech', '1');
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.Flag, 'tls_ech_enable_pqss', _('Enable PQ signature schemes'));
o.depends('tls_ech', '1');
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.DynamicList, 'tls_ech_config', _('ECH config'));
o.depends('tls_ech', '1');
o.modalonly = true;
}
if (features.with_utls) {
o = s.option(form.ListValue, 'tls_utls', _('uTLS fingerprint'),
_('uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.'));
o.value('', _('Disable'));
o.value('360');
o.value('android');
o.value('chrome');
o.value('chrome_psk');
o.value('chrome_psk_shuffle');
o.value('chrome_padding_psk_shuffle');
o.value('chrome_pq');
o.value('chrome_pq_psk');
o.value('edge');
o.value('firefox');
o.value('ios');
o.value('qq');
o.value('random');
o.value('randomized');
o.value('safari');
o.depends({'tls': '1', 'type': /^((?!hysteria2?$).)+$/});
o.validate = function(section_id, value) {
if (section_id) {
let tls_reality = this.map.findElement('id', 'cbid.homeproxy.%s.tls_reality'.format(section_id)).firstElementChild;
if (tls_reality.checked && !value)
return _('Expecting: %s').format(_('non-empty value'));
let vless_flow = this.map.lookupOption('vless_flow', section_id)[0].formvalue(section_id);
if ((tls_reality.checked || vless_flow) && ['360', 'android'].includes(value))
return _('Unsupported fingerprint!');
}
return true;
}
o.modalonly = true;
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
o.default = o.disabled;
o.depends({'tls': '1', 'type': 'vless'});
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_public_key', _('REALITY public key'));
o.depends('tls_reality', '1');
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'tls_reality_short_id', _('REALITY short ID'));
o.depends('tls_reality', '1');
o.modalonly = true;
}
/* TLS config end */
/* Extra settings start */
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'));
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
_('Enable UDP fragmentation.'));
o.default = o.disabled;
o.modalonly = true;
o = s.option(form.Flag, 'udp_over_tcp', _('UDP over TCP'),
_('Enable the SUoT protocol, requires server support. Conflict with multiplex.'));
o.default = o.disabled;
o.depends('type', 'socks');
o.depends({'type': 'shadowsocks', 'multiplex': '0'});
o.modalonly = true;
o = s.option(form.ListValue, 'udp_over_tcp_version', _('SUoT version'));
o.value('1', _('v1'));
o.value('2', _('v2'));
o.default = '2';
o.depends('udp_over_tcp', '1');
o.modalonly = true;
/* Extra settings end */
return s;
}
return view.extend({
load: function() {
return Promise.all([
uci.load('homeproxy'),
hp.getBuiltinFeatures()
]);
},
render: function(data) {
var m, s, o, ss, so;
var main_node = uci.get(data[0], 'config', 'main_node');
var routing_mode = uci.get(data[0], 'config', 'routing_mode');
var features = data[1];
/* Cache subscription information, it will be called multiple times */
var subinfo = [];
for (var suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
const url = new URL(suburl);
const urlhash = hp.calcStringMD5(suburl.replace(/#.*$/, ''));
const title = url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname;
subinfo.push({ 'hash': urlhash, 'title': title });
}
m = new form.Map('homeproxy', _('Edit nodes'));
s = m.section(form.NamedSection, 'subscription', 'homeproxy');
/* Node settings start */
/* User nodes start */
s.tab('node', _('Nodes'));
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
ss.addremove = true;
ss.filter = function(section_id) {
for (var info of subinfo)
if (info.hash === uci.get(data[0], section_id, 'grouphash'))
return false;
return true;
}
/* Import subscription links start */
/* Thanks to luci-app-shadowsocks-libev */
ss.handleLinkImport = function() {
var textarea = new ui.Textarea();
ui.showModal(_('Import share links'), [
E('p', _('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.')),
textarea.render(),
E('div', { class: 'right' }, [
E('button', {
class: 'btn',
click: ui.hideModal
}, [ _('Cancel') ]),
'',
E('button', {
class: 'btn cbi-button-action',
click: ui.createHandlerFn(this, function() {
var input_links = textarea.getValue().trim().split('\n');
if (input_links && input_links[0]) {
/* Remove duplicate lines */
input_links = input_links.reduce((pre, cur) =>
(!pre.includes(cur) && pre.push(cur), pre), []);
var allow_insecure = uci.get(data[0], 'subscription', 'allow_insecure');
var packet_encoding = uci.get(data[0], 'subscription', 'packet_encoding');
var imported_node = 0;
input_links.forEach((l) => {
var config = parseShareLink(l, features);
if (config) {
if (config.tls === '1' && allow_insecure === '1')
config.tls_insecure = '1'
if (['vless', 'vmess'].includes(config.type))
config.packet_encoding = packet_encoding
var nameHash = hp.calcStringMD5(config.label);
var sid = uci.add(data[0], 'node', nameHash);
Object.keys(config).forEach((k) => {
uci.set(data[0], sid, k, config[k]);
});
imported_node++;
}
});
if (imported_node === 0)
ui.addNotification(null, E('p', _('No valid share link found.')));
else
ui.addNotification(null, E('p', _('Successfully imported %s nodes of total %s.').format(
imported_node, input_links.length)));
return uci.save()
.then(L.bind(this.map.load, this.map))
.then(L.bind(this.map.reset, this.map))
.then(L.ui.hideModal)
.catch(function() {});
} else {
return ui.hideModal();
}
})
}, [ _('Import') ])
])
])
}
ss.renderSectionAdd = function(/* ... */) {
var el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments),
nameEl = el.querySelector('.cbi-section-create-name');
ui.addValidator(nameEl, 'uciname', true, (v) => {
var button = el.querySelector('.cbi-section-create > .cbi-button-add');
var uciconfig = this.uciconfig || this.map.config;
if (!v) {
button.disabled = true;
return true;
} else if (uci.get(uciconfig, v)) {
button.disabled = true;
return _('Expecting: %s').format(_('unique UCI identifier'));
} else {
button.disabled = null;
return true;
}
}, 'blur', 'keyup');
el.appendChild(E('button', {
'class': 'cbi-button cbi-button-add',
'title': _('Import share links'),
'click': ui.createHandlerFn(this, 'handleLinkImport')
}, [ _('Import share links') ]));
return el;
}
/* Import subscription links end */
/* User nodes end */
/* Subscription nodes start */
for (const info of subinfo) {
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
ss.filter = function(section_id) {
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
}
}
/* Subscription nodes end */
/* Node settings end */
/* Subscriptions settings start */
s.tab('subscription', _('Subscriptions'));
o = s.taboption('subscription', form.Flag, 'auto_update', _('Auto update'),
_('Auto update subscriptions and geodata.'));
o.default = o.disabled;
o.rmempty = false;
o = s.taboption('subscription', form.ListValue, 'auto_update_time', _('Update time'));
for (var i = 0; i < 24; i++)
o.value(i, i + ':00');
o.default = '2';
o.depends('auto_update', '1');
o = s.taboption('subscription', form.Flag, 'update_via_proxy', _('Update via proxy'),
_('Update subscriptions via proxy.'));
o.default = o.disabled;
o.rmempty = false;
o = s.taboption('subscription', form.DynamicList, 'subscription_url', _('Subscription URL-s'),
_('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.'));
o.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;
}
o = s.taboption('subscription', form.ListValue, 'filter_nodes', _('Filter nodes'),
_('Drop/keep specific nodes from subscriptions.'));
o.value('disabled', _('Disable'));
o.value('blacklist', _('Blacklist mode'));
o.value('whitelist', _('Whitelist mode'));
o.default = 'disabled';
o.rmempty = false;
o = s.taboption('subscription', form.DynamicList, 'filter_keywords', _('Filter keywords'),
_('Drop/keep nodes that contain the specific keywords. Regex is supported.'));
o.depends({'filter_nodes': 'disabled', '!reverse': true});
o.rmempty = false;
o = s.taboption('subscription', form.Value, 'user_agent', _('User-Agent'));
o.placeholder = 'Wget/1.21 (HomeProxy, like v2rayN)';
o = s.taboption('subscription', form.Flag, 'allow_insecure', _('Allow insecure'),
_('Allow insecure connection by default when add nodes from subscriptions.') +
'
' +
_('This is DANGEROUS, your traffic is almost like PLAIN TEXT! Use at your own risk!'));
o.default = o.disabled;
o.rmempty = false;
o.onchange = allowInsecureConfirm;
o = s.taboption('subscription', form.ListValue, 'packet_encoding', _('Default packet encoding'));
o.value('', _('none'));
o.value('packetaddr', _('packet addr (v2ray-core v5+)'));
o.value('xudp', _('Xudp (Xray-core)'));
o = s.taboption('subscription', form.Button, '_save_subscriptions', _('Save subscriptions settings'),
_('NOTE: Save current settings before updating subscriptions.'));
o.inputstyle = 'apply';
o.inputtitle = _('Save current settings');
o.onclick = function() {
ui.changes.apply(true);
return this.map.save(null, true);
}
o = s.taboption('subscription', form.Button, '_update_subscriptions', _('Update nodes from subscriptions'));
o.inputstyle = 'apply';
o.inputtitle = function(section_id) {
var sublist = uci.get(data[0], section_id, 'subscription_url') || [];
if (sublist.length > 0) {
return _('Update %s subscriptions').format(sublist.length);
} else {
this.readonly = true;
return _('No subscription available')
}
}
o.onclick = function() {
return fs.exec_direct('/etc/homeproxy/scripts/update_subscriptions.uc').then((res) => {
return location.reload();
}).catch((err) => {
ui.addNotification(null, E('p', _('An error occurred during updating subscriptions: %s').format(err)));
return this.map.reset();
});
}
o = s.taboption('subscription', form.Button, '_remove_subscriptions', _('Remove all nodes from subscriptions'));
o.inputstyle = 'reset';
o.inputtitle = function() {
var subnodes = [];
uci.sections(data[0], 'node', (res) => {
if (res.grouphash)
subnodes = subnodes.concat(res['.name'])
});
if (subnodes.length > 0) {
return _('Remove %s nodes').format(subnodes.length);
} else {
this.readonly = true;
return _('No subscription node');
}
}
o.onclick = function() {
var subnodes = [];
uci.sections(data[0], 'node', (res) => {
if (res.grouphash)
subnodes = subnodes.concat(res['.name'])
});
for (var i in subnodes)
uci.remove(data[0], subnodes[i]);
if (subnodes.includes(uci.get(data[0], 'config', 'main_node')))
uci.set(data[0], 'config', 'main_node', 'nil');
if (subnodes.includes(uci.get(data[0], 'config', 'main_udp_node')))
uci.set(data[0], 'config', 'main_udp_node', 'nil');
this.inputtitle = _('%s nodes removed').format(subnodes.length);
this.readonly = true;
return this.map.save(null, true);
}
/* Subscriptions settings end */
return m.render();
}
});