2
0

status.js 17 KB

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