summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-commands/ucode/controller/commands.uc
diff options
context:
space:
mode:
Diffstat (limited to 'applications/luci-app-commands/ucode/controller/commands.uc')
-rw-r--r--applications/luci-app-commands/ucode/controller/commands.uc256
1 files changed, 256 insertions, 0 deletions
diff --git a/applications/luci-app-commands/ucode/controller/commands.uc b/applications/luci-app-commands/ucode/controller/commands.uc
new file mode 100644
index 0000000000..9126d59eb0
--- /dev/null
+++ b/applications/luci-app-commands/ucode/controller/commands.uc
@@ -0,0 +1,256 @@
+// Copyright 2012-2022 Jo-Philipp Wich <jow@openwrt.org>
+// Licensed to the public under the Apache License 2.0.
+
+'use strict';
+
+import { basename, mkstemp, popen } from 'fs';
+import { urldecode } from 'luci.http';
+
+// Decode a given string into arguments following shell quoting rules
+// [[abc\ def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
+function parse_args(str) {
+ let args = [];
+
+ function isspace(c) {
+ if (c == 9 || c == 10 || c == 11 || c == 12 || c == 13 || c == 32)
+ return c;
+ }
+
+ function isquote(c) {
+ if (c == 34 || c == 39 || c == 96)
+ return c;
+ }
+
+ function isescape(c) {
+ if (c == 92)
+ return c;
+ }
+
+ function ismeta(c) {
+ if (c == 36 || c == 92 || c == 96)
+ return c;
+ }
+
+ // Scan substring defined by the indexes [s, e] of the string "str",
+ // perform unquoting and de-escaping on the fly and store the result
+ function unquote(start, end) {
+ let esc, quote, res = [];
+
+ for (let off = start; off < end; off++) {
+ const byte = ord(str, off);
+ const q = isquote(byte);
+ const e = isescape(byte);
+ const m = ismeta(byte);
+
+ if (esc) {
+ if (!m)
+ push(res, 92);
+
+ push(res, byte);
+ esc = false;
+ }
+ else if (e && quote != 39) {
+ esc = true;
+ }
+ else if (q && quote && q == quote) {
+ quote = null;
+ }
+ else if (q && !quote) {
+ quote = q;
+ }
+ else {
+ push(res, byte);
+ }
+ }
+
+ push(args, chr(...res));
+ }
+
+ // Find substring boundaries in "str". Ignore escaped or quoted
+ // whitespace, pass found start- and end-index for each substring
+ // to unquote()
+ let esc, start, quote;
+
+ for (let off = 0; off <= length(str); off++) {
+ const byte = ord(str, off);
+ const q = isquote(byte);
+ const s = isspace(byte) ?? (byte === null);
+ const e = isescape(byte);
+
+ if (esc) {
+ esc = false;
+ }
+ else if (e && quote != 39) {
+ esc = true;
+ start ??= off;
+ }
+ else if (q && quote && q == quote) {
+ quote = null;
+ }
+ else if (q && !quote) {
+ start ??= off;
+ quote = q;
+ }
+ else if (s && !quote) {
+ if (start !== null) {
+ unquote(start, off);
+ start = null;
+ }
+ }
+ else {
+ start ??= off;
+ }
+ }
+
+ // If the "quote" is still set we encountered an unfinished string
+ if (quote)
+ unquote(start, length(str));
+
+ return args;
+}
+
+function test_binary(str) {
+ for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
+ if (byte <= 8 || (byte >= 14 && byte <= 31))
+ return true;
+
+ return false;
+}
+
+function parse_cmdline(cmdid, args) {
+ if (uci.get('luci', cmdid) == 'command') {
+ let cmd = uci.get_all('luci', cmdid);
+ let argv = parse_args(cmd?.command);
+
+ if (cmd?.param == '1') {
+ if (length(args))
+ push(argv, ...(parse_args(urldecode(args)) ?? []));
+ else if (length(args = http.formvalue('args')))
+ push(argv, ...(parse_args(args) ?? []));
+ }
+
+ return map(argv, v => match(v, /[^\w.\/|-]/) ? `'${replace(v, "'", "'\\''")}'` : v);
+ }
+}
+
+function execute_command(callback, ...args) {
+ let argv = parse_cmdline(...args);
+
+ if (argv) {
+ let outfd = mkstemp();
+ let errfd = mkstemp();
+
+ const exitcode = system(`${join(' ', argv)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
+
+ outfd.seek(0);
+ errfd.seek(0);
+
+ const stdout = outfd.read(1024 * 512) ?? '';
+ const stderr = errfd.read(1024 * 512) ?? '';
+
+ outfd.close();
+ errfd.close();
+
+ const binary = test_binary(stdout);
+
+ callback({
+ ok: true,
+ command: join(' ', argv),
+ stdout: binary ? null : stdout,
+ stderr,
+ exitcode,
+ binary
+ });
+ }
+ else {
+ callback({
+ ok: false,
+ code: 404,
+ reason: "No such command"
+ });
+ }
+}
+
+function return_json(result) {
+ if (result.ok) {
+ http.prepare_content('application/json');
+ http.write_json(result);
+ }
+ else {
+ http.status(result.code, result.reason);
+ }
+}
+
+
+function return_html(result) {
+ if (result.ok) {
+ include('commands_public', result);
+ }
+ else {
+ http.status(result.code, result.reason);
+ }
+}
+
+return {
+ action_run: function(...args) {
+ execute_command(return_json, ...args);
+ },
+
+ action_download: function(...args) {
+ const argv = parse_cmdline(...args);
+
+ if (argv) {
+ const fd = popen(`${join(' ', argv)} 2>/dev/null`);
+
+ if (fd) {
+ let filename = replace(basename(argv[0]), /\W+/g, '.');
+ let chunk = fd.read(4096) ?? '';
+ let name;
+
+ if (test_binary(chunk)) {
+ http.header("Content-Disposition", `attachment; filename=${filename}.bin`);
+ http.prepare_content("application/octet-stream");
+ }
+ else {
+ http.header("Content-Disposition", `attachment; filename=${filename}.txt`);
+ http.prepare_content("text/plain");
+ }
+
+ while (length(chunk)) {
+ http.write(chunk);
+ chunk = fd.read(4096);
+ }
+
+ fd.close();
+ }
+ else {
+ http.status(500, "Failed to execute command");
+ }
+ }
+ else {
+ http.status(404, "No such command");
+ }
+ },
+
+ action_public: function(cmdid, ...args) {
+ let disp = false;
+
+ if (substr(cmdid, -1) == "s") {
+ disp = true;
+ cmdid = substr(cmdid, 0, -1);
+ }
+
+ if (cmdid &&
+ uci.get('luci', cmdid) == 'command' &&
+ uci.get('luci', cmdid, 'public') == '1')
+ {
+ if (disp)
+ execute_command(return_html, cmdid, ...args);
+ else
+ this.action_download(cmdid, args);
+ }
+ else {
+ http.status(403, "Access to command denied");
+ }
+ }
+};