luci/applications/luci-app-commands/ucode/controller/commands.uc
Jo-Philipp Wich dd1c538b2e luci-app-commands: rewrite to client side rendering
Rewrite the luci-app-command configuration to client side cbi forms and
port the server side templates and controller logic to ucode.

Also utilize a query string parameter to pass custom arguments.

Fixes: #5559
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
2022-10-25 01:03:38 +02:00

256 lignes
5,1 Kio
Ucode

// 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");
}
}
};