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