1603 lines
53 KiB
JavaScript
1603 lines
53 KiB
JavaScript
'use strict';
|
|
'require form';
|
|
'require poll';
|
|
'require uci';
|
|
'require ui';
|
|
'require view';
|
|
|
|
'require fchomo as hm';
|
|
'require tools.widgets as widgets';
|
|
|
|
function parseProxyGroupYaml(field, name, cfg) {
|
|
if (!cfg.type)
|
|
return null;
|
|
|
|
// key mapping
|
|
let config = hm.removeBlankAttrs({
|
|
id: cfg.hm_id,
|
|
label: cfg.hm_label,
|
|
type: cfg.type,
|
|
groups: cfg.proxies ? cfg.proxies.map((grop) => hm.preset_outbound.full.map(([key, label]) => key).includes(grop) ? grop : this.calcID(hm.glossary["proxy_group"].field, grop)) : null, // array
|
|
use: cfg.use ? cfg.use.map((prov) => this.calcID(hm.glossary["provider"].field, prov)) : null, // array
|
|
include_all: hm.bool2str(cfg["include-all"]), // bool
|
|
include_all_proxies: hm.bool2str(cfg["include-all-proxies"]), // bool
|
|
include_all_providers: hm.bool2str(cfg["include-all-providers"]), // bool
|
|
// Url-test fields
|
|
tolerance: cfg.tolerance,
|
|
// Load-balance fields
|
|
strategy: cfg.strategy,
|
|
// Override fields
|
|
disable_udp: hm.bool2str(cfg["disable-udp"]), // bool
|
|
// Health fields
|
|
url: cfg.url,
|
|
interval: cfg.interval,
|
|
timeout: cfg.timeout,
|
|
lazy: hm.bool2str(cfg.lazy), // bool
|
|
expected_status: cfg["expected-status"],
|
|
max_failed_times: cfg["max-failed-times"],
|
|
// General fields
|
|
filter: [cfg.filter], // array.string: string
|
|
exclude_filter: [cfg["exclude-filter"]], // array.string: string
|
|
exclude_type: [cfg["exclude-type"]], // array.string: string
|
|
hidden: hm.bool2str(cfg.hidden), // bool
|
|
icon: cfg.icon
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
function loadDNSServerLabel(section_id) {
|
|
delete this.keylist;
|
|
delete this.vallist;
|
|
|
|
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
|
this.value('system-dns', _('System DNS'));
|
|
this.value('block-dns', _('Block DNS queries'));
|
|
uci.sections(this.config, 'dns_server', (res) => {
|
|
if (res.enabled !== '0')
|
|
this.value(res['.name'], res.label);
|
|
});
|
|
|
|
return this.super('load', section_id);
|
|
}
|
|
function validateNameserver(section_id, value) {
|
|
const arr = value.trim().split(' ');
|
|
if (arr.length > 1 && arr.includes('block-dns'))
|
|
return _('Expecting: %s').format(_('If Block is selected, uncheck others'));
|
|
|
|
return true;
|
|
}
|
|
|
|
class DNSAddress {
|
|
constructor(address) {
|
|
this.input = address || '';
|
|
[this.addr, this.rawparams] = this.input.split('#');
|
|
if (this.rawparams) {
|
|
if (this.rawparams.match(/^[^=&]+(&|$)/))
|
|
this.rawparams = 'detour=' + this.rawparams
|
|
} else
|
|
this.rawparams = '';
|
|
this.params = new URLSearchParams(this.rawparams);
|
|
}
|
|
|
|
parseParam(param) {
|
|
return this.params.has(param) ? decodeURI(this.params.get(param)) : null;
|
|
}
|
|
|
|
setParam(param, value) {
|
|
if (value) {
|
|
this.params.set(param, value);
|
|
} else
|
|
this.params.delete(param);
|
|
|
|
return this
|
|
}
|
|
|
|
toString() {
|
|
return this.addr + (this.params.size === 0 ? '' : '#' +
|
|
['detour', 'h3', 'ecs', 'ecs-override'].map((k) => {
|
|
return this.params.has(k) ? '%s=%s'.format(k, encodeURI(this.params.get(k))) : null;
|
|
}).filter(v => v).join('&')
|
|
);
|
|
}
|
|
}
|
|
|
|
class RulesEntry {
|
|
constructor(entry) {
|
|
this.input = entry || '';
|
|
try {
|
|
let content = JSON.parse(this.input.trim());
|
|
Object.keys(content).forEach(key => this[key] = content[key]);
|
|
} catch {}
|
|
|
|
this.type ||= hm.rules_type[0][0];
|
|
this.payload ||= [
|
|
{type: hm.rules_type[0][0], factor: '', /* deny: false */},
|
|
//{type: 'DOMAIN-SUFFIX', factor: '.google.com', deny: true}
|
|
];
|
|
this.detour ||= hm.preset_outbound.full[0][0];
|
|
this.params ||= {/* src: false, no-resolve: true */};
|
|
this.subrule ||= false;
|
|
}
|
|
|
|
setKey(key, value) {
|
|
this[key] = value;
|
|
|
|
return this
|
|
}
|
|
|
|
getPayload(n) {
|
|
return this.payload[n] || {};
|
|
}
|
|
|
|
getPayloads() {
|
|
return this.payload || [];
|
|
}
|
|
|
|
setPayload(n, obj, limit) {
|
|
this.payload[n] ||= {};
|
|
|
|
Object.keys(obj).forEach((key) => {
|
|
this.payload[n][key] = obj[key] || null;
|
|
});
|
|
|
|
if (limit)
|
|
this.payload.splice(limit);
|
|
|
|
return this
|
|
}
|
|
|
|
getParam(param) {
|
|
return this.params?.[param] || null;
|
|
}
|
|
|
|
setParam(param, value) {
|
|
if (value) {
|
|
this.params[param] = value;
|
|
} else
|
|
this.params[param] = null;
|
|
|
|
return this
|
|
}
|
|
|
|
_payloadStrategy(payload) {
|
|
// LOGIC_TYPE,((payload1),(payload2))
|
|
if (payload.factor === null || ['undefined', 'boolean', 'number', 'string'].includes(typeof(payload.factor))) {
|
|
return (payload.deny ? 'NOT,((%s))' : '%s').format([payload.type, payload.factor ?? ''].join(','));
|
|
} else if (payload.factor?.constructor === Array) {
|
|
return `${payload.type},(%s)`.format(payload.factor.map(p => `(${this._payloadStrategy(p)})`).join(','));
|
|
} else if (payload.factor?.constructor === Object) {
|
|
throw new Error(`Factor type cannot be an object: '${JSON.stringify(payload.factor)}'`);
|
|
} else
|
|
throw new Error(`Factor type is incorrect: '${payload.factor}'`);
|
|
}
|
|
|
|
_toMihomo(rule, logical) {
|
|
let payload = this._payloadStrategy(logical ? {type: rule.type, factor: rule.payload} : rule.payload[0]);
|
|
|
|
if (rule.subrule)
|
|
return 'SUB-RULE,(%s),%s'.format(payload, rule.subrule);
|
|
else
|
|
if (rule.type === 'MATCH')
|
|
return [rule.type, rule.detour].join(',');
|
|
else
|
|
return [payload, rule.detour].concat(
|
|
rule.params ? ['no-resolve', 'src'].filter(k => rule.params[k]) : []
|
|
).join(',');
|
|
}
|
|
|
|
toString(format) {
|
|
format ||= 'json';
|
|
let logical = hm.rules_logical_type.map(e => e[0] || e).includes(this.type);
|
|
let rule, factor, detour, params;
|
|
|
|
if (logical) {
|
|
let n = hm.rules_logical_payload_count[this.type] ? hm.rules_logical_payload_count[this.type].high : 0;
|
|
factor = this.payload.slice(0, n);
|
|
} else
|
|
factor = [ {...this.payload[0], ...{type: this.type}} ];
|
|
|
|
if (!this.subrule) {
|
|
detour = this.detour;
|
|
params = this.params;
|
|
if (this.type === 'MATCH')
|
|
factor = [{type: 'MATCH'}];
|
|
}
|
|
|
|
rule = hm.removeBlankAttrs({
|
|
type: this.type,
|
|
payload: factor,
|
|
detour: detour || null,
|
|
params: params || null,
|
|
subrule: this.subrule || null,
|
|
});
|
|
|
|
if (format === 'json')
|
|
return JSON.stringify(rule);
|
|
else if (format === 'mihomo')
|
|
return this._toMihomo(rule, logical);
|
|
else
|
|
throw new Error(`Unknown format: '${format}'`);
|
|
}
|
|
}
|
|
|
|
function parseDNSYaml(field, name, cfg) {
|
|
let addr = new DNSAddress(cfg);
|
|
|
|
if (!addr.toString())
|
|
return null;
|
|
|
|
let detour = addr.parseParam('detour');
|
|
if (detour)
|
|
addr.setParam('detour', hm.preset_outbound.full.map(([key, label]) => key).includes(detour) ? detour : this.calcID(hm.glossary["proxy_group"].field, detour))
|
|
|
|
// key mapping
|
|
let config = {
|
|
id: this.calcID(field, cfg),
|
|
label: '%s %s'.format(cfg, _('(Imported)')),
|
|
address: addr.toString()
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
function parseDNSPolicyYaml(field, name, cfg) {
|
|
//console.info([name, cfg]);
|
|
|
|
let type = name.match(/^([^:]+):(.*)$/),
|
|
rules;
|
|
switch (type?.[1]) {
|
|
case 'geosite':
|
|
rules = type[2].split(',');
|
|
type = 'geosite';
|
|
break;
|
|
case 'rule-set':
|
|
rules = type[2].split(',').map((rule) => this.calcID(hm.glossary["ruleset"].field, rule));
|
|
type = 'rule_set';
|
|
break;
|
|
default:
|
|
rules = name.split(',');
|
|
type = 'domain';
|
|
break;
|
|
}
|
|
|
|
// key mapping
|
|
let config = {
|
|
id: this.calcID(field, name),
|
|
label: '%s %s'.format(name, _('(Imported)')),
|
|
type: type,
|
|
...Object.fromEntries([[type, rules]]),
|
|
server: (Array.isArray(cfg) ? cfg : [cfg]).map((dns) => this.calcID(hm.glossary["dns_server"].field, dns)),
|
|
//proxy: null
|
|
};
|
|
|
|
return config;
|
|
}
|
|
|
|
function parseRules(rule) {
|
|
// parse rules
|
|
// https://github.com/muink/mihomo/blob/8e6eb70e714d44f26ba407adbd7b255762f48b97/config/config.go#L1040-L1090
|
|
// https://github.com/muink/mihomo/blob/8e6eb70e714d44f26ba407adbd7b255762f48b97/rules/parser.go#L12
|
|
rule = rule.split(',');
|
|
let ruleName = rule[0].toUpperCase(),
|
|
logical_payload,
|
|
payload,
|
|
target,
|
|
params = [],
|
|
subrule;
|
|
|
|
let l = rule.length;
|
|
|
|
if (ruleName === 'SUB-RULE') {
|
|
subrule = rule.slice(1).join(',').match(/^\((.*)\)/); // SUB-RULE,(payload),subrule
|
|
if (subrule) {
|
|
[rule, subrule] = [subrule[1].split(',').concat('DIRECT'), rule.pop()];
|
|
ruleName = rule[0].toUpperCase();
|
|
l = rule.length;
|
|
} else
|
|
return null;
|
|
}
|
|
|
|
if (hm.rules_logical_type.map(o => o[0]).includes(ruleName)) {
|
|
target = rule.pop();
|
|
logical_payload = rule.slice(1).join(',').match(/^\(\((.*)\)\)$/); // LOGIC_TYPE,((payload1),(payload2))
|
|
if (logical_payload)
|
|
logical_payload = logical_payload[1].split('),(');
|
|
else
|
|
return null;
|
|
} else if (hm.rules_type.map(o => o[0]).includes(ruleName)) {
|
|
if (l < 2) return null; // error: format invalid
|
|
else if (ruleName === 'MATCH') l = 2;
|
|
else if (l >= 3) {
|
|
l = 3;
|
|
payload = rule[1];
|
|
}
|
|
target = rule[l-1];
|
|
params = rule.slice(l);
|
|
} else
|
|
return null;
|
|
|
|
// make entry
|
|
let entry = new RulesEntry();
|
|
entry.type = ruleName;
|
|
// parse payload
|
|
if (logical_payload)
|
|
for (let i=0; i < logical_payload.length; i++) {
|
|
let type, factor, deny;
|
|
|
|
// deny
|
|
deny = logical_payload[i].match(/^NOT,\(\((.*)\)\)$/);
|
|
if (deny)
|
|
[type, factor] = deny[1].split(',');
|
|
else
|
|
[type, factor] = logical_payload[i].split(',');
|
|
|
|
if (type === 'RULE-SET')
|
|
factor = this.calcID(hm.glossary["ruleset"].field, factor);
|
|
|
|
entry.setPayload(i, {type: type.toUpperCase(), factor: factor, deny: deny ? true : null});
|
|
}
|
|
else if (payload)
|
|
if (ruleName === 'RULE-SET')
|
|
entry.setPayload(0, {factor: this.calcID(hm.glossary["ruleset"].field, payload)});
|
|
else
|
|
entry.setPayload(0, {factor: payload});
|
|
params.forEach((param) => entry.setParam(param, true));
|
|
if (subrule)
|
|
entry.subrule = subrule;
|
|
else
|
|
entry.detour = hm.preset_outbound.full.map(([key, label]) => key).includes(target) ? target : this.calcID(hm.glossary["proxy_group"].field, target);
|
|
|
|
return entry.toString('json');
|
|
}
|
|
function parseRulesYaml(field, name, cfg) {
|
|
let id = this.calcID(field, cfg);
|
|
let entry = parseRules.call(this, cfg);
|
|
|
|
if (!entry)
|
|
return null;
|
|
|
|
// key mapping
|
|
let config = {
|
|
id: id,
|
|
label: '%s %s'.format(id.slice(0,7), _('(Imported)')),
|
|
entry: entry
|
|
};
|
|
|
|
return config;
|
|
}
|
|
function parseSubrulesYaml(field, name, cfg) {
|
|
cfg = cfg.match(/^([^:]+):(.+)$/);
|
|
|
|
if (!cfg)
|
|
return null;
|
|
|
|
let config = parseRulesYaml.call(this, field, name, cfg[2]);
|
|
|
|
return config ? Object.assign(config, {group: cfg[1]}) : null;
|
|
}
|
|
|
|
function boolToFlag(boolean) {
|
|
if (typeof(boolean) !== 'boolean')
|
|
return null;
|
|
|
|
switch(boolean) {
|
|
case true:
|
|
return '1';
|
|
case false:
|
|
return '0';
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
function flagToBool(flag) {
|
|
if (!flag)
|
|
return null;
|
|
|
|
switch(flag) {
|
|
case '1':
|
|
return true;
|
|
case '0':
|
|
return false;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function renderPayload(s, total, uciconfig) {
|
|
// common payload
|
|
let initPayload = function(o, n, key, uciconfig) {
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
|
|
}, o, n, key, uciconfig);
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {factor: value});
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
o.write = function() {};
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
}
|
|
let initDynamicPayload = function(o, n, key, uciconfig) {
|
|
o.allowduplicates = true;
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key] ?? '');
|
|
}, o, n, key, uciconfig);
|
|
o.validate = function(section_id, value) {
|
|
value = this.formvalue(section_id);
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
let rule = new RulesEntry(UIEl.getValue());
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
let limit = rule.getPayloads().length;
|
|
value.forEach((val) => {
|
|
rule.setPayload(n, {factor: val}); n++;
|
|
});
|
|
rule.setPayload(limit, {factor: null}, limit);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
|
|
return true;
|
|
}
|
|
o.write = function() {};
|
|
o.rmempty = true;
|
|
o.modalonly = true;
|
|
}
|
|
|
|
let o, prefix;
|
|
// StaticList payload
|
|
for (let n=0; n<total; n++) {
|
|
prefix = `payload${n}_`;
|
|
|
|
o = s.option(form.ListValue, prefix + 'type', _('Type') + ` ${n+1}`);
|
|
o.default = hm.rules_type[0][0];
|
|
hm.rules_type.forEach((res) => {
|
|
o.value.apply(o, res);
|
|
})
|
|
Object.keys(hm.rules_logical_payload_count).forEach((key) => {
|
|
if (n < hm.rules_logical_payload_count[key].low)
|
|
o.depends('type', key);
|
|
})
|
|
initPayload(o, n, 'type', uciconfig);
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {type: value});
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
|
|
o = s.option(form.Value, prefix + 'general', _('Factor') + ` ${n+1}`);
|
|
if (n === 0) {
|
|
o.depends({type: /\bDOMAIN\b/});
|
|
o.depends({type: /\bGEO(SITE|IP)\b/});
|
|
o.depends({type: /\bASN\b/});
|
|
o.depends({type: /\bPROCESS\b/});
|
|
}
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bDOMAIN\b/]]));
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bGEO(SITE|IP)\b/]]));
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bASN\b/]]));
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bPROCESS\b/]]));
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.Value, prefix + 'uint', _('Factor') + ` ${n+1}`);
|
|
o.datatype = 'uinteger';
|
|
if (n === 0)
|
|
o.depends('type', 'UID');
|
|
o.depends(prefix + 'type', 'UID');
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.Value, prefix + 'ip', _('Factor') + ` ${n+1}`);
|
|
o.datatype = 'cidr';
|
|
if (n === 0) {
|
|
o.depends({type: /\b(CIDR|CIDR6)\b/});
|
|
o.depends({type: /\bIP-SUFFIX\b/});
|
|
}
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\b(CIDR|CIDR6)\b/]]));
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bIP-SUFFIX\b/]]));
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.Value, prefix + 'port', _('Factor') + ` ${n+1}`);
|
|
o.datatype = 'or(port, portrange)';
|
|
if (n === 0)
|
|
o.depends({type: /\bPORT\b/});
|
|
o.depends(Object.fromEntries([[prefix + 'type', /\bPORT\b/]]));
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.ListValue, prefix + 'l4', _('Factor') + ` ${n+1}`);
|
|
o.value('udp', _('UDP'));
|
|
o.value('tcp', _('TCP'));
|
|
if (n === 0)
|
|
o.depends('type', 'NETWORK');
|
|
o.depends(prefix + 'type', 'NETWORK');
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.Value, prefix + 'dscp', _('Factor') + ` ${n+1}`);
|
|
o.datatype = 'range(0, 63)';
|
|
if (n === 0)
|
|
o.depends('type', 'DSCP');
|
|
o.depends(prefix + 'type', 'DSCP');
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
|
|
o = s.option(form.ListValue, prefix + 'rule_set', _('Factor') + ` ${n+1}`);
|
|
if (n === 0)
|
|
o.depends('type', 'RULE-SET');
|
|
o.depends(prefix + 'type', 'RULE-SET');
|
|
initPayload(o, n, 'factor', uciconfig);
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
hm.loadRulesetLabel.call(this, [], null, section_id);
|
|
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key];
|
|
}, o, n, 'factor', uciconfig)
|
|
|
|
o = s.option(form.Flag, prefix + 'NOT', _('NOT') + ` ${n+1}`);
|
|
o.default = o.disabled;
|
|
o.depends(Object.fromEntries([[prefix + 'type', /.+/]]));
|
|
initPayload(o, n, 'deny', uciconfig);
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key] ? true : false);
|
|
}, o, n, 'deny', uciconfig);
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
let rule = new RulesEntry(UIEl.getValue()).setPayload(n, {deny: flagToBool(value) || null});
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
}
|
|
|
|
// DynamicList payload
|
|
let extenbox = {};
|
|
Object.entries(hm.rules_logical_payload_count).filter(e => e[1].high === undefined).forEach((e) => {
|
|
let low = e[1].low;
|
|
let type = e[0];
|
|
if (!Array.isArray(extenbox[low]))
|
|
extenbox[low] = [];
|
|
extenbox[low].push(type);
|
|
})
|
|
Object.keys(extenbox).forEach((n) => {
|
|
prefix = `payload${n}_`;
|
|
|
|
o = s.option(hm.StaticList, prefix + 'type', _('Type') + ' ++');
|
|
o.default = hm.rules_type[0][0];
|
|
hm.rules_type.forEach((res) => {
|
|
o.value.apply(o, res);
|
|
})
|
|
extenbox[n].forEach((type) => {
|
|
o.depends('type', type);
|
|
})
|
|
initDynamicPayload(o, n, 'type', uciconfig);
|
|
o.validate = function(section_id, value) {
|
|
value = this.formvalue(section_id);
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
let rule = new RulesEntry(UIEl.getValue());
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
value.forEach((val) => {
|
|
rule.setPayload(n, {type: val}); n++;
|
|
});
|
|
rule.setPayload(n, {type: null}, n);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
|
|
return true;
|
|
}
|
|
|
|
o = s.option((hm.less_24_10 || !hm.pr7558_merged) ? hm.DynamicList : form.DynamicList, prefix + 'fused', _('Factor') + ' ++',
|
|
_('Content will not be verified, Please make sure you enter it correctly.'));
|
|
extenbox[n].forEach((type) => {
|
|
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
|
|
})
|
|
initDynamicPayload(o, n, 'factor', uciconfig);
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
let fusedval = [
|
|
['NETWORK', '-- NETWORK --'],
|
|
['udp', _('UDP')],
|
|
['tcp', _('TCP')],
|
|
['RULESET', '-- RULE-SET --']
|
|
];
|
|
hm.loadRulesetLabel.call(this, fusedval, null, section_id);
|
|
this.super('load', section_id);
|
|
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => e[key] ?? '');
|
|
}, o, n, 'factor', uciconfig)
|
|
|
|
o = s.option(hm.StaticList, prefix + 'NOTs', _('NOT') + ' ++',
|
|
_('<code>0</code> or <code>1</code> only.'));
|
|
o.value('0');
|
|
o.value('1');
|
|
extenbox[n].forEach((type) => {
|
|
o.depends(Object.fromEntries([['type', type], [prefix + 'type', /.+/]]));
|
|
})
|
|
initDynamicPayload(o, n, 'deny', uciconfig);
|
|
o.load = L.bind(function(n, key, uciconfig, section_id) {
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayloads().slice(n).map(e => boolToFlag(e[key] ? true : false));
|
|
}, o, n, 'deny', uciconfig);
|
|
o.validate = function(section_id, value) {
|
|
value = this.formvalue(section_id);
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
let rule = new RulesEntry(UIEl.getValue());
|
|
|
|
let n = this.option.match(/^payload(\d+)_/)[1];
|
|
let limit = rule.getPayloads().length;
|
|
value.forEach((value) => {
|
|
rule.setPayload(n, {deny: flagToBool(value) || null}); n++;
|
|
});
|
|
rule.setPayload(limit, {deny: null}, limit);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
|
|
return true;
|
|
}
|
|
})
|
|
}
|
|
|
|
function renderRules(s, uciconfig) {
|
|
let o;
|
|
|
|
o = s.option(form.DummyValue, 'entry', _('Entry'));
|
|
o.renderWidget = function(/* ... */) {
|
|
let El = form.DummyValue.prototype.renderWidget.apply(this, arguments);
|
|
|
|
El.firstChild.innerText = new RulesEntry(El.querySelector('input').value).toString('mihomo');
|
|
|
|
return El;
|
|
}
|
|
o.load = function(section_id) {
|
|
return form.DummyValue.prototype.load.call(this, section_id) || new RulesEntry().toString('json');
|
|
}
|
|
o.write = L.bind(form.AbstractValue.prototype.write, o);
|
|
o.remove = L.bind(form.AbstractValue.prototype.remove, o);
|
|
o.editable = true;
|
|
|
|
o = s.option(form.ListValue, 'type', _('Type'));
|
|
o.default = hm.rules_type[0][0];
|
|
[...hm.rules_type, ...hm.rules_logical_type].forEach((res) => {
|
|
o.value.apply(o, res);
|
|
})
|
|
o.load = function(section_id) {
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).type;
|
|
}
|
|
o.validate = function(section_id, value) {
|
|
// params only available for types other than
|
|
// https://github.com/muink/mihomo/blob/8e6eb70e714d44f26ba407adbd7b255762f48b97/config/config.go#L1050
|
|
// https://github.com/muink/mihomo/blob/8e6eb70e714d44f26ba407adbd7b255762f48b97/rules/parser.go#L12
|
|
if (['GEOIP', 'IP-ASN', 'IP-CIDR', 'IP-CIDR6', 'IP-SUFFIX', 'RULE-SET'].includes(value)) {
|
|
['no-resolve', 'src'].forEach((opt) => {
|
|
let UIEl = this.section.getUIElement(section_id, opt);
|
|
UIEl.node.querySelector('input').disabled = null;
|
|
});
|
|
} else {
|
|
['no-resolve', 'src'].forEach((opt) => {
|
|
let UIEl = this.section.getUIElement(section_id, opt);
|
|
UIEl.setValue('');
|
|
UIEl.node.querySelector('input').disabled = 'true';
|
|
});
|
|
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setParam('no-resolve').setParam('src');
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setKey('type', value);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
o.write = function() {};
|
|
o.rmempty = false;
|
|
o.modalonly = true;
|
|
|
|
renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count).map(e => e.low)), uciconfig);
|
|
|
|
o = s.option(hm.ListValue, 'detour', _('Proxy group'));
|
|
o.load = function(section_id) {
|
|
hm.loadProxyGroupLabel.call(this, hm.preset_outbound.full, section_id);
|
|
|
|
return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).detour;
|
|
}
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setKey('detour', value);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
o.write = function() {};
|
|
//o.depends('SUB-RULE', '');
|
|
o.editable = true;
|
|
|
|
o = s.option(form.Flag, 'src', _('src'),
|
|
_('Treat the <code>destination IP</code> as the <code>source IP</code>.'));
|
|
o.default = o.disabled;
|
|
o.load = function(section_id) {
|
|
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('src') ? true : false);
|
|
}
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setParam('src', flagToBool(value) || null);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
o.write = function() {};
|
|
o.depends('SUB-RULE', '');
|
|
o.modalonly = true;
|
|
|
|
o = s.option(form.Flag, 'no-resolve', _('no-resolve'),
|
|
_('Do not resolve the domain connection to IP for this match.</br>' +
|
|
'Only works for pure domain inbound connections without DNS resolution. e.g., socks5h'));
|
|
o.default = o.disabled;
|
|
o.load = function(section_id) {
|
|
return boolToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('no-resolve') ? true : false);
|
|
}
|
|
o.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setParam('no-resolve', flagToBool(value) || null);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
o.write = function() {};
|
|
o.depends('SUB-RULE', '');
|
|
o.modalonly = true;
|
|
}
|
|
|
|
return view.extend({
|
|
load() {
|
|
return Promise.all([
|
|
uci.load('fchomo')
|
|
]);
|
|
},
|
|
|
|
render(data) {
|
|
const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo');
|
|
|
|
let m, s, o, ss, so;
|
|
|
|
m = new form.Map('fchomo', _('Mihomo client'));
|
|
|
|
s = m.section(form.TypedSection);
|
|
s.render = function () {
|
|
poll.add(function() {
|
|
return hm.getServiceStatus('mihomo-c').then((isRunning) => {
|
|
hm.updateStatus(document.getElementById('_client_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-c', true);
|
|
});
|
|
});
|
|
|
|
return E('div', { class: 'cbi-section' }, [
|
|
E('p', [
|
|
hm.renderStatus('_client_bar', false, 'mihomo-c', true)
|
|
])
|
|
]);
|
|
}
|
|
|
|
s = m.section(form.NamedSection, 'routing', 'fchomo', null);
|
|
|
|
/* Proxy Group START */
|
|
s.tab('group', _('Proxy Group'));
|
|
|
|
/* Client switch */
|
|
o = s.taboption('group', form.Button, '_reload_client', _('Quick Reload'));
|
|
o.inputtitle = _('Reload');
|
|
o.inputstyle = 'apply';
|
|
o.onclick = L.bind(hm.handleReload, o, 'mihomo-c');
|
|
|
|
o = s.taboption('group', form.Flag, 'client_enabled', _('Enable'));
|
|
o.default = o.disabled;
|
|
|
|
/* Proxy Group */
|
|
o = s.taboption('group', form.SectionValue, '_group', hm.GridSection, 'proxy_group', null);
|
|
ss = o.subsection;
|
|
ss.addremove = true;
|
|
ss.rowcolors = true;
|
|
ss.sortable = true;
|
|
ss.nodescriptions = true;
|
|
ss.hm_modaltitle = [ _('Proxy Group'), _('Add a proxy group') ];
|
|
ss.hm_prefmt = hm.glossary[ss.sectiontype].prefmt;
|
|
ss.hm_field = hm.glossary[ss.sectiontype].field;
|
|
ss.hm_lowcase_only = true;
|
|
/* Import mihomo config start */
|
|
ss.handleYamlImport = function() {
|
|
const field = this.hm_field;
|
|
const o = new hm.HandleImport(this.map, this, _('Import mihomo config'),
|
|
_('Please type <code>%s</code> fields of mihomo config.</br>')
|
|
.format(field));
|
|
o.placeholder = 'proxy-groups:\n' +
|
|
'- name: "auto"\n' +
|
|
' type: url-test\n' +
|
|
' proxies:\n' +
|
|
' - ss1\n' +
|
|
' - ss2\n' +
|
|
' - vmess1\n' +
|
|
' tolerance: 150\n' +
|
|
' lazy: true\n' +
|
|
' expected-status: 204\n' +
|
|
' url: "https://cp.cloudflare.com/generate_204"\n' +
|
|
' interval: 300\n' +
|
|
' timeout: 5000\n' +
|
|
' max-failed-times: 5\n' +
|
|
'- name: "fallback-auto"\n' +
|
|
' type: fallback\n' +
|
|
' proxies:\n' +
|
|
' - DIRECT\n' +
|
|
' - auto\n' +
|
|
' url: "https://cp.cloudflare.com/generate_204"\n' +
|
|
' interval: 300\n' +
|
|
'- name: "load-balance"\n' +
|
|
' type: load-balance\n' +
|
|
' include-all: true\n' +
|
|
' url: "https://cp.cloudflare.com/generate_204"\n' +
|
|
' interval: 300\n' +
|
|
' lazy: false\n' +
|
|
' strategy: consistent-hashin\n' +
|
|
'- name: AllProxy\n' +
|
|
' type: select\n' +
|
|
' disable-udp: true\n' +
|
|
' include-all-proxies: true\n' +
|
|
' use:\n' +
|
|
' - provider1\n' +
|
|
'- name: AllProvider\n' +
|
|
' type: select\n' +
|
|
' include-all-providers: true\n' +
|
|
' filter: "(?i)港|hk|hongkong|hong kong"\n' +
|
|
' exclude-filter: "美|日"\n' +
|
|
' exclude-type: "Shadowsocks|Http"\n' +
|
|
' ...'
|
|
o.parseYaml = function(field, name, cfg) {
|
|
let config = hm.HandleImport.prototype.parseYaml.call(this, field, name, cfg);
|
|
|
|
return config ? parseProxyGroupYaml.call(this, field, name, config) : null;
|
|
};
|
|
|
|
return o.render();
|
|
}
|
|
ss.renderSectionAdd = function(/* ... */) {
|
|
let el = hm.GridSection.prototype.renderSectionAdd.apply(this, arguments);
|
|
|
|
el.appendChild(E('button', {
|
|
'class': 'cbi-button cbi-button-add',
|
|
'title': _('mihomo config'),
|
|
'click': ui.createHandlerFn(this, 'handleYamlImport')
|
|
}, [ _('Import mihomo config') ]));
|
|
|
|
return el;
|
|
}
|
|
/* Import mihomo config end */
|
|
|
|
ss.tab('field_general', _('General fields'));
|
|
ss.tab('field_override', _('Override fields'));
|
|
ss.tab('field_health', _('Health fields'));
|
|
|
|
/* General fields */
|
|
so = ss.taboption('field_general', form.Value, 'label', _('Label'));
|
|
so.load = L.bind(hm.loadDefaultLabel, so);
|
|
so.validate = function(section_id, value) {
|
|
if (value.match(/[,]/))
|
|
return _('Expecting: %s').format(_('not included ","'));
|
|
|
|
return hm.validateUniqueValue.call(this, section_id, value);
|
|
}
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable'));
|
|
so.default = so.enabled;
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.ListValue, 'type', _('Type'));
|
|
so.default = hm.proxy_group_type[0][0];
|
|
hm.proxy_group_type.forEach((res) => {
|
|
so.value.apply(so, res);
|
|
})
|
|
|
|
so = ss.taboption('field_general', form.MultiValue, 'groups', _('Group'));
|
|
hm.preset_outbound.full.forEach((res) => {
|
|
so.value.apply(so, res);
|
|
})
|
|
so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.full);
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.MultiValue, 'proxies', _('Node'));
|
|
so.value('', _('-- Please choose --'));
|
|
so.load = L.bind(hm.loadNodeLabel, so, [['', _('-- Please choose --')]]);
|
|
so.validate = function(section_id, value) {
|
|
if (this.section.getOption('include_all').formvalue(section_id) === '1' ||
|
|
this.section.getOption('include_all_proxies').formvalue(section_id) === '1')
|
|
this.getUIElement(section_id, this.option).node.setAttribute('disabled', '');
|
|
else
|
|
this.getUIElement(section_id, this.option).node.removeAttribute('disabled');
|
|
|
|
return true;
|
|
}
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.MultiValue, 'use', _('Provider'));
|
|
so.value('', _('-- Please choose --'));
|
|
so.load = L.bind(hm.loadProviderLabel, so, [['', _('-- Please choose --')]]);
|
|
so.validate = function(section_id, value) {
|
|
if (this.section.getOption('include_all').formvalue(section_id) === '1' ||
|
|
this.section.getOption('include_all_providers').formvalue(section_id) === '1')
|
|
this.getUIElement(section_id, this.option).node.setAttribute('disabled', '');
|
|
else
|
|
this.getUIElement(section_id, this.option).node.removeAttribute('disabled');
|
|
|
|
return true;
|
|
}
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.Flag, 'include_all', _('Include all'),
|
|
_('Includes all Proxy Node and Provider.'));
|
|
so.default = so.disabled;
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.Flag, 'include_all_proxies', _('Include all node'),
|
|
_('Includes all Proxy Node.'));
|
|
so.default = so.disabled;
|
|
so.editable = true;
|
|
|
|
so = ss.taboption('field_general', form.Flag, 'include_all_providers', _('Include all provider'),
|
|
_('Includes all Provider.'));
|
|
so.default = so.disabled;
|
|
so.editable = true;
|
|
|
|
/* Override fields */
|
|
so = ss.taboption('field_override', form.Flag, 'disable_udp', _('Disable UDP'));
|
|
so.default = so.disabled;
|
|
so.modalonly = true;
|
|
|
|
/* Health fields */
|
|
/* Url-test/Fallback/Load-balance */
|
|
so = ss.taboption('field_health', form.Value, 'url', _('Health check URL'));
|
|
so.default = hm.health_checkurls[0][0];
|
|
hm.health_checkurls.forEach((res) => {
|
|
so.value.apply(so, res);
|
|
})
|
|
so.validate = L.bind(hm.validateUrl, so);
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_health', form.Value, 'interval', _('Health check interval'),
|
|
_('In seconds. <code>%s</code> will be used if empty.').format('600'));
|
|
so.placeholder = '600';
|
|
so.validate = L.bind(hm.validateTimeDuration, so);
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_health', form.Value, 'timeout', _('Health check timeout'),
|
|
_('In millisecond. <code>%s</code> will be used if empty.').format('5000'));
|
|
so.datatype = 'uinteger';
|
|
so.placeholder = '5000';
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_health', form.Flag, 'lazy', _('Lazy'),
|
|
_('No testing is performed when this provider node is not in use.'));
|
|
so.default = so.enabled;
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_health', form.Value, 'expected_status', _('Health check expected status'),
|
|
_('Expected HTTP code. <code>204</code> will be used if empty. ') +
|
|
_('For format see <a target="_blank" href="%s" rel="noreferrer noopener">%s</a>.')
|
|
.format('https://wiki.metacubex.one/config/proxy-groups/#expected-status', _('Expected status')));
|
|
so.placeholder = '200/302/400-503';
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_health', form.Value, 'max_failed_times', _('Max count of failures'),
|
|
_('Exceeding this triggers a forced health check. <code>5</code> will be used if empty.'));
|
|
so.datatype = 'uinteger';
|
|
so.placeholder = '5';
|
|
so.depends({type: 'select', '!reverse': true});
|
|
so.modalonly = true;
|
|
|
|
/* Url-test fields */
|
|
so = ss.taboption('field_general', form.Value, 'tolerance', _('Node switch tolerance'),
|
|
_('In millisecond. <code>%s</code> will be used if empty.').format('150'));
|
|
so.datatype = 'uinteger';
|
|
so.placeholder = '150';
|
|
so.depends('type', 'url-test');
|
|
so.modalonly = true;
|
|
|
|
/* Load-balance fields */
|
|
so = ss.taboption('field_general', form.ListValue, 'strategy', _('Strategy'),
|
|
_('For details, see <a target="_blank" href="%s" rel="noreferrer noopener">%s</a>.')
|
|
.format('https://wiki.metacubex.one/config/proxy-groups/load-balance/#strategy', _('Strategy')));
|
|
so.default = hm.load_balance_strategy[0][0];
|
|
hm.load_balance_strategy.forEach((res) => {
|
|
so.value.apply(so, res);
|
|
})
|
|
so.depends('type', 'load-balance');
|
|
so.modalonly = true;
|
|
|
|
/* General fields */
|
|
so = ss.taboption('field_general', form.DynamicList, 'filter', _('Node filter'),
|
|
_('Filter nodes that meet keywords or regexps.'));
|
|
so.placeholder = '(?i)港|hk|hongkong|hong kong';
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_general', form.DynamicList, 'exclude_filter', _('Node exclude filter'),
|
|
_('Exclude nodes that meet keywords or regexps.'));
|
|
so.placeholder = 'xxx';
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_general', form.DynamicList, 'exclude_type', _('Node exclude type'),
|
|
_('Exclude matched node types. Available types see <a target="_blank" href="%s" rel="noreferrer noopener">here</a>.')
|
|
.format('https://wiki.metacubex.one/config/proxy-groups/#exclude-type'));
|
|
so.placeholder = 'Shadowsocks|Trojan';
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_general', form.Flag, 'hidden', _('Hidden'),
|
|
_('Returns hidden status in the API to hide the display of this proxy group.') + '</br>' +
|
|
_('requires front-end adaptation using the API.'));
|
|
so.default = so.disabled;
|
|
so.modalonly = true;
|
|
|
|
so = ss.taboption('field_general', form.Value, 'icon', _('Icon'),
|
|
_('Returns the string input for icon in the API to display in this proxy group.') + '</br>' +
|
|
_('requires front-end adaptation using the API.'));
|
|
so.modalonly = true;
|
|
/* Proxy Group END */
|
|
|
|
/* Routing rules START */
|
|
s.tab('rules', _('Routing rule'));
|
|
|
|
/* Routing rules */
|
|
o = s.taboption('rules', form.SectionValue, '_rules', hm.GridSection, 'rules', null);
|
|
ss = o.subsection;
|
|
ss.addremove = true;
|
|
ss.rowcolors = true;
|
|
ss.sortable = true;
|
|
ss.nodescriptions = true;
|
|
ss.hm_modaltitle = [ _('Routing rule'), _('Add a routing rule') ];
|
|
ss.hm_prefmt = hm.glossary[ss.sectiontype].prefmt;
|
|
ss.hm_field = hm.glossary[ss.sectiontype].field;
|
|
ss.hm_lowcase_only = false;
|
|
/* Import mihomo config start */
|
|
ss.handleYamlImport = function() {
|
|
const field = this.hm_field;
|
|
const o = new hm.HandleImport(this.map, this, _('Import mihomo config'),
|
|
_('Please type <code>%s</code> fields of mihomo config.</br>')
|
|
.format(field));
|
|
o.placeholder = 'rules:\n' +
|
|
'- DOMAIN,ad.com,REJECT\n' +
|
|
'- DOMAIN-REGEX,^abc.*com,auto\n' +
|
|
'- GEOSITE,youtube,PROXY\n' +
|
|
'- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve\n' +
|
|
'- IP-SUFFIX,8.8.8.8/24,auto\n' +
|
|
'- IP-ASN,13335,DIRECT\n' +
|
|
'- GEOIP,CN,DIRECT\n' +
|
|
'- PROCESS-PATH,/usr/bin/wget,auto\n' +
|
|
'- PROCESS-PATH-REGEX,.*bin/wget,auto\n' +
|
|
'- PROCESS-NAME,curl,auto\n' +
|
|
'- PROCESS-NAME-REGEX,curl$,auto\n' +
|
|
'- UID,1001,DIRECT\n' +
|
|
'- NETWORK,udp,DIRECT\n' +
|
|
'- DSCP,4,DIRECT\n' +
|
|
'- RULE-SET,google,GLOBAL,no-resolve\n' +
|
|
'- AND,((DST-PORT,443),(NETWORK,udp)),REJECT\n' +
|
|
'- OR,((NETWORK,UDP),(DOMAIN,baidu.com)),DIRECT\n' +
|
|
'- NOT,((DOMAIN,baidu.com)),auto\n' +
|
|
'- SUB-RULE,(NETWORK,tcp),sub-rule1\n' +
|
|
'- SUB-RULE,(OR,((NETWORK,udp),(DOMAIN,google.com))),sub-rule2\n' +
|
|
'- AND,((GEOIP,cn),(DSCP,12),(NETWORK,udp),(NOT,((IP-ASN,12345))),(DSCP,14),(NOT,((NETWORK,udp)))),DIRECT\n' +
|
|
'- MATCH,GLOBAL\n' +
|
|
' ...'
|
|
o.parseYaml = function(field, name, cfg) {
|
|
let config = hm.HandleImport.prototype.parseYaml.call(this, field, name, cfg);
|
|
|
|
return config ? parseRulesYaml.call(this, field, name, config) : null;
|
|
};
|
|
|
|
return o.render();
|
|
}
|
|
ss.renderSectionAdd = function(/* ... */) {
|
|
let el = hm.GridSection.prototype.renderSectionAdd.apply(this, arguments);
|
|
|
|
el.appendChild(E('button', {
|
|
'class': 'cbi-button cbi-button-add',
|
|
'title': _('mihomo config'),
|
|
'click': ui.createHandlerFn(this, 'handleYamlImport')
|
|
}, [ _('Import mihomo config') ]));
|
|
|
|
return el;
|
|
}
|
|
/* Import mihomo config end */
|
|
|
|
so = ss.option(form.Value, 'label', _('Label'));
|
|
so.load = L.bind(hm.loadDefaultLabel, so);
|
|
so.validate = L.bind(hm.validateUniqueValue, so);
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
|
so.default = so.enabled;
|
|
so.editable = true;
|
|
|
|
renderRules(ss, data[0]);
|
|
|
|
so = ss.option(form.ListValue, 'SUB-RULE', _('SUB-RULE'));
|
|
so.load = function(section_id) {
|
|
hm.loadSubRuleGroup.call(this, [['', _('-- Please choose --')]], section_id);
|
|
|
|
return new RulesEntry(uci.get(data[0], section_id, 'entry')).subrule || '';
|
|
}
|
|
so.validate = function(section_id, value) {
|
|
value = this.formvalue(section_id);
|
|
|
|
this.section.getUIElement(section_id, 'detour').node.querySelector('select').disabled = value ? 'true' : null;
|
|
|
|
return true;
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'entry');
|
|
|
|
let rule = new RulesEntry(UIEl.getValue()).setKey('subrule', value);
|
|
|
|
UIEl.node.previousSibling.innerText = rule.toString('mihomo');
|
|
UIEl.setValue(rule.toString('json'));
|
|
}
|
|
so.write = function() {};
|
|
so.modalonly = true;
|
|
/* Routing rules END */
|
|
|
|
/* Sub rules START */
|
|
s.tab('subrules', _('Sub rule'));
|
|
|
|
/* Sub rules */
|
|
o = s.taboption('subrules', form.SectionValue, '_subrules', hm.GridSection, 'subrules', null);
|
|
ss = o.subsection;
|
|
ss.addremove = true;
|
|
ss.rowcolors = true;
|
|
ss.sortable = true;
|
|
ss.nodescriptions = true;
|
|
ss.hm_modaltitle = [ _('Sub rule'), _('Add a sub rule') ];
|
|
ss.hm_prefmt = hm.glossary[ss.sectiontype].prefmt;
|
|
ss.hm_field = hm.glossary[ss.sectiontype].field;
|
|
ss.hm_lowcase_only = false;
|
|
/* Import mihomo config start */
|
|
ss.handleYamlImport = function() {
|
|
const field = this.hm_field;
|
|
const o = new hm.HandleImport(this.map, this, _('Import mihomo config'),
|
|
_('Please type <code>%s</code> fields of mihomo config.</br>')
|
|
.format(field));
|
|
o.placeholder = 'sub-rules:\n' +
|
|
' sub-rule1:\n' +
|
|
' - DOMAIN-SUFFIX,baidu.com,DIRECT\n' +
|
|
' - MATCH,GLOBAL\n' +
|
|
' sub-rule2:\n' +
|
|
' - IP-CIDR,1.1.1.1/32,REJECT\n' +
|
|
' - IP-CIDR,8.8.8.8/32,auto\n' +
|
|
' - DOMAIN,dns.alidns.com,REJECT\n' +
|
|
' ...'
|
|
o.appendcommand = ' | with_entries(.key as $k | .value |= map("\\($k):" + .)) | [.[][]]'
|
|
o.parseYaml = function(field, name, cfg) {
|
|
let config = hm.HandleImport.prototype.parseYaml.call(this, field, name, cfg);
|
|
|
|
return config ? parseSubrulesYaml.call(this, field, name, config) : null;
|
|
};
|
|
|
|
return o.render();
|
|
}
|
|
ss.renderSectionAdd = function(/* ... */) {
|
|
let el = hm.GridSection.prototype.renderSectionAdd.apply(this, arguments);
|
|
|
|
el.appendChild(E('button', {
|
|
'class': 'cbi-button cbi-button-add',
|
|
'title': _('mihomo config'),
|
|
'click': ui.createHandlerFn(this, 'handleYamlImport')
|
|
}, [ _('Import mihomo config') ]));
|
|
|
|
return el;
|
|
}
|
|
/* Import mihomo config end */
|
|
|
|
so = ss.option(form.Value, 'label', _('Label'));
|
|
so.load = L.bind(hm.loadDefaultLabel, so);
|
|
so.validate = L.bind(hm.validateUniqueValue, so);
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
|
so.default = so.enabled;
|
|
so.editable = true;
|
|
|
|
so = ss.option(form.Value, 'group', _('Sub rule group'));
|
|
so.value('sub-rule1');
|
|
so.rmempty = false;
|
|
so.validate = L.bind(hm.validateAuthUsername, so);
|
|
so.editable = true;
|
|
|
|
renderRules(ss, data[0]);
|
|
/* Sub rules END */
|
|
|
|
/* DNS settings START */
|
|
s.tab('dns', _('DNS settings'));
|
|
|
|
/* DNS settings */
|
|
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'fchomo', null);
|
|
ss = o.subsection;
|
|
|
|
so = ss.option(form.Value, 'dns_port', _('Listen port'));
|
|
so.datatype = 'port'
|
|
so.placeholder = '7853';
|
|
so.rmempty = false;
|
|
|
|
so = ss.option(form.Flag, 'ipv6', _('IPv6 support'));
|
|
so.default = so.enabled;
|
|
|
|
so = ss.option(form.MultiValue, 'boot_server', _('Boot DNS server'),
|
|
_('Used to resolve the domain of the DNS server. Must be IP.'));
|
|
so.default = 'default-dns';
|
|
so.load = L.bind(loadDNSServerLabel, so);
|
|
so.validate = L.bind(validateNameserver, so);
|
|
so.rmempty = false;
|
|
|
|
so = ss.option(form.MultiValue, 'bootnode_server', _('Boot DNS server (Node)'),
|
|
_('Used to resolve the domain of the Proxy node.'));
|
|
so.default = 'default-dns';
|
|
so.load = L.bind(loadDNSServerLabel, so);
|
|
so.validate = L.bind(validateNameserver, so);
|
|
so.rmempty = false;
|
|
|
|
so = ss.option(form.MultiValue, 'default_server', _('Default DNS server'));
|
|
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For non-poisoned domains)') : _('Final DNS server');
|
|
so.default = 'default-dns';
|
|
so.load = L.bind(loadDNSServerLabel, so);
|
|
so.validate = L.bind(validateNameserver, so);
|
|
so.rmempty = false;
|
|
|
|
so = ss.option(form.MultiValue, 'fallback_server', _('Fallback DNS server'));
|
|
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For poisoned domains)') : _('Fallback DNS server');
|
|
so.load = L.bind(loadDNSServerLabel, so);
|
|
so.validate = L.bind(validateNameserver, so);
|
|
so.onchange = function(ev, section_id, value) {
|
|
let ddesc = this.section.getUIElement(section_id, 'default_server').node.nextSibling;
|
|
let fdesc = ev.target.nextSibling;
|
|
if (value.length > 0) {
|
|
ddesc.innerHTML = _('Final DNS server (For non-poisoned domains)');
|
|
fdesc.innerHTML = _('Final DNS server (For poisoned domains)');
|
|
} else {
|
|
ddesc.innerHTML = _('Final DNS server');
|
|
fdesc.innerHTML = _('Fallback DNS server');
|
|
}
|
|
}
|
|
/* DNS settings END */
|
|
|
|
/* DNS server START */
|
|
s.tab('dns_server', _('DNS server'));
|
|
|
|
/* DNS server */
|
|
o = s.taboption('dns_server', form.SectionValue, '_dns_server', hm.GridSection, 'dns_server', null);
|
|
ss = o.subsection;
|
|
ss.addremove = true;
|
|
ss.rowcolors = true;
|
|
ss.sortable = true;
|
|
ss.nodescriptions = true;
|
|
ss.hm_modaltitle = [ _('DNS server'), _('Add a DNS server') ];
|
|
ss.hm_prefmt = hm.glossary[ss.sectiontype].prefmt;
|
|
ss.hm_field = hm.glossary[ss.sectiontype].field;
|
|
ss.hm_lowcase_only = true;
|
|
/* Import mihomo config start */
|
|
ss.handleYamlImport = function() {
|
|
const field = this.hm_field;
|
|
const o = new hm.HandleImport(this.map, this, _('Import mihomo config'),
|
|
_('Please type <code>%s</code> fields of mihomo config.</br>')
|
|
.format(field));
|
|
o.placeholder = 'nameserver:\n' +
|
|
'- 223.5.5.5\n' +
|
|
'- tls://8.8.4.4:853\n' +
|
|
'- https://doh.pub/dns-query#DIRECT\n' +
|
|
'- https://dns.alidns.com/dns-query#auto&h3=true&ecs=1.1.1.1/24\n' +
|
|
' ...'
|
|
o.parseYaml = function(field, name, cfg) {
|
|
let config = hm.HandleImport.prototype.parseYaml.call(this, field, name, cfg);
|
|
|
|
return config ? parseDNSYaml.call(this, field, name, config) : null;
|
|
};
|
|
|
|
return o.render();
|
|
}
|
|
ss.renderSectionAdd = function(/* ... */) {
|
|
let el = hm.GridSection.prototype.renderSectionAdd.apply(this, arguments);
|
|
|
|
el.appendChild(E('button', {
|
|
'class': 'cbi-button cbi-button-add',
|
|
'title': _('mihomo config'),
|
|
'click': ui.createHandlerFn(this, 'handleYamlImport')
|
|
}, [ _('Import mihomo config') ]));
|
|
|
|
return el;
|
|
}
|
|
/* Import mihomo config end */
|
|
|
|
so = ss.option(form.Value, 'label', _('Label'));
|
|
so.load = L.bind(hm.loadDefaultLabel, so);
|
|
so.validate = L.bind(hm.validateUniqueValue, so);
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
|
so.default = so.enabled;
|
|
so.editable = true;
|
|
|
|
so = ss.option(form.DummyValue, 'address', _('Address'));
|
|
so.write = L.bind(form.AbstractValue.prototype.write, so);
|
|
so.remove = L.bind(form.AbstractValue.prototype.remove, so);
|
|
so.editable = true;
|
|
|
|
so = ss.option(form.Value, 'addr', _('Address'));
|
|
so.load = function(section_id) {
|
|
return new DNSAddress(uci.get(data[0], section_id, 'address')).addr;
|
|
}
|
|
so.validate = function(section_id, value) {
|
|
if (value.match('#'))
|
|
return _('Expecting: %s').format(_('No add\'l params'));
|
|
|
|
// params only available on DoH
|
|
// https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/config/config.go#L1211C8-L1211C14
|
|
if (value.match(/^https?:\/\//)){
|
|
this.section.getUIElement(section_id, 'h3').node.querySelector('input').disabled = null;
|
|
this.section.getUIElement(section_id, 'ecs').node.querySelector('input').disabled = null;
|
|
this.section.getUIElement(section_id, 'ecs-override').node.querySelector('input').disabled = null;
|
|
} else {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3').setParam('ecs').setParam('ecs-override').toString();
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
|
|
['h3', 'ecs', 'ecs-override'].forEach((opt) => {
|
|
let UIEl = this.section.getUIElement(section_id, opt);
|
|
UIEl.setValue('');
|
|
UIEl.node.querySelector('input').disabled = 'true';
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = ('N' + UIEl.getValue()).replace(/^[^#]+/, value);
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
}
|
|
so.write = function() {};
|
|
so.rmempty = false;
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(hm.ListValue, 'detour', _('Proxy group'));
|
|
so.load = function(section_id) {
|
|
hm.loadProxyGroupLabel.call(this, hm.preset_outbound.dns, section_id);
|
|
|
|
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('detour');
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = new DNSAddress(UIEl.getValue()).setParam('detour', value).toString();
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
}
|
|
so.write = function() {};
|
|
so.editable = true;
|
|
|
|
so = ss.option(form.Flag, 'h3', _('HTTP/3'));
|
|
so.default = so.disabled;
|
|
so.load = function(section_id) {
|
|
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('h3') ? true : false);
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3', flagToBool(value) || null).toString();
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
}
|
|
so.write = function() {};
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Value, 'ecs', _('EDNS Client Subnet'));
|
|
so.datatype = 'cidr';
|
|
so.load = function(section_id) {
|
|
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs');
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs', value).toString();
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
}
|
|
so.write = function() {};
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Flag, 'ecs-override', _('ECS override'),
|
|
_('Override ECS in original request.'));
|
|
so.default = so.disabled;
|
|
so.load = function(section_id) {
|
|
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs-override') ? true : false);
|
|
}
|
|
so.onchange = function(ev, section_id, value) {
|
|
let UIEl = this.section.getUIElement(section_id, 'address');
|
|
|
|
let newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs-override', flagToBool(value) || null).toString();
|
|
|
|
UIEl.node.previousSibling.innerText = newvalue;
|
|
UIEl.setValue(newvalue);
|
|
}
|
|
so.write = function() {};
|
|
so.depends({'ecs': /.+/});
|
|
so.modalonly = true;
|
|
/* DNS server END */
|
|
|
|
/* DNS policy START */
|
|
s.tab('dns_policy', _('DNS policy'));
|
|
|
|
/* DNS policy */
|
|
o = s.taboption('dns_policy', form.SectionValue, '_dns_policy', hm.GridSection, 'dns_policy', null);
|
|
ss = o.subsection;
|
|
ss.addremove = true;
|
|
ss.rowcolors = true;
|
|
ss.sortable = true;
|
|
ss.nodescriptions = true;
|
|
ss.hm_modaltitle = [ _('DNS policy'), _('Add a DNS policy') ];
|
|
ss.hm_prefmt = hm.glossary[ss.sectiontype].prefmt;
|
|
ss.hm_field = hm.glossary[ss.sectiontype].field;
|
|
ss.hm_lowcase_only = false;
|
|
/* Import mihomo config start */
|
|
ss.handleYamlImport = function() {
|
|
const field = this.hm_field;
|
|
const o = new hm.HandleImport(this.map, this, _('Import mihomo config'),
|
|
_('Please type <code>%s</code> fields of mihomo config.</br>')
|
|
.format(field));
|
|
o.placeholder = 'nameserver-policy:\n' +
|
|
" 'www.baidu.com,.baidu.com': '223.5.5.5'\n" +
|
|
" '+.internal.crop.com': 'tls://8.8.4.4:853'\n" +
|
|
' "geosite:cn,private":\n' +
|
|
' - https://doh.pub/dns-query#DIRECT\n' +
|
|
' "rule-set:google": tls://8.8.4.4:853\n' +
|
|
' ...'
|
|
o.parseYaml = function(field, name, cfg) {
|
|
let config = hm.HandleImport.prototype.parseYaml.call(this, field, name, cfg);
|
|
|
|
return config ? parseDNSPolicyYaml.call(this, field, name, config) : null;
|
|
};
|
|
|
|
return o.render();
|
|
}
|
|
ss.renderSectionAdd = function(/* ... */) {
|
|
let el = hm.GridSection.prototype.renderSectionAdd.apply(this, arguments);
|
|
|
|
el.appendChild(E('button', {
|
|
'class': 'cbi-button cbi-button-add',
|
|
'title': _('mihomo config'),
|
|
'click': ui.createHandlerFn(this, 'handleYamlImport')
|
|
}, [ _('Import mihomo config') ]));
|
|
|
|
return el;
|
|
}
|
|
/* Import mihomo config end */
|
|
|
|
so = ss.option(form.Value, 'label', _('Label'));
|
|
so.load = L.bind(hm.loadDefaultLabel, so);
|
|
so.validate = L.bind(hm.validateUniqueValue, so);
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
|
so.default = so.enabled;
|
|
so.editable = true;
|
|
|
|
so = ss.option(form.ListValue, 'type', _('Type'));
|
|
so.value('domain', _('Domain'));
|
|
so.value('geosite', _('Geosite'));
|
|
so.value('rule_set', _('Rule set'));
|
|
so.default = 'domain';
|
|
|
|
so = ss.option(form.DynamicList, 'domain', _('Domain'),
|
|
_('Match domain. Support wildcards.'));
|
|
so.depends('type', 'domain');
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.DynamicList, 'geosite', _('Geosite'),
|
|
_('Match geosite.'));
|
|
so.depends('type', 'geosite');
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.MultiValue, 'rule_set', _('Rule set'),
|
|
_('Match rule set.'));
|
|
so.value('', _('-- Please choose --'));
|
|
so.load = L.bind(hm.loadRulesetLabel, so, [['', _('-- Please choose --')]], ['domain', 'classical']);
|
|
so.depends('type', 'rule_set');
|
|
so.modalonly = true;
|
|
|
|
so = ss.option(form.DummyValue, '_entry', _('Entry'));
|
|
so.load = function(section_id) {
|
|
const option = uci.get(data[0], section_id, 'type');
|
|
|
|
return uci.get(data[0], section_id, option)?.join(',');
|
|
}
|
|
so.modalonly = false;
|
|
|
|
so = ss.option(form.MultiValue, 'server', _('DNS server'));
|
|
so.value('default-dns');
|
|
so.default = 'default-dns';
|
|
so.load = L.bind(loadDNSServerLabel, so);
|
|
so.validate = L.bind(validateNameserver, so);
|
|
so.rmempty = false;
|
|
so.editable = true;
|
|
|
|
so = ss.option(hm.ListValue, 'proxy', _('Proxy group override'),
|
|
_('Override the Proxy group of DNS server.'));
|
|
so.default = hm.preset_outbound.direct[0][0];
|
|
hm.preset_outbound.direct.forEach((res) => {
|
|
so.value.apply(so, res);
|
|
})
|
|
so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.direct);
|
|
so.editable = true;
|
|
/* DNS policy END */
|
|
|
|
/* Fallback filter START */
|
|
s.tab('fallback_filter', _('Fallback filter'));
|
|
|
|
/* Fallback filter */
|
|
o = s.taboption('fallback_filter', form.SectionValue, '_fallback_filter', form.NamedSection, 'dns', 'fchomo', null);
|
|
o.depends({'fchomo.dns.fallback_server': /.+/});
|
|
ss = o.subsection;
|
|
|
|
so = ss.option(form.Flag, 'fallback_filter_geoip', _('Geoip enable'));
|
|
so.default = so.enabled;
|
|
|
|
so = ss.option(form.Value, 'fallback_filter_geoip_code', _('Geoip code'),
|
|
_('Match response with geoip.</br>') +
|
|
_('The matching <code>%s</code> will be deemed as not-poisoned.').format(_('IP')));
|
|
so.default = 'cn';
|
|
so.placeholder = 'cn';
|
|
so.rmempty = false;
|
|
so.retain = true;
|
|
so.depends('fallback_filter_geoip', '1');
|
|
|
|
so = ss.option(form.DynamicList, 'fallback_filter_geosite', _('Geosite'),
|
|
_('Match geosite.</br>') +
|
|
_('The matching <code>%s</code> will be deemed as poisoned.').format(_('Domain')));
|
|
|
|
so = ss.option(form.DynamicList, 'fallback_filter_ipcidr', _('IP CIDR'),
|
|
_('Match response with ipcidr.</br>') +
|
|
_('The matching <code>%s</code> will be deemed as poisoned.').format(_('IP')));
|
|
so.datatype = 'list(cidr)';
|
|
|
|
so = ss.option(form.DynamicList, 'fallback_filter_domain', _('Domain'),
|
|
_('Match domain. Support wildcards.</br>') +
|
|
_('The matching <code>%s</code> will be deemed as poisoned.').format(_('Domain')));
|
|
/* Fallback filter END */
|
|
|
|
return m.render();
|
|
}
|
|
});
|