commands.uc 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // Copyright 2012-2022 Jo-Philipp Wich <jow@openwrt.org>
  2. // Licensed to the public under the Apache License 2.0.
  3. 'use strict';
  4. import { basename, mkstemp, popen } from 'fs';
  5. import { urldecode } from 'luci.http';
  6. // Decode a given string into arguments following shell quoting rules
  7. // [[abc\ def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
  8. function parse_args(str) {
  9. let args = [];
  10. function isspace(c) {
  11. if (c == 9 || c == 10 || c == 11 || c == 12 || c == 13 || c == 32)
  12. return c;
  13. }
  14. function isquote(c) {
  15. if (c == 34 || c == 39 || c == 96)
  16. return c;
  17. }
  18. function isescape(c) {
  19. if (c == 92)
  20. return c;
  21. }
  22. function ismeta(c) {
  23. if (c == 36 || c == 92 || c == 96)
  24. return c;
  25. }
  26. // Scan substring defined by the indexes [s, e] of the string "str",
  27. // perform unquoting and de-escaping on the fly and store the result
  28. function unquote(start, end) {
  29. let esc, quote, res = [];
  30. for (let off = start; off < end; off++) {
  31. const byte = ord(str, off);
  32. const q = isquote(byte);
  33. const e = isescape(byte);
  34. const m = ismeta(byte);
  35. if (esc) {
  36. if (!m)
  37. push(res, 92);
  38. push(res, byte);
  39. esc = false;
  40. }
  41. else if (e && quote != 39) {
  42. esc = true;
  43. }
  44. else if (q && quote && q == quote) {
  45. quote = null;
  46. }
  47. else if (q && !quote) {
  48. quote = q;
  49. }
  50. else {
  51. push(res, byte);
  52. }
  53. }
  54. push(args, chr(...res));
  55. }
  56. // Find substring boundaries in "str". Ignore escaped or quoted
  57. // whitespace, pass found start- and end-index for each substring
  58. // to unquote()
  59. let esc, start, quote;
  60. for (let off = 0; off <= length(str); off++) {
  61. const byte = ord(str, off);
  62. const q = isquote(byte);
  63. const s = isspace(byte) ?? (byte === null);
  64. const e = isescape(byte);
  65. if (esc) {
  66. esc = false;
  67. }
  68. else if (e && quote != 39) {
  69. esc = true;
  70. start ??= off;
  71. }
  72. else if (q && quote && q == quote) {
  73. quote = null;
  74. }
  75. else if (q && !quote) {
  76. start ??= off;
  77. quote = q;
  78. }
  79. else if (s && !quote) {
  80. if (start !== null) {
  81. unquote(start, off);
  82. start = null;
  83. }
  84. }
  85. else {
  86. start ??= off;
  87. }
  88. }
  89. // If the "quote" is still set we encountered an unfinished string
  90. if (quote)
  91. unquote(start, length(str));
  92. return args;
  93. }
  94. function test_binary(str) {
  95. for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
  96. if (byte <= 8 || (byte >= 14 && byte <= 31))
  97. return true;
  98. return false;
  99. }
  100. function parse_cmdline(cmdid, args) {
  101. if (uci.get('luci', cmdid) == 'command') {
  102. let cmd = uci.get_all('luci', cmdid);
  103. let argv = parse_args(cmd?.command);
  104. if (cmd?.param == '1') {
  105. if (length(args))
  106. push(argv, ...(parse_args(urldecode(args)) ?? []));
  107. else if (length(args = http.formvalue('args')))
  108. push(argv, ...(parse_args(args) ?? []));
  109. }
  110. return map(argv, v => match(v, /[^\w.\/|-]/) ? `'${replace(v, "'", "'\\''")}'` : v);
  111. }
  112. }
  113. function execute_command(callback, ...args) {
  114. let argv = parse_cmdline(...args);
  115. if (argv) {
  116. let outfd = mkstemp();
  117. let errfd = mkstemp();
  118. const exitcode = system(`${join(' ', argv)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
  119. outfd.seek(0);
  120. errfd.seek(0);
  121. const stdout = outfd.read(1024 * 512) ?? '';
  122. const stderr = errfd.read(1024 * 512) ?? '';
  123. outfd.close();
  124. errfd.close();
  125. const binary = test_binary(stdout);
  126. callback({
  127. ok: true,
  128. command: join(' ', argv),
  129. stdout: binary ? null : stdout,
  130. stderr,
  131. exitcode,
  132. binary
  133. });
  134. }
  135. else {
  136. callback({
  137. ok: false,
  138. code: 404,
  139. reason: "No such command"
  140. });
  141. }
  142. }
  143. function return_json(result) {
  144. if (result.ok) {
  145. http.prepare_content('application/json');
  146. http.write_json(result);
  147. }
  148. else {
  149. http.status(result.code, result.reason);
  150. }
  151. }
  152. function return_html(result) {
  153. if (result.ok) {
  154. include('commands_public', result);
  155. }
  156. else {
  157. http.status(result.code, result.reason);
  158. }
  159. }
  160. return {
  161. action_run: function(...args) {
  162. execute_command(return_json, ...args);
  163. },
  164. action_download: function(...args) {
  165. const argv = parse_cmdline(...args);
  166. if (argv) {
  167. const fd = popen(`${join(' ', argv)} 2>/dev/null`);
  168. if (fd) {
  169. let filename = replace(basename(argv[0]), /\W+/g, '.');
  170. let chunk = fd.read(4096) ?? '';
  171. let name;
  172. if (test_binary(chunk)) {
  173. http.header("Content-Disposition", `attachment; filename=${filename}.bin`);
  174. http.prepare_content("application/octet-stream");
  175. }
  176. else {
  177. http.header("Content-Disposition", `attachment; filename=${filename}.txt`);
  178. http.prepare_content("text/plain");
  179. }
  180. while (length(chunk)) {
  181. http.write(chunk);
  182. chunk = fd.read(4096);
  183. }
  184. fd.close();
  185. }
  186. else {
  187. http.status(500, "Failed to execute command");
  188. }
  189. }
  190. else {
  191. http.status(404, "No such command");
  192. }
  193. },
  194. action_public: function(cmdid, ...args) {
  195. let disp = false;
  196. if (substr(cmdid, -1) == "s") {
  197. disp = true;
  198. cmdid = substr(cmdid, 0, -1);
  199. }
  200. if (cmdid &&
  201. uci.get('luci', cmdid) == 'command' &&
  202. uci.get('luci', cmdid, 'public') == '1')
  203. {
  204. if (disp)
  205. execute_command(return_html, cmdid, ...args);
  206. else
  207. this.action_download(cmdid, args);
  208. }
  209. else {
  210. http.status(403, "Access to command denied");
  211. }
  212. }
  213. };