luci-base: dispatcher: rework dispatching and menu filtering logic
[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 xml = require "luci.xml"
9 local http = require "luci.http"
10 local nixio = require "nixio", require "nixio.util"
11
12 module("luci.dispatcher", package.seeall)
13 context = util.threadlocal()
14 uci = require "luci.model.uci"
15 i18n = require "luci.i18n"
16 _M.fs = fs
17
18 -- Index table
19 local index = nil
20
21 local function check_fs_depends(spec)
22 local fs = require "nixio.fs"
23
24 for path, kind in pairs(spec) do
25 if kind == "directory" then
26 local empty = true
27 for entry in (fs.dir(path) or function() end) do
28 empty = false
29 break
30 end
31 if empty then
32 return false
33 end
34 elseif kind == "executable" then
35 if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then
36 return false
37 end
38 elseif kind == "file" then
39 if fs.stat(path, "type") ~= "reg" then
40 return false
41 end
42 end
43 end
44
45 return true
46 end
47
48 local function check_uci_depends_options(conf, s, opts)
49 local uci = require "luci.model.uci"
50
51 if type(opts) == "string" then
52 return (s[".type"] == opts)
53 elseif opts == true then
54 for option, value in pairs(s) do
55 if option:byte(1) ~= 46 then
56 return true
57 end
58 end
59 elseif type(opts) == "table" then
60 for option, value in pairs(opts) do
61 local sval = s[option]
62 if type(sval) == "table" then
63 local found = false
64 for _, v in ipairs(sval) do
65 if v == value then
66 found = true
67 break
68 end
69 end
70 if not found then
71 return false
72 end
73 elseif value == true then
74 if sval == nil then
75 return false
76 end
77 else
78 if sval ~= value then
79 return false
80 end
81 end
82 end
83 end
84
85 return true
86 end
87
88 local function check_uci_depends_section(conf, sect)
89 local uci = require "luci.model.uci"
90
91 for section, options in pairs(sect) do
92 local stype = section:match("^@([A-Za-z0-9_%-]+)$")
93 if stype then
94 local found = false
95 uci:foreach(conf, stype, function(s)
96 if check_uci_depends_options(conf, s, options) then
97 found = true
98 return false
99 end
100 end)
101 if not found then
102 return false
103 end
104 else
105 local s = uci:get_all(conf, section)
106 if not s or not check_uci_depends_options(conf, s, options) then
107 return false
108 end
109 end
110 end
111
112 return true
113 end
114
115 local function check_uci_depends(conf)
116 local uci = require "luci.model.uci"
117
118 for config, values in pairs(conf) do
119 if values == true then
120 local found = false
121 uci:foreach(config, nil, function(s)
122 found = true
123 return false
124 end)
125 if not found then
126 return false
127 end
128 elseif type(values) == "table" then
129 if not check_uci_depends_section(config, values) then
130 return false
131 end
132 end
133 end
134
135 return true
136 end
137
138 local function check_acl_depends(require_groups, groups)
139 if type(require_groups) == "table" and #require_groups > 0 then
140 local writable = false
141
142 for _, group in ipairs(require_groups) do
143 local read = false
144 local write = false
145 if type(groups) == "table" and type(groups[group]) == "table" then
146 for _, perm in ipairs(groups[group]) do
147 if perm == "read" then
148 read = true
149 elseif perm == "write" then
150 write = true
151 end
152 end
153 end
154 if not read and not write then
155 return nil
156 elseif write then
157 writable = true
158 end
159 end
160
161 return writable
162 end
163
164 return true
165 end
166
167 local function check_depends(spec)
168 if type(spec.depends) ~= "table" then
169 return true
170 end
171
172 if type(spec.depends.fs) == "table" then
173 local satisfied = false
174 local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs }
175 for _, alternative in ipairs(alternatives) do
176 if check_fs_depends(alternative) then
177 satisfied = true
178 break
179 end
180 end
181 if not satisfied then
182 return false
183 end
184 end
185
186 if type(spec.depends.uci) == "table" then
187 local satisfied = false
188 local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci }
189 for _, alternative in ipairs(alternatives) do
190 if check_uci_depends(alternative) then
191 satisfied = true
192 break
193 end
194 end
195 if not satisfied then
196 return false
197 end
198 end
199
200 return true
201 end
202
203 local function target_to_json(target, module)
204 local action
205
206 if target.type == "call" then
207 action = {
208 ["type"] = "call",
209 ["module"] = module,
210 ["function"] = target.name,
211 ["parameters"] = target.argv
212 }
213 elseif target.type == "view" then
214 action = {
215 ["type"] = "view",
216 ["path"] = target.view
217 }
218 elseif target.type == "template" then
219 action = {
220 ["type"] = "template",
221 ["path"] = target.view
222 }
223 elseif target.type == "cbi" then
224 action = {
225 ["type"] = "cbi",
226 ["path"] = target.model,
227 ["config"] = target.config
228 }
229 elseif target.type == "form" then
230 action = {
231 ["type"] = "form",
232 ["path"] = target.model
233 }
234 elseif target.type == "firstchild" then
235 action = {
236 ["type"] = "firstchild"
237 }
238 elseif target.type == "firstnode" then
239 action = {
240 ["type"] = "firstchild",
241 ["recurse"] = true
242 }
243 elseif target.type == "arcombine" then
244 if type(target.targets) == "table" then
245 action = {
246 ["type"] = "arcombine",
247 ["targets"] = {
248 target_to_json(target.targets[1], module),
249 target_to_json(target.targets[2], module)
250 }
251 }
252 end
253 elseif target.type == "alias" then
254 action = {
255 ["type"] = "alias",
256 ["path"] = table.concat(target.req, "/")
257 }
258 elseif target.type == "rewrite" then
259 action = {
260 ["type"] = "rewrite",
261 ["path"] = table.concat(target.req, "/"),
262 ["remove"] = target.n
263 }
264 end
265
266 if target.post and action then
267 action.post = target.post
268 end
269
270 return action
271 end
272
273 local function tree_to_json(node, json)
274 local fs = require "nixio.fs"
275 local util = require "luci.util"
276
277 if type(node.nodes) == "table" then
278 for subname, subnode in pairs(node.nodes) do
279 local spec = {
280 title = xml.striptags(subnode.title),
281 order = subnode.order
282 }
283
284 if subnode.leaf then
285 spec.wildcard = true
286 end
287
288 if subnode.cors then
289 spec.cors = true
290 end
291
292 if subnode.setuser then
293 spec.setuser = subnode.setuser
294 end
295
296 if subnode.setgroup then
297 spec.setgroup = subnode.setgroup
298 end
299
300 if type(subnode.target) == "table" then
301 spec.action = target_to_json(subnode.target, subnode.module)
302 end
303
304 if type(subnode.file_depends) == "table" then
305 for _, v in ipairs(subnode.file_depends) do
306 spec.depends = spec.depends or {}
307 spec.depends.fs = spec.depends.fs or {}
308
309 local ft = fs.stat(v, "type")
310 if ft == "dir" then
311 spec.depends.fs[v] = "directory"
312 elseif v:match("/s?bin/") then
313 spec.depends.fs[v] = "executable"
314 else
315 spec.depends.fs[v] = "file"
316 end
317 end
318 end
319
320 if type(subnode.uci_depends) == "table" then
321 for k, v in pairs(subnode.uci_depends) do
322 spec.depends = spec.depends or {}
323 spec.depends.uci = spec.depends.uci or {}
324 spec.depends.uci[k] = v
325 end
326 end
327
328 if type(subnode.acl_depends) == "table" then
329 for _, acl in ipairs(subnode.acl_depends) do
330 spec.depends = spec.depends or {}
331 spec.depends.acl = spec.depends.acl or {}
332 spec.depends.acl[#spec.depends.acl + 1] = acl
333 end
334 end
335
336 if (subnode.sysauth_authenticator ~= nil) or
337 (subnode.sysauth ~= nil and subnode.sysauth ~= false)
338 then
339 if subnode.sysauth_authenticator == "htmlauth" then
340 spec.auth = {
341 login = true,
342 methods = { "cookie:sysauth" }
343 }
344 elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then
345 spec.auth = {
346 login = false,
347 methods = { "query:auth", "cookie:sysauth" }
348 }
349 elseif subnode.module == "luci.controller.admin.uci" then
350 spec.auth = {
351 login = false,
352 methods = { "param:sid" }
353 }
354 end
355 elseif subnode.sysauth == false then
356 spec.auth = {}
357 end
358
359 if not spec.action then
360 spec.title = nil
361 end
362
363 spec.satisfied = check_depends(spec)
364 json.children = json.children or {}
365 json.children[subname] = tree_to_json(subnode, spec)
366 end
367 end
368
369 return json
370 end
371
372 function build_url(...)
373 local path = {...}
374 local url = { http.getenv("SCRIPT_NAME") or "" }
375
376 local p
377 for _, p in ipairs(path) do
378 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
379 url[#url+1] = "/"
380 url[#url+1] = p
381 end
382 end
383
384 if #path == 0 then
385 url[#url+1] = "/"
386 end
387
388 return table.concat(url, "")
389 end
390
391
392 function error404(message)
393 http.status(404, "Not Found")
394 message = message or "Not Found"
395
396 local function render()
397 local template = require "luci.template"
398 template.render("error404", {message=message})
399 end
400
401 if not util.copcall(render) then
402 http.prepare_content("text/plain")
403 http.write(message)
404 end
405
406 return false
407 end
408
409 function error500(message)
410 util.perror(message)
411 if not context.template_header_sent then
412 http.status(500, "Internal Server Error")
413 http.prepare_content("text/plain")
414 http.write(message)
415 else
416 require("luci.template")
417 if not util.copcall(luci.template.render, "error500", {message=message}) then
418 http.prepare_content("text/plain")
419 http.write(message)
420 end
421 end
422 return false
423 end
424
425 local function determine_request_language()
426 local conf = require "luci.config"
427 assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'")
428
429 local lang = conf.main.lang or "auto"
430 if lang == "auto" then
431 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
432 for aclang in aclang:gmatch("[%w_-]+") do
433 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
434 if country and culture then
435 local cc = "%s_%s" %{ country, culture:lower() }
436 if conf.languages[cc] then
437 lang = cc
438 break
439 elseif conf.languages[country] then
440 lang = country
441 break
442 end
443 elseif conf.languages[aclang] then
444 lang = aclang
445 break
446 end
447 end
448 end
449
450 if lang == "auto" then
451 lang = i18n.default
452 end
453
454 i18n.setlanguage(lang)
455 end
456
457 function httpdispatch(request, prefix)
458 http.context.request = request
459
460 local r = {}
461 context.request = r
462
463 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
464
465 if prefix then
466 for _, node in ipairs(prefix) do
467 r[#r+1] = node
468 end
469 end
470
471 local node
472 for node in pathinfo:gmatch("[^/%z]+") do
473 r[#r+1] = node
474 end
475
476 determine_request_language()
477
478 local stat, err = util.coxpcall(function()
479 dispatch(context.request)
480 end, error500)
481
482 http.close()
483
484 --context._disable_memtrace()
485 end
486
487 local function require_post_security(target, args)
488 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
489 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
490 end
491
492 if type(target) == "table" then
493 if type(target.post) == "table" then
494 local param_name, required_val, request_val
495
496 for param_name, required_val in pairs(target.post) do
497 request_val = http.formvalue(param_name)
498
499 if (type(required_val) == "string" and
500 request_val ~= required_val) or
501 (required_val == true and request_val == nil)
502 then
503 return false
504 end
505 end
506
507 return true
508 end
509
510 return (target.post == true)
511 end
512
513 return false
514 end
515
516 function test_post_security()
517 if http.getenv("REQUEST_METHOD") ~= "POST" then
518 http.status(405, "Method Not Allowed")
519 http.header("Allow", "POST")
520 return false
521 end
522
523 if http.formvalue("token") ~= context.authtoken then
524 http.status(403, "Forbidden")
525 luci.template.render("csrftoken")
526 return false
527 end
528
529 return true
530 end
531
532 local function session_retrieve(sid, allowed_users)
533 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
534 local sacl = util.ubus("session", "access", { ubus_rpc_session = sid })
535
536 if type(sdat) == "table" and
537 type(sdat.values) == "table" and
538 type(sdat.values.token) == "string" and
539 (not allowed_users or
540 util.contains(allowed_users, sdat.values.username))
541 then
542 uci:set_session_id(sid)
543 return sid, sdat.values, type(sacl) == "table" and sacl or {}
544 end
545
546 return nil, nil, nil
547 end
548
549 local function session_setup(user, pass)
550 local login = util.ubus("session", "login", {
551 username = user,
552 password = pass,
553 timeout = tonumber(luci.config.sauth.sessiontime)
554 })
555
556 local rp = context.requestpath
557 and table.concat(context.requestpath, "/") or ""
558
559 if type(login) == "table" and
560 type(login.ubus_rpc_session) == "string"
561 then
562 util.ubus("session", "set", {
563 ubus_rpc_session = login.ubus_rpc_session,
564 values = { token = sys.uniqueid(16) }
565 })
566 nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n"
567 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
568
569 return session_retrieve(login.ubus_rpc_session)
570 end
571 nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n"
572 %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" }))
573 end
574
575 local function check_authentication(method)
576 local auth_type, auth_param = method:match("^(%w+):(.+)$")
577 local sid, sdat
578
579 if auth_type == "cookie" then
580 sid = http.getcookie(auth_param)
581 elseif auth_type == "param" then
582 sid = http.formvalue(auth_param)
583 elseif auth_type == "query" then
584 sid = http.formvalue(auth_param, true)
585 end
586
587 return session_retrieve(sid)
588 end
589
590 local function merge_trees(node_a, node_b)
591 for k, v in pairs(node_b) do
592 if k == "children" then
593 node_a.children = node_a.children or {}
594
595 for name, spec in pairs(v) do
596 node_a.children[name] = merge_trees(node_a.children[name] or {}, spec)
597 end
598 else
599 node_a[k] = v
600 end
601 end
602
603 if type(node_a.action) == "table" and
604 node_a.action.type == "firstchild" and
605 node_a.children == nil
606 then
607 node_a.satisfied = false
608 end
609
610 return node_a
611 end
612
613 local function apply_tree_acls(node, acl)
614 if type(node.children) == "table" then
615 for _, child in pairs(node.children) do
616 apply_tree_acls(child, acl)
617 end
618 end
619
620 local perm
621 if type(node.depends) == "table" then
622 perm = check_acl_depends(node.depends.acl, acl["access-group"])
623 else
624 perm = true
625 end
626
627 if perm == nil then
628 node.satisfied = false
629 elseif perm == false then
630 node.readonly = true
631 end
632 end
633
634 function menu_json(acl)
635 local tree = context.tree or createtree()
636 local lua_tree = tree_to_json(tree, {
637 action = {
638 ["type"] = "firstchild",
639 ["recurse"] = true
640 }
641 })
642
643 local json_tree = createtree_json()
644 local menu_tree = merge_trees(lua_tree, json_tree)
645
646 if acl then
647 apply_tree_acls(menu_tree, acl)
648 end
649
650 return menu_tree
651 end
652
653 local function init_template_engine(ctx)
654 local tpl = require "luci.template"
655 local media = luci.config.main.mediaurlbase
656
657 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
658 media = nil
659 for name, theme in pairs(luci.config.themes) do
660 if name:sub(1,1) ~= "." and pcall(tpl.Template,
661 "themes/%s/header" % fs.basename(theme)) then
662 media = theme
663 end
664 end
665 assert(media, "No valid theme found")
666 end
667
668 local function _ifattr(cond, key, val, noescape)
669 if cond then
670 local env = getfenv(3)
671 local scope = (type(env.self) == "table") and env.self
672 if type(val) == "table" then
673 if not next(val) then
674 return ''
675 else
676 val = util.serialize_json(val)
677 end
678 end
679
680 val = tostring(val or
681 (type(env[key]) ~= "function" and env[key]) or
682 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
683
684 if noescape ~= true then
685 val = xml.pcdata(val)
686 end
687
688 return string.format(' %s="%s"', tostring(key), val)
689 else
690 return ''
691 end
692 end
693
694 tpl.context.viewns = setmetatable({
695 write = http.write;
696 include = function(name) tpl.Template(name):render(getfenv(2)) end;
697 translate = i18n.translate;
698 translatef = i18n.translatef;
699 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
700 striptags = xml.striptags;
701 pcdata = xml.pcdata;
702 media = media;
703 theme = fs.basename(media);
704 resource = luci.config.main.resourcebase;
705 ifattr = function(...) return _ifattr(...) end;
706 attr = function(...) return _ifattr(true, ...) end;
707 url = build_url;
708 }, {__index=function(tbl, key)
709 if key == "controller" then
710 return build_url()
711 elseif key == "REQUEST_URI" then
712 return build_url(unpack(ctx.requestpath))
713 elseif key == "FULL_REQUEST_URI" then
714 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
715 local query = http.getenv("QUERY_STRING")
716 if query and #query > 0 then
717 url[#url+1] = "?"
718 url[#url+1] = query
719 end
720 return table.concat(url, "")
721 elseif key == "token" then
722 return ctx.authtoken
723 else
724 return rawget(tbl, key) or _G[key]
725 end
726 end})
727
728 return tpl
729 end
730
731 local function is_authenticated(auth)
732 if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then
733 local sid, sdat, sacl
734 for _, method in ipairs(auth.methods) do
735 sid, sdat, sacl = check_authentication(method)
736
737 if sid and sdat and sacl then
738 return sid, sdat, sacl
739 end
740 end
741 end
742 end
743
744 local function ctx_append(ctx, name, node)
745 ctx.path = ctx.path or {}
746 ctx.path[#ctx.path + 1] = name
747
748 ctx.acls = ctx.acls or {}
749
750 local acls = (type(node.depends) == "table" and type(node.depends.acl) == "table") and node.depends.acl or {}
751 for _, acl in ipairs(acls) do
752 ctx.acls[_] = acl
753 end
754
755 ctx.auth = node.auth or ctx.auth
756 ctx.cors = node.cors or ctx.cors
757 ctx.suid = node.setuser or ctx.suid
758 ctx.sgid = node.setgroup or ctx.sgid
759
760 return ctx
761 end
762
763 local function node_weight(node)
764 local weight = node.order or 9999
765
766 if weight > 9999 then
767 weight = 9999
768 end
769
770 if type(node.auth) == "table" and node.auth.login then
771 weight = weight + 10000
772 end
773
774 return weight
775 end
776
777 local function resolve_firstchild(node, sacl, login_allowed, ctx)
778 local candidate = nil
779 local candidate_ctx = nil
780
781 for name, child in pairs(node.children) do
782 if child.satisfied then
783 if not sacl then
784 local _
785 _, _, sacl = is_authenticated(node.auth)
786 end
787
788 local cacl = (type(child.depends) == "table") and child.depends.acl or nil
789 local login = login_allowed or (type(child.auth) == "table" and child.auth.login)
790 if login or check_acl_depends(cacl, sacl and sacl["access-group"]) ~= nil then
791 if child.title and type(child.action) == "table" then
792 local child_ctx = ctx_append(util.clone(ctx, true), name, child)
793 if child.action.type == "firstchild" then
794 if not candidate or node_weight(candidate) > node_weight(child) then
795 local have_grandchild = resolve_firstchild(child, sacl, login, child_ctx)
796 if have_grandchild then
797 candidate = child
798 candidate_ctx = child_ctx
799 end
800 end
801 elseif not child.firstchild_ineligible then
802 if not candidate or node_weight(candidate) > node_weight(child) then
803 candidate = child
804 candidate_ctx = child_ctx
805 end
806 end
807 end
808 end
809 end
810 end
811
812 if candidate then
813 for k, v in pairs(candidate_ctx) do
814 ctx[k] = v
815 end
816
817 return true
818 end
819
820 return false
821 end
822
823 local function resolve_page(tree, request_path)
824 local node = tree
825 local sacl = nil
826 local login = false
827 local ctx = {}
828
829 for i, s in ipairs(request_path) do
830 node = node.children and node.children[s]
831
832 if not node or not node.satisfied then
833 break
834 end
835
836 ctx_append(ctx, s, node)
837
838 if not sacl then
839 local _
840 _, _, sacl = is_authenticated(node.auth)
841 end
842
843 if not login and type(node.auth) == "table" and node.auth.login then
844 login = true
845 end
846
847 if node.wildcard then
848 ctx.request_args = {}
849 ctx.request_path = util.clone(ctx.path, true)
850
851 for j = i + 1, #request_path do
852 ctx.request_path[j] = request_path[j]
853 ctx.request_args[j - i] = request_path[j]
854 end
855
856 break
857 end
858 end
859
860 if node and type(node.action) == "table" and node.action.type == "firstchild" then
861 resolve_firstchild(node, sacl, login, ctx)
862 end
863
864 ctx.acls = ctx.acls or {}
865 ctx.path = ctx.path or {}
866 ctx.request_args = ctx.request_args or {}
867 ctx.request_path = ctx.request_path or util.clone(request_path, true)
868
869 node = tree
870
871 for _, s in ipairs(ctx.path or {}) do
872 node = node.children[s]
873 assert(node, "Internal node resolve error")
874 end
875
876 return node, ctx
877 end
878
879 function dispatch(request)
880 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
881 local ctx = context
882
883 local auth, cors, suid, sgid
884 local menu = menu_json()
885 local page, lookup_ctx = resolve_page(menu, request)
886 local action = (page and type(page.action) == "table") and page.action or {}
887
888 local tpl = init_template_engine(ctx)
889
890 ctx.args = lookup_ctx.request_args
891 ctx.path = lookup_ctx.path
892 ctx.dispatched = page
893
894 ctx.requestpath = ctx.requestpath or lookup_ctx.request_path
895 ctx.requestargs = ctx.requestargs or lookup_ctx.request_args
896 ctx.requested = ctx.requested or page
897
898 if type(lookup_ctx.auth) == "table" and next(lookup_ctx.auth) then
899 local sid, sdat, sacl = is_authenticated(lookup_ctx.auth)
900
901 if not (sid and sdat and sacl) and lookup_ctx.auth.login then
902 local user = http.getenv("HTTP_AUTH_USER")
903 local pass = http.getenv("HTTP_AUTH_PASS")
904
905 if user == nil and pass == nil then
906 user = http.formvalue("luci_username")
907 pass = http.formvalue("luci_password")
908 end
909
910 if user and pass then
911 sid, sdat, sacl = session_setup(user, pass)
912 end
913
914 if not sid then
915 context.path = {}
916
917 http.status(403, "Forbidden")
918 http.header("X-LuCI-Login-Required", "yes")
919
920 local scope = { duser = "root", fuser = user }
921 local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope)
922 if ok then
923 return res
924 end
925 return tpl.render("sysauth", scope)
926 end
927
928 http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{
929 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
930 })
931
932 http.redirect(build_url(unpack(ctx.requestpath)))
933 return
934 end
935
936 if not sid or not sdat or not sacl then
937 http.status(403, "Forbidden")
938 http.header("X-LuCI-Login-Required", "yes")
939 return
940 end
941
942 ctx.authsession = sid
943 ctx.authtoken = sdat.token
944 ctx.authuser = sdat.username
945 ctx.authacl = sacl
946 end
947
948 if #lookup_ctx.acls > 0 then
949 local perm = check_acl_depends(lookup_ctx.acls, ctx.authacl and ctx.authacl["access-group"])
950 if perm == nil then
951 http.status(403, "Forbidden")
952 return
953 end
954
955 if page then
956 page.readonly = not perm
957 end
958 end
959
960 if action.type == "arcombine" then
961 action = (#lookup_ctx.request_args > 0) and action.targets[2] or action.targets[1]
962 end
963
964 if lookup_ctx.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
965 luci.http.status(200, "OK")
966 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
967 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
968 return
969 end
970
971 if require_post_security(action) then
972 if not test_post_security() then
973 return
974 end
975 end
976
977 if lookup_ctx.sgid then
978 sys.process.setgroup(lookup_ctx.sgid)
979 end
980
981 if lookup_ctx.suid then
982 sys.process.setuser(lookup_ctx.suid)
983 end
984
985 if action.type == "view" then
986 tpl.render("view", { view = action.path })
987
988 elseif action.type == "call" then
989 local ok, mod = util.copcall(require, action.module)
990 if not ok then
991 error500(mod)
992 return
993 end
994
995 local func = mod[action["function"]]
996
997 assert(func ~= nil,
998 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?')
999
1000 assert(type(func) == "function",
1001 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' ..
1002 'of type "' .. type(func) .. '".')
1003
1004 local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {}
1005 for _, s in ipairs(lookup_ctx.request_args) do
1006 argv[#argv + 1] = s
1007 end
1008
1009 local ok, err = util.copcall(func, unpack(argv))
1010 if not ok then
1011 error500(err)
1012 end
1013
1014 --elseif action.type == "firstchild" then
1015 -- tpl.render("empty_node_placeholder", getfenv(1))
1016
1017 elseif action.type == "alias" then
1018 local sub_request = {}
1019 for name in action.path:gmatch("[^/]+") do
1020 sub_request[#sub_request + 1] = name
1021 end
1022
1023 for _, s in ipairs(lookup_ctx.request_args) do
1024 sub_request[#sub_request + 1] = s
1025 end
1026
1027 dispatch(sub_request)
1028
1029 elseif action.type == "rewrite" then
1030 local sub_request = { unpack(request) }
1031 for i = 1, action.remove do
1032 table.remove(sub_request, 1)
1033 end
1034
1035 local n = 1
1036 for s in action.path:gmatch("[^/]+") do
1037 table.insert(sub_request, n, s)
1038 n = n + 1
1039 end
1040
1041 for _, s in ipairs(lookup_ctx.request_args) do
1042 sub_request[#sub_request + 1] = s
1043 end
1044
1045 dispatch(sub_request)
1046
1047 elseif action.type == "template" then
1048 tpl.render(action.path, getfenv(1))
1049
1050 elseif action.type == "cbi" then
1051 _cbi({ config = action.config, model = action.path }, unpack(lookup_ctx.request_args))
1052
1053 elseif action.type == "form" then
1054 _form({ model = action.path }, unpack(lookup_ctx.request_args))
1055
1056 else
1057 if not menu.children then
1058 error404("No root node was registered, this usually happens if no module was installed.\n" ..
1059 "Install luci-mod-admin-full and retry. " ..
1060 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
1061 else
1062 error404("No page is registered at '/" .. table.concat(lookup_ctx.request_path, "/") .. "'.\n" ..
1063 "If this url belongs to an extension, make sure it is properly installed.\n" ..
1064 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
1065 end
1066 end
1067 end
1068
1069 local function hash_filelist(files)
1070 local fprint = {}
1071 local n = 0
1072
1073 for i, file in ipairs(files) do
1074 local st = fs.stat(file)
1075 if st then
1076 fprint[n + 1] = '%x' % st.ino
1077 fprint[n + 2] = '%x' % st.mtime
1078 fprint[n + 3] = '%x' % st.size
1079 n = n + 3
1080 end
1081 end
1082
1083 return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".")
1084 end
1085
1086 local function read_cachefile(file, reader)
1087 local euid = sys.process.info("uid")
1088 local fuid = fs.stat(file, "uid")
1089 local mode = fs.stat(file, "modestr")
1090
1091 if euid ~= fuid or mode ~= "rw-------" then
1092 return nil
1093 end
1094
1095 return reader(file)
1096 end
1097
1098 function createindex()
1099 local controllers = { }
1100 local base = "%s/controller/" % util.libpath()
1101 local _, path
1102
1103 for path in (fs.glob("%s*.lua" % base) or function() end) do
1104 controllers[#controllers+1] = path
1105 end
1106
1107 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
1108 controllers[#controllers+1] = path
1109 end
1110
1111 local cachefile
1112
1113 if indexcache then
1114 cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) }
1115
1116 local res = read_cachefile(cachefile, function(path) return loadfile(path)() end)
1117 if res then
1118 index = res
1119 return res
1120 end
1121
1122 for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do
1123 fs.unlink(file)
1124 end
1125 end
1126
1127 index = {}
1128
1129 for _, path in ipairs(controllers) do
1130 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
1131 local mod = require(modname)
1132 assert(mod ~= true,
1133 "Invalid controller file found\n" ..
1134 "The file '" .. path .. "' contains an invalid module line.\n" ..
1135 "Please verify whether the module name is set to '" .. modname ..
1136 "' - It must correspond to the file path!")
1137
1138 local idx = mod.index
1139 if type(idx) == "function" then
1140 index[modname] = idx
1141 end
1142 end
1143
1144 if cachefile then
1145 local f = nixio.open(cachefile, "w", 600)
1146 f:writeall(util.get_bytecode(index))
1147 f:close()
1148 end
1149 end
1150
1151 function createtree_json()
1152 local json = require "luci.jsonc"
1153 local tree = {}
1154
1155 local schema = {
1156 action = "table",
1157 auth = "table",
1158 cors = "boolean",
1159 depends = "table",
1160 order = "number",
1161 setgroup = "string",
1162 setuser = "string",
1163 title = "string",
1164 wildcard = "boolean",
1165 firstchild_ineligible = "boolean"
1166 }
1167
1168 local files = {}
1169 local cachefile
1170
1171 for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do
1172 files[#files+1] = file
1173 end
1174
1175 if indexcache then
1176 cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) }
1177
1178 local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end)
1179 if res then
1180 return res
1181 end
1182
1183 for file in (fs.glob("%s.*.json" % indexcache) or function() end) do
1184 fs.unlink(file)
1185 end
1186 end
1187
1188 for _, file in ipairs(files) do
1189 local data = json.parse(fs.readfile(file) or "")
1190 if type(data) == "table" then
1191 for path, spec in pairs(data) do
1192 if type(spec) == "table" then
1193 local node = tree
1194
1195 for s in path:gmatch("[^/]+") do
1196 if s == "*" then
1197 node.wildcard = true
1198 break
1199 end
1200
1201 node.children = node.children or {}
1202 node.children[s] = node.children[s] or {}
1203 node = node.children[s]
1204 end
1205
1206 if node ~= tree then
1207 for k, t in pairs(schema) do
1208 if type(spec[k]) == t then
1209 node[k] = spec[k]
1210 end
1211 end
1212
1213 node.satisfied = check_depends(spec)
1214 end
1215 end
1216 end
1217 end
1218 end
1219
1220 if cachefile then
1221 local f = nixio.open(cachefile, "w", 600)
1222 f:writeall(json.stringify(tree))
1223 f:close()
1224 end
1225
1226 return tree
1227 end
1228
1229 -- Build the index before if it does not exist yet.
1230 function createtree()
1231 if not index then
1232 createindex()
1233 end
1234
1235 local ctx = context
1236 local tree = {nodes={}, inreq=true}
1237
1238 ctx.treecache = setmetatable({}, {__mode="v"})
1239 ctx.tree = tree
1240
1241 local scope = setmetatable({}, {__index = luci.dispatcher})
1242
1243 for k, v in pairs(index) do
1244 scope._NAME = k
1245 setfenv(v, scope)
1246 v()
1247 end
1248
1249 return tree
1250 end
1251
1252 function assign(path, clone, title, order)
1253 local obj = node(unpack(path))
1254 obj.nodes = nil
1255 obj.module = nil
1256
1257 obj.title = title
1258 obj.order = order
1259
1260 setmetatable(obj, {__index = _create_node(clone)})
1261
1262 return obj
1263 end
1264
1265 function entry(path, target, title, order)
1266 local c = node(unpack(path))
1267
1268 c.target = target
1269 c.title = title
1270 c.order = order
1271 c.module = getfenv(2)._NAME
1272
1273 return c
1274 end
1275
1276 -- enabling the node.
1277 function get(...)
1278 return _create_node({...})
1279 end
1280
1281 function node(...)
1282 local c = _create_node({...})
1283
1284 c.module = getfenv(2)._NAME
1285 c.auto = nil
1286
1287 return c
1288 end
1289
1290 function lookup(...)
1291 local i, path = nil, {}
1292 for i = 1, select('#', ...) do
1293 local name, arg = nil, tostring(select(i, ...))
1294 for name in arg:gmatch("[^/]+") do
1295 path[#path+1] = name
1296 end
1297 end
1298
1299 for i = #path, 1, -1 do
1300 local node = context.treecache[table.concat(path, ".", 1, i)]
1301 if node and (i == #path or node.leaf) then
1302 return node, build_url(unpack(path))
1303 end
1304 end
1305 end
1306
1307 function _create_node(path)
1308 if #path == 0 then
1309 return context.tree
1310 end
1311
1312 local name = table.concat(path, ".")
1313 local c = context.treecache[name]
1314
1315 if not c then
1316 local last = table.remove(path)
1317 local parent = _create_node(path)
1318
1319 c = {nodes={}, auto=true, inreq=true}
1320
1321 parent.nodes[last] = c
1322 context.treecache[name] = c
1323 end
1324
1325 return c
1326 end
1327
1328 -- Subdispatchers --
1329
1330 function firstchild()
1331 return { type = "firstchild" }
1332 end
1333
1334 function firstnode()
1335 return { type = "firstnode" }
1336 end
1337
1338 function alias(...)
1339 return { type = "alias", req = { ... } }
1340 end
1341
1342 function rewrite(n, ...)
1343 return { type = "rewrite", n = n, req = { ... } }
1344 end
1345
1346 function call(name, ...)
1347 return { type = "call", argv = {...}, name = name }
1348 end
1349
1350 function post_on(params, name, ...)
1351 return {
1352 type = "call",
1353 post = params,
1354 argv = { ... },
1355 name = name
1356 }
1357 end
1358
1359 function post(...)
1360 return post_on(true, ...)
1361 end
1362
1363
1364 function template(name)
1365 return { type = "template", view = name }
1366 end
1367
1368 function view(name)
1369 return { type = "view", view = name }
1370 end
1371
1372
1373 function _cbi(self, ...)
1374 local cbi = require "luci.cbi"
1375 local tpl = require "luci.template"
1376 local http = require "luci.http"
1377 local util = require "luci.util"
1378
1379 local config = self.config or {}
1380 local maps = cbi.load(self.model, ...)
1381
1382 local state = nil
1383
1384 local function has_uci_access(config, level)
1385 local rv = util.ubus("session", "access", {
1386 ubus_rpc_session = context.authsession,
1387 scope = "uci", object = config,
1388 ["function"] = level
1389 })
1390
1391 return (type(rv) == "table" and rv.access == true) or false
1392 end
1393
1394 local i, res
1395 for i, res in ipairs(maps) do
1396 if util.instanceof(res, cbi.SimpleForm) then
1397 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
1398 % self.model)
1399
1400 io.stderr:write("please change %s to use the form() action instead.\n"
1401 % table.concat(context.request, "/"))
1402 end
1403
1404 res.flow = config
1405 local cstate = res:parse()
1406 if cstate and (not state or cstate < state) then
1407 state = cstate
1408 end
1409 end
1410
1411 local function _resolve_path(path)
1412 return type(path) == "table" and build_url(unpack(path)) or path
1413 end
1414
1415 if config.on_valid_to and state and state > 0 and state < 2 then
1416 http.redirect(_resolve_path(config.on_valid_to))
1417 return
1418 end
1419
1420 if config.on_changed_to and state and state > 1 then
1421 http.redirect(_resolve_path(config.on_changed_to))
1422 return
1423 end
1424
1425 if config.on_success_to and state and state > 0 then
1426 http.redirect(_resolve_path(config.on_success_to))
1427 return
1428 end
1429
1430 if config.state_handler then
1431 if not config.state_handler(state, maps) then
1432 return
1433 end
1434 end
1435
1436 http.header("X-CBI-State", state or 0)
1437
1438 if not config.noheader then
1439 tpl.render("cbi/header", {state = state})
1440 end
1441
1442 local redirect
1443 local messages
1444 local applymap = false
1445 local pageaction = true
1446 local parsechain = { }
1447 local writable = false
1448
1449 for i, res in ipairs(maps) do
1450 if res.apply_needed and res.parsechain then
1451 local c
1452 for _, c in ipairs(res.parsechain) do
1453 parsechain[#parsechain+1] = c
1454 end
1455 applymap = true
1456 end
1457
1458 if res.redirect then
1459 redirect = redirect or res.redirect
1460 end
1461
1462 if res.pageaction == false then
1463 pageaction = false
1464 end
1465
1466 if res.message then
1467 messages = messages or { }
1468 messages[#messages+1] = res.message
1469 end
1470 end
1471
1472 for i, res in ipairs(maps) do
1473 local is_readable_map = has_uci_access(res.config, "read")
1474 local is_writable_map = has_uci_access(res.config, "write")
1475
1476 writable = writable or is_writable_map
1477
1478 res:render({
1479 firstmap = (i == 1),
1480 redirect = redirect,
1481 messages = messages,
1482 pageaction = pageaction,
1483 parsechain = parsechain,
1484 readable = is_readable_map,
1485 writable = is_writable_map
1486 })
1487 end
1488
1489 if not config.nofooter then
1490 tpl.render("cbi/footer", {
1491 flow = config,
1492 pageaction = pageaction,
1493 redirect = redirect,
1494 state = state,
1495 autoapply = config.autoapply,
1496 trigger_apply = applymap,
1497 writable = writable
1498 })
1499 end
1500 end
1501
1502 function cbi(model, config)
1503 return {
1504 type = "cbi",
1505 post = { ["cbi.submit"] = true },
1506 config = config,
1507 model = model
1508 }
1509 end
1510
1511
1512 function arcombine(trg1, trg2)
1513 return {
1514 type = "arcombine",
1515 env = getfenv(),
1516 targets = {trg1, trg2}
1517 }
1518 end
1519
1520
1521 function _form(self, ...)
1522 local cbi = require "luci.cbi"
1523 local tpl = require "luci.template"
1524 local http = require "luci.http"
1525
1526 local maps = luci.cbi.load(self.model, ...)
1527 local state = nil
1528
1529 local i, res
1530 for i, res in ipairs(maps) do
1531 local cstate = res:parse()
1532 if cstate and (not state or cstate < state) then
1533 state = cstate
1534 end
1535 end
1536
1537 http.header("X-CBI-State", state or 0)
1538 tpl.render("header")
1539 for i, res in ipairs(maps) do
1540 res:render()
1541 end
1542 tpl.render("footer")
1543 end
1544
1545 function form(model)
1546 return {
1547 type = "form",
1548 post = { ["cbi.submit"] = true },
1549 model = model
1550 }
1551 end
1552
1553 translate = i18n.translate
1554
1555 -- This function does not actually translate the given argument but
1556 -- is used by build/i18n-scan.pl to find translatable entries.
1557 function _(text)
1558 return text
1559 end