Merge pull request #1576 from stangri/luci-app-advanced-reboot
[project/luci.git] / modules / luci-base / 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 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
10
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
15 _M.fs = fs
16
17 -- Index table
18 local index = nil
19
20 -- Fastindex
21 local fi
22
23
24 function build_url(...)
25 local path = {...}
26 local url = { http.getenv("SCRIPT_NAME") or "" }
27
28 local p
29 for _, p in ipairs(path) do
30 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
31 url[#url+1] = "/"
32 url[#url+1] = p
33 end
34 end
35
36 if #path == 0 then
37 url[#url+1] = "/"
38 end
39
40 return table.concat(url, "")
41 end
42
43 function node_visible(node)
44 if node then
45 return not (
46 (not node.title or #node.title == 0) or
47 (not node.target or node.hidden == true) or
48 (type(node.target) == "table" and node.target.type == "firstchild" and
49 (type(node.nodes) ~= "table" or not next(node.nodes)))
50 )
51 end
52 return false
53 end
54
55 function node_childs(node)
56 local rv = { }
57 if node then
58 local k, v
59 for k, v in util.spairs(node.nodes,
60 function(a, b)
61 return (node.nodes[a].order or 100)
62 < (node.nodes[b].order or 100)
63 end)
64 do
65 if node_visible(v) then
66 rv[#rv+1] = k
67 end
68 end
69 end
70 return rv
71 end
72
73
74 function error404(message)
75 http.status(404, "Not Found")
76 message = message or "Not Found"
77
78 require("luci.template")
79 if not util.copcall(luci.template.render, "error404") then
80 http.prepare_content("text/plain")
81 http.write(message)
82 end
83 return false
84 end
85
86 function error500(message)
87 util.perror(message)
88 if not context.template_header_sent then
89 http.status(500, "Internal Server Error")
90 http.prepare_content("text/plain")
91 http.write(message)
92 else
93 require("luci.template")
94 if not util.copcall(luci.template.render, "error500", {message=message}) then
95 http.prepare_content("text/plain")
96 http.write(message)
97 end
98 end
99 return false
100 end
101
102 function httpdispatch(request, prefix)
103 http.context.request = request
104
105 local r = {}
106 context.request = r
107
108 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
109
110 if prefix then
111 for _, node in ipairs(prefix) do
112 r[#r+1] = node
113 end
114 end
115
116 for node in pathinfo:gmatch("[^/]+") do
117 r[#r+1] = node
118 end
119
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
122 end, error500)
123
124 http.close()
125
126 --context._disable_memtrace()
127 end
128
129 local function require_post_security(target)
130 if type(target) == "table" then
131 if type(target.post) == "table" then
132 local param_name, required_val, request_val
133
134 for param_name, required_val in pairs(target.post) do
135 request_val = http.formvalue(param_name)
136
137 if (type(required_val) == "string" and
138 request_val ~= required_val) or
139 (required_val == true and
140 (request_val == nil or request_val == ""))
141 then
142 return false
143 end
144 end
145
146 return true
147 end
148
149 return (target.post == true)
150 end
151
152 return false
153 end
154
155 function test_post_security()
156 if http.getenv("REQUEST_METHOD") ~= "POST" then
157 http.status(405, "Method Not Allowed")
158 http.header("Allow", "POST")
159 return false
160 end
161
162 if http.formvalue("token") ~= context.authtoken then
163 http.status(403, "Forbidden")
164 luci.template.render("csrftoken")
165 return false
166 end
167
168 return true
169 end
170
171 local function session_retrieve(sid, allowed_users)
172 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
173
174 if type(sdat) == "table" and
175 type(sdat.values) == "table" and
176 type(sdat.values.token) == "string" and
177 (not allowed_users or
178 util.contains(allowed_users, sdat.values.username))
179 then
180 return sid, sdat.values
181 end
182
183 return nil, nil
184 end
185
186 local function session_setup(user, pass, allowed_users)
187 if util.contains(allowed_users, user) then
188 local login = util.ubus("session", "login", {
189 username = user,
190 password = pass,
191 timeout = tonumber(luci.config.sauth.sessiontime)
192 })
193
194 local rp = context.requestpath
195 and table.concat(context.requestpath, "/") or ""
196
197 if type(login) == "table" and
198 type(login.ubus_rpc_session) == "string"
199 then
200 util.ubus("session", "set", {
201 ubus_rpc_session = login.ubus_rpc_session,
202 values = { token = sys.uniqueid(16) }
203 })
204
205 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
206 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
207
208 return session_retrieve(login.ubus_rpc_session)
209 end
210
211 io.stderr:write("luci: failed login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
213 end
214
215 return nil, nil
216 end
217
218 function dispatch(request)
219 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
220 local ctx = context
221 ctx.path = request
222
223 local conf = require "luci.config"
224 assert(conf.main,
225 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
226
227 local i18n = require "luci.i18n"
228 local lang = conf.main.lang or "auto"
229 if lang == "auto" then
230 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
231 for aclang in aclang:gmatch("[%w_-]+") do
232 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
233 if country and culture then
234 local cc = "%s_%s" %{ country, culture:lower() }
235 if conf.languages[cc] then
236 lang = cc
237 break
238 elseif conf.languages[country] then
239 lang = country
240 break
241 end
242 elseif conf.languages[aclang] then
243 lang = aclang
244 break
245 end
246 end
247 end
248 if lang == "auto" then
249 lang = i18n.default
250 end
251 i18n.setlanguage(lang)
252
253 local c = ctx.tree
254 local stat
255 if not c then
256 c = createtree()
257 end
258
259 local track = {}
260 local args = {}
261 ctx.args = args
262 ctx.requestargs = ctx.requestargs or args
263 local n
264 local preq = {}
265 local freq = {}
266
267 for i, s in ipairs(request) do
268 preq[#preq+1] = s
269 freq[#freq+1] = s
270 c = c.nodes[s]
271 n = i
272 if not c then
273 break
274 end
275
276 util.update(track, c)
277
278 if c.leaf then
279 break
280 end
281 end
282
283 if c and c.leaf then
284 for j=n+1, #request do
285 args[#args+1] = request[j]
286 freq[#freq+1] = request[j]
287 end
288 end
289
290 ctx.requestpath = ctx.requestpath or freq
291 ctx.path = preq
292
293 if track.i18n then
294 i18n.loadc(track.i18n)
295 end
296
297 -- Init template engine
298 if (c and c.index) or not track.notemplate then
299 local tpl = require("luci.template")
300 local media = track.mediaurlbase or luci.config.main.mediaurlbase
301 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
302 media = nil
303 for name, theme in pairs(luci.config.themes) do
304 if name:sub(1,1) ~= "." and pcall(tpl.Template,
305 "themes/%s/header" % fs.basename(theme)) then
306 media = theme
307 end
308 end
309 assert(media, "No valid theme found")
310 end
311
312 local function _ifattr(cond, key, val)
313 if cond then
314 local env = getfenv(3)
315 local scope = (type(env.self) == "table") and env.self
316 if type(val) == "table" then
317 if not next(val) then
318 return ''
319 else
320 val = util.serialize_json(val)
321 end
322 end
323 return string.format(
324 ' %s="%s"', tostring(key),
325 util.pcdata(tostring( val
326 or (type(env[key]) ~= "function" and env[key])
327 or (scope and type(scope[key]) ~= "function" and scope[key])
328 or "" ))
329 )
330 else
331 return ''
332 end
333 end
334
335 tpl.context.viewns = setmetatable({
336 write = http.write;
337 include = function(name) tpl.Template(name):render(getfenv(2)) end;
338 translate = i18n.translate;
339 translatef = i18n.translatef;
340 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
341 striptags = util.striptags;
342 pcdata = util.pcdata;
343 media = media;
344 theme = fs.basename(media);
345 resource = luci.config.main.resourcebase;
346 ifattr = function(...) return _ifattr(...) end;
347 attr = function(...) return _ifattr(true, ...) end;
348 url = build_url;
349 }, {__index=function(table, key)
350 if key == "controller" then
351 return build_url()
352 elseif key == "REQUEST_URI" then
353 return build_url(unpack(ctx.requestpath))
354 elseif key == "token" then
355 return ctx.authtoken
356 else
357 return rawget(table, key) or _G[key]
358 end
359 end})
360 end
361
362 track.dependent = (track.dependent ~= false)
363 assert(not track.dependent or not track.auto,
364 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
365 "has no parent node so the access to this location has been denied.\n" ..
366 "This is a software bug, please report this message at " ..
367 "https://github.com/openwrt/luci/issues"
368 )
369
370 if track.sysauth and not ctx.authsession then
371 local authen = track.sysauth_authenticator
372 local _, sid, sdat, default_user, allowed_users
373
374 if type(authen) == "string" and authen ~= "htmlauth" then
375 error500("Unsupported authenticator %q configured" % authen)
376 return
377 end
378
379 if type(track.sysauth) == "table" then
380 default_user, allowed_users = nil, track.sysauth
381 else
382 default_user, allowed_users = track.sysauth, { track.sysauth }
383 end
384
385 if type(authen) == "function" then
386 _, sid = authen(sys.user.checkpasswd, allowed_users)
387 else
388 sid = http.getcookie("sysauth")
389 end
390
391 sid, sdat = session_retrieve(sid, allowed_users)
392
393 if not (sid and sdat) and authen == "htmlauth" then
394 local user = http.getenv("HTTP_AUTH_USER")
395 local pass = http.getenv("HTTP_AUTH_PASS")
396
397 if user == nil and pass == nil then
398 user = http.formvalue("luci_username")
399 pass = http.formvalue("luci_password")
400 end
401
402 sid, sdat = session_setup(user, pass, allowed_users)
403
404 if not sid then
405 local tmpl = require "luci.template"
406
407 context.path = {}
408
409 http.status(403, "Forbidden")
410 tmpl.render(track.sysauth_template or "sysauth", {
411 duser = default_user,
412 fuser = user
413 })
414
415 return
416 end
417
418 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
419 http.redirect(build_url(unpack(ctx.requestpath)))
420 end
421
422 if not sid or not sdat then
423 http.status(403, "Forbidden")
424 return
425 end
426
427 ctx.authsession = sid
428 ctx.authtoken = sdat.token
429 ctx.authuser = sdat.username
430 end
431
432 if c and require_post_security(c.target) then
433 if not test_post_security(c) then
434 return
435 end
436 end
437
438 if track.setgroup then
439 sys.process.setgroup(track.setgroup)
440 end
441
442 if track.setuser then
443 sys.process.setuser(track.setuser)
444 end
445
446 local target = nil
447 if c then
448 if type(c.target) == "function" then
449 target = c.target
450 elseif type(c.target) == "table" then
451 target = c.target.target
452 end
453 end
454
455 if c and (c.index or type(target) == "function") then
456 ctx.dispatched = c
457 ctx.requested = ctx.requested or ctx.dispatched
458 end
459
460 if c and c.index then
461 local tpl = require "luci.template"
462
463 if util.copcall(tpl.render, "indexer", {}) then
464 return true
465 end
466 end
467
468 if type(target) == "function" then
469 util.copcall(function()
470 local oldenv = getfenv(target)
471 local module = require(c.module)
472 local env = setmetatable({}, {__index=
473
474 function(tbl, key)
475 return rawget(tbl, key) or module[key] or oldenv[key]
476 end})
477
478 setfenv(target, env)
479 end)
480
481 local ok, err
482 if type(c.target) == "table" then
483 ok, err = util.copcall(target, c.target, unpack(args))
484 else
485 ok, err = util.copcall(target, unpack(args))
486 end
487 assert(ok,
488 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
489 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
490 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
491 else
492 local root = node()
493 if not root or not root.target then
494 error404("No root node was registered, this usually happens if no module was installed.\n" ..
495 "Install luci-mod-admin-full and retry. " ..
496 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
497 else
498 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
499 "If this url belongs to an extension, make sure it is properly installed.\n" ..
500 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
501 end
502 end
503 end
504
505 function createindex()
506 local controllers = { }
507 local base = "%s/controller/" % util.libpath()
508 local _, path
509
510 for path in (fs.glob("%s*.lua" % base) or function() end) do
511 controllers[#controllers+1] = path
512 end
513
514 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
515 controllers[#controllers+1] = path
516 end
517
518 if indexcache then
519 local cachedate = fs.stat(indexcache, "mtime")
520 if cachedate then
521 local realdate = 0
522 for _, obj in ipairs(controllers) do
523 local omtime = fs.stat(obj, "mtime")
524 realdate = (omtime and omtime > realdate) and omtime or realdate
525 end
526
527 if cachedate > realdate and sys.process.info("uid") == 0 then
528 assert(
529 sys.process.info("uid") == fs.stat(indexcache, "uid")
530 and fs.stat(indexcache, "modestr") == "rw-------",
531 "Fatal: Indexcache is not sane!"
532 )
533
534 index = loadfile(indexcache)()
535 return index
536 end
537 end
538 end
539
540 index = {}
541
542 for _, path in ipairs(controllers) do
543 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
544 local mod = require(modname)
545 assert(mod ~= true,
546 "Invalid controller file found\n" ..
547 "The file '" .. path .. "' contains an invalid module line.\n" ..
548 "Please verify whether the module name is set to '" .. modname ..
549 "' - It must correspond to the file path!")
550
551 local idx = mod.index
552 assert(type(idx) == "function",
553 "Invalid controller file found\n" ..
554 "The file '" .. path .. "' contains no index() function.\n" ..
555 "Please make sure that the controller contains a valid " ..
556 "index function and verify the spelling!")
557
558 index[modname] = idx
559 end
560
561 if indexcache then
562 local f = nixio.open(indexcache, "w", 600)
563 f:writeall(util.get_bytecode(index))
564 f:close()
565 end
566 end
567
568 -- Build the index before if it does not exist yet.
569 function createtree()
570 if not index then
571 createindex()
572 end
573
574 local ctx = context
575 local tree = {nodes={}, inreq=true}
576 local modi = {}
577
578 ctx.treecache = setmetatable({}, {__mode="v"})
579 ctx.tree = tree
580 ctx.modifiers = modi
581
582 -- Load default translation
583 require "luci.i18n".loadc("base")
584
585 local scope = setmetatable({}, {__index = luci.dispatcher})
586
587 for k, v in pairs(index) do
588 scope._NAME = k
589 setfenv(v, scope)
590 v()
591 end
592
593 local function modisort(a,b)
594 return modi[a].order < modi[b].order
595 end
596
597 for _, v in util.spairs(modi, modisort) do
598 scope._NAME = v.module
599 setfenv(v.func, scope)
600 v.func()
601 end
602
603 return tree
604 end
605
606 function modifier(func, order)
607 context.modifiers[#context.modifiers+1] = {
608 func = func,
609 order = order or 0,
610 module
611 = getfenv(2)._NAME
612 }
613 end
614
615 function assign(path, clone, title, order)
616 local obj = node(unpack(path))
617 obj.nodes = nil
618 obj.module = nil
619
620 obj.title = title
621 obj.order = order
622
623 setmetatable(obj, {__index = _create_node(clone)})
624
625 return obj
626 end
627
628 function entry(path, target, title, order)
629 local c = node(unpack(path))
630
631 c.target = target
632 c.title = title
633 c.order = order
634 c.module = getfenv(2)._NAME
635
636 return c
637 end
638
639 -- enabling the node.
640 function get(...)
641 return _create_node({...})
642 end
643
644 function node(...)
645 local c = _create_node({...})
646
647 c.module = getfenv(2)._NAME
648 c.auto = nil
649
650 return c
651 end
652
653 function _create_node(path)
654 if #path == 0 then
655 return context.tree
656 end
657
658 local name = table.concat(path, ".")
659 local c = context.treecache[name]
660
661 if not c then
662 local last = table.remove(path)
663 local parent = _create_node(path)
664
665 c = {nodes={}, auto=true}
666 -- the node is "in request" if the request path matches
667 -- at least up to the length of the node path
668 if parent.inreq and context.path[#path+1] == last then
669 c.inreq = true
670 end
671 parent.nodes[last] = c
672 context.treecache[name] = c
673 end
674 return c
675 end
676
677 -- Subdispatchers --
678
679 function _firstchild()
680 local path = { unpack(context.path) }
681 local name = table.concat(path, ".")
682 local node = context.treecache[name]
683
684 local lowest
685 if node and node.nodes and next(node.nodes) then
686 local k, v
687 for k, v in pairs(node.nodes) do
688 if not lowest or
689 (v.order or 100) < (node.nodes[lowest].order or 100)
690 then
691 lowest = k
692 end
693 end
694 end
695
696 assert(lowest ~= nil,
697 "The requested node contains no childs, unable to redispatch")
698
699 path[#path+1] = lowest
700 dispatch(path)
701 end
702
703 function firstchild()
704 return { type = "firstchild", target = _firstchild }
705 end
706
707 function alias(...)
708 local req = {...}
709 return function(...)
710 for _, r in ipairs({...}) do
711 req[#req+1] = r
712 end
713
714 dispatch(req)
715 end
716 end
717
718 function rewrite(n, ...)
719 local req = {...}
720 return function(...)
721 local dispatched = util.clone(context.dispatched)
722
723 for i=1,n do
724 table.remove(dispatched, 1)
725 end
726
727 for i, r in ipairs(req) do
728 table.insert(dispatched, i, r)
729 end
730
731 for _, r in ipairs({...}) do
732 dispatched[#dispatched+1] = r
733 end
734
735 dispatch(dispatched)
736 end
737 end
738
739
740 local function _call(self, ...)
741 local func = getfenv()[self.name]
742 assert(func ~= nil,
743 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
744
745 assert(type(func) == "function",
746 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
747 'of type "' .. type(func) .. '".')
748
749 if #self.argv > 0 then
750 return func(unpack(self.argv), ...)
751 else
752 return func(...)
753 end
754 end
755
756 function call(name, ...)
757 return {type = "call", argv = {...}, name = name, target = _call}
758 end
759
760 function post_on(params, name, ...)
761 return {
762 type = "call",
763 post = params,
764 argv = { ... },
765 name = name,
766 target = _call
767 }
768 end
769
770 function post(...)
771 return post_on(true, ...)
772 end
773
774
775 local _template = function(self, ...)
776 require "luci.template".render(self.view)
777 end
778
779 function template(name)
780 return {type = "template", view = name, target = _template}
781 end
782
783
784 local function _cbi(self, ...)
785 local cbi = require "luci.cbi"
786 local tpl = require "luci.template"
787 local http = require "luci.http"
788
789 local config = self.config or {}
790 local maps = cbi.load(self.model, ...)
791
792 local state = nil
793
794 for i, res in ipairs(maps) do
795 res.flow = config
796 local cstate = res:parse()
797 if cstate and (not state or cstate < state) then
798 state = cstate
799 end
800 end
801
802 local function _resolve_path(path)
803 return type(path) == "table" and build_url(unpack(path)) or path
804 end
805
806 if config.on_valid_to and state and state > 0 and state < 2 then
807 http.redirect(_resolve_path(config.on_valid_to))
808 return
809 end
810
811 if config.on_changed_to and state and state > 1 then
812 http.redirect(_resolve_path(config.on_changed_to))
813 return
814 end
815
816 if config.on_success_to and state and state > 0 then
817 http.redirect(_resolve_path(config.on_success_to))
818 return
819 end
820
821 if config.state_handler then
822 if not config.state_handler(state, maps) then
823 return
824 end
825 end
826
827 http.header("X-CBI-State", state or 0)
828
829 if not config.noheader then
830 tpl.render("cbi/header", {state = state})
831 end
832
833 local redirect
834 local messages
835 local applymap = false
836 local pageaction = true
837 local parsechain = { }
838
839 for i, res in ipairs(maps) do
840 if res.apply_needed and res.parsechain then
841 local c
842 for _, c in ipairs(res.parsechain) do
843 parsechain[#parsechain+1] = c
844 end
845 applymap = true
846 end
847
848 if res.redirect then
849 redirect = redirect or res.redirect
850 end
851
852 if res.pageaction == false then
853 pageaction = false
854 end
855
856 if res.message then
857 messages = messages or { }
858 messages[#messages+1] = res.message
859 end
860 end
861
862 for i, res in ipairs(maps) do
863 res:render({
864 firstmap = (i == 1),
865 applymap = applymap,
866 redirect = redirect,
867 messages = messages,
868 pageaction = pageaction,
869 parsechain = parsechain
870 })
871 end
872
873 if not config.nofooter then
874 tpl.render("cbi/footer", {
875 flow = config,
876 pageaction = pageaction,
877 redirect = redirect,
878 state = state,
879 autoapply = config.autoapply
880 })
881 end
882 end
883
884 function cbi(model, config)
885 return {
886 type = "cbi",
887 post = { ["cbi.submit"] = "1" },
888 config = config,
889 model = model,
890 target = _cbi
891 }
892 end
893
894
895 local function _arcombine(self, ...)
896 local argv = {...}
897 local target = #argv > 0 and self.targets[2] or self.targets[1]
898 setfenv(target.target, self.env)
899 target:target(unpack(argv))
900 end
901
902 function arcombine(trg1, trg2)
903 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
904 end
905
906
907 local function _form(self, ...)
908 local cbi = require "luci.cbi"
909 local tpl = require "luci.template"
910 local http = require "luci.http"
911
912 local maps = luci.cbi.load(self.model, ...)
913 local state = nil
914
915 for i, res in ipairs(maps) do
916 local cstate = res:parse()
917 if cstate and (not state or cstate < state) then
918 state = cstate
919 end
920 end
921
922 http.header("X-CBI-State", state or 0)
923 tpl.render("header")
924 for i, res in ipairs(maps) do
925 res:render()
926 end
927 tpl.render("footer")
928 end
929
930 function form(model)
931 return {
932 type = "cbi",
933 post = { ["cbi.submit"] = "1" },
934 model = model,
935 target = _form
936 }
937 end
938
939 translate = i18n.translate
940
941 -- This function does not actually translate the given argument but
942 -- is used by build/i18n-scan.pl to find translatable entries.
943 function _(text)
944 return text
945 end