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