applications: add new application luci-app-commands which allows configuring custom...
authorJo-Philipp Wich <jow@openwrt.org>
Wed, 21 Nov 2012 16:22:04 +0000 (16:22 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Wed, 21 Nov 2012 16:22:04 +0000 (16:22 +0000)
applications/luci-commands/Makefile [new file with mode: 0644]
applications/luci-commands/luasrc/controller/commands.lua [new file with mode: 0644]
applications/luci-commands/luasrc/model/cbi/commands.lua [new file with mode: 0644]
applications/luci-commands/luasrc/view/commands.htm [new file with mode: 0644]

diff --git a/applications/luci-commands/Makefile b/applications/luci-commands/Makefile
new file mode 100644 (file)
index 0000000..64801c2
--- /dev/null
@@ -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 (file)
index 0000000..cd921f9
--- /dev/null
@@ -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 (file)
index 0000000..1359eb2
--- /dev/null
@@ -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 (file)
index 0000000..d9faa33
--- /dev/null
@@ -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%>