luci-base: remove security token from urls
[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 k, v
31         for k, v in pairs(context.urltoken) do
32                 url[#url+1] = "/;"
33                 url[#url+1] = http.urlencode(k)
34                 url[#url+1] = "="
35                 url[#url+1] = http.urlencode(v)
36         end
37
38         local p
39         for _, p in ipairs(path) do
40                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
41                         url[#url+1] = "/"
42                         url[#url+1] = p
43                 end
44         end
45
46         return table.concat(url, "")
47 end
48
49 function node_visible(node)
50    if node then
51           return not (
52                  (not node.title or #node.title == 0) or
53                  (not node.target or node.hidden == true) or
54                  (type(node.target) == "table" and node.target.type == "firstchild" and
55                   (type(node.nodes) ~= "table" or not next(node.nodes)))
56           )
57    end
58    return false
59 end
60
61 function node_childs(node)
62         local rv = { }
63         if node then
64                 local k, v
65                 for k, v in util.spairs(node.nodes,
66                         function(a, b)
67                                 return (node.nodes[a].order or 100)
68                                      < (node.nodes[b].order or 100)
69                         end)
70                 do
71                         if node_visible(v) then
72                                 rv[#rv+1] = k
73                         end
74                 end
75         end
76         return rv
77 end
78
79
80 function error404(message)
81         http.status(404, "Not Found")
82         message = message or "Not Found"
83
84         require("luci.template")
85         if not util.copcall(luci.template.render, "error404") then
86                 http.prepare_content("text/plain")
87                 http.write(message)
88         end
89         return false
90 end
91
92 function error500(message)
93         util.perror(message)
94         if not context.template_header_sent then
95                 http.status(500, "Internal Server Error")
96                 http.prepare_content("text/plain")
97                 http.write(message)
98         else
99                 require("luci.template")
100                 if not util.copcall(luci.template.render, "error500", {message=message}) then
101                         http.prepare_content("text/plain")
102                         http.write(message)
103                 end
104         end
105         return false
106 end
107
108 function authenticator.htmlauth(validator, accs, default)
109         local user = http.formvalue("luci_username")
110         local pass = http.formvalue("luci_password")
111
112         if user and validator(user, pass) then
113                 return user
114         end
115
116         require("luci.i18n")
117         require("luci.template")
118         context.path = {}
119         http.status(403, "Forbidden")
120         luci.template.render("sysauth", {duser=default, fuser=user})
121
122         return false
123
124 end
125
126 function httpdispatch(request, prefix)
127         http.context.request = request
128
129         local r = {}
130         context.request = r
131         context.urltoken = {}
132
133         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
134
135         if prefix then
136                 for _, node in ipairs(prefix) do
137                         r[#r+1] = node
138                 end
139         end
140
141         for node in pathinfo:gmatch("[^/]+") do
142                 r[#r+1] = node
143         end
144
145         local stat, err = util.coxpcall(function()
146                 dispatch(context.request)
147         end, error500)
148
149         http.close()
150
151         --context._disable_memtrace()
152 end
153
154 local function require_post_security(target)
155         if type(target) == "table" then
156                 if type(target.post) == "table" then
157                         local param_name, required_val, request_val
158
159                         for param_name, required_val in pairs(target.post) do
160                                 request_val = http.formvalue(param_name)
161
162                                 if (type(required_val) == "string" and
163                                     request_val ~= required_val) or
164                                    (required_val == true and
165                                     (request_val == nil or request_val == ""))
166                                 then
167                                         return false
168                                 end
169                         end
170
171                         return true
172                 end
173
174                 return (target.post == true)
175         end
176
177         return false
178 end
179
180 function dispatch(request)
181         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
182         local ctx = context
183         ctx.path = request
184
185         local conf = require "luci.config"
186         assert(conf.main,
187                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
188
189         local lang = conf.main.lang or "auto"
190         if lang == "auto" then
191                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
192                 for lpat in aclang:gmatch("[%w-]+") do
193                         lpat = lpat and lpat:gsub("-", "_")
194                         if conf.languages[lpat] then
195                                 lang = lpat
196                                 break
197                         end
198                 end
199         end
200         require "luci.i18n".setlanguage(lang)
201
202         local c = ctx.tree
203         local stat
204         if not c then
205                 c = createtree()
206         end
207
208         local track = {}
209         local args = {}
210         ctx.args = args
211         ctx.requestargs = ctx.requestargs or args
212         local n
213         local token = ctx.urltoken
214         local preq = {}
215         local freq = {}
216
217         for i, s in ipairs(request) do
218                 preq[#preq+1] = s
219                 freq[#freq+1] = s
220                 c = c.nodes[s]
221                 n = i
222                 if not c then
223                         break
224                 end
225
226                 util.update(track, c)
227
228                 if c.leaf then
229                         break
230                 end
231         end
232
233         if c and c.leaf then
234                 for j=n+1, #request do
235                         args[#args+1] = request[j]
236                         freq[#freq+1] = request[j]
237                 end
238         end
239
240         ctx.requestpath = ctx.requestpath or freq
241         ctx.path = preq
242
243         if track.i18n then
244                 i18n.loadc(track.i18n)
245         end
246
247         -- Init template engine
248         if (c and c.index) or not track.notemplate then
249                 local tpl = require("luci.template")
250                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
251                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
252                         media = nil
253                         for name, theme in pairs(luci.config.themes) do
254                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
255                                  "themes/%s/header" % fs.basename(theme)) then
256                                         media = theme
257                                 end
258                         end
259                         assert(media, "No valid theme found")
260                 end
261
262                 local function _ifattr(cond, key, val)
263                         if cond then
264                                 local env = getfenv(3)
265                                 local scope = (type(env.self) == "table") and env.self
266                                 return string.format(
267                                         ' %s="%s"', tostring(key),
268                                         util.pcdata(tostring( val
269                                          or (type(env[key]) ~= "function" and env[key])
270                                          or (scope and type(scope[key]) ~= "function" and scope[key])
271                                          or "" ))
272                                 )
273                         else
274                                 return ''
275                         end
276                 end
277
278                 tpl.context.viewns = setmetatable({
279                    write       = http.write;
280                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
281                    translate   = i18n.translate;
282                    translatef  = i18n.translatef;
283                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
284                    striptags   = util.striptags;
285                    pcdata      = util.pcdata;
286                    media       = media;
287                    theme       = fs.basename(media);
288                    resource    = luci.config.main.resourcebase;
289                    ifattr      = function(...) return _ifattr(...) end;
290                    attr        = function(...) return _ifattr(true, ...) end;
291                    url         = build_url;
292                 }, {__index=function(table, key)
293                         if key == "controller" then
294                                 return build_url()
295                         elseif key == "REQUEST_URI" then
296                                 return build_url(unpack(ctx.requestpath))
297                         elseif key == "token" then
298                                 return ctx.authtoken
299                         else
300                                 return rawget(table, key) or _G[key]
301                         end
302                 end})
303         end
304
305         track.dependent = (track.dependent ~= false)
306         assert(not track.dependent or not track.auto,
307                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
308                 "has no parent node so the access to this location has been denied.\n" ..
309                 "This is a software bug, please report this message at " ..
310                 "http://luci.subsignal.org/trac/newticket"
311         )
312
313         if track.sysauth then
314                 local authen = type(track.sysauth_authenticator) == "function"
315                  and track.sysauth_authenticator
316                  or authenticator[track.sysauth_authenticator]
317
318                 local def  = (type(track.sysauth) == "string") and track.sysauth
319                 local accs = def and {track.sysauth} or track.sysauth
320                 local sess = ctx.authsession
321                 if not sess then
322                         sess = http.getcookie("sysauth")
323                         sess = sess and sess:match("^[a-f0-9]*$")
324                 end
325
326                 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
327                 local user, token
328
329                 if sdat then
330                         user = sdat.user
331                         token = sdat.token
332                 else
333                         local eu = http.getenv("HTTP_AUTH_USER")
334                         local ep = http.getenv("HTTP_AUTH_PASS")
335                         if eu and ep and sys.user.checkpasswd(eu, ep) then
336                                 authen = function() return eu end
337                         end
338                 end
339
340                 if not util.contains(accs, user) then
341                         if authen then
342                                 local user, sess = authen(sys.user.checkpasswd, accs, def)
343                                 local token
344                                 if not user or not util.contains(accs, user) then
345                                         return
346                                 else
347                                         if not sess then
348                                                 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
349                                                 if sdat then
350                                                         token = sys.uniqueid(16)
351                                                         util.ubus("session", "set", {
352                                                                 ubus_rpc_session = sdat.ubus_rpc_session,
353                                                                 values = {
354                                                                         user = user,
355                                                                         token = token,
356                                                                         section = sys.uniqueid(16)
357                                                                 }
358                                                         })
359                                                         sess = sdat.ubus_rpc_session
360                                                 end
361                                         end
362
363                                         if sess and token then
364                                                 http.header("Set-Cookie", 'sysauth=%s; path=%s/' %{
365                                                    sess, build_url()
366                                                 })
367
368                                                 ctx.authsession = sess
369                                                 ctx.authtoken = token
370                                                 ctx.authuser = user
371
372                                                 http.redirect(build_url(unpack(ctx.requestpath)))
373                                         end
374                                 end
375                         else
376                                 http.status(403, "Forbidden")
377                                 return
378                         end
379                 else
380                         ctx.authsession = sess
381                         ctx.authtoken = token
382                         ctx.authuser = user
383                 end
384         end
385
386         if c and require_post_security(c.target) then
387                 if http.getenv("REQUEST_METHOD") ~= "POST" then
388                         http.status(405, "Method Not Allowed")
389                         http.header("Allow", "POST")
390                         return
391                 end
392
393                 if http.formvalue("token") ~= ctx.authtoken then
394                         http.status(403, "Forbidden")
395                         luci.template.render("csrftoken")
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