2 LuCI - Lua Configuration Interface
4 Copyright 2012 Jo-Philipp Wich <jow@openwrt.org>
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
10 http://www.apache.org/licenses/LICENSE-2.0
14 module("luci.controller.commands", package.seeall)
17 entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80)
18 entry({"admin", "system", "commands", "dashboard"}, template("commands"), _("Dashboard"), 1)
19 entry({"admin", "system", "commands", "config"}, cbi("commands"), _("Configure"), 2)
20 entry({"admin", "system", "commands", "run"}, call("action_run"), nil, 3).leaf = true
21 entry({"admin", "system", "commands", "download"}, call("action_download"), nil, 3).leaf = true
23 entry({"command"}, call("action_public"), nil, 1).leaf = true
26 --- Decode a given string into arguments following shell quoting rules
27 --- [[abc \def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
28 local function parse_args(str)
31 local function isspace(c)
32 if c == 9 or c == 10 or c == 11 or c == 12 or c == 13 or c == 32 then
37 local function isquote(c)
38 if c == 34 or c == 39 or c == 96 then
43 local function isescape(c)
49 local function ismeta(c)
50 if c == 36 or c == 92 or c == 96 then
55 --- Convert given table of byte values into a Lua string and append it to
56 --- the "args" table. Segment byte value sequence into chunks of 256 values
57 --- to not trip over the parameter limit for string.char()
58 local function putstr(bytes)
62 local chr = string.char
67 for off = 1, len, csz do
68 chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len)))
71 args[#args+1] = table.concat(chunks)
74 --- Scan substring defined by the indexes [s, e] of the string "str",
75 --- perform unquoting and de-escaping on the fly and store the result in
76 --- a table of byte values which is passed to putstr()
77 local function unquote(s, e)
82 local byte = str:byte(off)
83 local q = isquote(byte)
84 local e = isescape(byte)
85 local m = ismeta(byte)
90 if m then res[#res+1] = 92 end
93 elseif q and quote and q == quote then
95 elseif q and not quote then
98 if m then res[#res+1] = 92 end
106 --- Find substring boundaries in "str". Ignore escaped or quoted
107 --- whitespace, pass found start- and end-index for each substring
109 local off, esc, start, quote
110 for off = 1, #str + 1 do
111 local byte = str:byte(off)
112 local q = isquote(byte)
113 local s = isspace(byte) or (off > #str)
114 local e = isescape(byte)
120 elseif q and quote and q == quote then
122 elseif q and not quote then
125 elseif s and not quote then
127 unquote(start, off - 1)
135 --- If the "quote" is still set we encountered an unfinished string
143 local function parse_cmdline(cmdid, args)
144 local uci = require "luci.model.uci".cursor()
145 if uci:get("luci", cmdid) == "command" then
146 local cmd = uci:get_all("luci", cmdid)
147 local argv = parse_args(cmd.command)
150 if cmd.param == "1" and args then
151 for i, v in ipairs(parse_args(luci.http.urldecode(args))) do
156 for i, v in ipairs(argv) do
157 if v:match("[^%w%.%-i/]") then
158 argv[i] = '"%s"' % v:gsub('"', '\\"')
166 function action_run(...)
167 local fs = require "nixio.fs"
168 local argv = parse_cmdline(...)
170 local outfile = os.tmpname()
171 local errfile = os.tmpname()
173 local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile })
174 local stdout = fs.readfile(outfile, 1024 * 512) or ""
175 local stderr = fs.readfile(errfile, 1024 * 512) or ""
180 local binary = not not (stdout:match("[%z\1-\8\14-\31]"))
182 luci.http.prepare_content("application/json")
183 luci.http.write_json({
184 command = table.concat(argv, " "),
185 stdout = not binary and stdout,
191 luci.http.status(404, "No such command")
195 function action_download(...)
196 local fs = require "nixio.fs"
197 local argv = parse_cmdline(...)
199 local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null")
201 local chunk = fd:read(4096) or ""
203 if chunk:match("[%z\1-\8\14-\31]") then
204 luci.http.header("Content-Disposition", "attachment; filename=%s"
205 % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
206 luci.http.prepare_content("application/octet-stream")
208 luci.http.header("Content-Disposition", "attachment; filename=%s"
209 % fs.basename(argv[1]):gsub("%W+", ".") .. ".txt")
210 luci.http.prepare_content("text/plain")
214 luci.http.write(chunk)
215 chunk = fd:read(4096)
220 luci.http.status(500, "Failed to execute command")
223 luci.http.status(404, "No such command")
227 function action_public(cmdid, args)
228 local uci = require "luci.model.uci".cursor()
230 uci:get("luci", cmdid) == "command" and
231 uci:get("luci", cmdid, "public") == "1"
233 action_download(cmdid, args)
235 luci.http.status(403, "Access to command denied")