9fbde94c6aa7eb74015ae4bf2c2422fbd7336c51
[project/luci.git] / applications / luci-commands / luasrc / controller / commands.lua
1 --[[
2 LuCI - Lua Configuration Interface
3
4 Copyright 2012 Jo-Philipp Wich <jow@openwrt.org>
5
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
9
10         http://www.apache.org/licenses/LICENSE-2.0
11
12 ]]--
13
14 module("luci.controller.commands", package.seeall)
15
16 function index()
17         entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80).i18n = "commands"
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
22
23         entry({"command"}, call("action_public"), nil, 1).leaf = true
24 end
25
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)
29         local args = { }
30
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
33                         return c
34                 end
35         end
36
37         local function isquote(c)
38                 if c == 34 or c == 39 or c == 96 then
39                         return c
40                 end
41         end
42
43         local function isescape(c)
44                 if c == 92 then
45                         return c
46                 end
47         end
48
49         local function ismeta(c)
50                 if c == 36 or c == 92 or c == 96 then
51                         return c
52                 end
53         end
54
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)
59                 local chunks = { }
60                 local csz = 256
61                 local upk = unpack
62                 local chr = string.char
63                 local min = math.min
64                 local len = #bytes
65                 local off
66
67                 for off = 1, len, csz do
68                         chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len)))
69                 end
70
71                 args[#args+1] = table.concat(chunks)
72         end
73
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)
78                 local off, esc, quote
79                 local res = { }
80
81                 for off = s, e do
82                         local byte = str:byte(off)
83                         local q = isquote(byte)
84                         local e = isescape(byte)
85                         local m = ismeta(byte)
86
87                         if e then
88                                 esc = true
89                         elseif esc then
90                                 if m then res[#res+1] = 92 end
91                                 res[#res+1] = byte
92                                 esc = false
93                         elseif q and quote and q == quote then
94                                 quote = nil
95                         elseif q and not quote then
96                                 quote = q
97                         else
98                                 if m then res[#res+1] = 92 end
99                                 res[#res+1] = byte
100                         end
101                 end
102
103                 putstr(res)
104         end
105
106         --- Find substring boundaries in "str". Ignore escaped or quoted
107         --- whitespace, pass found start- and end-index for each substring
108         --- to unquote()
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)
115
116                 if esc then
117                         esc = false
118                 elseif e then
119                         esc = true
120                 elseif q and quote and q == quote then
121                         quote = nil
122                 elseif q and not quote then
123                         start = start or off
124                         quote = q
125                 elseif s and not quote then
126                         if start then
127                                 unquote(start, off - 1)
128                                 start = nil
129                         end
130                 else
131                         start = start or off
132                 end
133         end
134
135         --- If the "quote" is still set we encountered an unfinished string
136         if quote then
137                 unquote(start, #str)
138         end
139
140         return args
141 end
142
143 local function parse_cmdline(cmdid, args)
144         local uci  = require "luci.model.uci".cursor()
145         local path = luci.dispatcher.context.requestpath
146
147         if uci:get("luci", cmdid) == "command" then
148                 local cmd = uci:get_all("luci", cmdid)
149                 local argv = parse_args(cmd.command)
150                 local i, v
151
152                 if cmd.param == "1" and args then
153                         for i, v in ipairs(parse_args(luci.http.urldecode(args))) do
154                                 argv[#argv+1] = v
155                         end
156                 end
157
158                 for i, v in ipairs(argv) do
159                         if v:match("[^%w%.%-i/]") then
160                                 argv[i] = '"%s"' % v:gsub('"', '\\"')
161                         end
162                 end
163
164                 return argv
165         end
166 end
167
168 function action_run(...)
169         local fs   = require "nixio.fs"
170         local argv = parse_cmdline(...)
171         if argv then
172                 local outfile = os.tmpname()
173                 local errfile = os.tmpname()
174
175                 local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile })
176                 local stdout = fs.readfile(outfile, 1024 * 512) or ""
177                 local stderr = fs.readfile(errfile, 1024 * 512) or ""
178
179                 fs.unlink(outfile)
180                 fs.unlink(errfile)
181
182                 local binary = not not (stdout:match("[%z\1-\8\14-\31]"))
183
184                 luci.http.prepare_content("application/json")
185                 luci.http.write_json({
186                         command  = table.concat(argv, " "),
187                         stdout   = not binary and stdout,
188                         stderr   = stderr,
189                         exitcode = rv,
190                         binary   = binary
191                 })
192         else
193                 luci.http.status(404, "No such command")
194         end
195 end
196
197 function action_download(...)
198         local fs   = require "nixio.fs"
199         local argv = parse_cmdline(...)
200         if argv then
201                 local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null")
202                 if fd then
203                         local chunk = fd:read(4096) or ""
204                         local name
205                         if chunk:match("[%z\1-\8\14-\31]") then
206                                 luci.http.header("Content-Disposition", "attachment; filename=%s"
207                                                  % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
208                                 luci.http.prepare_content("application/octet-stream")
209                         else
210                                 luci.http.header("Content-Disposition", "attachment; filename=%s"
211                                                  % fs.basename(argv[1]):gsub("%W+", ".") .. ".txt")
212                                 luci.http.prepare_content("text/plain")
213                         end
214
215                         while chunk do
216                                 luci.http.write(chunk)
217                                 chunk = fd:read(4096)
218                         end
219
220                         fd:close()
221                 else
222                         luci.http.status(500, "Failed to execute command")
223                 end
224         else
225                 luci.http.status(404, "No such command")
226         end
227 end
228
229 function action_public(cmdid, args)
230         local uci = require "luci.model.uci".cursor()
231         if uci:get("luci", cmdid) == "command" and
232            uci:get("luci", cmdid, "public") == "1"
233         then
234                 action_download(cmdid, args)
235         else
236                 luci.http.status(403, "Access to command denied")
237         end
238 end