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