commands.lua 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. -- Copyright 2012 Jo-Philipp Wich <jow@openwrt.org>
  2. -- Licensed to the public under the Apache License 2.0.
  3. module("luci.controller.commands", package.seeall)
  4. function index()
  5. entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80)
  6. entry({"admin", "system", "commands", "dashboard"}, template("commands"), _("Dashboard"), 1)
  7. entry({"admin", "system", "commands", "config"}, cbi("commands"), _("Configure"), 2)
  8. entry({"admin", "system", "commands", "run"}, call("action_run"), nil, 3).leaf = true
  9. entry({"admin", "system", "commands", "download"}, call("action_download"), nil, 3).leaf = true
  10. entry({"command"}, call("action_public"), nil, 1).leaf = true
  11. end
  12. --- Decode a given string into arguments following shell quoting rules
  13. --- [[abc \def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
  14. local function parse_args(str)
  15. local args = { }
  16. local function isspace(c)
  17. if c == 9 or c == 10 or c == 11 or c == 12 or c == 13 or c == 32 then
  18. return c
  19. end
  20. end
  21. local function isquote(c)
  22. if c == 34 or c == 39 or c == 96 then
  23. return c
  24. end
  25. end
  26. local function isescape(c)
  27. if c == 92 then
  28. return c
  29. end
  30. end
  31. local function ismeta(c)
  32. if c == 36 or c == 92 or c == 96 then
  33. return c
  34. end
  35. end
  36. --- Convert given table of byte values into a Lua string and append it to
  37. --- the "args" table. Segment byte value sequence into chunks of 256 values
  38. --- to not trip over the parameter limit for string.char()
  39. local function putstr(bytes)
  40. local chunks = { }
  41. local csz = 256
  42. local upk = unpack
  43. local chr = string.char
  44. local min = math.min
  45. local len = #bytes
  46. local off
  47. for off = 1, len, csz do
  48. chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len)))
  49. end
  50. args[#args+1] = table.concat(chunks)
  51. end
  52. --- Scan substring defined by the indexes [s, e] of the string "str",
  53. --- perform unquoting and de-escaping on the fly and store the result in
  54. --- a table of byte values which is passed to putstr()
  55. local function unquote(s, e)
  56. local off, esc, quote
  57. local res = { }
  58. for off = s, e do
  59. local byte = str:byte(off)
  60. local q = isquote(byte)
  61. local e = isescape(byte)
  62. local m = ismeta(byte)
  63. if e then
  64. esc = true
  65. elseif esc then
  66. if m then res[#res+1] = 92 end
  67. res[#res+1] = byte
  68. esc = false
  69. elseif q and quote and q == quote then
  70. quote = nil
  71. elseif q and not quote then
  72. quote = q
  73. else
  74. if m then res[#res+1] = 92 end
  75. res[#res+1] = byte
  76. end
  77. end
  78. putstr(res)
  79. end
  80. --- Find substring boundaries in "str". Ignore escaped or quoted
  81. --- whitespace, pass found start- and end-index for each substring
  82. --- to unquote()
  83. local off, esc, start, quote
  84. for off = 1, #str + 1 do
  85. local byte = str:byte(off)
  86. local q = isquote(byte)
  87. local s = isspace(byte) or (off > #str)
  88. local e = isescape(byte)
  89. if esc then
  90. esc = false
  91. elseif e then
  92. esc = true
  93. elseif q and quote and q == quote then
  94. quote = nil
  95. elseif q and not quote then
  96. start = start or off
  97. quote = q
  98. elseif s and not quote then
  99. if start then
  100. unquote(start, off - 1)
  101. start = nil
  102. end
  103. else
  104. start = start or off
  105. end
  106. end
  107. --- If the "quote" is still set we encountered an unfinished string
  108. if quote then
  109. unquote(start, #str)
  110. end
  111. return args
  112. end
  113. local function parse_cmdline(cmdid, args)
  114. local uci = require "luci.model.uci".cursor()
  115. if uci:get("luci", cmdid) == "command" then
  116. local cmd = uci:get_all("luci", cmdid)
  117. local argv = parse_args(cmd.command)
  118. local i, v
  119. if cmd.param == "1" and args then
  120. for i, v in ipairs(parse_args(luci.http.urldecode(args))) do
  121. argv[#argv+1] = v
  122. end
  123. end
  124. for i, v in ipairs(argv) do
  125. if v:match("[^%w%.%-i/]") then
  126. argv[i] = '"%s"' % v:gsub('"', '\\"')
  127. end
  128. end
  129. return argv
  130. end
  131. end
  132. function execute_command(callback, ...)
  133. local fs = require "nixio.fs"
  134. local argv = parse_cmdline(...)
  135. if argv then
  136. local outfile = os.tmpname()
  137. local errfile = os.tmpname()
  138. local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile })
  139. local stdout = fs.readfile(outfile, 1024 * 512) or ""
  140. local stderr = fs.readfile(errfile, 1024 * 512) or ""
  141. fs.unlink(outfile)
  142. fs.unlink(errfile)
  143. local binary = not not (stdout:match("[%z\1-\8\14-\31]"))
  144. callback({
  145. ok = true,
  146. command = table.concat(argv, " "),
  147. stdout = not binary and stdout,
  148. stderr = stderr,
  149. exitcode = rv,
  150. binary = binary
  151. })
  152. else
  153. callback({
  154. ok = false,
  155. code = 404,
  156. reason = "No such command"
  157. })
  158. end
  159. end
  160. function return_json(result)
  161. if result.ok then
  162. luci.http.prepare_content("application/json")
  163. luci.http.write_json(result)
  164. else
  165. luci.http.status(result.code, result.reason)
  166. end
  167. end
  168. function action_run(...)
  169. execute_command(return_json, ...)
  170. end
  171. function return_html(result)
  172. if result.ok then
  173. require("luci.template")
  174. luci.template.render("commands_public", {
  175. exitcode = result.exitcode,
  176. stdout = result.stdout,
  177. stderr = result.stderr
  178. })
  179. else
  180. luci.http.status(result.code, result.reason)
  181. end
  182. end
  183. function action_download(...)
  184. local fs = require "nixio.fs"
  185. local argv = parse_cmdline(...)
  186. if argv then
  187. local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null")
  188. if fd then
  189. local chunk = fd:read(4096) or ""
  190. local name
  191. if chunk:match("[%z\1-\8\14-\31]") then
  192. luci.http.header("Content-Disposition", "attachment; filename=%s"
  193. % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
  194. luci.http.prepare_content("application/octet-stream")
  195. else
  196. luci.http.header("Content-Disposition", "attachment; filename=%s"
  197. % fs.basename(argv[1]):gsub("%W+", ".") .. ".txt")
  198. luci.http.prepare_content("text/plain")
  199. end
  200. while chunk do
  201. luci.http.write(chunk)
  202. chunk = fd:read(4096)
  203. end
  204. fd:close()
  205. else
  206. luci.http.status(500, "Failed to execute command")
  207. end
  208. else
  209. luci.http.status(404, "No such command")
  210. end
  211. end
  212. function action_public(cmdid, args)
  213. local disp = false
  214. if string.sub(cmdid, -1) == "s" then
  215. disp = true
  216. cmdid = string.sub(cmdid, 1, -2)
  217. end
  218. local uci = require "luci.model.uci".cursor()
  219. if cmdid and
  220. uci:get("luci", cmdid) == "command" and
  221. uci:get("luci", cmdid, "public") == "1"
  222. then
  223. if disp then
  224. execute_command(return_html, cmdid, args)
  225. else
  226. action_download(cmdid, args)
  227. end
  228. else
  229. luci.http.status(403, "Access to command denied")
  230. end
  231. end