applications: add new application luci-app-commands which allows configuring custom shell commands for invocation through the gui
This commit is contained in:
parent
3a04258ba0
commit
3e59bb699b
4 changed files with 425 additions and 0 deletions
4
applications/luci-commands/Makefile
Normal file
4
applications/luci-commands/Makefile
Normal file
|
@ -0,0 +1,4 @@
|
|||
PO = commands
|
||||
|
||||
include ../../build/config.mk
|
||||
include ../../build/module.mk
|
237
applications/luci-commands/luasrc/controller/commands.lua
Normal file
237
applications/luci-commands/luasrc/controller/commands.lua
Normal file
|
@ -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
|
37
applications/luci-commands/luasrc/model/cbi/commands.lua
Normal file
37
applications/luci-commands/luasrc/model/cbi/commands.lua
Normal file
|
@ -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
|
147
applications/luci-commands/luasrc/view/commands.htm
Normal file
147
applications/luci-commands/luasrc/view/commands.htm
Normal file
|
@ -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%>
|
Loading…
Reference in a new issue