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