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