'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 { 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') + ' ++', _('0 or 1 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 destination IP as the source IP.')); 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.
' + '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 %s fields of mihomo config.
') .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. %s 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. %s 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. 204 will be used if empty. ') + _('For format see %s.') .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. 5 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. %s 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 %s.') .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 here.') .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.') + '
' + _('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.') + '
' + _('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 %s fields of mihomo config.
') .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 %s fields of mihomo config.
') .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 %s fields of mihomo config.
') .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 %s fields of mihomo config.
') .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.
') + _('The matching %s 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.
') + _('The matching %s will be deemed as poisoned.').format(_('Domain'))); so = ss.option(form.DynamicList, 'fallback_filter_ipcidr', _('IP CIDR'), _('Match response with ipcidr.
') + _('The matching %s will be deemed as poisoned.').format(_('IP'))); so.datatype = 'list(cidr)'; so = ss.option(form.DynamicList, 'fallback_filter_domain', _('Domain'), _('Match domain. Support wildcards.
') + _('The matching %s will be deemed as poisoned.').format(_('Domain'))); /* Fallback filter END */ return m.render(); } });