luci-app-https-dns-proxy: rewrite in javascript
[project/luci.git] / applications / luci-app-https-dns-proxy / htdocs / luci-static / resources / view / https-dns-proxy / overview.js
1 // Copyright 2023 MOSSDeF, Stan Grishin <stangri@melmac.ca>
2 // This code wouldn't have been possible without help from:
3 // - [@jow-](https://github.com/jow-)
4 // - [@stokito](https://github.com/stokito)
5 // - [@vsviridov](https://github.com/vsviridov)
6
7 "use strict";
8 "require form";
9 "require rpc";
10 "require uci";
11 "require view";
12 "require https-dns-proxy.status as hdp";
13
14 var pkg = {
15 get Name() {
16 return "https-dns-proxy";
17 },
18 get URL() {
19 return "https://docs.openwrt.melmac.net/" + pkg.Name + "/";
20 },
21 templateToRegexp: function (template) {
22 return RegExp(
23 "^" +
24 template
25 .split(/(\{\w+\})/g)
26 .map((part) => {
27 let placeholder = part.match(/^\{(\w+)\}$/);
28 if (placeholder) return `(?<${placeholder[1]}>.*?)`;
29 else return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30 })
31 .join("") +
32 "$"
33 );
34 },
35 templateToResolver: function (template, args) {
36 return template.replace(/{(\w+)}/g, (_, v) => args[v]);
37 },
38 };
39
40 return view.extend({
41 load: function () {
42 return Promise.all([
43 L.resolveDefault(hdp.getPlatformSupport(pkg.Name), {}),
44 L.resolveDefault(hdp.getProviders(pkg.Name), {}),
45 uci.load(pkg.Name),
46 uci.load("dhcp"),
47 ]);
48 },
49
50 render: function (data) {
51 var reply = {
52 platform: (data[0] && data[0][pkg.Name]) || {
53 http2_support: null,
54 http3_support: null,
55 },
56 providers: (data[1] && data[1][pkg.Name]) || { providers: [] },
57 };
58 reply.providers.sort(function (a, b) {
59 return _(a.title).localeCompare(_(b.title));
60 });
61 reply.providers.push({
62 title: "Custom",
63 template: "{option}",
64 params: { option: { type: "text" } },
65 });
66
67 var status, m, s, o, p;
68 var text;
69
70 status = new hdp.status();
71
72 m = new form.Map(pkg.Name, _("HTTPS DNS Proxy - Configuration"));
73
74 s = m.section(form.NamedSection, "config", pkg.Name);
75 o = s.option(
76 form.ListValue,
77 "dnsmasq_config_update",
78 _("Update DNSMASQ Config on Start/Stop"),
79 _(
80 "If update option is selected, the %s'DNS forwardings' section of DHCP and DNS%s will be automatically updated to use selected DoH providers (%smore information%s)."
81 ).format(
82 '<a href="' + L.url("admin", "network", "dhcp") + '">',
83 "</a>",
84 '<a href="' + pkg.URL + "#default-settings" + '" target="_blank">',
85 "</a>"
86 )
87 );
88 o.value("*", _("Update all configs"));
89 var sections = uci.sections("dhcp", "dnsmasq");
90 sections.forEach((element) => {
91 var description;
92 var key;
93 if (element[".name"] === uci.resolveSID("dhcp", element[".name"])) {
94 key = element[".index"];
95 description = "dnsmasq[" + element[".index"] + "]";
96 } else {
97 key = element[".name"];
98 description = element[".name"];
99 }
100 o.value(key, _("Update %s only").format(description));
101 });
102 o.value("-", _("Do not update configs"));
103 o.default = "*";
104
105 o = s.option(
106 form.ListValue,
107 "force_dns",
108 _("Force Router DNS"),
109 _("Forces Router DNS use on local devices, also known as DNS Hijacking.")
110 );
111 o.value("0", _("Let local devices use their own DNS servers if set"));
112 o.value("1", _("Force Router DNS server to all local devices"));
113 o.default = "1";
114
115 o = s.option(
116 form.ListValue,
117 "canary_domains_icloud",
118 _("Canary Domains iCloud"),
119 _(
120 "Blocks access to iCloud Private Relay resolvers, forcing local devices to use router for DNS resolution (%smore information%s)."
121 ).format(
122 '<a href="' + pkg.URL + "#canary_domains_icloud" + '" target="_blank">',
123 "</a>"
124 )
125 );
126 o.value("0", _("Let local devices use iCloud Private Relay"));
127 o.value("1", _("Force Router DNS server to all local devices"));
128 o.depends("force_dns", "1");
129 o.default = "1";
130
131 o = s.option(
132 form.ListValue,
133 "canary_domains_mozilla",
134 _("Canary Domains Mozilla"),
135 _(
136 "Blocks access to Mozilla Encrypted resolvers, forcing local devices to use router for DNS resolution (%smore information%s)."
137 ).format(
138 '<a href="' +
139 pkg.URL +
140 "#canary_domains_mozilla" +
141 '" target="_blank">',
142 "</a>"
143 )
144 );
145 o.value("0", _("Let local devices use Mozilla Private Relay"));
146 o.value("1", _("Force Router DNS server to all local devices"));
147 o.depends("force_dns", "1");
148 o.default = "1";
149
150 text = "";
151 if (!reply.platform.http2_support)
152 text +=
153 _(
154 "Please note that %s is not supported on this system (%smore information%s)."
155 ).format(
156 "<i>HTTP/2</i>",
157 '<a href="' + pkg.URL + "#http2-support" + '" target="_blank">',
158 "</a>"
159 ) + "<br />";
160 if (!reply.platform.http3_support)
161 text +=
162 _(
163 "Please note that %s is not supported on this system (%smore information%s)."
164 ).format(
165 "<i>HTTP/3 (QUIC)</i>",
166 '<a href="' + pkg.URL + "#http3-quic-support" + '" target="_blank">',
167 "</a>"
168 ) + "<br />";
169
170 s = m.section(
171 form.GridSection,
172 "https-dns-proxy",
173 _("HTTPS DNS Proxy - Instances"),
174 text
175 );
176 s.rowcolors = true;
177 s.sortable = true;
178 s.anonymous = true;
179 s.addremove = true;
180
181 s.sectiontitle = (section_id) => {
182 var provText;
183 var found;
184 reply.providers.forEach((prov) => {
185 var option;
186 let regexp = pkg.templateToRegexp(prov.template);
187 let resolver = uci.get(pkg.Name, section_id, "resolver_url");
188 resolver = resolver === undefined ? null : resolver;
189 if (!found && resolver && regexp.test(resolver)) {
190 found = true;
191 provText = _(prov.title);
192 let match = resolver.match(regexp);
193 if (match[1] != null) {
194 if (
195 prov.params &&
196 prov.params.option &&
197 prov.params.option.options
198 ) {
199 prov.params.option.options.forEach((opt) => {
200 if (opt.value === match[1]) {
201 option = _(opt.description);
202 }
203 });
204 provText += " (" + option + ")";
205 } else {
206 if (match[1] !== "") provText += " (" + match[1] + ")";
207 }
208 }
209 }
210 });
211 return provText || _("Unknown");
212 };
213
214 var _provider;
215 _provider = s.option(form.ListValue, "_provider", _("Provider"));
216 _provider.modalonly = true;
217 _provider.cfgvalue = function (section_id) {
218 let resolver = this.map.data.get(
219 this.map.config,
220 section_id,
221 "resolver_url"
222 );
223 if (resolver === undefined || resolver === null) return null;
224 let found;
225 let ret;
226 reply.providers.forEach((prov, i) => {
227 let regexp = pkg.templateToRegexp(prov.template);
228 if (!found && regexp.test(resolver)) {
229 found = true;
230 ret = prov.template;
231 }
232 });
233 return ret || "";
234 };
235 _provider.write = function (section_id, formvalue) {
236 uci.set(pkg.Name, section_id, "resolver_url", formvalue);
237 };
238
239 reply.providers.forEach((prov, i) => {
240 if (prov.http2_only && !reply.platform.http2_support) return;
241 if (prov.http3_only && !reply.platform.http3_support) return;
242 _provider.value(prov.template, _(prov.title));
243 if (
244 prov.params &&
245 prov.params.option &&
246 prov.params.option.type &&
247 prov.params.option.type === "select"
248 ) {
249 let optName = prov.params.option.description || _("Parameter");
250 var _paramList = s.option(form.ListValue, "_paramList_" + i, optName);
251 _paramList.template = prov.template;
252 _paramList.modalonly = true;
253 if (prov.params.option.default) {
254 _paramList.default = prov.params.option.default;
255 }
256 prov.params.option.options.forEach((opt) => {
257 let val = opt.value || "";
258 let descr = opt.description || "";
259 _paramList.value(val, descr);
260 });
261 _paramList.depends("_provider", prov.template);
262 _paramList.write = function (section_id, formvalue) {
263 let template = this.map.data.get(
264 this.map.config,
265 section_id,
266 "resolver_url"
267 );
268 if (_paramList.template !== template) return 0;
269 let resolver = pkg.templateToResolver(template, {
270 option: formvalue || "",
271 });
272 uci.set(pkg.Name, section_id, "resolver_url", resolver);
273 };
274 _paramList.remove = _paramList.write;
275 } else if (
276 prov.params &&
277 prov.params.option &&
278 prov.params.option.type &&
279 prov.params.option.type === "text"
280 ) {
281 let optName = prov.params.option.description || _("Parameter");
282 var _paramText = s.option(form.Value, "_paramText_" + i, optName);
283 _paramText.template = prov.template;
284 _paramText.modalonly = true;
285 _paramText.depends("_provider", prov.template);
286 _paramText.optional = !(
287 prov.params.option.default && prov.params.option.default !== ""
288 );
289 _paramText.cfgvalue = function (section_id) {
290 let resolver = this.map.data.get(
291 this.map.config,
292 section_id,
293 "resolver_url"
294 );
295 if (resolver === undefined || resolver === null) return null;
296 let regexp = pkg.templateToRegexp(prov.template);
297 let match = resolver.match(regexp);
298 return (match && match[1]) || null;
299 };
300 _paramText.write = function (section_id, formvalue) {
301 let template = this.map.data.get(
302 this.map.config,
303 section_id,
304 "resolver_url"
305 );
306 if (_paramText.template !== template) return 0;
307 let resolver = pkg.templateToResolver(template, {
308 option: formvalue || "",
309 });
310 uci.set(pkg.Name, section_id, "resolver_url", resolver);
311 };
312 _paramText.remove = _paramText.write;
313 }
314 });
315
316 o = s.option(form.Value, "bootstrap_dns", _("Bootstrap DNS"));
317 o.default = "";
318 o.modalonly = true;
319 o.optional = true;
320 o = s.option(form.Value, "listen_addr", _("Listen Address"));
321 o.datatype = "ipaddr";
322 o.default = "";
323 o.optional = true;
324 o.placeholder = "127.0.0.1";
325 var n = 0;
326 o = s.option(form.Value, "listen_port", _("Listen Port"));
327 o.datatype = "port";
328 o.default = "";
329 o.optional = true;
330 o.placeholder = n + 5053;
331 o = s.option(form.Value, "user", _("Run As User"));
332 o.default = "";
333 o.modalonly = true;
334 o.optional = true;
335 o = s.option(form.Value, "group", _("Run As Group"));
336 o.default = "";
337 o.modalonly = true;
338 o.optional = true;
339 o = s.option(form.Value, "dscp_codepoint", _("DSCP Codepoint"));
340 o.datatype = "and(uinteger, range(0,63))";
341 o.default = "";
342 o.modalonly = true;
343 o.optional = true;
344 o = s.option(form.Value, "verbosity", _("Logging Verbosity"));
345 o.datatype = "and(uinteger, range(0,4))";
346 o.default = "";
347 o.modalonly = true;
348 o.optional = true;
349 o = s.option(form.Value, "logfile", _("Logging File Path"));
350 o.default = "";
351 o.modalonly = true;
352 o.optional = true;
353 o = s.option(form.Value, "polling_interval", _("Polling Interval"));
354 o.datatype = "and(uinteger, range(5,3600))";
355 o.default = "";
356 o.modalonly = true;
357 o.optional = true;
358 o = s.option(form.Value, "proxy_server", _("Proxy Server"));
359 o.default = "";
360 o.modalonly = true;
361 o.optional = true;
362 o = s.option(form.ListValue, "use_http1", _("Use HTTP/1"));
363 o.modalonly = true;
364 o.optional = true;
365 o.rmempty = true;
366 o.value("", _("Use negotiated HTTP version"));
367 o.value("1", _("Force use of HTTP/1"));
368 o.default = "";
369 o = s.option(
370 form.ListValue,
371 "use_ipv6_resolvers_only",
372 _("Use IPv6 resolvers")
373 );
374 o.modalonly = true;
375 o.optional = true;
376 o.rmempty = true;
377 o.value("", _("Use any family DNS resolvers"));
378 o.value("1", _("Force use of IPv6 DNS resolvers"));
379 o.default = "";
380
381 return Promise.all([status.render(), m.render()]);
382 },
383 });