summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-commands/ucode
diff options
context:
space:
mode:
Diffstat (limited to 'applications/luci-app-commands/ucode')
-rw-r--r--applications/luci-app-commands/ucode/controller/commands.uc256
-rw-r--r--applications/luci-app-commands/ucode/template/commands.ut179
-rw-r--r--applications/luci-app-commands/ucode/template/commands_public.ut48
3 files changed, 483 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");
+ }
+ }
+};
diff --git a/applications/luci-app-commands/ucode/template/commands.ut b/applications/luci-app-commands/ucode/template/commands.ut
new file mode 100644
index 0000000000..8e5ce0b486
--- /dev/null
+++ b/applications/luci-app-commands/ucode/template/commands.ut
@@ -0,0 +1,179 @@
+{#
+ Copyright 2012-2022 Jo-Philipp Wich <jo@mein.io>
+ Licensed to the public under the Apache License 2.0.
+-#}
+
+{%
+ include('header', { css: `
+ .commands {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .commandbox {
+ flex: 0 0 30%;
+ margin: .5em;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .commandbox > p,
+ .commandbox > p > * {
+ display: block;
+ }
+
+ .commandbox div {
+ margin-top: auto;
+ }
+ ` });
+-%}
+
+<script type="text/javascript">//<![CDATA[
+ var stxhr = new XHR();
+
+ function command_run(ev, id)
+ {
+ var args;
+ var field = document.getElementById(id);
+ if (field)
+ args = encodeURIComponent(field.value);
+
+ var legend = document.getElementById('command-rc-legend');
+ var output = document.getElementById('command-rc-output');
+
+ if (legend && output)
+ {
+ output.innerHTML =
+ '<img src="{{ resource }}/icons/loading.gif" alt="{{ _('Loading') }}" style="vertical-align:middle" /> ' +
+ '{{ _('Waiting for command to complete...') }}'
+ ;
+
+ legend.parentNode.style.display = 'block';
+ legend.style.display = 'inline';
+
+ stxhr.get('{{ dispatcher.build_url('admin/system/commands/run') }}/' + id + (args ? '?args=' + args : ''), null,
+ function(x, st)
+ {
+ if (st)
+ {
+ if (st.binary)
+ st.stdout = '[{{ _('Binary data not displayed, download instead.') }}]';
+
+ legend.style.display = 'none';
+ output.innerHTML = String.format(
+ '<pre><strong># %h\n</strong>%h<span style="color:red">%h</span></pre>' +
+ '<div class="alert-message warning">%s ({{ _('Code:') }} %d)</div>',
+ st.command, st.stdout, st.stderr,
+ (st.exitcode == 0) ? '{{ _('Command successful') }}' : '{{ _('Command failed') }}',
+ st.exitcode);
+ }
+ else
+ {
+ legend.style.display = 'none';
+ output.innerHTML = '<span class="error">{{ _('Failed to execute command!') }}</span>';
+ }
+
+ location.hash = '#output';
+ }
+ );
+ }
+
+ ev.preventDefault();
+ }
+
+ function command_download(ev, id)
+ {
+ var args;
+ var field = document.getElementById(id);
+ if (field)
+ args = encodeURIComponent(field.value);
+
+ location.href = '{{ dispatcher.build_url('admin/system/commands/download') }}/' + id + (args ? '/' + args : '');
+
+ ev.preventDefault();
+ }
+
+ function command_link(ev, id)
+ {
+ var legend = document.getElementById('command-rc-legend');
+ var output = document.getElementById('command-rc-output');
+
+ var args;
+ var field = document.getElementById(id);
+ if (field)
+ args = encodeURIComponent(field.value);
+
+ if (legend && output)
+ {
+ var prefix = location.protocol + '//' + location.host + '{{ dispatcher.build_url('command') }}/';
+ var suffix = (args ? '?args=' + args : '');
+
+ var link = prefix + id + suffix;
+ var link_nodownload = prefix + id + "s" + suffix;
+
+ legend.style.display = 'none';
+ output.parentNode.style.display = 'block';
+ output.innerHTML = String.format(
+ '<div class="alert-message"><p>{{ _('Download execution result') }} <a href="%s">%s</a></p><p>{{ _('Or display result') }} <a href="%s">%s</a></p></div>',
+ link, link, link_nodownload, link_nodownload
+ );
+
+ location.hash = '#output';
+ }
+
+ ev.preventDefault();
+ }
+
+//]]></script>
+
+{%
+ const commands = [];
+
+ uci.foreach('luci', 'command', s => push(commands, s));
+-%}
+
+<form method="get" action="{{ entityencode(FULL_REQUEST_URI) }}">
+ <div class="cbi-map">
+ <h2 name="content">{{ _('Custom Commands') }}</h2>
+
+ {% if (length(commands) == 0): %}
+ <div class="cbi-section">
+ <div class="table cbi-section-table">
+ <div class="tr cbi-section-table-row">
+ <p>
+ <em>{{ _('This section contains no values yet') }}</em>
+ </p>
+ </div>
+ </div>
+ </div>
+ {% else %}
+ <div class="commands">
+ {% for (let command in commands): %}
+ <div class="commandbox">
+ <h3>{{ entityencode(command.name) }}</h3>
+ <p>{{ _('Command:') }} <code>{{ entityencode(command.command) }}</code></p>
+ {% if (command.param == "1"): %}
+ <p>{{ _('Arguments:') }} <input type="text" id="{{ command['.name'] }}" /></p>
+ {% endif %}
+ <div>
+ <button class="cbi-button cbi-button-apply" onclick="command_run(event, '{{ command['.name'] }}')">{{ _('Run') }}</button>
+ <button class="cbi-button cbi-button-download" onclick="command_download(event, '{{ command['.name'] }}')">{{ _('Download') }}</button>
+ {% if (command.public == "1"): %}
+ <button class="cbi-button cbi-button-link" onclick="command_link(event, '{{ command['.name'] }}')">{{ _('Link') }}</button>
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+
+ <a name="output"></a>
+ </div>
+ {% endif %}
+ </div>
+
+ <fieldset class="cbi-section" style="display:none">
+ <legend id="command-rc-legend">{{ _('Collecting data...') }}</legend>
+ <span id="command-rc-output"></span>
+ </fieldset>
+</form>
+
+{% include('footer') %}
diff --git a/applications/luci-app-commands/ucode/template/commands_public.ut b/applications/luci-app-commands/ucode/template/commands_public.ut
new file mode 100644
index 0000000000..aef072f802
--- /dev/null
+++ b/applications/luci-app-commands/ucode/template/commands_public.ut
@@ -0,0 +1,48 @@
+{#
+ Copyright 2016 t123yh <t123yh@outlook.com>
+ Copyright 2022 Jo-Philipp Wich <jo@mein.io>
+ Licensed to the public under the Apache License 2.0.
+-#}
+
+{%
+ include('header', { blank_page: true, css: `
+ .alert-success {
+ color: #3c763d;
+ background-color: #dff0d8;
+ border-color: #d6e9c6;
+ }
+
+ .alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ }
+
+ .alert-warning {
+ color: #8a6d3b;
+ background-color: #fcf8e3;
+ border-color: #faebcc;
+ }
+ ` });
+-%}
+
+<div class="alert alert-success" role="alert">
+ {% if (exitcode == 0): %}
+ {{ _('Command executed successfully.') }}
+ {% else %}
+ {{ sprintf(_('Command exited with status code %d'), exitcode) }}
+ {% endif %}
+</div>
+
+{% if (length(stdout)): %}
+ <h3>{{ _('Standard Output') }}</h3>
+ <pre>{{ entityencode(stdout) }}</pre>
+{% endif %}
+
+{% if (length(stderr)): %}
+ <h3>{{ _('Standard Error') }}</h3>
+ <pre>{{ entityencode(stderr) }}</pre>
+{% endif %}
+
+{% include('footer', { blank_page: true }) %}