op-packages/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js
github-actions[bot] 9c5c48e5f2 🌈 Sync 2026-06-30 20:43:37
2026-06-30 20:43:37 +08:00

1896 lines
62 KiB
JavaScript

'use strict';
'require form';
'require poll';
'require uci';
'require ui';
'require view';
'require fchomo as hm';
'require tools.widgets as widgets';
const parseProxyGroupYaml = hm.parseYaml.extend({
key_mapping(cfg) {
if (!cfg.type)
return null;
// key mapping // 2026/06/10
let config = hm.removeBlankAttrs({
id: this.id,
label: this.label,
type: cfg.type,
groups: cfg.proxies ? cfg.proxies.map((grop) => hm.preset_outbound.proxy.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: this.bool2str(cfg["include-all"]), // bool
include_all_proxies: this.bool2str(cfg["include-all-proxies"]), // bool
include_all_providers: this.bool2str(cfg["include-all-providers"]), // bool
empty_fallback: cfg["empty-fallback"] ? hm.preset_outbound.proxy.map(([key, label]) => key).includes(cfg["empty-fallback"]) ? cfg["empty-fallback"] : this.calcID(hm.glossary["proxy_group"].field, cfg["empty-fallback"]) : null, // string
// Url-test fields
tolerance: cfg.tolerance,
// Load-balance fields
strategy: cfg.strategy,
// Override fields
disable_udp: this.bool2str(cfg["disable-udp"]), // bool
// Health fields
url: cfg.url,
interval: cfg.interval,
timeout: cfg.timeout,
lazy: this.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: this.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);
// disable-qtypes
// https://dns.google/dns-query#GLOBAL&disable-qtype-1=true&disable-qtype-28=true&disable-qtype-33=true&disable-qtype-65=true
let qtypes = [...this.params.keys()].filter(e => e.match(/^disable-qtype-\d+$/)).map(e => e.replace('disable-qtype-', ''))
if (!hm.isEmpty(qtypes))
this.params.set('disable-qtypes', qtypes);
}
parseParam(param) {
switch (param) {
case 'disable-qtypes': // https://github.com/miekg/dns/blob/master/types.go#L26
return this.params.has(param) ? this.params.get(param).split(',') : [];
default:
return this.params.has(param) ? decodeURI(this.params.get(param)) : null;
}
}
setParam(param, value) {
switch (param) {
case 'disable-qtypes':
if (!hm.isEmpty(value))
this.params.set(param, value);
else
this.params.delete(param);
break;
default:
if (value)
this.params.set(param, value);
else
this.params.delete(param);
}
return this
}
toString() {
return this.addr + (this.params.size === 0 ? '' : '#' +
['detour', 'h3', 'skip-cert-verify', 'ecs', 'ecs-override', 'disable-ipv4', 'disable-ipv6'].map((k) => {
return this.params.has(k) ? '%s=%s'.format(k, encodeURI(this.params.get(k))) : null;
}).filter(v => v).join('&') +
(function(k) {
return this.params.has(k) ? '&' + this.params.get(k).split(',').map(v => `disable-qtype-${v}=true`).join('&') : '';
}.bind(this, 'disable-qtypes')())
).replace('#&', '#');
}
}
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}'`);
}
}
const parseDNSYaml = hm.parseYaml.extend({
key_mapping(cfg) {
let addr = new DNSAddress(cfg);
if (!addr.toString())
return null;
let detour = addr.parseParam('detour');
if (detour)
addr.setParam('detour', hm.preset_outbound.dns.map(([key, label]) => key).includes(detour) ? detour : detour === 'RULES' ? '' : this.calcID(hm.glossary["proxy_group"].field, detour));
// key mapping // 2026/01/17
let config = {
id: this.id,
label: this.label,
address: addr.toString()
};
return config;
}
});
const parseDNSPolicyYaml = hm.parseYaml.extend({
key_mapping(cfg) {
//console.info([this.name, cfg]);
let type = this.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 = this.name.split(',');
type = 'domain';
break;
}
// key mapping // 2026/01/17
let config = {
id: this.id,
label: this.label,
type: type,
...Object.fromEntries([[type, rules]]),
server: (Array.isArray(cfg) ? cfg : [cfg]).map((dns) => this.calcID(hm.glossary["dns_server"].field, dns)),
//proxy: null // fchomo unique features
};
return config;
}
});
const parseRulesYaml = hm.parseYaml.extend({
key_mapping(cfg) {
let entry = this.parseRules(cfg);
if (!entry)
return null;
// key mapping // 2026/06/12
let config = {
id: this.id,
label: '%s %s'.format(this.id.slice(0,7), _('(Imported)')),
entry: entry
};
return config;
},
ParseRulePayload(ruleRaw, needTarget) {
// parse rules
// https://github.com/muink/mihomo/blob/300eb8b12a75504c4bd4a6037d2f6503fd3b347f/rules/common/base.go#L48-L76
let item = ruleRaw.split(",");
let tp = item[0].toUpperCase(),
payload,
target,
params = [];
if (item.length > 1) {
switch (tp) {
case "MATCH":
// MATCH doesn't contain payload and params
target = item[1];
break;
case "NOT":
case "OR":
case "AND":
case "SUB-RULE":
case "DOMAIN-REGEX":
case "PROCESS-NAME-REGEX":
case "PROCESS-PATH-REGEX":
// some type of rules that has comma in payload and don't need params
if (needTarget)
target = item.pop(); // don't have params so target must at the end of slices
payload = item.slice(1).join(",");
break;
default:
payload = item[1];
if (item.length > 2) {
if (needTarget) {
target = item[2];
if (item.length > 3)
params = item.slice(3);
} else
params = item.slice(2);
}
}
}
return [ tp, payload, target, params ];
},
ParseRule(tp, payload, target, params) {
// parse rules
// https://github.com/muink/mihomo/blob/ea19cda0c9b666aa0fc1b0412ae6fbc0ea9d44e0/rules/parser.go#L12
// nested ParseRule
let logical_payload, subrule;
if (tp === 'SUB-RULE') {
payload = payload.match(/^\((.*)\)$/); // SUB-RULE,(payload),subrule
if (payload)
[tp, payload, target, params, subrule] = [...this.ParseRulePayload(payload[1], false), target];
else
return null;
}
if (hm.rules_logical_type.map(e => e[0] || e).includes(tp)) {
logical_payload = payload.match(/^\(\((.*)\)\)$/); // LOGIC_TYPE,((payload1),(payload2),(payload3)),DIRECT
if (logical_payload)
logical_payload = logical_payload[1].split('),(');
else
return null;
}
// make entry
let entry = new RulesEntry();
entry.type = tp;
// 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 (tp === 'RULE-SET')
entry.setPayload(0, {factor: this.calcID(hm.glossary["ruleset"].field, payload)});
else
entry.setPayload(0, {factor: payload});
// parse target/subrule
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);
// parse params
params.forEach((param) => entry.setParam(param, true));
return entry;
},
parseRules(line) {
// parse rules
// https://github.com/muink/mihomo/blob/ad730ae9744971dfaa75bda20cfc2ffe07eeb59d/config/config.go#L1090-L1118
let [tp, payload, target, params] = this.ParseRulePayload(line, true);
if (!target)
return null; // error: format invalid
let parsed = this.ParseRule(tp, payload, target, params);
return parsed.toString('json');
}
});
const parseSubrulesYaml = parseRulesYaml.extend({
key_mapping(cfg) {
cfg = cfg.match(/^([^:]+):(.+)$/);
if (!cfg)
return null;
let config = new parseRulesYaml(this.field, this.name, cfg[2]).output(); // 2024/08/05
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 + 'rematchname', _('Factor') + ` ${n+1}`);
o.value('rematch1');
o.validate = hm.validateAuthUsername;
if (n === 0)
o.depends('type', 'REMATCH-NAME');
o.depends(prefix + 'type', 'REMATCH-NAME');
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: /\bCIDR6?\b/});
o.depends({type: /\bIP-SUFFIX\b/});
}
o.depends(Object.fromEntries([[prefix + 'type', /\bCIDR6?\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 = 'and(uinteger, 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.loadLabel.call(this, hm.loadLabelValues(this.config, 'ruleset'), 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_25_12 ? hm.DynamicList : form.DynamicList, prefix + 'fused', _('Factor') + ' ++', // @less_25_12
_('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.loadLabel.call(this, [
...fusedval,
...hm.loadLabelValues(this.config, 'ruleset')
], 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 = form.AbstractValue.prototype.write;
o.remove = form.AbstractValue.prototype.remove;
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
if (hm.rules_type_allowparms.includes(value)) {
['no-resolve', 'src'].forEach((opt) => {
let UIEl = this.section.getUIElement(section_id, opt);
UIEl.node.querySelector('input').removeAttribute('disabled');
});
} 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.loadLabel.call(this, [
...hm.preset_outbound.full,
...hm.loadLabelValues(this.config, 'proxy_group')
], 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', ''); // work on subrules not rules
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;
}
function renderPolicies(s, uciconfig) {
let o;
o = s.option(form.ListValue, 'type', _('Type'));
o.value('domain', _('Domain'));
o.value('geosite', _('Geosite'));
o.value('rule_set', _('Rule set'));
o.default = 'domain';
o = s.option(form.DynamicList, 'domain', _('Domain'),
_('Match domain. Support wildcards.'));
o.depends('type', 'domain');
o.modalonly = true;
o = s.option(form.DynamicList, 'geosite', _('Geosite'),
_('Match geosite.'));
o.depends('type', 'geosite');
o.modalonly = true;
o = s.option(form.MultiValue, 'rule_set', _('Rule set'),
_('Match rule set.'));
o.value('', _('-- Please choose --'));
o.load = function(section_id) {
return hm.loadLabel.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'ruleset', {behaviors: ['domain', 'classical']})
], section_id);
}
o.depends('type', 'rule_set');
o.modalonly = true;
o = s.option(form.DummyValue, '_entry', _('Entry'));
o.load = function(section_id) {
const option = uci.get(uciconfig, section_id, 'type');
return uci.get(uciconfig, section_id, option)?.join(',');
}
o.modalonly = false;
o = s.option(form.MultiValue, 'server', _('DNS server'));
o.value('default-dns');
o.default = 'default-dns';
o.load = loadDNSServerLabel;
o.validate = validateNameserver;
o.rmempty = false;
o.editable = true;
o = s.option(hm.ListValue, 'proxy', _('Proxy group override'),
_('Override the Proxy group of DNS server.'));
o.default = hm.preset_outbound.direct[0][0];
hm.preset_outbound.direct.forEach((res) => {
o.value.apply(o, res);
})
o.load = function(section_id) {
return hm.loadLabel.call(this, [
...hm.preset_outbound.direct,
...hm.loadLabelValues(this.config, 'proxy_group')
], section_id);
}
o.editable = 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' +
' empty-fallback: COMPATIBLE\n' +
' url: "https://cp.cloudflare.com/generate_204"\n' +
' interval: 300\n' +
' lazy: false\n' +
' strategy: consistent-hashing\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 = parseProxyGroupYaml;
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 = hm.loadDefaultLabel;
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', hm.MultiValue, 'groups', _('Group')); // @pr8758_merged
hm.preset_outbound.proxy.forEach((res) => {
so.value.apply(so, res);
})
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
...hm.preset_outbound.proxy,
...hm.loadLabelValues(this.config, 'proxy_group')
], section_id);
}
so.editable = true;
so = ss.taboption('field_general', hm.MultiValue, 'proxies', _('Node')); // @pr8758_merged
so.value('', _('-- Please choose --'));
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'node')
], section_id);
}
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', hm.MultiValue, 'use', _('Provider')); // @pr8758_merged
so.value('', _('-- Please choose --'));
so.keep_order = true;
so.load = function(section_id) {
return hm.loadLabel.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'provider')
], section_id);
}
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;
so = ss.taboption('field_general', form.Value, 'empty_fallback', _('Empty fallback'));
so.default = ''; // COMPATIBLE
hm.preset_outbound.proxy.forEach((res) => {
so.value.apply(so, res);
})
so.load = function(section_id) {
return hm.loadLabel.call(this, [
...hm.preset_outbound.proxy,
...hm.loadLabelValues(this.config, 'node')
], section_id);
}
so.textvalue = hm.textvalue2Value;
/* 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 = hm.validateUrl;
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 = hm.validateTimeDuration;
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-WILDCARD,*.google.com,auto\n' +
'- DOMAIN-REGEX,^abc.*com,auto\n' +
'- GEOSITE,youtube,GLOBAL\n' +
'- IP-CIDR,127.0.0.0/8,DIRECT,no-resolve\n' +
'- IP-CIDR6,2620:0:2d0:200::7/32,auto\n' +
'- IP-SUFFIX,8.8.8.8/24,auto\n' +
'- IP-ASN,13335,DIRECT\n' +
'- GEOIP,CN,DIRECT\n' +
'- SRC-GEOIP,cn,DIRECT\n' +
'- SRC-IP-ASN,9808,DIRECT\n' +
'- SRC-IP-CIDR,192.168.1.201/32,DIRECT\n' +
'- SRC-IP-SUFFIX,192.168.1.201/8,DIRECT\n' +
'- DST-PORT,80,DIRECT\n' +
'- SRC-PORT,7777,DIRECT\n' +
'- PROCESS-PATH,/usr/bin/wget,auto\n' +
'- PROCESS-PATH-WILDCARD,/usr/*/wget,GLOBAL\n' +
'- PROCESS-PATH-REGEX,.*bin/wget,auto\n' +
'- PROCESS-PATH-REGEX,(?i).*Application\\\\chrome.*,GLOBAL\n' +
'- PROCESS-NAME,curl,auto\n' +
'- PROCESS-NAME-WILDCARD,*telegram*,GLOBAL\n' +
'- PROCESS-NAME-REGEX,curl$,auto\n' +
'- PROCESS-NAME-REGEX,(?i)Telegram,GLOBAL\n' +
'- PROCESS-NAME-REGEX,.*telegram.*,GLOBAL\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 = parseRulesYaml;
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 = hm.loadDefaultLabel;
so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
so.default = so.enabled;
so.editable = true;
so.validate = function(/* ... */) {
let n = 0;
return hm.validatePresetIDs.call(this, [
['select', 'type'],
['select', `payload${n}_` + 'rule_set']
], ...arguments);
}
renderRules(ss, data[0]);
so = ss.option(form.ListValue, 'SUB-RULE', _('SUB-RULE'));
so.load = function(section_id) {
hm.loadLabel.call(this, [
['', _('-- Please choose --')],
...hm.loadLabelValues(this.config, 'subrule-group')
], section_id);
return new RulesEntry(uci.get(data[0], section_id, 'entry')).subrule || '';
}
so.validate = function(section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'detour');
value = this.formvalue(section_id);
UIEl.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 = parseSubrulesYaml;
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 = hm.loadDefaultLabel;
so.validate = hm.validateUniqueValue;
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 = hm.validateAuthUsername;
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.Value, 'routing_mark', _('Listen routing mark (Fwmark)'));
so.datatype = 'uinteger'
so = ss.option(form.Flag, 'ipv6', _('IPv6 support'));
so.default = so.enabled;
so = ss.option(form.MultiValue, 'boot_server', _('Bootstrap DNS server'),
_('Used to resolve the domain of the DNS server. Must be IP.'));
so.default = 'default-dns';
so.load = loadDNSServerLabel;
so.validate = validateNameserver;
so.rmempty = false;
so = ss.option(form.MultiValue, 'bootnode_server', _('Bootstrap DNS server (Node)'),
_('Used to resolve the domain of the Proxy node.'));
so.default = 'default-dns';
so.load = loadDNSServerLabel;
so.validate = validateNameserver;
so.rmempty = false;
const ddesc_disabled = _('Final DNS server');
const fdesc_disabled = _('Fallback DNS server');
const ddesc_enabled = _('Final DNS server (For non-poisoned domains)') + '</br>' +
_('Used to resolve domains that can be directly connected. Can use domestic DNS servers or ECS.');
const fdesc_enabled = _('Final DNS server (For poisoned domains)') + '</br>' +
_('Used to resolve domains you want to proxy. Recommended to configure %s for DNS servers.').format(_('Proxy Group'));
so = ss.option(form.MultiValue, 'default_server', _('Default DNS server'));
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? ddesc_enabled : ddesc_disabled;
so.default = 'default-dns';
so.load = loadDNSServerLabel;
so.validate = validateNameserver;
so.rmempty = false;
so = ss.option(form.MultiValue, 'fallback_server', _('Fallback DNS server'));
so.description = uci.get(data[0], so.section.section, 'fallback_server') ? fdesc_enabled : fdesc_disabled;
so.load = loadDNSServerLabel;
so.validate = validateNameserver;
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 = ddesc_enabled;
fdesc.innerHTML = fdesc_enabled;
} else {
ddesc.innerHTML = ddesc_disabled;
fdesc.innerHTML = fdesc_disabled;
}
}
/* 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 = 'dns:\n' +
' default-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' +
' nameserver-policy:\n' +
" 'geosite:category-ads-all': rcode://refused\n" +
" '+.arpa': '10.0.0.1'\n" +
" 'rule-set:cn':\n" +
' - https://doh.pub/dns-query\n' +
' - https://dns.alidns.com/dns-query\n' +
' nameserver:\n' +
' - https://doh.pub/dns-query\n' +
' - https://dns.alidns.com/dns-query\n' +
' fallback:\n' +
' - tls://8.8.4.4\n' +
' - tls://1.1.1.1\n' +
' proxy-server-nameserver:\n' +
' - https://doh.pub/dns-query\n' +
' proxy-server-nameserver-policy:\n' +
" 'www.yournode.com': '223.5.5.5'\n" +
' ...'
o.overridecommand = '.dns | pick(["default-nameserver", "proxy-server-nameserver", "nameserver", "fallback", "nameserver-policy", "proxy-server-nameserver-policy"]) | with(.["nameserver-policy"]; . = [.[]] | flatten) | with(.["proxy-server-nameserver-policy"]; . = [.[]] | flatten) | [.[][]] | unique'
o.parseYaml = parseDNSYaml;
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 = hm.loadDefaultLabel;
so.validate = hm.validateUniqueValue;
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 = form.AbstractValue.prototype.write;
so.remove = form.AbstractValue.prototype.remove;
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').removeAttribute('disabled');
} else {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('h3').toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
['h3'].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.loadLabel.call(this, [
...hm.preset_outbound.dns,
...hm.loadLabelValues(this.config, 'proxy_group')
], 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.Flag, 'skip-cert-verify', _('Skip cert verify'),
_('Donot verifying server certificate.') +
'<br/>' +
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
so.default = so.disabled;
so.load = function(section_id) {
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('skip-cert-verify') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('skip-cert-verify', 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 the existing 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;
so = ss.option(form.Flag, 'disable-ipv4', _('Filter record: %s').format('A'));
so.default = so.disabled;
so.load = function(section_id) {
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-ipv4') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-ipv4', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true;
so = ss.option(form.Flag, 'disable-ipv6', _('Filter record: %s').format('AAAA'));
so.default = so.disabled;
so.load = function(section_id) {
return boolToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-ipv6') ? true : false);
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-ipv6', flagToBool(value) || null).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true;
so = ss.option(form.DynamicList, 'disable-qtypes', _('Filter record type:'));
so.datatype = 'uinteger';
so.placeholder = '65';
so.load = function(section_id) {
return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('disable-qtypes');
}
so.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'address');
let newvalue = new DNSAddress(UIEl.getValue()).setParam('disable-qtypes', value).toString();
UIEl.node.previousSibling.innerText = newvalue;
UIEl.setValue(newvalue);
}
so.write = function() {};
so.modalonly = true;
/* DNS server END */
/* Bootstrap DNS policy (Node) START */
s.tab('dns_node_policy', _('Bootstrap DNS policy (Node)'));
/* Bootstrap DNS policy (Node) */
o = s.taboption('dns_node_policy', form.SectionValue, '_dns_node_policy', hm.GridSection, 'dns_node_policy', null);
ss = o.subsection;
ss.addremove = true;
ss.rowcolors = true;
ss.sortable = true;
ss.nodescriptions = true;
ss.hm_modaltitle = [ _('Bootstrap DNS policy (Node)'), _('Add a Bootstrap DNS policy (Node)') ];
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 = 'proxy-server-nameserver-policy:\n' +
" 'www.yournode.com': '223.5.5.5'\n" +
' ...'
o.parseYaml = parseDNSPolicyYaml;
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 = hm.loadDefaultLabel;
so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
so.default = so.enabled;
so.editable = true;
renderPolicies(ss, data[0]);
/* Bootstrap DNS policy (Node) 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 = parseDNSPolicyYaml;
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 = hm.loadDefaultLabel;
so.validate = hm.validateUniqueValue;
so.modalonly = true;
so = ss.option(form.Flag, 'enabled', _('Enable'));
so.default = so.enabled;
so.editable = true;
so.validate = function(/* ... */) {
return hm.validatePresetIDs.call(this, [
['select', 'type'],
['', 'rule_set']
], ...arguments);
}
renderPolicies(ss, data[0]);
/* 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();
}
});