123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- -- Copyright 2012 Jo-Philipp Wich <jow@openwrt.org>
- -- Licensed to the public under the Apache License 2.0.
- module("luci.controller.commands", package.seeall)
- function index()
- entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80)
- 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()
- 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 execute_command(callback, ...)
- 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]"))
- callback({
- ok = true,
- command = table.concat(argv, " "),
- stdout = not binary and stdout,
- stderr = stderr,
- exitcode = rv,
- binary = binary
- })
- else
- callback({
- ok = false,
- code = 404,
- reason = "No such command"
- })
- end
- end
- function return_json(result)
- if result.ok then
- luci.http.prepare_content("application/json")
- luci.http.write_json(result)
- else
- luci.http.status(result.code, result.reason)
- end
- end
- function action_run(...)
- execute_command(return_json, ...)
- end
- function return_html(result)
- if result.ok then
- require("luci.template")
- luci.template.render("commands_public", {
- exitcode = result.exitcode,
- stdout = result.stdout,
- stderr = result.stderr
- })
- else
- luci.http.status(result.code, result.reason)
- end
- end
- function action_download(...)
- local fs = require "nixio.fs"
- 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"
- % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
- luci.http.prepare_content("application/octet-stream")
- else
- luci.http.header("Content-Disposition", "attachment; filename=%s"
- % fs.basename(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 disp = false
- if string.sub(cmdid, -1) == "s" then
- disp = true
- cmdid = string.sub(cmdid, 1, -2)
- end
- local uci = require "luci.model.uci".cursor()
- if cmdid and
- uci:get("luci", cmdid) == "command" and
- uci:get("luci", cmdid, "public") == "1"
- then
- if disp then
- execute_command(return_html, cmdid, args)
- else
- action_download(cmdid, args)
- end
- else
- luci.http.status(403, "Access to command denied")
- end
- end
|