luci-lua-runtime: dispatcher.lua: re-add test_post_security()
[project/luci.git] / modules / luci-lua-runtime / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 module("luci.dispatcher", package.seeall)
6
7 local http = _G.L.http
8
9 context = setmetatable({}, {
10 __index = function(t, k)
11 if k == "request" or k == "requestpath" then
12 return _G.L.ctx.request_path
13 elseif k == "requestargs" then
14 return _G.L.ctx.request_args
15 else
16 return _G.L.ctx[k]
17 end
18 end
19 })
20
21 uci = require "luci.model.uci"
22 uci:set_session_id(_G.L.ctx.authsession)
23
24 i18n = require "luci.i18n"
25 i18n.setlanguage(_G.L.dispatcher.lang)
26
27 build_url = _G.L.dispatcher.build_url
28 menu_json = _G.L.dispatcher.menu_json
29 error404 = _G.L.dispatcher.error404
30 error500 = _G.L.dispatcher.error500
31
32 function is_authenticated(auth)
33 local session = _G.L.dispatcher.is_authenticated(auth)
34 if session then
35 return session.sid, session.data, session.acls
36 end
37 end
38
39 function assign(path, clone, title, order)
40 local obj = node(unpack(path))
41
42 obj.title = title
43 obj.order = order
44
45 setmetatable(obj, {__index = node(unpack(clone))})
46
47 return obj
48 end
49
50 function entry(path, target, title, order)
51 local c = node(unpack(path))
52
53 c.title = title
54 c.order = order
55 c.action = target
56
57 return c
58 end
59
60 -- enabling the node.
61 function get(...)
62 return node(...)
63 end
64
65 function node(...)
66 local p = table.concat({ ... }, "/")
67
68 if not __entries[p] then
69 __entries[p] = {}
70 end
71
72 return __entries[p]
73 end
74
75 function lookup(...)
76 local i, path = nil, {}
77 for i = 1, select('#', ...) do
78 local name, arg = nil, tostring(select(i, ...))
79 for name in arg:gmatch("[^/]+") do
80 path[#path+1] = name
81 end
82 end
83
84 local node = menu_json()
85 for i = 1, #path do
86 node = node.children[path[i]]
87
88 if not node then
89 return nil
90 elseif node.leaf then
91 break
92 end
93 end
94
95 return node, build_url(unpack(path))
96 end
97
98
99 function process_lua_controller(path)
100 local base = "/usr/lib/lua/luci/controller/"
101 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
102 local mod = require(modname)
103 assert(mod ~= true,
104 "Invalid controller file found\n" ..
105 "The file '" .. path .. "' contains an invalid module line.\n" ..
106 "Please verify whether the module name is set to '" .. modname ..
107 "' - It must correspond to the file path!")
108
109 local idx = mod.index
110 if type(idx) ~= "function" then
111 return nil
112 end
113
114 local entries = {}
115
116 __entries = entries
117 __controller = modname
118
119 setfenv(idx, setmetatable({}, { __index = luci.dispatcher }))()
120
121 __entries = nil
122 __controller = nil
123
124 -- fixup gathered node specs
125 for path, entry in pairs(entries) do
126 if entry.leaf then
127 entry.wildcard = true
128 end
129
130 if type(entry.file_depends) == "table" then
131 for _, v in ipairs(entry.file_depends) do
132 entry.depends = entry.depends or {}
133 entry.depends.fs = entry.depends.fs or {}
134
135 local ft = fs.stat(v, "type")
136 if ft == "dir" then
137 entry.depends.fs[v] = "directory"
138 elseif v:match("/s?bin/") then
139 entry.depends.fs[v] = "executable"
140 else
141 entry.depends.fs[v] = "file"
142 end
143 end
144 end
145
146 if type(entry.uci_depends) == "table" then
147 for k, v in pairs(entry.uci_depends) do
148 entry.depends = entry.depends or {}
149 entry.depends.uci = entry.depends.uci or {}
150 entry.depends.uci[k] = v
151 end
152 end
153
154 if type(entry.acl_depends) == "table" then
155 for _, acl in ipairs(entry.acl_depends) do
156 entry.depends = entry.depends or {}
157 entry.depends.acl = entry.depends.acl or {}
158 entry.depends.acl[#entry.depends.acl + 1] = acl
159 end
160 end
161
162 if (entry.sysauth_authenticator ~= nil) or
163 (entry.sysauth ~= nil and entry.sysauth ~= false)
164 then
165 if entry.sysauth_authenticator == "htmlauth" then
166 entry.auth = {
167 login = true,
168 methods = { "cookie:sysauth_https", "cookie:sysauth_http" }
169 }
170 elseif path == "rpc" and modname == "luci.controller.rpc" then
171 entry.auth = {
172 login = false,
173 methods = { "query:auth", "cookie:sysauth_https", "cookie:sysauth_http", "cookie:sysauth" }
174 }
175 elseif modname == "luci.controller.admin.uci" then
176 entry.auth = {
177 login = false,
178 methods = { "param:sid" }
179 }
180 end
181 elseif entry.sysauth == false then
182 entry.auth = {}
183 end
184
185 if entry.action == nil and type(entry.target) == "table" then
186 entry.action = entry.target
187 entry.target = nil
188 end
189
190 entry.leaf = nil
191
192 entry.file_depends = nil
193 entry.uci_depends = nil
194 entry.acl_depends = nil
195
196 entry.sysauth = nil
197 entry.sysauth_authenticator = nil
198 end
199
200 return entries
201 end
202
203 function invoke_cbi_action(model, config, ...)
204 local cbi = require "luci.cbi"
205 local tpl = require "luci.template"
206 local util = require "luci.util"
207
208 if not config then
209 config = {}
210 end
211
212 local maps = cbi.load(model, ...)
213
214 local state = nil
215
216 local function has_uci_access(config, level)
217 local rv = util.ubus("session", "access", {
218 ubus_rpc_session = context.authsession,
219 scope = "uci", object = config,
220 ["function"] = level
221 })
222
223 return (type(rv) == "table" and rv.access == true) or false
224 end
225
226 local i, res
227 for i, res in ipairs(maps) do
228 if util.instanceof(res, cbi.SimpleForm) then
229 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
230 % model)
231
232 io.stderr:write("please change %s to use the form() action instead.\n"
233 % table.concat(context.request, "/"))
234 end
235
236 res.flow = config
237 local cstate = res:parse()
238 if cstate and (not state or cstate < state) then
239 state = cstate
240 end
241 end
242
243 local function _resolve_path(path)
244 return type(path) == "table" and build_url(unpack(path)) or path
245 end
246
247 if config.on_valid_to and state and state > 0 and state < 2 then
248 http:redirect(_resolve_path(config.on_valid_to))
249 return
250 end
251
252 if config.on_changed_to and state and state > 1 then
253 http:redirect(_resolve_path(config.on_changed_to))
254 return
255 end
256
257 if config.on_success_to and state and state > 0 then
258 http:redirect(_resolve_path(config.on_success_to))
259 return
260 end
261
262 if config.state_handler then
263 if not config.state_handler(state, maps) then
264 return
265 end
266 end
267
268 http:header("X-CBI-State", state or 0)
269
270 if not config.noheader then
271 _G.L.include("cbi/header", {state = state})
272 end
273
274 local redirect
275 local messages
276 local applymap = false
277 local pageaction = true
278 local parsechain = { }
279 local writable = false
280
281 for i, res in ipairs(maps) do
282 if res.apply_needed and res.parsechain then
283 local c
284 for _, c in ipairs(res.parsechain) do
285 parsechain[#parsechain+1] = c
286 end
287 applymap = true
288 end
289
290 if res.redirect then
291 redirect = redirect or res.redirect
292 end
293
294 if res.pageaction == false then
295 pageaction = false
296 end
297
298 if res.message then
299 messages = messages or { }
300 messages[#messages+1] = res.message
301 end
302 end
303
304 for i, res in ipairs(maps) do
305 local is_readable_map = has_uci_access(res.config, "read")
306 local is_writable_map = has_uci_access(res.config, "write")
307
308 writable = writable or is_writable_map
309
310 res:render({
311 firstmap = (i == 1),
312 redirect = redirect,
313 messages = messages,
314 pageaction = pageaction,
315 parsechain = parsechain,
316 readable = is_readable_map,
317 writable = is_writable_map
318 })
319 end
320
321 if not config.nofooter then
322 _G.L.include("cbi/footer", {
323 flow = config,
324 pageaction = pageaction,
325 redirect = redirect,
326 state = state,
327 autoapply = config.autoapply,
328 trigger_apply = applymap,
329 writable = writable
330 })
331 end
332 end
333
334 function invoke_form_action(model, ...)
335 local cbi = require "luci.cbi"
336 local tpl = require "luci.template"
337
338 local maps = luci.cbi.load(model, ...)
339 local state = nil
340
341 local i, res
342 for i, res in ipairs(maps) do
343 local cstate = res:parse()
344 if cstate and (not state or cstate < state) then
345 state = cstate
346 end
347 end
348
349 http:header("X-CBI-State", state or 0)
350 _G.L.include("header")
351 for i, res in ipairs(maps) do
352 res:render()
353 end
354 _G.L.include("footer")
355 end
356
357 function render_lua_template(path)
358 local tpl = require "luci.template"
359
360 tpl.render(path, getfenv(1))
361 end
362
363 function test_post_security()
364 if http:getenv("REQUEST_METHOD") ~= "POST" then
365 http:status(405, "Method Not Allowed")
366 http:header("Allow", "POST")
367 return false
368 end
369
370 if http:formvalue("token") ~= context.authtoken then
371 http:status(403, "Forbidden")
372 _G.L.include("csrftoken")
373 return false
374 end
375
376 return true
377 end
378
379
380 function call(name, ...)
381 return {
382 ["type"] = "call",
383 ["module"] = __controller,
384 ["function"] = name,
385 ["parameters"] = select('#', ...) > 0 and {...} or nil
386 }
387 end
388
389 function post(name, ...)
390 return {
391 ["type"] = "call",
392 ["module"] = __controller,
393 ["function"] = name,
394 ["parameters"] = select('#', ...) > 0 and {...} or nil,
395 ["post"] = true
396 }
397 end
398
399 function view(name)
400 return {
401 ["type"] = "view",
402 ["path"] = name
403 }
404 end
405
406 function template(name)
407 return {
408 ["type"] = "template",
409 ["path"] = name
410 }
411 end
412
413 function cbi(model, config)
414 return {
415 ["type"] = "call",
416 ["module"] = "luci.dispatcher",
417 ["function"] = "invoke_cbi_action",
418 ["parameters"] = { model, config or {} },
419 ["post"] = {
420 ["cbi.submit"] = true
421 }
422 }
423 end
424
425 function form(model)
426 return {
427 ["type"] = "call",
428 ["module"] = "luci.dispatcher",
429 ["function"] = "invoke_form_action",
430 ["parameters"] = { model },
431 ["post"] = {
432 ["cbi.submit"] = true
433 }
434 }
435 end
436
437 function firstchild()
438 return {
439 ["type"] = "firstchild"
440 }
441 end
442
443 function firstnode()
444 return {
445 ["type"] = "firstchild",
446 ["recurse"] = true
447 }
448 end
449
450 function arcombine(trg1, trg2)
451 return {
452 ["type"] = "arcombine",
453 ["targets"] = { trg1, trg2 } --,
454 --env = getfenv(),
455 }
456 end
457
458 function alias(...)
459 return {
460 ["type"] = "alias",
461 ["path"] = table.concat({ ... }, "/")
462 }
463 end
464
465 function rewrite(n, ...)
466 return {
467 ["type"] = "rewrite",
468 ["path"] = table.concat({ ... }, "/"),
469 ["remove"] = n
470 }
471 end
472
473
474 translate = i18n.translate
475
476 -- This function does not actually translate the given argument but
477 -- is used by build/i18n-scan.pl to find translatable entries.
478 function _(text)
479 return text
480 end