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