1 -- Copyright 2012 Jo-Philipp Wich <jow@openwrt.org>
2 -- Licensed to the public under the Apache License 2.0.
4 module("luci.controller.commands", package.seeall)
7 entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80)
8 entry({"admin", "system", "commands", "dashboard"}, template("commands"), _("Dashboard"), 1)
9 entry({"admin", "system", "commands", "config"}, cbi("commands"), _("Configure"), 2)
10 entry({"admin", "system", "commands", "run"}, call("action_run"), nil, 3).leaf = true
11 entry({"admin", "system", "commands", "download"}, call("action_download"), nil, 3).leaf = true
13 entry({"command"}, call("action_public"), nil, 1).leaf = true
16 --- Decode a given string into arguments following shell quoting rules
17 --- [[abc \def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
18 local function parse_args(str)
21 local function isspace(c)
22 if c == 9 or c == 10 or c == 11 or c == 12 or c == 13 or c == 32 then
27 local function isquote(c)
28 if c == 34 or c == 39 or c == 96 then
33 local function isescape(c)
39 local function ismeta(c)
40 if c == 36 or c == 92 or c == 96 then
45 --- Convert given table of byte values into a Lua string and append it to
46 --- the "args" table. Segment byte value sequence into chunks of 256 values
47 --- to not trip over the parameter limit for string.char()
48 local function putstr(bytes)
52 local chr = string.char
57 for off = 1, len, csz do
58 chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len)))
61 args[#args+1] = table.concat(chunks)
64 --- Scan substring defined by the indexes [s, e] of the string "str",
65 --- perform unquoting and de-escaping on the fly and store the result in
66 --- a table of byte values which is passed to putstr()
67 local function unquote(s, e)
72 local byte = str:byte(off)
73 local q = isquote(byte)
74 local e = isescape(byte)
75 local m = ismeta(byte)
80 if m then res[#res+1] = 92 end
83 elseif q and quote and q == quote then
85 elseif q and not quote then
88 if m then res[#res+1] = 92 end
96 --- Find substring boundaries in "str". Ignore escaped or quoted
97 --- whitespace, pass found start- and end-index for each substring
99 local off, esc, start, quote
100 for off = 1, #str + 1 do
101 local byte = str:byte(off)
102 local q = isquote(byte)
103 local s = isspace(byte) or (off > #str)
104 local e = isescape(byte)
110 elseif q and quote and q == quote then
112 elseif q and not quote then
115 elseif s and not quote then
117 unquote(start, off - 1)
125 --- If the "quote" is still set we encountered an unfinished string
133 local function parse_cmdline(cmdid, args)
134 local uci = require "luci.model.uci".cursor()
135 if uci:get("luci", cmdid) == "command" then
136 local cmd = uci:get_all("luci", cmdid)
137 local argv = parse_args(cmd.command)
140 if cmd.param == "1" and args then
141 for i, v in ipairs(parse_args(luci.http.urldecode(args))) do
146 for i, v in ipairs(argv) do
147 if v:match("[^%w%.%-i/]") then
148 argv[i] = '"%s"' % v:gsub('"', '\\"')
156 function execute_command(callback, ...)
157 local fs = require "nixio.fs"
158 local argv = parse_cmdline(...)
160 local outfile = os.tmpname()
161 local errfile = os.tmpname()
163 local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile })
164 local stdout = fs.readfile(outfile, 1024 * 512) or ""
165 local stderr = fs.readfile(errfile, 1024 * 512) or ""
170 local binary = not not (stdout:match("[%z\1-\8\14-\31]"))
174 command = table.concat(argv, " "),
175 stdout = not binary and stdout,
184 reason = "No such command"
189 function return_json(result)
191 luci.http.prepare_content("application/json")
192 luci.http.write_json(result)
194 luci.http.status(result.code, result.reason)
198 function action_run(...)
199 execute_command(return_json, ...)
202 function return_html(result)
204 require("luci.template")
205 luci.template.render("commands_public", {
206 exitcode = result.exitcode,
207 stdout = result.stdout,
208 stderr = result.stderr
211 luci.http.status(result.code, result.reason)
216 function action_download(...)
217 local fs = require "nixio.fs"
218 local argv = parse_cmdline(...)
220 local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null")
222 local chunk = fd:read(4096) or ""
224 if chunk:match("[%z\1-\8\14-\31]") then
225 luci.http.header("Content-Disposition", "attachment; filename=%s"
226 % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
227 luci.http.prepare_content("application/octet-stream")
229 luci.http.header("Content-Disposition", "attachment; filename=%s"
230 % fs.basename(argv[1]):gsub("%W+", ".") .. ".txt")
231 luci.http.prepare_content("text/plain")
235 luci.http.write(chunk)
236 chunk = fd:read(4096)
241 luci.http.status(500, "Failed to execute command")
244 luci.http.status(404, "No such command")
249 function action_public(cmdid, args)
251 if string.sub(cmdid, -1) == "s" then
253 cmdid = string.sub(cmdid, 1, -2)
255 local uci = require "luci.model.uci".cursor()
257 uci:get("luci", cmdid) == "command" and
258 uci:get("luci", cmdid, "public") == "1"
261 execute_command(return_html, cmdid, args)
263 action_download(cmdid, args)
266 luci.http.status(403, "Access to command denied")