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