luci-app-pbr: sync with principal package
[project/luci.git] / applications / luci-app-pbr / htdocs / luci-static / resources / pbr / status.js
1 // Copyright 2022 Stan Grishin <stangri@melmac.ca>
2 // This code wouldn't have been possible without help from [@vsviridov](https://github.com/vsviridov)
3
4 "require ui";
5 "require rpc";
6 "require form";
7 "require baseclass";
8
9 var pkg = {
10 get Name() {
11 return "pbr";
12 },
13 get URL() {
14 return "https://docs.openwrt.melmac.net/" + pkg.Name + "/";
15 },
16 };
17
18 var getGateways = rpc.declare({
19 object: "luci." + pkg.Name,
20 method: "getGateways",
21 params: ["name"],
22 });
23
24 var getInitList = rpc.declare({
25 object: "luci." + pkg.Name,
26 method: "getInitList",
27 params: ["name"],
28 });
29
30 var getInitStatus = rpc.declare({
31 object: "luci." + pkg.Name,
32 method: "getInitStatus",
33 params: ["name"],
34 });
35
36 var getInterfaces = rpc.declare({
37 object: "luci." + pkg.Name,
38 method: "getInterfaces",
39 params: ["name"],
40 });
41
42 var getPlatformSupport = rpc.declare({
43 object: "luci." + pkg.Name,
44 method: "getPlatformSupport",
45 params: ["name"],
46 });
47
48 var _setInitAction = rpc.declare({
49 object: "luci." + pkg.Name,
50 method: "setInitAction",
51 params: ["name", "action"],
52 expect: { result: false },
53 });
54
55 var RPC = {
56 listeners: [],
57 on: function (event, callback) {
58 var pair = { event: event, callback: callback };
59 this.listeners.push(pair);
60 return function unsubscribe() {
61 this.listeners = this.listeners.filter(function (listener) {
62 return listener !== pair;
63 });
64 }.bind(this);
65 },
66 emit: function (event, data) {
67 this.listeners.forEach(function (listener) {
68 if (listener.event === event) {
69 listener.callback(data);
70 }
71 });
72 },
73 getInitList: function (name) {
74 getInitList(name).then(
75 function (result) {
76 this.emit("getInitList", result);
77 }.bind(this)
78 );
79 },
80 getInitStatus: function (name) {
81 getInitStatus(name).then(
82 function (result) {
83 this.emit("getInitStatus", result);
84 }.bind(this)
85 );
86 },
87 getGateways: function (name) {
88 getGateways(name).then(
89 function (result) {
90 this.emit("getGateways", result);
91 }.bind(this)
92 );
93 },
94 getPlatformSupport: function (name) {
95 getPlatformSupport(name).then(
96 function (result) {
97 this.emit("getPlatformSupport", result);
98 }.bind(this)
99 );
100 },
101 getInterfaces: function (name) {
102 getInterfaces(name).then(
103 function (result) {
104 this.emit("getInterfaces", result);
105 }.bind(this)
106 );
107 },
108 setInitAction: function (name, action) {
109 _setInitAction(name, action).then(
110 function (result) {
111 this.emit("setInitAction", result);
112 }.bind(this)
113 );
114 },
115 };
116
117 var status = baseclass.extend({
118 render: function () {
119 return Promise.all([
120 L.resolveDefault(getInitStatus(pkg.Name), {}),
121 // L.resolveDefault(getGateways(pkg.Name), {}),
122 ]).then(function (data) {
123 // var replyStatus = data[0];
124 // var replyGateways = data[1];
125 var reply;
126 var text;
127
128 if (data[0] && data[0][pkg.Name]) {
129 reply = data[0][pkg.Name];
130 } else {
131 reply = {
132 enabled: null,
133 running: null,
134 running_iptables: null,
135 running_nft: null,
136 running_nft_file: null,
137 version: null,
138 gateways: null,
139 errors: [],
140 warnings: [],
141 };
142 }
143
144 var header = E("h2", {}, _("Policy Based Routing - Status"));
145 var statusTitle = E(
146 "label",
147 { class: "cbi-value-title" },
148 _("Service Status")
149 );
150 if (reply.version) {
151 text = _("Version %s").format(reply.version) + " - ";
152 if (reply.running) {
153 text += _("Running");
154 if (reply.running_iptables) {
155 text += " (" + _("iptables mode") + ").";
156 } else if (reply.running_nft_file) {
157 text += " (" + _("fw4 nft file mode") + ").";
158 } else if (reply.running_nft) {
159 text += " (" + _("nft mode") + ").";
160 } else {
161 text += ".";
162 }
163 } else {
164 if (reply.enabled) {
165 text += _("Stopped.");
166 } else {
167 text += _("Stopped (Disabled).");
168 }
169 }
170 } else {
171 text = _("Not installed or not found");
172 }
173 var statusText = E("div", {}, text);
174 var statusField = E("div", { class: "cbi-value-field" }, statusText);
175 var statusDiv = E("div", { class: "cbi-value" }, [
176 statusTitle,
177 statusField,
178 ]);
179
180 var gatewaysDiv = [];
181 if (reply.gateways) {
182 var gatewaysTitle = E(
183 "label",
184 { class: "cbi-value-title" },
185 _("Service Gateways")
186 );
187 text = _(
188 "The %s indicates default gateway. See the %sREADME%s for details."
189 ).format(
190 "<strong>✓</strong>",
191 '<a href="' + pkg.URL + '#AWordAboutDefaultRouting" target="_blank">',
192 "</a>"
193 );
194 var gatewaysDescr = E("div", { class: "cbi-value-description" }, text);
195 var gatewaysText = E("div", {}, reply.gateways);
196 var gatewaysField = E("div", { class: "cbi-value-field" }, [
197 gatewaysText,
198 gatewaysDescr,
199 ]);
200 gatewaysDiv = E("div", { class: "cbi-value" }, [
201 gatewaysTitle,
202 gatewaysField,
203 ]);
204 }
205
206 var warningsDiv = [];
207 if (reply.warnings && reply.warnings.length) {
208 var textLabelsTable = {
209 warningResolverNotSupported: _(
210 "Resolver set (%s) is not supported on this system."
211 ).format(L.uci.get(pkg.Name, "config", "resolver_set")),
212 warningAGHVersionTooLow: _(
213 "Installed AdGuardHome (%s) doesn't support 'ipset_file' option."
214 ),
215 warningPolicyProcessCMD: _("%s"),
216 warningTorUnsetParams: _(
217 "Please unset 'src_addr', 'src_port' and 'dest_port' for policy '%s'"
218 ),
219 warningTorUnsetProto: _(
220 "Please unset 'proto' or set 'proto' to 'all' for policy '%s'"
221 ),
222 warningTorUnsetChainIpt: _(
223 "Please unset 'chain' or set 'chain' to 'PREROUTING' for policy '%s'"
224 ),
225 warningTorUnsetChainNft: _(
226 "Please unset 'chain' or set 'chain' to 'prerouting' for policy '%s'"
227 ),
228 warningInvalidOVPNConfig: _(
229 "Invalid OpenVPN config for %s interface"
230 ),
231 warningOutdatedWebUIApp: _(
232 "The WebUI application is outdated (version %s), please update it"
233 ),
234 warningBadNftCallsInUserFile: _(
235 "Incompatible nft calls detected in user include file, disabling fw4 nft file support."
236 ),
237 warningDnsmasqInstanceNoConfdir: _(
238 "Dnsmasq instance (%s) targeted in settings, but it doesn't have its own confdir."
239 ),
240 };
241 var warningsTitle = E(
242 "label",
243 { class: "cbi-value-title" },
244 _("Service Warnings")
245 );
246 var text = "";
247 reply.warnings.forEach((element) => {
248 if (element.id && textLabelsTable[element.id]) {
249 if (element.id !== "warningPolicyProcessCMD") {
250 text +=
251 (textLabelsTable[element.id] + ".").format(
252 element.extra || " "
253 ) + "<br />";
254 }
255 } else {
256 text += _("Unknown warning") + "<br />";
257 }
258 });
259 var warningsText = E("div", {}, text);
260 var warningsField = E(
261 "div",
262 { class: "cbi-value-field" },
263 warningsText
264 );
265 warningsDiv = E("div", { class: "cbi-value" }, [
266 warningsTitle,
267 warningsField,
268 ]);
269 }
270
271 var errorsDiv = [];
272 if (reply.errors && reply.errors.length) {
273 var textLabelsTable = {
274 errorConfigValidation: _("Config (%s) validation failure").format(
275 "/etc/config/" + pkg.Name
276 ),
277 errorNoIpFull: _("%s binary cannot be found").format("ip-full"),
278 errorNoIptables: _("%s binary cannot be found").format("iptables"),
279 errorNoIpset: _(
280 "Resolver set support (%s) requires ipset, but ipset binary cannot be found"
281 ).format(L.uci.get(pkg.Name, "config", "resolver_set")),
282 errorNoNft: _(
283 "Resolver set support (%s) requires nftables, but nft binary cannot be found"
284 ).format(L.uci.get(pkg.Name, "config", "resolver_set")),
285 errorResolverNotSupported: _(
286 "Resolver set (%s) is not supported on this system"
287 ).format(L.uci.get(pkg.Name, "config", "resolver_set")),
288 errorServiceDisabled: _(
289 "The %s service is currently disabled"
290 ).format(pkg.Name),
291 errorNoWanGateway: _(
292 "The %s service failed to discover WAN gateway"
293 ).format(pkg.Name),
294 errorNoWanInterface: _(
295 "The %s inteface not found, you need to set the 'pbr.config.procd_wan_interface' option"
296 ),
297 errorNoWanInterfaceHint: _(
298 "Refer to https://docs.openwrt.melmac.net/pbr/#procd_wan_interface"
299 ),
300 errorIpsetNameTooLong: _(
301 "The ipset name '%s' is longer than allowed 31 characters"
302 ),
303 errorNftsetNameTooLong: _(
304 "The nft set name '%s' is longer than allowed 255 characters"
305 ),
306 errorUnexpectedExit: _(
307 "Unexpected exit or service termination: '%s'"
308 ),
309 errorPolicyNoSrcDest: _(
310 "Policy '%s' has no source/destination parameters"
311 ),
312 errorPolicyNoInterface: _("Policy '%s' has no assigned interface"),
313 errorPolicyUnknownInterface: _(
314 "Policy '%s' has an unknown interface"
315 ),
316 errorPolicyProcessCMD: _("%s"),
317 errorFailedSetup: _("Failed to set up '%s'"),
318 errorFailedReload: _("Failed to reload '%s'"),
319 errorUserFileNotFound: _("Custom user file '%s' not found or empty"),
320 errorUserFileSyntax: _("Syntax error in custom user file '%s'"),
321 errorUserFileRunning: _("Error running custom user file '%s'"),
322 errorUserFileNoCurl: _(
323 "Use of 'curl' is detected in custom user file '%s', but 'curl' isn't installed"
324 ),
325 errorNoGateways: _("Failed to set up any gateway"),
326 errorResolver: _("Resolver '%s'"),
327 errorPolicyProcessNoIpv6: _(
328 "Skipping IPv6 policy '%s' as IPv6 support is disabled"
329 ),
330 errorPolicyProcessUnknownFwmark: _(
331 "Unknown packet mark for interface '%s'"
332 ),
333 errorPolicyProcessMismatchFamily: _(
334 "Mismatched IP family between in policy '%s'"
335 ),
336 errorPolicyProcessUnknownProtocol: _(
337 "Unknown protocol in policy '%s'"
338 ),
339 errorPolicyProcessInsertionFailed: _(
340 "Insertion failed for both IPv4 and IPv6 for policy '%s'"
341 ),
342 errorPolicyProcessInsertionFailedIpv4: _(
343 "Insertion failed for IPv4 for policy '%s'"
344 ),
345 errorInterfaceRoutingEmptyValues: _(
346 "Received empty tid/mark or interface name when setting up routing"
347 ),
348 errorFailedToResolve: _("Failed to resolve '%s'"),
349 errorInvalidOVPNConfig: _(
350 "Invalid OpenVPN config for '%s' interface"
351 ),
352 errorNftFileInstall: _("Failed to install fw4 nft file '%s'"),
353 errorNoDownloadWithSecureReload: _(
354 "Policy '%s' refers to URL which can't be downloaded in 'secure_reload' mode!"
355 ),
356 errorDownloadUrlNoHttps: _(
357 "Failed to download '%s', HTTPS is not supported!"
358 ),
359 errorDownloadUrl: _("Failed to download '%s'!"),
360 errorFileSchemaRequiresCurl: _(
361 "The file:// schema requires curl, but it's not detected on this system!"
362 ),
363 errorTryFailed: _("Command failed: %s"),
364 };
365 var errorsTitle = E(
366 "label",
367 { class: "cbi-value-title" },
368 _("Service Errors")
369 );
370 var text = "";
371 reply.errors.forEach((element) => {
372 if (element.id && textLabelsTable[element.id]) {
373 if (element.id !== "errorPolicyProcessCMD") {
374 text +=
375 (textLabelsTable[element.id] + "!").format(
376 element.extra || " "
377 ) + "<br />";
378 }
379 } else {
380 text += _("Unknown error!") + "<br />";
381 }
382 });
383 text += _("Errors encountered, please check the %sREADME%s!").format(
384 '<a href="' + pkg.URL + '" target="_blank">',
385 "</a><br />"
386 );
387 var errorsText = E("div", {}, text);
388 var errorsField = E("div", { class: "cbi-value-field" }, errorsText);
389 errorsDiv = E("div", { class: "cbi-value" }, [
390 errorsTitle,
391 errorsField,
392 ]);
393 }
394
395 var btn_gap = E("span", {}, "&#160;&#160;");
396 var btn_gap_long = E(
397 "span",
398 {},
399 "&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;"
400 );
401
402 var btn_start = E(
403 "button",
404 {
405 class: "btn cbi-button cbi-button-apply",
406 disabled: true,
407 click: function (ev) {
408 ui.showModal(null, [
409 E(
410 "p",
411 { class: "spinning" },
412 _("Starting %s service").format(pkg.Name)
413 ),
414 ]);
415 return RPC.setInitAction(pkg.Name, "start");
416 },
417 },
418 _("Start")
419 );
420
421 var btn_action = E(
422 "button",
423 {
424 class: "btn cbi-button cbi-button-apply",
425 disabled: true,
426 click: function (ev) {
427 ui.showModal(null, [
428 E(
429 "p",
430 { class: "spinning" },
431 _("Restarting %s service").format(pkg.Name)
432 ),
433 ]);
434 return RPC.setInitAction(pkg.Name, "restart");
435 },
436 },
437 _("Restart")
438 );
439
440 var btn_stop = E(
441 "button",
442 {
443 class: "btn cbi-button cbi-button-reset",
444 disabled: true,
445 click: function (ev) {
446 ui.showModal(null, [
447 E(
448 "p",
449 { class: "spinning" },
450 _("Stopping %s service").format(pkg.Name)
451 ),
452 ]);
453 return RPC.setInitAction(pkg.Name, "stop");
454 },
455 },
456 _("Stop")
457 );
458
459 var btn_enable = E(
460 "button",
461 {
462 class: "btn cbi-button cbi-button-apply",
463 disabled: true,
464 click: function (ev) {
465 ui.showModal(null, [
466 E(
467 "p",
468 { class: "spinning" },
469 _("Enabling %s service").format(pkg.Name)
470 ),
471 ]);
472 return RPC.setInitAction(pkg.Name, "enable");
473 },
474 },
475 _("Enable")
476 );
477
478 var btn_disable = E(
479 "button",
480 {
481 class: "btn cbi-button cbi-button-reset",
482 disabled: true,
483 click: function (ev) {
484 ui.showModal(null, [
485 E(
486 "p",
487 { class: "spinning" },
488 _("Disabling %s service").format(pkg.Name)
489 ),
490 ]);
491 return RPC.setInitAction(pkg.Name, "disable");
492 },
493 },
494 _("Disable")
495 );
496
497 if (reply.enabled) {
498 btn_enable.disabled = true;
499 btn_disable.disabled = false;
500 if (reply.running) {
501 btn_start.disabled = true;
502 btn_action.disabled = false;
503 btn_stop.disabled = false;
504 } else {
505 btn_start.disabled = false;
506 btn_action.disabled = true;
507 btn_stop.disabled = true;
508 }
509 } else {
510 btn_start.disabled = true;
511 btn_action.disabled = true;
512 btn_stop.disabled = true;
513 btn_enable.disabled = false;
514 btn_disable.disabled = true;
515 }
516
517 var buttonsTitle = E(
518 "label",
519 { class: "cbi-value-title" },
520 _("Service Control")
521 );
522 var buttonsText = E("div", {}, [
523 btn_start,
524 btn_gap,
525 btn_action,
526 btn_gap,
527 btn_stop,
528 btn_gap_long,
529 btn_enable,
530 btn_gap,
531 btn_disable,
532 ]);
533 var buttonsField = E("div", { class: "cbi-value-field" }, buttonsText);
534 var buttonsDiv = reply.version
535 ? E("div", { class: "cbi-value" }, [buttonsTitle, buttonsField])
536 : "";
537 return E("div", {}, [
538 header,
539 statusDiv,
540 gatewaysDiv,
541 warningsDiv,
542 errorsDiv,
543 buttonsDiv,
544 ]);
545 });
546 },
547 });
548
549 RPC.on("setInitAction", function (reply) {
550 ui.hideModal();
551 location.reload();
552 });
553
554 return L.Class.extend({
555 status: status,
556 getInterfaces: getInterfaces,
557 getPlatformSupport: getPlatformSupport,
558 });