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