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