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