Fix requestpath, export authuser
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17         http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 --- LuCI web dispatcher.
28 local fs = require "nixio.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
34
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37
38 authenticator = {}
39
40 -- Index table
41 local index = nil
42
43 -- Fastindex
44 local fi
45
46
47 --- Build the URL relative to the server webroot from given virtual path.
48 -- @param ...   Virtual path
49 -- @return              Relative URL
50 function build_url(...)
51         local path = {...}
52         local sn = http.getenv("SCRIPT_NAME") or ""
53         for k, v in pairs(context.urltoken) do
54                 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
55         end
56         return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
57 end
58
59 --- Send a 404 error code and render the "error404" template if available.
60 -- @param message       Custom error message (optional)
61 -- @return                      false
62 function error404(message)
63         luci.http.status(404, "Not Found")
64         message = message or "Not Found"
65
66         require("luci.template")
67         if not luci.util.copcall(luci.template.render, "error404") then
68                 luci.http.prepare_content("text/plain")
69                 luci.http.write(message)
70         end
71         return false
72 end
73
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message       Custom error message (optional)#
76 -- @return                      false
77 function error500(message)
78         luci.util.perror(message)
79         if not context.template_header_sent then
80                 luci.http.status(500, "Internal Server Error")
81                 luci.http.prepare_content("text/plain")
82                 luci.http.write(message)
83         else
84                 require("luci.template")
85                 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
86                         luci.http.prepare_content("text/plain")
87                         luci.http.write(message)
88                 end
89         end
90         return false
91 end
92
93 function authenticator.htmlauth(validator, accs, default)
94         local user = luci.http.formvalue("username")
95         local pass = luci.http.formvalue("password")
96
97         if user and validator(user, pass) then
98                 return user
99         end
100
101         require("luci.i18n")
102         require("luci.template")
103         context.path = {}
104         luci.template.render("sysauth", {duser=default, fuser=user})
105         return false
106
107 end
108
109 --- Dispatch an HTTP request.
110 -- @param request       LuCI HTTP Request object
111 function httpdispatch(request, prefix)
112         luci.http.context.request = request
113
114         local r = {}
115         context.request = r
116         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
117
118         if prefix then
119                 for _, node in ipairs(prefix) do
120                         r[#r+1] = node
121                 end
122         end
123
124         for node in pathinfo:gmatch("[^/]+") do
125                 r[#r+1] = node
126         end
127
128         local stat, err = util.coxpcall(function()
129                 dispatch(context.request)
130         end, error500)
131
132         luci.http.close()
133
134         --context._disable_memtrace()
135 end
136
137 --- Dispatches a LuCI virtual path.
138 -- @param request       Virtual path
139 function dispatch(request)
140         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
141         local ctx = context
142         ctx.path = request
143         ctx.urltoken   = ctx.urltoken or {}
144
145         local conf = require "luci.config"
146         assert(conf.main,
147                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
148
149         local lang = conf.main.lang
150         if lang == "auto" then
151                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
152                 for lpat in aclang:gmatch("[%w-]+") do
153                         lpat = lpat and lpat:gsub("-", "_")
154                         if conf.languages[lpat] then
155                                 lang = lpat
156                                 break
157                         end
158                 end
159         end
160         require "luci.i18n".setlanguage(lang)
161
162         local c = ctx.tree
163         local stat
164         if not c then
165                 c = createtree()
166         end
167
168         local track = {}
169         local args = {}
170         ctx.args = args
171         ctx.requestargs = ctx.requestargs or args
172         local n
173         local t = true
174         local token = ctx.urltoken
175         local preq = {}
176         local freq = {}
177
178         for i, s in ipairs(request) do
179                 local tkey, tval
180                 if t then
181                         tkey, tval = s:match(";(%w+)=(.*)")
182                 end
183
184                 if tkey then
185                         token[tkey] = tval
186                 else
187                         t = false
188                         preq[#preq+1] = s
189                         freq[#freq+1] = s
190                         c = c.nodes[s]
191                         n = i
192                         if not c then
193                                 break
194                         end
195
196                         util.update(track, c)
197
198                         if c.leaf then
199                                 break
200                         end
201                 end
202         end
203
204         if c and c.leaf then
205                 for j=n+1, #request do
206                         args[#args+1] = request[j]
207                         freq[#freq+1] = request[j]
208                 end
209         end
210
211         ctx.requestpath = ctx.requestpath or freq
212         ctx.path = preq
213
214         if track.i18n then
215                 require("luci.i18n").loadc(track.i18n)
216         end
217
218         -- Init template engine
219         if (c and c.index) or not track.notemplate then
220                 local tpl = require("luci.template")
221                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
222                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
223                         media = nil
224                         for name, theme in pairs(luci.config.themes) do
225                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
226                                  "themes/%s/header" % fs.basename(theme)) then
227                                         media = theme
228                                 end
229                         end
230                         assert(media, "No valid theme found")
231                 end
232
233                 tpl.context.viewns = setmetatable({
234                    write       = luci.http.write;
235                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
236                    translate   = function(...) return require("luci.i18n").translate(...) end;
237                    striptags   = util.striptags;
238                    media       = media;
239                    theme       = fs.basename(media);
240                    resource    = luci.config.main.resourcebase
241                 }, {__index=function(table, key)
242                         if key == "controller" then
243                                 return build_url()
244                         elseif key == "REQUEST_URI" then
245                                 return build_url(unpack(ctx.requestpath))
246                         else
247                                 return rawget(table, key) or _G[key]
248                         end
249                 end})
250         end
251
252         track.dependent = (track.dependent ~= false)
253         assert(not track.dependent or not track.auto, "Access Violation")
254
255         if track.sysauth then
256                 local sauth = require "luci.sauth"
257
258                 local authen = type(track.sysauth_authenticator) == "function"
259                  and track.sysauth_authenticator
260                  or authenticator[track.sysauth_authenticator]
261
262                 local def  = (type(track.sysauth) == "string") and track.sysauth
263                 local accs = def and {track.sysauth} or track.sysauth
264                 local sess = ctx.authsession
265                 local verifytoken = false
266                 if not sess then
267                         sess = luci.http.getcookie("sysauth")
268                         sess = sess and sess:match("^[a-f0-9]*$")
269                         verifytoken = true
270                 end
271
272                 local sdat = sauth.read(sess)
273                 local user
274
275                 if sdat then
276                         sdat = loadstring(sdat)
277                         setfenv(sdat, {})
278                         sdat = sdat()
279                         if not verifytoken or ctx.urltoken.stok == sdat.token then
280                                 user = sdat.user
281                         end
282                 else
283                         local eu = http.getenv("HTTP_AUTH_USER")
284                         local ep = http.getenv("HTTP_AUTH_PASS")
285                         if eu and ep and luci.sys.user.checkpasswd(eu, ep) then
286                                 authen = function() return eu end
287                         end
288                 end
289
290                 if not util.contains(accs, user) then
291                         if authen then
292                                 ctx.urltoken.stok = nil
293                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
294                                 if not user or not util.contains(accs, user) then
295                                         return
296                                 else
297                                         local sid = sess or luci.sys.uniqueid(16)
298                                         if not sess then
299                                                 local token = luci.sys.uniqueid(16)
300                                                 sauth.write(sid, util.get_bytecode({
301                                                         user=user,
302                                                         token=token,
303                                                         secret=luci.sys.uniqueid(16)
304                                                 }))
305                                                 ctx.urltoken.stok = token
306                                         end
307                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
308                                         ctx.authsession = sid
309                                         ctx.authuser = user
310                                 end
311                         else
312                                 luci.http.status(403, "Forbidden")
313                                 return
314                         end
315                 else
316                         ctx.authsession = sess
317                         ctx.authuser = user
318                 end
319         end
320
321         if track.setgroup then
322                 luci.sys.process.setgroup(track.setgroup)
323         end
324
325         if track.setuser then
326                 luci.sys.process.setuser(track.setuser)
327         end
328
329         local target = nil
330         if c then
331                 if type(c.target) == "function" then
332                         target = c.target
333                 elseif type(c.target) == "table" then
334                         target = c.target.target
335                 end
336         end
337
338         if c and (c.index or type(target) == "function") then
339                 ctx.dispatched = c
340                 ctx.requested = ctx.requested or ctx.dispatched
341         end
342
343         if c and c.index then
344                 local tpl = require "luci.template"
345
346                 if util.copcall(tpl.render, "indexer", {}) then
347                         return true
348                 end
349         end
350
351         if type(target) == "function" then
352                 util.copcall(function()
353                         local oldenv = getfenv(target)
354                         local module = require(c.module)
355                         local env = setmetatable({}, {__index=
356
357                         function(tbl, key)
358                                 return rawget(tbl, key) or module[key] or oldenv[key]
359                         end})
360
361                         setfenv(target, env)
362                 end)
363
364                 if type(c.target) == "table" then
365                         target(c.target, unpack(args))
366                 else
367                         target(unpack(args))
368                 end
369         else
370                 error404()
371         end
372 end
373
374 --- Generate the dispatching index using the best possible strategy.
375 function createindex()
376         local path = luci.util.libpath() .. "/controller/"
377         local suff = { ".lua", ".lua.gz" }
378
379         if luci.util.copcall(require, "luci.fastindex") then
380                 createindex_fastindex(path, suff)
381         else
382                 createindex_plain(path, suff)
383         end
384 end
385
386 --- Generate the dispatching index using the fastindex C-indexer.
387 -- @param path          Controller base directory
388 -- @param suffixes      Controller file suffixes
389 function createindex_fastindex(path, suffixes)
390         index = {}
391
392         if not fi then
393                 fi = luci.fastindex.new("index")
394                 for _, suffix in ipairs(suffixes) do
395                         fi.add(path .. "*" .. suffix)
396                         fi.add(path .. "*/*" .. suffix)
397                 end
398         end
399         fi.scan()
400
401         for k, v in pairs(fi.indexes) do
402                 index[v[2]] = v[1]
403         end
404 end
405
406 --- Generate the dispatching index using the native file-cache based strategy.
407 -- @param path          Controller base directory
408 -- @param suffixes      Controller file suffixes
409 function createindex_plain(path, suffixes)
410         local controllers = { }
411         for _, suffix in ipairs(suffixes) do
412                 nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers)
413                 nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers)
414         end
415
416         if indexcache then
417                 local cachedate = fs.stat(indexcache, "mtime")
418                 if cachedate then
419                         local realdate = 0
420                         for _, obj in ipairs(controllers) do
421                                 local omtime = fs.stat(path .. "/" .. obj, "mtime")
422                                 realdate = (omtime and omtime > realdate) and omtime or realdate
423                         end
424
425                         if cachedate > realdate then
426                                 assert(
427                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
428                                         and fs.stat(indexcache, "modestr") == "rw-------",
429                                         "Fatal: Indexcache is not sane!"
430                                 )
431
432                                 index = loadfile(indexcache)()
433                                 return index
434                         end
435                 end
436         end
437
438         index = {}
439
440         for i,c in ipairs(controllers) do
441                 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
442                 for _, suffix in ipairs(suffixes) do
443                         module = module:gsub(suffix.."$", "")
444                 end
445
446                 local mod = require(module)
447                 local idx = mod.index
448
449                 if type(idx) == "function" then
450                         index[module] = idx
451                 end
452         end
453
454         if indexcache then
455                 local f = nixio.open(indexcache, "w", 600)
456                 f:writeall(util.get_bytecode(index))
457                 f:close()
458         end
459 end
460
461 --- Create the dispatching tree from the index.
462 -- Build the index before if it does not exist yet.
463 function createtree()
464         if not index then
465                 createindex()
466         end
467
468         local ctx  = context
469         local tree = {nodes={}}
470         local modi = {}
471
472         ctx.treecache = setmetatable({}, {__mode="v"})
473         ctx.tree = tree
474         ctx.modifiers = modi
475
476         -- Load default translation
477         require "luci.i18n".loadc("default")
478
479         local scope = setmetatable({}, {__index = luci.dispatcher})
480
481         for k, v in pairs(index) do
482                 scope._NAME = k
483                 setfenv(v, scope)
484                 v()
485         end
486
487         local function modisort(a,b)
488                 return modi[a].order < modi[b].order
489         end
490
491         for _, v in util.spairs(modi, modisort) do
492                 scope._NAME = v.module
493                 setfenv(v.func, scope)
494                 v.func()
495         end
496
497         return tree
498 end
499
500 --- Register a tree modifier.
501 -- @param       func    Modifier function
502 -- @param       order   Modifier order value (optional)
503 function modifier(func, order)
504         context.modifiers[#context.modifiers+1] = {
505                 func = func,
506                 order = order or 0,
507                 module
508                         = getfenv(2)._NAME
509         }
510 end
511
512 --- Clone a node of the dispatching tree to another position.
513 -- @param       path    Virtual path destination
514 -- @param       clone   Virtual path source
515 -- @param       title   Destination node title (optional)
516 -- @param       order   Destination node order value (optional)
517 -- @return                      Dispatching tree node
518 function assign(path, clone, title, order)
519         local obj  = node(unpack(path))
520         obj.nodes  = nil
521         obj.module = nil
522
523         obj.title = title
524         obj.order = order
525
526         setmetatable(obj, {__index = _create_node(clone)})
527
528         return obj
529 end
530
531 --- Create a new dispatching node and define common parameters.
532 -- @param       path    Virtual path
533 -- @param       target  Target function to call when dispatched.
534 -- @param       title   Destination node title
535 -- @param       order   Destination node order value (optional)
536 -- @return                      Dispatching tree node
537 function entry(path, target, title, order)
538         local c = node(unpack(path))
539
540         c.target = target
541         c.title  = title
542         c.order  = order
543         c.module = getfenv(2)._NAME
544
545         return c
546 end
547
548 --- Fetch or create a dispatching node without setting the target module or
549 -- enabling the node.
550 -- @param       ...             Virtual path
551 -- @return                      Dispatching tree node
552 function get(...)
553         return _create_node({...})
554 end
555
556 --- Fetch or create a new dispatching node.
557 -- @param       ...             Virtual path
558 -- @return                      Dispatching tree node
559 function node(...)
560         local c = _create_node({...})
561
562         c.module = getfenv(2)._NAME
563         c.auto = nil
564
565         return c
566 end
567
568 function _create_node(path, cache)
569         if #path == 0 then
570                 return context.tree
571         end
572
573         cache = cache or context.treecache
574         local name = table.concat(path, ".")
575         local c = cache[name]
576
577         if not c then
578                 local new = {nodes={}, auto=true, path=util.clone(path)}
579                 local last = table.remove(path)
580
581                 c = _create_node(path, cache)
582
583                 c.nodes[last] = new
584                 cache[name] = new
585
586                 return new
587         else
588                 return c
589         end
590 end
591
592 -- Subdispatchers --
593
594 --- Create a redirect to another dispatching node.
595 -- @param       ...             Virtual path destination
596 function alias(...)
597         local req = {...}
598         return function(...)
599                 for _, r in ipairs({...}) do
600                         req[#req+1] = r
601                 end
602
603                 dispatch(req)
604         end
605 end
606
607 --- Rewrite the first x path values of the request.
608 -- @param       n               Number of path values to replace
609 -- @param       ...             Virtual path to replace removed path values with
610 function rewrite(n, ...)
611         local req = {...}
612         return function(...)
613                 local dispatched = util.clone(context.dispatched)
614
615                 for i=1,n do
616                         table.remove(dispatched, 1)
617                 end
618
619                 for i, r in ipairs(req) do
620                         table.insert(dispatched, i, r)
621                 end
622
623                 for _, r in ipairs({...}) do
624                         dispatched[#dispatched+1] = r
625                 end
626
627                 dispatch(dispatched)
628         end
629 end
630
631
632 local function _call(self, ...)
633         if #self.argv > 0 then
634                 return getfenv()[self.name](unpack(self.argv), ...)
635         else
636                 return getfenv()[self.name](...)
637         end
638 end
639
640 --- Create a function-call dispatching target.
641 -- @param       name    Target function of local controller
642 -- @param       ...             Additional parameters passed to the function
643 function call(name, ...)
644         return {type = "call", argv = {...}, name = name, target = _call}
645 end
646
647
648 local _template = function(self, ...)
649         require "luci.template".render(self.view)
650 end
651
652 --- Create a template render dispatching target.
653 -- @param       name    Template to be rendered
654 function template(name)
655         return {type = "template", view = name, target = _template}
656 end
657
658
659 local function _cbi(self, ...)
660         local cbi = require "luci.cbi"
661         local tpl = require "luci.template"
662         local http = require "luci.http"
663
664         local config = self.config or {}
665         local maps = cbi.load(self.model, ...)
666
667         local state = nil
668
669         for i, res in ipairs(maps) do
670                 res.flow = config
671                 local cstate = res:parse()
672                 if cstate and (not state or cstate < state) then
673                         state = cstate
674                 end
675         end
676
677         local function _resolve_path(path)
678                 return type(path) == "table" and build_url(unpack(path)) or path
679         end
680
681         if config.on_valid_to and state and state > 0 and state < 2 then
682                 http.redirect(_resolve_path(config.on_valid_to))
683                 return
684         end
685
686         if config.on_changed_to and state and state > 1 then
687                 http.redirect(_resolve_path(config.on_changed_to))
688                 return
689         end
690
691         if config.on_success_to and state and state > 0 then
692                 http.redirect(_resolve_path(config.on_success_to))
693                 return
694         end
695
696         if config.state_handler then
697                 if not config.state_handler(state, maps) then
698                         return
699                 end
700         end
701
702         local pageaction = true
703         http.header("X-CBI-State", state or 0)
704         if not config.noheader then
705                 tpl.render("cbi/header", {state = state})
706         end
707         for i, res in ipairs(maps) do
708                 res:render()
709                 if res.pageaction == false then
710                         pageaction = false
711                 end
712         end
713         if not config.nofooter then
714                 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
715         end
716 end
717
718 --- Create a CBI model dispatching target.
719 -- @param       model   CBI model to be rendered
720 function cbi(model, config)
721         return {type = "cbi", config = config, model = model, target = _cbi}
722 end
723
724
725 local function _arcombine(self, ...)
726         local argv = {...}
727         local target = #argv > 0 and self.targets[2] or self.targets[1]
728         setfenv(target.target, self.env)
729         target:target(unpack(argv))
730 end
731
732 --- Create a combined dispatching target for non argv and argv requests.
733 -- @param trg1  Overview Target
734 -- @param trg2  Detail Target
735 function arcombine(trg1, trg2)
736         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
737 end
738
739
740 local function _form(self, ...)
741         local cbi = require "luci.cbi"
742         local tpl = require "luci.template"
743         local http = require "luci.http"
744
745         local maps = luci.cbi.load(self.model, ...)
746         local state = nil
747
748         for i, res in ipairs(maps) do
749                 local cstate = res:parse()
750                 if cstate and (not state or cstate < state) then
751                         state = cstate
752                 end
753         end
754
755         http.header("X-CBI-State", state or 0)
756         tpl.render("header")
757         for i, res in ipairs(maps) do
758                 res:render()
759         end
760         tpl.render("footer")
761 end
762
763 --- Create a CBI form model dispatching target.
764 -- @param       model   CBI form model tpo be rendered
765 function form(model)
766         return {type = "cbi", model = model, target = _form}
767 end