824 lines
28 KiB
JavaScript
824 lines
28 KiB
JavaScript
'use strict';
|
||
'require form';
|
||
'require fs';
|
||
'require network';
|
||
'require poll';
|
||
'require rpc';
|
||
'require uci';
|
||
'require ui';
|
||
'require view';
|
||
|
||
'require fchomo as hm';
|
||
'require tools.firewall as fwtool';
|
||
'require tools.widgets as widgets';
|
||
|
||
const callResVersion = rpc.declare({
|
||
object: 'luci.fchomo',
|
||
method: 'resources_get_version',
|
||
params: ['type', 'repo'],
|
||
expect: { '': {} }
|
||
});
|
||
|
||
const callCrondSet = rpc.declare({
|
||
object: 'luci.fchomo',
|
||
method: 'crond_set',
|
||
params: ['type', 'expr'],
|
||
expect: { '': {} }
|
||
});
|
||
|
||
function getRandom(min, max) {
|
||
const floatRandom = Math.random()
|
||
const difference = max - min
|
||
|
||
// A random number between 0 and the difference
|
||
const random = Math.round(difference * floatRandom)
|
||
|
||
return random + min
|
||
}
|
||
|
||
function handleResUpdate(type, repo) {
|
||
const callResUpdate = rpc.declare({
|
||
object: 'luci.fchomo',
|
||
method: 'resources_update',
|
||
params: ['type', 'repo'],
|
||
expect: { '': {} }
|
||
});
|
||
|
||
// Dynamic repo
|
||
let label;
|
||
if (repo) {
|
||
const section_id = this.section.section;
|
||
let weight = document.getElementById(this.cbid(section_id));
|
||
if (weight)
|
||
repo = weight.firstChild.value,
|
||
label = weight.firstChild.selectedOptions[0].label;
|
||
}
|
||
|
||
return L.resolveDefault(callResUpdate(type, repo), {}).then((res) => {
|
||
switch (res.status) {
|
||
case 0:
|
||
this.description = (repo ? label + ' ' : '') + _('Successfully updated.');
|
||
break;
|
||
case 1:
|
||
this.description = (repo ? label + ' ' : '') + _('Update failed.');
|
||
break;
|
||
case 2:
|
||
this.description = (repo ? label + ' ' : '') + _('Already in updating.');
|
||
break;
|
||
case 3:
|
||
this.description = (repo ? label + ' ' : '') + _('Already at the latest version.');
|
||
break;
|
||
default:
|
||
this.description = (repo ? label + ' ' : '') + _('Unknown error.');
|
||
break;
|
||
}
|
||
|
||
return this.map.reset();
|
||
});
|
||
}
|
||
|
||
function renderResVersion(El, type, repo) {
|
||
return L.resolveDefault(callResVersion(type, repo), {}).then((res) => {
|
||
let resEl = E([
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-apply',
|
||
'click': ui.createHandlerFn(this, handleResUpdate, type, repo)
|
||
}, [ _('Check update') ]),
|
||
updateResVersion(E('span', { style: 'border: unset; font-weight: bold; align-items: center' }), res.version)
|
||
]);
|
||
|
||
if (El) {
|
||
El.appendChild(resEl);
|
||
El.lastChild.style.display = 'flex';
|
||
} else
|
||
El = resEl;
|
||
|
||
return El;
|
||
});
|
||
}
|
||
|
||
function updateResVersion(El, version) {
|
||
if (El) {
|
||
El.style.color = version ? 'green' : 'red';
|
||
El.innerHTML = ' %s'.format(version || _('not found'));
|
||
}
|
||
|
||
return El;
|
||
}
|
||
|
||
function renderNATBehaviorTest(El) {
|
||
let resEl = E('div', { 'class': 'control-group' }, [
|
||
E('select', {
|
||
'id': '_status_nattest_l4proto',
|
||
'class': 'cbi-input-select',
|
||
'style': 'width: 5em'
|
||
}, [
|
||
E('option', { 'value': 'udp' }, 'UDP'),
|
||
E('option', { 'value': 'tcp' }, 'TCP')
|
||
]),
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-apply',
|
||
'click': ui.createHandlerFn(this, function() {
|
||
const stun = this.formvalue(this.section.section);
|
||
const l4proto = document.getElementById('_status_nattest_l4proto').value;
|
||
const l4proto_idx = document.getElementById('_status_nattest_l4proto').selectedIndex;
|
||
|
||
return fs.exec_direct('/etc/fchomo/scripts/natcheck.sh', [stun, l4proto, getRandom(32768, 61000)]).then((stdout) => {
|
||
this.description = '<details><summary>' + _('Expand/Collapse result') + '</summary>' + stdout + '</details>';
|
||
|
||
return this.map.reset().then((res) => {
|
||
document.getElementById('_status_nattest_l4proto').selectedIndex = l4proto_idx;
|
||
});
|
||
});
|
||
})
|
||
}, [ _('Check') ])
|
||
]);
|
||
|
||
let newEl = E('div', { style: 'font-weight: bold; align-items: center; display: flex' }, []);
|
||
if (El) {
|
||
newEl.appendChild(E([El, resEl]));
|
||
} else
|
||
newEl.appendChild(resEl);
|
||
|
||
return newEl;
|
||
}
|
||
|
||
return view.extend({
|
||
load() {
|
||
return Promise.all([
|
||
uci.load('fchomo'),
|
||
hm.getFeatures(),
|
||
network.getHostHints(),
|
||
hm.getServiceStatus('mihomo-c'),
|
||
hm.getClashAPI('mihomo-c'),
|
||
hm.getServiceStatus('mihomo-s'),
|
||
hm.getClashAPI('mihomo-s'),
|
||
callResVersion('geoip').then((res) => { return res.version }),
|
||
callResVersion('geosite').then((res) => { return res.version })
|
||
]);
|
||
},
|
||
|
||
render(data) {
|
||
const features = data[1];
|
||
const hosts = data[2]?.hosts;
|
||
const CisRunning = data[3];
|
||
const CclashAPI = data[4];
|
||
const SisRunning = data[5];
|
||
const SclashAPI = data[6];
|
||
const res_ver_geoip = data[7];
|
||
const res_ver_geosite = data[8];
|
||
|
||
const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo');
|
||
|
||
let m, s, o, ss, so;
|
||
|
||
m = new form.Map('fchomo', _('FullCombo Mihomo'),
|
||
'<img src="' + hm.sharktaikogif + '" title="Ciallo~(∠・ω< )⌒☆" height="52"></img>');
|
||
|
||
s = m.section(form.NamedSection, 'config', 'fchomo');
|
||
|
||
/* Overview START */
|
||
s.tab('status', _('Overview'));
|
||
|
||
/* Service status */
|
||
o = s.taboption('status', form.SectionValue, '_status', form.NamedSection, 'config', 'fchomo', _('Service status'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.DummyValue, '_core_version', _('Core version'));
|
||
so.cfgvalue = function() {
|
||
return E('strong', [features.core_version || _('Unknown')]);
|
||
}
|
||
|
||
so = ss.option(form.DummyValue, '_luciapp_version', _('Application version'));
|
||
so.cfgvalue = function() {
|
||
return E('strong', [features.luciapp_version || _('Unknown')]);
|
||
}
|
||
|
||
so = ss.option(form.DummyValue, '_client_status', _('Client status'));
|
||
so.cfgvalue = function() { return hm.renderStatus('_client_bar', CisRunning ? { ...CclashAPI, dashboard_repo: dashboard_repo } : false, 'mihomo-c') }
|
||
poll.add(function() {
|
||
return hm.getServiceStatus('mihomo-c').then((isRunning) => {
|
||
hm.updateStatus(document.getElementById('_client_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-c');
|
||
});
|
||
})
|
||
|
||
so = ss.option(form.DummyValue, '_server_status', _('Server status'));
|
||
so.cfgvalue = function() { return hm.renderStatus('_server_bar', SisRunning ? { ...SclashAPI, dashboard_repo: dashboard_repo } : false, 'mihomo-s') }
|
||
poll.add(function() {
|
||
return hm.getServiceStatus('mihomo-s').then((isRunning) => {
|
||
hm.updateStatus(document.getElementById('_server_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-s');
|
||
});
|
||
})
|
||
|
||
so = ss.option(form.Button, '_reload', _('Reload All'));
|
||
so.inputtitle = _('Reload');
|
||
so.inputstyle = 'apply';
|
||
so.onclick = L.bind(hm.handleReload, so, null);
|
||
|
||
so = ss.option(form.DummyValue, '_conn_check', _('Connection check'));
|
||
so.cfgvalue = function() {
|
||
const callConnStat = rpc.declare({
|
||
object: 'luci.fchomo',
|
||
method: 'connection_check',
|
||
params: ['url'],
|
||
expect: { '': {} }
|
||
});
|
||
|
||
const ElId = '_connection_check_results';
|
||
|
||
return E([
|
||
E('button', {
|
||
'class': 'cbi-button cbi-button-apply',
|
||
'click': ui.createHandlerFn(this, function() {
|
||
let weight = document.getElementById(ElId);
|
||
|
||
weight.innerHTML = '';
|
||
return hm.checkurls.forEach((site) => {
|
||
L.resolveDefault(callConnStat(site[0]), {}).then((res) => {
|
||
weight.innerHTML += '<span style="color:%s"> %s</span>'.format((res.httpcode && res.httpcode.match(/^20\d$/)) ? 'green' : 'red', site[1]);
|
||
});
|
||
});
|
||
})
|
||
}, [ _('Check') ]),
|
||
E('strong', { id: ElId }, [
|
||
E('span', { style: 'color:gray' }, ' ' + _('unchecked'))
|
||
])
|
||
]);
|
||
}
|
||
|
||
so = ss.option(form.Value, '_nattest', _('Check routerself NAT Behavior'));
|
||
so.default = hm.stunserver[0][0];
|
||
hm.stunserver.forEach((res) => {
|
||
so.value.apply(so, res);
|
||
})
|
||
so.rmempty = false;
|
||
if (!features.hm_has_stunclient) {
|
||
so.description = _('To check NAT Behavior you need to install <a href="%s"><b>stuntman-client</b></a> first')
|
||
.format('https://github.com/muink/openwrt-stuntman');
|
||
so.readonly = true;
|
||
} else {
|
||
so.renderWidget = function(/* ... */) {
|
||
let El = form.Value.prototype.renderWidget.apply(this, arguments);
|
||
|
||
return renderNATBehaviorTest.call(this, El);
|
||
}
|
||
}
|
||
so.onchange = function(ev, section_id, value) {
|
||
this.default = value;
|
||
}
|
||
so.write = function() {};
|
||
so.remove = function() {};
|
||
|
||
/* Resources management */
|
||
o = s.taboption('status', form.SectionValue, '_config', form.NamedSection, 'resources', 'fchomo', _('Resources management'));
|
||
ss = o.subsection;
|
||
|
||
if (!res_ver_geoip || !res_ver_geosite) {
|
||
so = ss.option(form.Button, '_upload_initia', _('Upload initial package'));
|
||
so.inputstyle = 'action';
|
||
so.inputtitle = _('Upload...');
|
||
so.onclick = L.bind(hm.uploadInitialPack, so);
|
||
}
|
||
|
||
so = ss.option(form.Flag, 'auto_update', _('Auto update'),
|
||
_('Auto update resources.'));
|
||
so.default = so.disabled;
|
||
so.rmempty = false;
|
||
so.write = function(section_id, formvalue) {
|
||
if (formvalue == 1) {
|
||
callCrondSet('resources', uci.get(data[0], section_id, 'auto_update_expr'));
|
||
} else
|
||
callCrondSet('resources');
|
||
|
||
return this.super('write', section_id, formvalue);
|
||
}
|
||
|
||
so = ss.option(form.Value, 'auto_update_expr', _('Cron expression'),
|
||
_('The default value is 2:00 every day.'));
|
||
so.default = '0 2 * * *';
|
||
so.placeholder = '0 2 * * *';
|
||
so.rmempty = false;
|
||
so.retain = true;
|
||
so.depends('auto_update', '1');
|
||
so.write = function(section_id, formvalue) {
|
||
callCrondSet('resources', formvalue);
|
||
|
||
return this.super('write', section_id, formvalue);
|
||
};
|
||
so.remove = function(section_id) {
|
||
callCrondSet('resources');
|
||
|
||
return this.super('remove', section_id);
|
||
};
|
||
|
||
so = ss.option(form.ListValue, '_dashboard_version', _('Dashboard version'));
|
||
so.default = hm.dashrepos[0][0];
|
||
hm.dashrepos.forEach((repo) => {
|
||
so.value.apply(so, repo);
|
||
})
|
||
so.renderWidget = function(/* ... */) {
|
||
let El = form.ListValue.prototype.renderWidget.apply(this, arguments);
|
||
|
||
El.classList.add('control-group');
|
||
El.firstChild.style.width = '10em';
|
||
|
||
return renderResVersion.call(this, El, 'dashboard', this.default);
|
||
}
|
||
so.onchange = function(ev, section_id, value) {
|
||
this.default = value;
|
||
|
||
let weight = ev.target;
|
||
if (weight)
|
||
return L.resolveDefault(callResVersion('dashboard', value), {}).then((res) => {
|
||
updateResVersion(weight.lastChild, res.version);
|
||
});
|
||
}
|
||
so.write = function() {};
|
||
|
||
so = ss.option(form.DummyValue, '_geoip_version', _('GeoIP version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'geoip') };
|
||
|
||
so = ss.option(form.DummyValue, '_geosite_version', _('GeoSite version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'geosite') };
|
||
|
||
so = ss.option(form.DummyValue, '_asn_version', _('ASN version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'asn') };
|
||
|
||
so = ss.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_ip4') };
|
||
|
||
so = ss.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_ip6') };
|
||
|
||
so = ss.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'gfw_list') };
|
||
|
||
so = ss.option(form.DummyValue, '_china_list_version', _('China list version'));
|
||
so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_list') };
|
||
/* Overview END */
|
||
|
||
/* General START */
|
||
s.tab('general', _('General'));
|
||
|
||
/* General settings */
|
||
o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('General settings'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.ListValue, 'mode', _('Operation mode'));
|
||
so.value('direct', _('Direct'));
|
||
so.value('rule', _('Rule'));
|
||
so.value('global', _('Global'));
|
||
so.default = 'rule';
|
||
|
||
so = ss.option(form.ListValue, 'find_process_mode', _('Process matching mode'));
|
||
so.value('always', _('Enable'));
|
||
so.value('strict', _('Auto'));
|
||
so.value('off', _('Disable'));
|
||
so.default = 'off';
|
||
|
||
so = ss.option(form.ListValue, 'log_level', _('Log level'));
|
||
so.value('silent', _('Silent'));
|
||
so.value('error', _('Error'));
|
||
so.value('warning', _('Warning'));
|
||
so.value('info', _('Info'));
|
||
so.value('debug', _('Debug'));
|
||
so.default = 'warning';
|
||
|
||
so = ss.option(form.Flag, 'etag_support', _('ETag support'));
|
||
so.default = so.enabled;
|
||
|
||
so = ss.option(form.Flag, 'ipv6', _('IPv6 support'));
|
||
so.default = so.enabled;
|
||
|
||
so = ss.option(form.Flag, 'unified_delay', _('Unified delay'));
|
||
so.default = so.disabled;
|
||
|
||
so = ss.option(form.Flag, 'tcp_concurrent', _('TCP concurrency'));
|
||
so.default = so.disabled;
|
||
|
||
so = ss.option(form.Value, 'keep_alive_interval', _('TCP-Keep-Alive interval'),
|
||
_('In seconds. <code>%s</code> will be used if empty.').format('30'));
|
||
so.placeholder = '30';
|
||
so.validate = L.bind(hm.validateTimeDuration, so);
|
||
|
||
so = ss.option(form.Value, 'keep_alive_idle', _('TCP-Keep-Alive idle timeout'),
|
||
_('In seconds. <code>%s</code> will be used if empty.').format('600'));
|
||
so.placeholder = '600';
|
||
so.validate = L.bind(hm.validateTimeDuration, so);
|
||
|
||
/* Global Authentication */
|
||
o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('Global Authentication'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.DynamicList, 'authentication', _('User Authentication'));
|
||
so.datatype = 'list(string)';
|
||
so.placeholder = 'user1:pass1';
|
||
so.validate = L.bind(hm.validateAuth, so);
|
||
|
||
so = ss.option(form.DynamicList, 'skip_auth_prefixes', _('No Authentication IP ranges'));
|
||
so.datatype = 'list(cidr)';
|
||
so.placeholder = '127.0.0.1/8';
|
||
/* General END */
|
||
|
||
/* Inbound START */
|
||
s.tab('inbound', _('Inbound'));
|
||
|
||
/* Listen ports */
|
||
o = s.taboption('inbound', form.SectionValue, '_inbound', form.NamedSection, 'inbound', 'fchomo', _('Listen ports'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.Value, 'mixed_port', _('Mixed port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '7890';
|
||
so.rmempty = false;
|
||
|
||
so = ss.option(form.Value, 'redir_port', _('Redir port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '7891';
|
||
so.rmempty = false;
|
||
|
||
so = ss.option(form.Value, 'tproxy_port', _('Tproxy port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '7892';
|
||
so.rmempty = false;
|
||
|
||
so = ss.option(form.Value, 'tunnel_port', _('DNS port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '7893';
|
||
so.rmempty = false;
|
||
|
||
so = ss.option(form.ListValue, 'proxy_mode', _('Proxy mode'));
|
||
so.value('redir', _('Redirect TCP'));
|
||
if (features.hm_has_tproxy)
|
||
so.value('redir_tproxy', _('Redirect TCP + TProxy UDP'));
|
||
if (features.hm_has_ip_full && features.hm_has_tun) {
|
||
so.value('redir_tun', _('Redirect TCP + Tun UDP'));
|
||
so.value('tun', _('Tun TCP/UDP'));
|
||
} else
|
||
so.description = _('To enable Tun support, you need to install <code>ip-full</code> and <code>kmod-tun</code>');
|
||
so.default = 'redir_tproxy';
|
||
so.rmempty = false;
|
||
|
||
/* Tun settings */
|
||
o = s.taboption('inbound', form.SectionValue, '_inbound', form.NamedSection, 'inbound', 'fchomo', _('Tun settings'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.RichListValue || form.ListValue, 'tun_stack', _('Stack'),
|
||
_('Tun stack.'));
|
||
so.value('system', _('System'), _('Less compatibility and sometimes better performance.'));
|
||
if (features.with_gvisor) {
|
||
so.value('gvisor', _('gVisor'), _('Based on google/gvisor.'));
|
||
so.value('mixed', _('Mixed'), _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.'));
|
||
}
|
||
so.default = 'system';
|
||
so.rmempty = false;
|
||
if (!form.RichListValue)
|
||
so.onchange = function(ev, section_id, value) {
|
||
var desc = ev.target.nextSibling;
|
||
if (value === 'mixed')
|
||
desc.innerHTML = _('Mixed <code>system</code> TCP stack and <code>gVisor</code> UDP stack.');
|
||
else if (value === 'gvisor')
|
||
desc.innerHTML = _('Based on google/gvisor.');
|
||
else if (value === 'system')
|
||
desc.innerHTML = _('Less compatibility and sometimes better performance.');
|
||
}
|
||
|
||
so = ss.option(form.Value, 'tun_mtu', _('MTU'));
|
||
so.datatype = 'uinteger';
|
||
so.placeholder = '9000';
|
||
|
||
so = ss.option(form.Flag, 'tun_gso', _('Generic segmentation offload'));
|
||
so.default = so.disabled;
|
||
|
||
so = ss.option(form.Value, 'tun_gso_max_size', _('Segment maximum size'));
|
||
so.datatype = 'uinteger';
|
||
so.placeholder = '65536';
|
||
|
||
so = ss.option(form.Value, 'tun_udp_timeout', _('UDP NAT expiration time'),
|
||
_('In seconds. <code>%s</code> will be used if empty.').format('300'));
|
||
so.placeholder = '300';
|
||
so.validate = L.bind(hm.validateTimeDuration, so);
|
||
|
||
so = ss.option(form.Flag, 'tun_endpoint_independent_nat', _('Endpoint-Independent NAT'),
|
||
_('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.'));
|
||
so.default = so.disabled;
|
||
/* Inbound END */
|
||
|
||
/* TLS START */
|
||
s.tab('tls', _('TLS'));
|
||
|
||
/* TLS settings */
|
||
o = s.taboption('tls', form.SectionValue, '_tls', form.NamedSection, 'tls', 'fchomo', null);
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.ListValue, 'global_client_fingerprint', _('Global client fingerprint'));
|
||
so.default = hm.tls_client_fingerprints[0][0];
|
||
hm.tls_client_fingerprints.forEach((res) => {
|
||
so.value.apply(so, res);
|
||
})
|
||
|
||
so = ss.option(form.Value, 'tls_cert_path', _('API TLS certificate path'));
|
||
so.datatype = 'file';
|
||
so.value('/etc/ssl/acme/example.crt');
|
||
|
||
so = ss.option(form.Value, 'tls_key_path', _('API TLS private key path'));
|
||
so.datatype = 'file';
|
||
so.value('/etc/ssl/acme/example.key');
|
||
/* TLS END */
|
||
|
||
/* API START */
|
||
s.tab('api', _('API'));
|
||
|
||
/* API settings */
|
||
o = s.taboption('api', form.SectionValue, '_api', form.NamedSection, 'api', 'fchomo', null);
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.ListValue, 'dashboard_repo', _('Select Dashboard'));
|
||
so.default = hm.dashrepos[0][0];
|
||
so.load = function(section_id) {
|
||
delete this.keylist;
|
||
delete this.vallist;
|
||
|
||
hm.dashrepos.forEach((repo) => {
|
||
L.resolveDefault(callResVersion('dashboard', repo[0]), {}).then((res) => {
|
||
this.value(repo[0], repo[1] + ' - ' + (res.version || _('Not Installed')));
|
||
});
|
||
});
|
||
|
||
return this.super('load', section_id);
|
||
}
|
||
so.rmempty = false;
|
||
|
||
so = ss.option(form.DynamicList, 'external_controller_cors_allow_origins', _('CORS Allow origins'),
|
||
_('CORS allowed origins, <code>*</code> will be used if empty.'));
|
||
so.placeholder = 'https://yacd.metacubex.one';
|
||
|
||
so = ss.option(form.Flag, 'external_controller_cors_allow_private_network', _('CORS Allow private network'),
|
||
_('Allow access from private network.</br>' +
|
||
'To access the API on a private network from a public website, it must be enabled.'));
|
||
so.default = so.enabled;
|
||
|
||
so = ss.option(form.Value, 'external_controller_port', _('API HTTP port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '9090';
|
||
|
||
so = ss.option(form.Value, 'external_controller_tls_port', _('API HTTPS port'));
|
||
so.datatype = 'port';
|
||
so.placeholder = '9443';
|
||
so.depends({'fchomo.tls.tls_cert_path': /^\/.+/, 'fchomo.tls.tls_key_path': /^\/.+/});
|
||
|
||
so = ss.option(form.Value, 'external_doh_server', _('API DoH service'));
|
||
so.placeholder = '/dns-query';
|
||
so.depends({'external_controller_tls_port': /\d+/});
|
||
|
||
so = ss.option(form.Value, 'secret', _('API secret'),
|
||
_('Random will be used if empty.'));
|
||
so.password = true;
|
||
/* API END */
|
||
|
||
/* Sniffer START */
|
||
s.tab('sniffer', _('Sniffer'));
|
||
|
||
/* Sniffer settings */
|
||
o = s.taboption('sniffer', form.SectionValue, '_sniffer', form.NamedSection, 'sniffer', 'fchomo', _('Sniffer settings'));
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.Flag, 'override_destination', _('Override destination'),
|
||
_('Override the connection destination address with the sniffed domain.'));
|
||
so.default = so.enabled;
|
||
|
||
so = ss.option(form.DynamicList, 'force_domain', _('Forced sniffing domain'));
|
||
so.datatype = 'list(string)';
|
||
|
||
so = ss.option(form.DynamicList, 'skip_domain', _('Skiped sniffing domain'));
|
||
so.datatype = 'list(string)';
|
||
|
||
so = ss.option(form.DynamicList, 'skip_src_address', _('Skiped sniffing src address'));
|
||
so.datatype = 'list(cidr)';
|
||
|
||
so = ss.option(form.DynamicList, 'skip_dst_address', _('Skiped sniffing dst address'));
|
||
so.datatype = 'list(cidr)';
|
||
|
||
/* Sniff protocol settings */
|
||
o = s.taboption('sniffer', form.SectionValue, '_sniffer_sniff', form.GridSection, 'sniff', _('Sniff protocol'));
|
||
ss = o.subsection;
|
||
ss.anonymous = true;
|
||
ss.addremove = false;
|
||
ss.rowcolors = true;
|
||
ss.sortable = true;
|
||
ss.nodescriptions = true;
|
||
|
||
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
||
so.default = so.enabled;
|
||
so.editable = true;
|
||
|
||
so = ss.option(form.ListValue, 'protocol', _('Protocol'));
|
||
so.value('HTTP');
|
||
so.value('TLS');
|
||
so.value('QUIC');
|
||
so.readonly = true;
|
||
|
||
so = ss.option(form.DynamicList, 'ports', _('Ports'));
|
||
so.datatype = 'list(or(port, portrange))';
|
||
|
||
so = ss.option(form.Flag, 'override_destination', _('Override destination'));
|
||
so.default = so.enabled;
|
||
so.editable = true;
|
||
/* Sniffer END */
|
||
|
||
/* Experimental START */
|
||
s.tab('experimental', _('Experimental'));
|
||
|
||
/* Experimental settings */
|
||
o = s.taboption('experimental', form.SectionValue, '_experimental', form.NamedSection, 'experimental', 'fchomo', null);
|
||
ss = o.subsection;
|
||
|
||
so = ss.option(form.Flag, 'quic_go_disable_gso', _('Disable GSO of quic-go'));
|
||
so.default = so.disabled;
|
||
|
||
so = ss.option(form.Flag, 'quic_go_disable_ecn', _('Disable ECN of quic-go'));
|
||
so.default = so.disabled;
|
||
|
||
so = ss.option(form.Flag, 'dialer_ip4p_convert', _('Enable <a target="_blank" href="%s" rel="noreferrer noopener">IP4P</a> conversion for outbound connections')
|
||
.format('https://github.com/heiher/natmap/wiki/faq#%E5%9F%9F%E5%90%8D%E8%AE%BF%E9%97%AE%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84'));
|
||
so.default = so.disabled;
|
||
/* Experimental END */
|
||
|
||
/* ACL START */
|
||
s.tab('control', _('Access Control'));
|
||
|
||
/* Access Control settings */
|
||
o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'routing', 'fchomo', null);
|
||
ss = o.subsection;
|
||
|
||
/* Interface control */
|
||
ss.tab('interface', _('Interface Control'));
|
||
|
||
so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'),
|
||
_('Only process traffic from specific interfaces. Leave empty for all.'));
|
||
so.multiple = true;
|
||
so.noaliases = true;
|
||
|
||
so = ss.taboption('interface', widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
||
_('Bind outbound traffic to specific interface. Leave empty to auto detect.</br>') +
|
||
_('Priority: Proxy Node > Proxy Group > Global.'));
|
||
so.multiple = false;
|
||
so.noaliases = true;
|
||
|
||
so = ss.taboption('interface', form.Value, 'route_table_id', _('Routing table ID'));
|
||
so.ucisection = 'config';
|
||
so.datatype = 'uinteger';
|
||
so.placeholder = '2022';
|
||
so.rmempty = false;
|
||
|
||
so = ss.taboption('interface', form.Value, 'route_rule_pref', _('Routing rule priority'));
|
||
so.ucisection = 'config';
|
||
so.datatype = 'uinteger';
|
||
so.placeholder = '9000';
|
||
so.rmempty = false;
|
||
|
||
so = ss.taboption('interface', form.Value, 'self_mark', _('Routing mark'),
|
||
_('Priority: Proxy Node > Proxy Group > Global.'));
|
||
so.ucisection = 'config';
|
||
so.datatype = 'uinteger';
|
||
so.placeholder = '200';
|
||
so.rmempty = false;
|
||
|
||
so = ss.taboption('interface', form.Value, 'tproxy_mark', _('Tproxy Fwmark'));
|
||
so.ucisection = 'config';
|
||
so.placeholder = '201 or 0xc9/0xff';
|
||
so.rmempty = false;
|
||
|
||
so = ss.taboption('interface', form.Value, 'tun_mark', _('Tun Fwmark'));
|
||
so.ucisection = 'config';
|
||
so.placeholder = '202 or 0xca/0xff';
|
||
so.rmempty = false;
|
||
|
||
/* Access control */
|
||
ss.tab('access_control', _('Access Control'));
|
||
|
||
so = ss.taboption('access_control', form.ListValue, 'lan_filter', _('Users filter mode'));
|
||
so.value('', _('All allowed'));
|
||
so.value('white_list', _('White list'));
|
||
so.value('black_list', _('Black list'));
|
||
|
||
so = fwtool.addIPOption(ss, 'access_control', 'lan_direct_ipv4_ips', _('Direct IPv4 IP-s'), null, 'ipv4', hosts, true);
|
||
so.depends('lan_filter', 'black_list');
|
||
|
||
so = fwtool.addIPOption(ss, 'access_control', 'lan_direct_ipv6_ips', _('Direct IPv6 IP-s'), null, 'ipv6', hosts, true);
|
||
so.depends({'lan_filter': 'black_list', 'fchomo.global.ipv6': '1'});
|
||
|
||
so = fwtool.addMACOption(ss, 'access_control', 'lan_direct_mac_addrs', _('Direct MAC-s'), null, hosts);
|
||
so.depends('lan_filter', 'black_list');
|
||
|
||
so = fwtool.addIPOption(ss, 'access_control', 'lan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'), null, 'ipv4', hosts, true);
|
||
so.depends('lan_filter', 'white_list');
|
||
|
||
so = fwtool.addIPOption(ss, 'access_control', 'lan_proxy_ipv6_ips', _('Proxy IPv6 IP-s'), null, 'ipv6', hosts, true);
|
||
so.depends({'lan_filter': 'white_list', 'fchomo.global.ipv6': '1'});
|
||
|
||
so = fwtool.addMACOption(ss, 'access_control', 'lan_proxy_mac_addrs', _('Proxy MAC-s'), null, hosts);
|
||
so.depends('lan_filter', 'white_list');
|
||
|
||
so = ss.taboption('access_control', form.Flag, 'proxy_router', _('Proxy routerself'));
|
||
so.default = so.enabled;
|
||
|
||
/* Routing control */
|
||
ss.tab('routing_control', _('Routing Control'));
|
||
|
||
so = ss.taboption('routing_control', form.MultiValue, 'routing_tcpport', _('Routing ports') + ' (TCP)',
|
||
_('Specify target ports to be proxied. Multiple ports must be separated by commas.'));
|
||
so.create = true;
|
||
hm.routing_port_type.forEach((res) => {
|
||
if (res[0] !== 'common_udpport')
|
||
so.value.apply(so, res);
|
||
})
|
||
so.validate = L.bind(hm.validateCommonPort, so);
|
||
|
||
so = ss.taboption('routing_control', form.MultiValue, 'routing_udpport', _('Routing ports') + ' (UDP)',
|
||
_('Specify target ports to be proxied. Multiple ports must be separated by commas.'));
|
||
so.create = true;
|
||
hm.routing_port_type.forEach((res) => {
|
||
if (res[0] !== 'common_tcpport')
|
||
so.value.apply(so, res);
|
||
})
|
||
so.validate = L.bind(hm.validateCommonPort, so);
|
||
|
||
so = ss.taboption('routing_control', form.ListValue, 'routing_mode', _('Routing mode'),
|
||
_('Routing mode of the traffic enters mihomo via firewall rules.'));
|
||
so.value('', _('All allowed'));
|
||
so.value('bypass_cn', _('Bypass CN'));
|
||
so.value('routing_gfw', _('Routing GFW'));
|
||
|
||
so = ss.taboption('routing_control', form.Flag, 'routing_domain', _('Handle domain'),
|
||
_('Routing mode will be handle domain.'));
|
||
so.default = so.disabled;
|
||
if (!features.hm_has_dnsmasq_full) {
|
||
so.description = _('To enable, you need to install <code>dnsmasq-full</code>.');
|
||
so.readonly = true;
|
||
uci.set(data[0], so.section.section, so.option, '');
|
||
uci.save();
|
||
}
|
||
so.depends('routing_mode', 'bypass_cn');
|
||
so.depends('routing_mode', 'routing_gfw');
|
||
|
||
so = ss.taboption('routing_control', form.ListValue, 'routing_dscp_mode', _('Routing DSCP'));
|
||
so.value('', _('All allowed'));
|
||
so.value('bypass_dscp', _('Bypass DSCP'));
|
||
so.value('routing_dscp', _('Routing DSCP'));
|
||
|
||
so = ss.taboption('routing_control', form.Value, 'routing_dscp_list', _('DSCP list'));
|
||
so.placeholder = '0,10,12,14,63';
|
||
so.validate = function(section_id, value) {
|
||
if (!value)
|
||
return true;
|
||
else if (value.match('^(6[0-3]|[1-5]?[0-9])(,(6[0-3]|[1-5]?[0-9]))*$') === null)
|
||
return _('Expecting: %s').format(_('One or more numbers in the range 0-63 separated by commas'));
|
||
|
||
return true;
|
||
}
|
||
so.rmempty = false;
|
||
so.depends('routing_dscp_mode', 'bypass_dscp');
|
||
so.depends('routing_dscp_mode', 'routing_dscp');
|
||
|
||
/* Custom Direct list */
|
||
ss.tab('direct_list', _('Custom Direct List'));
|
||
|
||
so = ss.taboption('direct_list', hm.TextValue, 'direct_list.yaml', null);
|
||
so.rows = 20;
|
||
so.default = 'FQDN:\nIPCIDR:\nIPCIDR6:\n';
|
||
so.placeholder = "FQDN:\n- mask.icloud.com\n- mask-h2.icloud.com\n- mask.apple-dns.net\nIPCIDR:\n- '223.0.0.0/12'\nIPCIDR6:\n- '2400:3200::/32'\n";
|
||
so.load = function(section_id) {
|
||
return L.resolveDefault(hm.readFile('resources', this.option), '');
|
||
}
|
||
so.write = function(section_id, formvalue) {
|
||
return hm.writeFile('resources', this.option, formvalue);
|
||
}
|
||
so.remove = function(section_id) {
|
||
return hm.writeFile('resources', this.option);
|
||
}
|
||
so.rmempty = false;
|
||
|
||
/* Custom Proxy list */
|
||
ss.tab('proxy_list', _('Custom Proxy List'));
|
||
|
||
so = ss.taboption('proxy_list', hm.TextValue, 'proxy_list.yaml', null);
|
||
so.rows = 20;
|
||
so.default = 'FQDN:\nIPCIDR:\nIPCIDR6:\n';
|
||
so.placeholder = "FQDN:\n- www.google.com\nIPCIDR:\n- '91.105.192.0/23'\nIPCIDR6:\n- '2001:67c:4e8::/48'\n";
|
||
so.load = function(section_id) {
|
||
return L.resolveDefault(hm.readFile('resources', this.option), '');
|
||
}
|
||
so.write = function(section_id, formvalue) {
|
||
return hm.writeFile('resources', this.option, formvalue);
|
||
}
|
||
so.remove = function(section_id) {
|
||
return hm.writeFile('resources', this.option);
|
||
}
|
||
so.rmempty = false;
|
||
/* ACL END */
|
||
|
||
return m.render();
|
||
}
|
||
});
|