// Copyright 2023 MOSSDeF, Stan Grishin // This code wouldn't have been possible without help from: // - [@jow-](https://github.com/jow-) // - [@stokito](https://github.com/stokito) // - [@vsviridov](https://github.com/vsviridov) // noinspection JSAnnotator "use strict"; "require form"; "require rpc"; "require view"; "require https-dns-proxy.status as hdp"; var pkg = { get Name() { return "https-dns-proxy"; }, get URL() { return "https://docs.openwrt.melmac.net/" + pkg.Name + "/"; }, templateToRegexp: function (template) { return RegExp( "^" + template .split(/(\{\w+\})/g) .map((part) => { let placeholder = part.match(/^\{(\w+)\}$/); if (placeholder) return `(?<${placeholder[1]}>.*?)`; else return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }) .join("") + "$" ); }, templateToResolver: function (template, args) { return template.replace(/{(\w+)}/g, (_, v) => args[v]); }, }; return view.extend({ load: function () { return Promise.all([ L.resolveDefault(hdp.getPlatformSupport(pkg.Name), {}), L.resolveDefault(hdp.getProviders(pkg.Name), {}), L.resolveDefault(L.uci.load(pkg.Name), {}), L.resolveDefault(L.uci.load("dhcp"), {}), ]); }, render: function (data) { var reply = { platform: (data[0] && data[0][pkg.Name]) || { http2_support: null, http3_support: null, }, providers: (data[1] && data[1][pkg.Name]) || { providers: [] }, }; reply.providers.sort(function (a, b) { return _(a.title).localeCompare(_(b.title)); }); reply.providers.push({ title: "Custom", template: "{option}", params: { option: { type: "text" } }, }); var status, m, s, o, p; var text; status = new hdp.status(); m = new form.Map(pkg.Name, _("HTTPS DNS Proxy - Configuration")); s = m.section(form.NamedSection, "config", pkg.Name); o = s.option( form.ListValue, "dnsmasq_config_update_option", _("Update DNSMASQ Config on Start/Stop"), _( "If update option is selected, the %s'DNS Forwards' section of DHCP and DNS%s will be automatically updated to use selected DoH providers (%smore information%s)." ).format( '', "", '', "" ) ); o.value("*", _("Update all configs")); o.value("+", _("Update select configs")); o.value("-", _("Do not update configs")); o.default = "*"; o.retain = true; o.cfgvalue = function (section_id) { let val = this.map.data.get( this.map.config, section_id, "dnsmasq_config_update" ); if (val && val[0]) { switch (val[0]) { case "*": case "-": return val[0]; default: return "+"; } } else return "*"; }; o.write = function (section_id, formvalue) { L.uci.set(pkg.Name, section_id, "dnsmasq_config_update", formvalue); }; o = s.option( form.MultiValue, "dnsmasq_config_update", _("Select the DNSMASQ Configs to update") ); Object.values(L.uci.sections("dhcp", "dnsmasq")).forEach(function ( element ) { var description; var key; if (element[".name"] === L.uci.resolveSID("dhcp", element[".name"])) { key = element[".index"]; description = "dnsmasq[" + element[".index"] + "]"; } else { key = element[".name"]; description = element[".name"]; } o.value(key, description); }); o.depends("dnsmasq_config_update_option", "+"); o.retain = true; o = s.option( form.ListValue, "force_dns", _("Force Router DNS"), _("Forces Router DNS use on local devices, also known as DNS Hijacking.") ); o.value("0", _("Let local devices use their own DNS servers if set")); o.value("1", _("Force Router DNS server to all local devices")); o.default = "1"; o = s.option( form.ListValue, "canary_domains_icloud", _("Canary Domains iCloud"), _( "Blocks access to iCloud Private Relay resolvers, forcing local devices to use router for DNS resolution (%smore information%s)." ).format( '', "" ) ); o.value("0", _("Let local devices use iCloud Private Relay")); o.value("1", _("Force Router DNS server to all local devices")); o.depends("force_dns", "1"); o.default = "1"; o = s.option( form.ListValue, "canary_domains_mozilla", _("Canary Domains Mozilla"), _( "Blocks access to Mozilla Encrypted resolvers, forcing local devices to use router for DNS resolution (%smore information%s)." ).format( '', "" ) ); o.value("0", _("Let local devices use Mozilla Private Relay")); o.value("1", _("Force Router DNS server to all local devices")); o.depends("force_dns", "1"); o.default = "1"; text = ""; if (!reply.platform.http2_support) text += _( "Please note that %s is not supported on this system (%smore information%s)." ).format( "HTTP/2", '', "" ) + "
"; if (!reply.platform.http3_support) text += _( "Please note that %s is not supported on this system (%smore information%s)." ).format( "HTTP/3 (QUIC)", '', "" ) + "
"; s = m.section( form.GridSection, "https-dns-proxy", _("HTTPS DNS Proxy - Instances"), text ); s.rowcolors = true; s.sortable = true; s.anonymous = true; s.addremove = true; s.sectiontitle = (section_id) => { var provText; var found; reply.providers.forEach((prov) => { var option; let regexp = pkg.templateToRegexp(prov.template); let resolver = L.uci.get(pkg.Name, section_id, "resolver_url"); resolver = resolver === undefined ? null : resolver; if (!found && resolver && regexp.test(resolver)) { found = true; provText = _(prov.title); let match = resolver.match(regexp); if (match[1] != null) { if ( prov.params && prov.params.option && prov.params.option.options ) { prov.params.option.options.forEach((opt) => { if (opt.value === match[1]) { option = _(opt.description); } }); provText += " (" + option + ")"; } else { if (match[1] !== "") provText += " (" + match[1] + ")"; } } } }); return provText || _("Unknown"); }; var _provider; _provider = s.option(form.ListValue, "_provider", _("Provider")); _provider.modalonly = true; _provider.cfgvalue = function (section_id) { let resolver = this.map.data.get( this.map.config, section_id, "resolver_url" ); if (resolver === undefined || resolver === null) return null; let found; let ret; reply.providers.forEach((prov, i) => { let regexp = pkg.templateToRegexp(prov.template); if (!found && regexp.test(resolver)) { found = true; ret = prov.template; } }); return ret || ""; }; _provider.write = function (section_id, formvalue) { L.uci.set(pkg.Name, section_id, "resolver_url", formvalue); }; reply.providers.forEach((prov, i) => { if (prov.http2_only && !reply.platform.http2_support) return; if (prov.http3_only && !reply.platform.http3_support) return; _provider.value(prov.template, _(prov.title)); if ( prov.params && prov.params.option && prov.params.option.type && prov.params.option.type === "select" ) { let optName = prov.params.option.description || _("Parameter"); var _paramList = s.option(form.ListValue, "_paramList_" + i, optName); _paramList.template = prov.template; _paramList.modalonly = true; if (prov.params.option.default) { _paramList.default = prov.params.option.default; } prov.params.option.options.forEach((opt) => { let val = opt.value || ""; let descr = opt.description || ""; _paramList.value(val, descr); }); _paramList.depends("_provider", prov.template); _paramList.write = function (section_id, formvalue) { let template = this.map.data.get( this.map.config, section_id, "resolver_url" ); if (!formvalue && _paramList.template !== template) return 0; let resolver = pkg.templateToResolver(_paramList.template, { option: formvalue || "", }); L.uci.set(pkg.Name, section_id, "resolver_url", resolver); }; _paramList.remove = _paramList.write; } else if ( prov.params && prov.params.option && prov.params.option.type && prov.params.option.type === "text" ) { let optName = prov.params.option.description || _("Parameter"); var _paramText = s.option(form.Value, "_paramText_" + i, optName); _paramText.template = prov.template; _paramText.modalonly = true; _paramText.depends("_provider", prov.template); _paramText.optional = !( prov.params.option.default && prov.params.option.default !== "" ); _paramText.cfgvalue = function (section_id) { let resolver = this.map.data.get( this.map.config, section_id, "resolver_url" ); if (resolver === undefined || resolver === null) return null; let regexp = pkg.templateToRegexp(prov.template); let match = resolver.match(regexp); return (match && match[1]) || null; }; _paramText.write = function (section_id, formvalue) { let template = this.map.data.get( this.map.config, section_id, "resolver_url" ); if (!formvalue && _paramText.template !== template) return 0; let resolver = pkg.templateToResolver(_paramText.template, { option: formvalue || "", }); L.uci.set(pkg.Name, section_id, "resolver_url", resolver); }; _paramText.remove = _paramText.write; } }); o = s.option(form.Value, "bootstrap_dns", _("Bootstrap DNS")); o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "listen_addr", _("Listen Address")); o.datatype = "ipaddr"; o.default = ""; o.optional = true; o.placeholder = "127.0.0.1"; o = s.option(form.Value, "listen_port", _("Listen Port")); o.datatype = "port"; o.default = ""; o.optional = true; o.placeholder = "5053"; o = s.option(form.Value, "user", _("Run As User")); o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "group", _("Run As Group")); o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "dscp_codepoint", _("DSCP Codepoint")); o.datatype = "and(uinteger, range(0,63))"; o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "verbosity", _("Logging Verbosity")); o.datatype = "and(uinteger, range(0,4))"; o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "logfile", _("Logging File Path")); o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "polling_interval", _("Polling Interval")); o.datatype = "and(uinteger, range(5,3600))"; o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.Value, "proxy_server", _("Proxy Server")); o.default = ""; o.modalonly = true; o.optional = true; o = s.option(form.ListValue, "use_http1", _("Use HTTP/1")); o.modalonly = true; o.optional = true; o.rmempty = true; o.value("", _("Use negotiated HTTP version")); o.value("1", _("Force use of HTTP/1")); o.default = ""; o = s.option( form.ListValue, "use_ipv6_resolvers_only", _("Use IPv6 resolvers") ); o.modalonly = true; o.optional = true; o.rmempty = true; o.value("", _("Use any family DNS resolvers")); o.value("1", _("Force use of IPv6 DNS resolvers")); o.default = ""; return Promise.all([status.render(), m.render()]); }, });