luci-app-https-dns-proxy: rewrite in javascript
[project/luci.git] / applications / luci-app-https-dns-proxy / htdocs / luci-static / resources / https-dns-proxy / status.js
1 // Copyright MOSSDeF, 2023 Stan Grishin <stangri@melmac.ca>
2 // This code wouldn't have been possible without help from:
3 // - [@stokito](https://github.com/stokito)
4 // - [@vsviridov](https://github.com/vsviridov)
5
6 "require ui";
7 "require rpc";
8 "require uci";
9 "require form";
10 "require baseclass";
11
12 var pkg = {
13 get Name() {
14 return "https-dns-proxy";
15 },
16 get URL() {
17 return "https://docs.openwrt.melmac.net/" + pkg.Name + "/";
18 },
19 templateToRegexp: function (template) {
20 return RegExp(
21 "^" +
22 template
23 .split(/(\{\w+\})/g)
24 .map((part) => {
25 let placeholder = part.match(/^\{(\w+)\}$/);
26 if (placeholder) return `(?<${placeholder[1]}>.*?)`;
27 else return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28 })
29 .join("") +
30 "$"
31 );
32 },
33 };
34
35 var getInitList = rpc.declare({
36 object: "luci." + pkg.Name,
37 method: "getInitList",
38 params: ["name"],
39 });
40
41 var getInitStatus = rpc.declare({
42 object: "luci." + pkg.Name,
43 method: "getInitStatus",
44 params: ["name"],
45 });
46
47 var getPlatformSupport = rpc.declare({
48 object: "luci." + pkg.Name,
49 method: "getPlatformSupport",
50 params: ["name"],
51 });
52
53 var getProviders = rpc.declare({
54 object: "luci." + pkg.Name,
55 method: "getProviders",
56 params: ["name"],
57 });
58
59 var getRuntime = rpc.declare({
60 object: "luci." + pkg.Name,
61 method: "getRuntime",
62 params: ["name"],
63 });
64
65 var _setInitAction = rpc.declare({
66 object: "luci." + pkg.Name,
67 method: "setInitAction",
68 params: ["name", "action"],
69 expect: { result: false },
70 });
71
72 var RPC = {
73 listeners: [],
74 on: function (event, callback) {
75 var pair = { event: event, callback: callback };
76 this.listeners.push(pair);
77 return function unsubscribe() {
78 this.listeners = this.listeners.filter(function (listener) {
79 return listener !== pair;
80 });
81 }.bind(this);
82 },
83 emit: function (event, data) {
84 this.listeners.forEach(function (listener) {
85 if (listener.event === event) {
86 listener.callback(data);
87 }
88 });
89 },
90 getInitList: function (name) {
91 getInitList(name).then(
92 function (result) {
93 this.emit("getInitList", result);
94 }.bind(this)
95 );
96 },
97 getInitStatus: function (name) {
98 getInitStatus(name).then(
99 function (result) {
100 this.emit("getInitStatus", result);
101 }.bind(this)
102 );
103 },
104 getPlatformSupport: function (name) {
105 getPlatformSupport(name).then(
106 function (result) {
107 this.emit("getPlatformSupport", result);
108 }.bind(this)
109 );
110 },
111 getProviders: function (name) {
112 getProviders(name).then(
113 function (result) {
114 this.emit("getProviders", result);
115 }.bind(this)
116 );
117 },
118 getRuntime: function (name) {
119 getRuntime(name).then(
120 function (result) {
121 this.emit("getRuntime", result);
122 }.bind(this)
123 );
124 },
125 setInitAction: function (name, action) {
126 _setInitAction(name, action).then(
127 function (result) {
128 this.emit("setInitAction", result);
129 }.bind(this)
130 );
131 },
132 };
133
134 var status = baseclass.extend({
135 render: function () {
136 return Promise.all([
137 L.resolveDefault(getInitStatus(pkg.Name), {}),
138 L.resolveDefault(getProviders(pkg.Name), {}),
139 L.resolveDefault(getRuntime(pkg.Name), {}),
140 ]).then(function (data) {
141 var text;
142 var reply = {
143 status: (data[0] && data[0][pkg.Name]) || {
144 enabled: null,
145 running: null,
146 force_dns_active: null,
147 version: null,
148 },
149 providers: (data[1] && data[1][pkg.Name]) || { providers: [] },
150 runtime: (data[2] && data[2][pkg.Name]) || { instances: [] },
151 };
152 reply.providers.sort(function (a, b) {
153 return _(a.title).localeCompare(_(b.title));
154 });
155 reply.providers.push({
156 title: "Custom",
157 template: "{option}",
158 params: { option: { type: "text" } },
159 });
160
161 var header = E("h2", {}, _("HTTPS DNS Proxy - Status"));
162 var statusTitle = E(
163 "label",
164 { class: "cbi-value-title" },
165 _("Service Status")
166 );
167 if (reply.status.version) {
168 if (reply.status.running) {
169 text = _("Version %s - Running.").format(reply.status.version);
170 if (reply.status.force_dns_active) {
171 text += "<br />" + _("Force DNS ports:");
172 reply.status.force_dns_ports.forEach((element) => {
173 text += " " + element;
174 });
175 text += ".";
176 }
177 } else {
178 if (reply.status.enabled) {
179 text = _("Version %s - Stopped.").format(reply.status.version);
180 } else {
181 text = _("Version %s - Stopped (Disabled).").format(
182 reply.status.version
183 );
184 }
185 }
186 } else {
187 text = _("Not installed or not found");
188 }
189 var statusText = E("div", {}, text);
190 var statusField = E("div", { class: "cbi-value-field" }, statusText);
191 var statusDiv = E("div", { class: "cbi-value" }, [
192 statusTitle,
193 statusField,
194 ]);
195
196 var instancesDiv = [];
197 if (reply.runtime.instances) {
198 var instancesTitle = E(
199 "label",
200 { class: "cbi-value-title" },
201 _("Service Instances")
202 );
203 text = _("See the %sREADME%s for details.").format(
204 '<a href="' +
205 pkg.URL +
206 '#a-word-about-default-routing " target="_blank">',
207 "</a>"
208 );
209 var instancesDescr = E("div", { class: "cbi-value-description" }, "");
210
211 text = "";
212 Object.values(reply.runtime.instances).forEach((element) => {
213 var resolver;
214 var address;
215 var port;
216 var name;
217 var option;
218 var found;
219 element.command.forEach((param, index, arr) => {
220 if (param === "-r") resolver = arr[index + 1];
221 if (param === "-a") address = arr[index + 1];
222 if (param === "-p") port = arr[index + 1];
223 });
224 resolver = resolver || "Unknown";
225 address = address || "127.0.0.1";
226 port = port || "Unknown";
227 reply.providers.forEach((prov) => {
228 let regexp = pkg.templateToRegexp(prov.template);
229 if (!found && regexp.test(resolver)) {
230 found = true;
231 name = _(prov.title);
232 let match = resolver.match(regexp);
233 if (match[1] != null) {
234 if (
235 prov.params &&
236 prov.params.option &&
237 prov.params.option.options
238 ) {
239 prov.params.option.options.forEach((opt) => {
240 if (opt.value === match[1]) option = _(opt.description);
241 });
242 name += " (" + option + ")";
243 } else {
244 if (match[1] != "") name += " (" + match[1] + ")";
245 }
246 }
247 }
248 });
249 if (address === "127.0.0.1")
250 text += _("%s%s%s proxy on port %s.%s").format(
251 "<strong>",
252 name,
253 "</strong>",
254 port,
255 "<br />"
256 );
257 else
258 text += _("%s%s%s proxy at %s on port %s.%s").format(
259 "<strong>",
260 name,
261 "</strong>",
262 address,
263 port,
264 "<br />"
265 );
266 });
267 var instancesText = E("div", {}, text);
268 var instancesField = E("div", { class: "cbi-value-field" }, [
269 instancesText,
270 instancesDescr,
271 ]);
272 instancesDiv = E("div", { class: "cbi-value" }, [
273 instancesTitle,
274 instancesField,
275 ]);
276 }
277
278 var btn_gap = E("span", {}, "&#160;&#160;");
279 var btn_gap_long = E(
280 "span",
281 {},
282 "&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;"
283 );
284 var btn_start = E(
285 "button",
286 {
287 class: "btn cbi-button cbi-button-apply",
288 disabled: true,
289 click: function (ev) {
290 ui.showModal(null, [
291 E(
292 "p",
293 { class: "spinning" },
294 _("Starting %s service").format(pkg.Name)
295 ),
296 ]);
297 return RPC.setInitAction(pkg.Name, "start");
298 },
299 },
300 _("Start")
301 );
302
303 var btn_action = E(
304 "button",
305 {
306 class: "btn cbi-button cbi-button-apply",
307 disabled: true,
308 click: function (ev) {
309 ui.showModal(null, [
310 E(
311 "p",
312 { class: "spinning" },
313 _("Restarting %s service").format(pkg.Name)
314 ),
315 ]);
316 return RPC.setInitAction(pkg.Name, "restart");
317 },
318 },
319 _("Restart")
320 );
321
322 var btn_stop = E(
323 "button",
324 {
325 class: "btn cbi-button cbi-button-reset",
326 disabled: true,
327 click: function (ev) {
328 ui.showModal(null, [
329 E(
330 "p",
331 { class: "spinning" },
332 _("Stopping %s service").format(pkg.Name)
333 ),
334 ]);
335 return RPC.setInitAction(pkg.Name, "stop");
336 },
337 },
338 _("Stop")
339 );
340
341 var btn_enable = E(
342 "button",
343 {
344 class: "btn cbi-button cbi-button-apply",
345 disabled: true,
346 click: function (ev) {
347 ui.showModal(null, [
348 E(
349 "p",
350 { class: "spinning" },
351 _("Enabling %s service").format(pkg.Name)
352 ),
353 ]);
354 return RPC.setInitAction(pkg.Name, "enable");
355 },
356 },
357 _("Enable")
358 );
359
360 var btn_disable = E(
361 "button",
362 {
363 class: "btn cbi-button cbi-button-reset",
364 disabled: true,
365 click: function (ev) {
366 ui.showModal(null, [
367 E(
368 "p",
369 { class: "spinning" },
370 _("Disabling %s service").format(pkg.Name)
371 ),
372 ]);
373 return RPC.setInitAction(pkg.Name, "disable");
374 },
375 },
376 _("Disable")
377 );
378
379 if (reply.status.enabled) {
380 btn_enable.disabled = true;
381 btn_disable.disabled = false;
382 if (reply.status.running) {
383 btn_start.disabled = true;
384 btn_action.disabled = false;
385 btn_stop.disabled = false;
386 } else {
387 btn_start.disabled = false;
388 btn_action.disabled = true;
389 btn_stop.disabled = true;
390 }
391 } else {
392 btn_start.disabled = true;
393 btn_action.disabled = true;
394 btn_stop.disabled = true;
395 btn_enable.disabled = false;
396 btn_disable.disabled = true;
397 }
398
399 var buttonsTitle = E(
400 "label",
401 { class: "cbi-value-title" },
402 _("Service Control")
403 );
404 var buttonsText = E("div", {}, [
405 btn_start,
406 btn_gap,
407 btn_action,
408 btn_gap,
409 btn_stop,
410 btn_gap_long,
411 btn_enable,
412 btn_gap,
413 btn_disable,
414 ]);
415 var buttonsField = E("div", { class: "cbi-value-field" }, buttonsText);
416 var buttonsDiv = reply.status.version ?
417 E('div', {class: 'cbi-value'}, [
418 buttonsTitle,
419 buttonsField,
420 ]) : '';
421 return E("div", {}, [header, statusDiv, instancesDiv, buttonsDiv]);
422 });
423 },
424 });
425
426 RPC.on("setInitAction", function (reply) {
427 ui.hideModal();
428 location.reload();
429 });
430
431 return L.Class.extend({
432 status: status,
433 getPlatformSupport: getPlatformSupport,
434 getProviders: getProviders,
435 getRuntime: getRuntime,
436 });