status.js 10 KB

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