diff options
author | Jo-Philipp Wich <jow@openwrt.org> | 2012-11-21 16:22:04 +0000 |
---|---|---|
committer | Jo-Philipp Wich <jow@openwrt.org> | 2012-11-21 16:22:04 +0000 |
commit | 3e59bb699ba4ede24fd9d9ad9c8c50955908c9b2 (patch) | |
tree | 69bca0baf12990db0042dcd50013ee82b01b0533 /applications/luci-commands | |
parent | 3a04258ba0f3cc470c3527808607d9267e391142 (diff) |
applications: add new application luci-app-commands which allows configuring custom shell commands for invocation through the gui
Diffstat (limited to 'applications/luci-commands')
-rw-r--r-- | applications/luci-commands/Makefile | 4 | ||||
-rw-r--r-- | applications/luci-commands/luasrc/controller/commands.lua | 237 | ||||
-rw-r--r-- | applications/luci-commands/luasrc/model/cbi/commands.lua | 37 | ||||
-rw-r--r-- | applications/luci-commands/luasrc/view/commands.htm | 147 |
4 files changed, 425 insertions, 0 deletions
diff --git a/applications/luci-commands/Makefile b/applications/luci-commands/Makefile new file mode 100644 index 000000000..64801c2b2 --- /dev/null +++ b/applications/luci-commands/Makefile @@ -0,0 +1,4 @@ +PO = commands + +include ../../build/config.mk +include ../../build/module.mk diff --git a/applications/luci-commands/luasrc/controller/commands.lua b/applications/luci-commands/luasrc/controller/commands.lua new file mode 100644 index 000000000..cd921f9f2 --- /dev/null +++ b/applications/luci-commands/luasrc/controller/commands.lua @@ -0,0 +1,237 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2012 Jo-Philipp Wich <jow@openwrt.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +module("luci.controller.commands", package.seeall) + +function index() + entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80).i18n = "commands" + entry({"admin", "system", "commands", "dashboard"}, template("commands"), _("Dashboard"), 1) + entry({"admin", "system", "commands", "config"}, cbi("commands"), _("Configure"), 2) + entry({"admin", "system", "commands", "run"}, call("action_run"), nil, 3).leaf = true + entry({"admin", "system", "commands", "download"}, call("action_download"), nil, 3).leaf = true + + entry({"command"}, call("action_public"), nil, 1).leaf = true +end + +--- Decode a given string into arguments following shell quoting rules +--- [[abc \def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]] +local function parse_args(str) + local args = { } + + local function isspace(c) + if c == 9 or c == 10 or c == 11 or c == 12 or c == 13 or c == 32 then + return c + end + end + + local function isquote(c) + if c == 34 or c == 39 or c == 96 then + return c + end + end + + local function isescape(c) + if c == 92 then + return c + end + end + + local function ismeta(c) + if c == 36 or c == 92 or c == 96 then + return c + end + end + + --- Convert given table of byte values into a Lua string and append it to + --- the "args" table. Segment byte value sequence into chunks of 256 values + --- to not trip over the parameter limit for string.char() + local function putstr(bytes) + local chunks = { } + local csz = 256 + local upk = unpack + local chr = string.char + local min = math.min + local len = #bytes + local off + + for off = 1, len, csz do + chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len))) + end + + args[#args+1] = table.concat(chunks) + end + + --- Scan substring defined by the indexes [s, e] of the string "str", + --- perform unquoting and de-escaping on the fly and store the result in + --- a table of byte values which is passed to putstr() + local function unquote(s, e) + local off, esc, quote + local res = { } + + for off = s, e do + local byte = str:byte(off) + local q = isquote(byte) + local e = isescape(byte) + local m = ismeta(byte) + + if e then + esc = true + elseif esc then + if m then res[#res+1] = 92 end + res[#res+1] = byte + esc = false + elseif q and quote and q == quote then + quote = nil + elseif q and not quote then + quote = q + else + if m then res[#res+1] = 92 end + res[#res+1] = byte + end + end + + putstr(res) + end + + --- Find substring boundaries in "str". Ignore escaped or quoted + --- whitespace, pass found start- and end-index for each substring + --- to unquote() + local off, esc, start, quote + for off = 1, #str + 1 do + local byte = str:byte(off) + local q = isquote(byte) + local s = isspace(byte) or (off > #str) + local e = isescape(byte) + + if esc then + esc = false + elseif e then + esc = true + elseif q and quote and q == quote then + quote = nil + elseif q and not quote then + start = start or off + quote = q + elseif s and not quote then + if start then + unquote(start, off - 1) + start = nil + end + else + start = start or off + end + end + + --- If the "quote" is still set we encountered an unfinished string + if quote then + unquote(start, #str) + end + + return args +end + +local function parse_cmdline(cmdid, args) + local uci = require "luci.model.uci".cursor() + local path = luci.dispatcher.context.requestpath + + if uci:get("luci", cmdid) == "command" then + local cmd = uci:get_all("luci", cmdid) + local argv = parse_args(cmd.command) + local i, v + + if cmd.param == "1" and args then + for i, v in ipairs(parse_args(luci.http.urldecode(args))) do + argv[#argv+1] = v + end + end + + for i, v in ipairs(argv) do + if v:match("[^%w%.%-i/]") then + argv[i] = '"%s"' % v:gsub('"', '\\"') + end + end + + return argv + end +end + +function action_run(...) + local fs = require "nixio.fs" + local argv = parse_cmdline(...) + if argv then + local outfile = os.tmpname() + local errfile = os.tmpname() + + local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile }) + local stdout = fs.readfile(outfile, 1024 * 512) or "" + local stderr = fs.readfile(errfile, 1024 * 512) or "" + + fs.unlink(outfile) + fs.unlink(errfile) + + local binary = not not (stdout:match("[%z\1-\8\14-\31]")) + + luci.http.prepare_content("application/json") + luci.http.write_json({ + command = table.concat(argv, " "), + stdout = not binary and stdout, + stderr = stderr, + exitcode = rv, + binary = binary + }) + else + luci.http.status(404, "No such command") + end +end + +function action_download(...) + local argv = parse_cmdline(...) + if argv then + local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null") + if fd then + local chunk = fd:read(4096) or "" + local name + if chunk:match("[%z\1-\8\14-\31]") then + luci.http.header("Content-Disposition", "attachment; filename=%s" + % argv[1]:gsub("%W+", ".") .. ".bin") + luci.http.prepare_content("application/octet-stream") + else + luci.http.header("Content-Disposition", "attachment; filename=%s" + % argv[1]:gsub("%W+", ".") .. ".txt") + luci.http.prepare_content("text/plain") + end + + while chunk do + luci.http.write(chunk) + chunk = fd:read(4096) + end + + fd:close() + else + luci.http.status(500, "Failed to execute command") + end + else + luci.http.status(404, "No such command") + end +end + +function action_public(cmdid, args) + local uci = require "luci.model.uci".cursor() + if uci:get("luci", cmdid) == "command" and + uci:get("luci", cmdid, "public") == "1" + then + action_download(cmdid, args) + else + luci.http.status(403, "Access to command denied") + end +end diff --git a/applications/luci-commands/luasrc/model/cbi/commands.lua b/applications/luci-commands/luasrc/model/cbi/commands.lua new file mode 100644 index 000000000..1359eb2ac --- /dev/null +++ b/applications/luci-commands/luasrc/model/cbi/commands.lua @@ -0,0 +1,37 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2012 Jo-Philipp Wich <jow@openwrt.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +local m, s + +m = Map("luci", translate("Custom Commands"), + translate("This page allows you to configure custom shell commands which can be easily invoked from the web interface.")) + +s = m:section(TypedSection, "command", "") +s.template = "cbi/tblsection" +s.anonymous = true +s.addremove = true + + +s:option(Value, "name", translate("Description"), + translate("A short textual description of the configured command")) + +s:option(Value, "command", translate("Command"), + translate("Command line to execute")) + +s:option(Flag, "param", translate("Custom arguments"), + translate("Allow the user to provide additional command line arguments")) + +s:option(Flag, "public", translate("Public access"), + translate("Allow executing the command and downloading its output without prior authentication")) + +return m diff --git a/applications/luci-commands/luasrc/view/commands.htm b/applications/luci-commands/luasrc/view/commands.htm new file mode 100644 index 000000000..d9faa3329 --- /dev/null +++ b/applications/luci-commands/luasrc/view/commands.htm @@ -0,0 +1,147 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2012 Jo-Philipp Wich <jow@openwrt.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +-%> + +<%+header%> + +<script type="text/javascript" src="<%=resource%>/cbi.js"></script> +<script type="text/javascript">//<![CDATA[ + var stxhr = new XHR(); + + function command_run(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('<%=luci.dispatcher.build_url("admin", "system", "commands", "run")%>/' + id + (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'; + } + ); + } + } + + function command_download(id) + { + var args; + var field = document.getElementById(id); + if (field) + args = encodeURIComponent(field.value); + + location.href = '<%=luci.dispatcher.build_url("admin", "system", "commands", "download")%>/' + id + (args ? '/' + args : ''); + } + + function command_link(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 link = location.protocol + '//' + location.hostname + + (location.port ? ':' + location.port : '') + + location.pathname.split(';')[0] + 'command/' + + id + (args ? '/' + args : ''); + + legend.style.display = 'none'; + output.parentNode.style.display = 'block'; + output.innerHTML = String.format( + '<div class="alert-message warning"><%:Access command with%> <a href="%s">%s</a></div>', + link, link + ); + + location.hash = '#output'; + } + } + +//]]></script> + +<% + local uci = require "luci.model.uci".cursor() + local commands = { } + + uci:foreach("luci", "command", function(s) commands[#commands+1] = s end) +%> + +<form method="get" action="<%=pcdata(luci.http.getenv("REQUEST_URI"))%>"> + <div class="cbi-map"> + <h2><a id="content" name="content"><%:Custom Commands%></a></h2> + + <fieldset class="cbi-section"> + <% local _, command; for _, command in ipairs(commands) do %> + <div style="width:30%; float:left; height:150px; position:relative"> + <h3><%=pcdata(command.name)%></h3> + <p><%:Command:%> <code><%=pcdata(command.command)%></code></p> + <% if command.param == "1" then %> + <p><%:Arguments:%> <input style="width: 50%" type="text" value="openwrt.org" id="<%=command['.name']%>" /></p> + <% end %> + <div style="position:absolute; left:0; bottom:20px"> + <input type="button" value="<%:Run%>" class="cbi-button cbi-button-apply" onclick="command_run('<%=command['.name']%>')" /> + <input type="button" value="<%:Download%>" class="cbi-button cbi-button-download" onclick="command_download('<%=command['.name']%>')" /> + <% if command.public == "1" then %> + <input type="button" value="<%:Link%>" class="cbi-button cbi-button-link" onclick="command_link('<%=command['.name']%>')" /> + <% end %> + </div> + </div> + <% end %> + + <br style="clear:both" /><br /> + <a name="output"></a> + </fieldset> + </div> + + <fieldset class="cbi-section" style="display:none"> + <legend id="command-rc-legend"><%:Collecting data...%></legend> + <span id="command-rc-output"></span> + </fieldset> +</form> + +<%+footer%> |