libs/web: Added several sanity checks to avoid local privilege escalation
[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 = luci.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         return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
51 end
52
53 --- Send a 404 error code and render the "error404" template if available.
54 -- @param message       Custom error message (optional)
55 -- @return                      false
56 function error404(message)
57         luci.http.status(404, "Not Found")
58         message = message or "Not Found"
59
60         require("luci.template")
61         if not luci.util.copcall(luci.template.render, "error404") then
62                 luci.http.prepare_content("text/plain")
63                 luci.http.write(message)
64         end
65         return false
66 end
67
68 --- Send a 500 error code and render the "error500" template if available.
69 -- @param message       Custom error message (optional)#
70 -- @return                      false
71 function error500(message)
72         luci.http.status(500, "Internal Server Error")
73
74         require("luci.template")
75         if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
76                 luci.http.prepare_content("text/plain")
77                 luci.http.write(message)
78         end
79         return false
80 end
81
82 function authenticator.htmlauth(validator, accs, default)
83         local user = luci.http.formvalue("username")
84         local pass = luci.http.formvalue("password")
85         
86         if user and validator(user, pass) then
87                 return user
88         end
89         
90         require("luci.i18n")
91         require("luci.template")
92         context.path = {}
93         luci.template.render("sysauth", {duser=default, fuser=user})
94         return false
95         
96 end
97
98 --- Dispatch an HTTP request.
99 -- @param request       LuCI HTTP Request object
100 function httpdispatch(request)
101         luci.http.context.request = request
102         context.request = {}
103         local pathinfo = request:getenv("PATH_INFO") or ""
104
105         for node in pathinfo:gmatch("[^/]+") do
106                 table.insert(context.request, node)
107         end
108
109         local stat, err = util.copcall(dispatch, context.request)
110         if not stat then
111                 error500(err)
112         end
113         
114         luci.http.close()
115 end
116
117 --- Dispatches a LuCI virtual path.
118 -- @param request       Virtual path
119 function dispatch(request)
120         local ctx = context
121         ctx.path = request
122         
123         require "luci.i18n".setlanguage(require "luci.config".main.lang)
124         
125         local c = ctx.tree
126         local stat
127         if not c then
128                 c = createtree()
129         end
130         
131         local track = {}
132         local args = {}
133         context.args = args
134         local n
135
136         for i, s in ipairs(request) do
137                 c = c.nodes[s]
138                 n = i
139                 if not c then
140                         break
141                 end
142
143                 util.update(track, c)
144                 
145                 if c.leaf then
146                         break
147                 end
148         end
149
150         if c and c.leaf then
151                 for j=n+1, #request do
152                         table.insert(args, request[j])
153                 end
154         end
155
156         if track.i18n then
157                 require("luci.i18n").loadc(track.i18n)
158         end
159         
160         -- Init template engine
161         if not track.notemplate then
162                 local tpl = require("luci.template")
163                 local viewns = {}
164                 tpl.context.viewns = viewns
165                 viewns.write       = luci.http.write
166                 viewns.translate   = function(...) return require("luci.i18n").translate(...) end
167                 viewns.striptags   = util.striptags
168                 viewns.controller  = luci.http.getenv("SCRIPT_NAME")
169                 viewns.media       = luci.config.main.mediaurlbase
170                 viewns.resource    = luci.config.main.resourcebase
171                 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
172         end
173         
174         assert(not track.dependent or not track.auto, "Access Violation")
175         
176         if track.sysauth then
177                 local sauth = require "luci.sauth"
178                 
179                 local authen = type(track.sysauth_authenticator) == "function"
180                  and track.sysauth_authenticator
181                  or authenticator[track.sysauth_authenticator]
182                  
183                 local def  = (type(track.sysauth) == "string") and track.sysauth
184                 local accs = def and {track.sysauth} or track.sysauth
185                 local sess = luci.http.getcookie("sysauth")
186                 sess = sess and sess:match("^[A-F0-9]+$")
187                 local user = sauth.read(sess)
188                 
189                 if not util.contains(accs, user) then
190                         if authen then
191                                 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
192                                 if not user or not util.contains(accs, user) then
193                                         return
194                                 else
195                                         local sid = sess or luci.sys.uniqueid(16)
196                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
197                                         if not sess then
198                                                 sauth.write(sid, user)
199                                         end
200                                 end
201                         else
202                                 luci.http.status(403, "Forbidden")
203                                 return
204                         end
205                 end
206         end
207
208         if track.setgroup then
209                 luci.sys.process.setgroup(track.setgroup)
210         end
211
212         if track.setuser then
213                 luci.sys.process.setuser(track.setuser)
214         end
215
216         if c and type(c.target) == "function" then
217                 context.dispatched = c
218                 
219                 util.copcall(function()
220                         util.updfenv(c.target, require(c.module))
221                 end)
222                 
223                 c.target(unpack(args))
224         else
225                 error404()
226         end
227 end
228
229 --- Generate the dispatching index using the best possible strategy.
230 function createindex()
231         local path = luci.util.libpath() .. "/controller/"
232         local suff = ".lua"
233         
234         if luci.util.copcall(require, "luci.fastindex") then
235                 createindex_fastindex(path, suff)
236         else
237                 createindex_plain(path, suff)
238         end
239 end
240
241 --- Generate the dispatching index using the fastindex C-indexer.
242 -- @param path          Controller base directory
243 -- @param suffix        Controller file suffix
244 function createindex_fastindex(path, suffix)
245         index = {}
246                 
247         if not fi then
248                 fi = luci.fastindex.new("index")
249                 fi.add(path .. "*" .. suffix)
250                 fi.add(path .. "*/*" .. suffix)
251         end
252         fi.scan()
253         
254         for k, v in pairs(fi.indexes) do
255                 index[v[2]] = v[1]
256         end
257 end
258
259 --- Generate the dispatching index using the native file-cache based strategy.
260 -- @param path          Controller base directory
261 -- @param suffix        Controller file suffix
262 function createindex_plain(path, suffix)
263         if indexcache then
264                 local cachedate = fs.mtime(indexcache)
265                 if cachedate and cachedate > fs.mtime(path) then
266
267                         assert(
268                                 sys.process.info("uid") == fs.stat(indexcache, "uid")
269                                 and fs.stat(indexcache, "mode") == "rw-------",
270                                 "Fatal: Indexcache is not sane!"
271                         )
272
273                         index = loadfile(indexcache)()
274                         return index
275                 end             
276         end
277         
278         index = {}
279
280         local controllers = util.combine(
281                 luci.fs.glob(path .. "*" .. suffix) or {},
282                 luci.fs.glob(path .. "*/*" .. suffix) or {}
283         )
284
285         for i,c in ipairs(controllers) do
286                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
287                 local mod = require(module)
288                 local idx = mod.index
289                 
290                 if type(idx) == "function" then
291                         index[module] = idx
292                 end
293         end
294         
295         if indexcache then
296                 fs.writefile(indexcache, util.get_bytecode(index))
297                 fs.chmod(indexcache, "a-rwx,u+rw")
298         end
299 end
300
301 --- Create the dispatching tree from the index.
302 -- Build the index before if it does not exist yet.
303 function createtree()
304         if not index then
305                 createindex()
306         end
307         
308         local ctx  = context
309         local tree = {nodes={}}
310         
311         ctx.treecache = setmetatable({}, {__mode="v"})
312         ctx.tree = tree
313         
314         -- Load default translation
315         require "luci.i18n".loadc("default")
316         
317         local scope = setmetatable({}, {__index = _G})
318         for k,v in pairs(luci.dispatcher) do
319                 if type(v) == "function" then
320                         scope[k] = v
321                 end
322         end
323
324         for k, v in pairs(index) do
325                 scope._NAME = k
326                 setfenv(v, scope)
327                 v()
328         end
329         
330         return tree
331 end
332
333 --- Clone a node of the dispatching tree to another position.
334 -- @param       path    Virtual path destination
335 -- @param       clone   Virtual path source
336 -- @param       title   Destination node title (optional)
337 -- @param       order   Destination node order value (optional)
338 -- @return                      Dispatching tree node
339 function assign(path, clone, title, order)
340         local obj  = node(unpack(path))
341         obj.nodes  = nil
342         obj.module = nil
343         
344         obj.title = title
345         obj.order = order
346         
347         local c = context.tree
348         for k, v in ipairs(clone) do
349                 if not c.nodes[v] then
350                         c.nodes[v] = {nodes={}}
351                 end
352
353                 c = c.nodes[v]
354         end
355         
356         setmetatable(obj, {__index = c})
357         
358         return obj
359 end
360
361 --- Create a new dispatching node and define common parameters.
362 -- @param       path    Virtual path
363 -- @param       target  Target function to call when dispatched. 
364 -- @param       title   Destination node title
365 -- @param       order   Destination node order value (optional)
366 -- @return                      Dispatching tree node
367 function entry(path, target, title, order)
368         local c = node(unpack(path))
369         
370         c.target = target
371         c.title  = title
372         c.order  = order
373         c.module = getfenv(2)._NAME
374
375         return c
376 end
377
378 --- Fetch or create a new dispatching node.
379 -- @param       ...             Virtual path
380 -- @return                      Dispatching tree node
381 function node(...)
382         local c = _create_node(arg)
383
384         c.module = getfenv(2)._NAME
385         c.path = arg
386         c.auto = nil
387
388         return c
389 end
390
391 function _create_node(path, cache)
392         if #path == 0 then
393                 return context.tree
394         end
395         
396         cache = cache or context.treecache
397         local name = table.concat(path, ".")
398         local c = cache[name]
399         
400         if not c then
401                 local last = table.remove(path)
402                 c = _create_node(path, cache)
403                 
404                 local new = {nodes={}, auto=true}
405                 c.nodes[last] = new
406                 cache[name] = new
407                 
408                 return new
409         else
410                 return c
411         end
412 end
413
414 -- Subdispatchers --
415
416 --- Create a redirect to another dispatching node.
417 -- @param       ...             Virtual path destination
418 function alias(...)
419         local req = arg
420         return function()
421                 dispatch(req)
422         end
423 end
424
425 --- Rewrite the first x path values of the request.
426 -- @param       n               Number of path values to replace
427 -- @param       ...             Virtual path to replace removed path values with
428 function rewrite(n, ...)
429         local req = arg
430         return function()
431                 for i=1,n do 
432                         table.remove(context.path, 1)
433                 end
434                 
435                 for i,r in ipairs(req) do
436                         table.insert(context.path, i, r)
437                 end
438                 
439                 dispatch()
440         end
441 end
442
443 --- Create a function-call dispatching target.
444 -- @param       name    Target function of local controller 
445 -- @param       ...             Additional parameters passed to the function
446 function call(name, ...)
447         local argv = {...}
448         return function() return getfenv()[name](unpack(argv)) end
449 end
450
451 --- Create a template render dispatching target.
452 -- @param       name    Template to be rendered
453 function template(name)
454         return function()
455                 require("luci.template")
456                 luci.template.render(name)
457         end
458 end
459
460 --- Create a CBI model dispatching target.
461 -- @param       model   CBI model tpo be rendered
462 function cbi(model)
463         return function(...)
464                 require("luci.cbi")
465                 require("luci.template")
466
467                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
468                 if not stat then
469                         error500(maps)
470                         return true
471                 end
472
473                 for i, res in ipairs(maps) do
474                         local stat, err = luci.util.copcall(res.parse, res)
475                         if not stat then
476                                 error500(err)
477                                 return true
478                         end
479                 end
480
481                 luci.template.render("cbi/header")
482                 for i, res in ipairs(maps) do
483                         res:render()
484                 end
485                 luci.template.render("cbi/footer")
486         end
487 end
488
489 --- Create a CBI form model dispatching target.
490 -- @param       model   CBI form model tpo be rendered
491 function form(model)
492         return function(...)
493                 require("luci.cbi")
494                 require("luci.template")
495
496                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
497                 if not stat then
498                         error500(maps)
499                         return true
500                 end
501
502                 for i, res in ipairs(maps) do
503                         local stat, err = luci.util.copcall(res.parse, res)
504                         if not stat then
505                                 error500(err)
506                                 return true
507                         end
508                 end
509
510                 luci.template.render("header")
511                 for i, res in ipairs(maps) do
512                         res:render()
513                 end
514                 luci.template.render("footer")
515         end
516 end