RPC initial authentication API completed
[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 module("luci.dispatcher", package.seeall)
29 require("luci.util")
30 require("luci.init")
31 require("luci.http")
32 require("luci.sys")
33 require("luci.fs")
34
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         dispatch(context.request)
110         luci.http.close()
111 end
112
113 --- Dispatches a LuCI virtual path.
114 -- @param request       Virtual path
115 function dispatch(request)
116         context.path = request
117         
118         require("luci.i18n")
119         luci.i18n.setlanguage(require("luci.config").main.lang)
120         
121         if not context.tree then
122                 createtree()
123         end
124         
125         local c = context.tree
126         local track = {}
127         local args = {}
128         context.args = args
129         local n
130
131         for i, s in ipairs(request) do
132                 c = c.nodes[s]
133                 n = i
134                 if not c then
135                         break
136                 end
137
138                 for k, v in pairs(c) do
139                         track[k] = v
140                 end
141                 
142                 if c.leaf then
143                         break
144                 end
145         end
146
147         if c and c.leaf then
148                 for j=n+1, #request do
149                         table.insert(args, request[j])
150                 end
151         end
152
153         if track.i18n then
154                 require("luci.i18n").loadc(track.i18n)
155         end
156         
157         -- Init template engine
158         local tpl = require("luci.template")
159         local viewns = {}
160         tpl.context.viewns = viewns
161         viewns.write       = luci.http.write
162         viewns.translate   = function(...) return require("luci.i18n").translate(...) end
163         viewns.striptags   = luci.util.striptags
164         viewns.controller  = luci.http.getenv("SCRIPT_NAME")
165         viewns.media       = luci.config.main.mediaurlbase
166         viewns.resource    = luci.config.main.resourcebase
167         viewns.REQUEST_URI = luci.http.getenv("SCRIPT_NAME") .. (luci.http.getenv("PATH_INFO") or "")
168         
169         if track.dependent then
170                 local stat, err = pcall(assert, not track.auto)
171                 if not stat then
172                         error500(err)
173                         return
174                 end
175         end
176         
177         if track.sysauth then
178                 require("luci.sauth")
179                 local authen = type(track.sysauth_authenticator) == "function"
180                  and track.sysauth_authenticator
181                  or authenticator[track.sysauth_authenticator]
182                 local def  = (type(track.sysauth) == "string") and track.sysauth
183                 local accs = def and {track.sysauth} or track.sysauth
184                 local sess = luci.http.getcookie("sysauth")
185                 sess = sess and sess:match("^[A-F0-9]+$")
186                 local user = luci.sauth.read(sess)
187                 
188                 if not luci.util.contains(accs, user) then
189                         if authen then
190                                 local user = authen(luci.sys.user.checkpasswd, accs, def)
191                                 if not user or not luci.util.contains(accs, user) then
192                                         return
193                                 else
194                                         local sid = luci.sys.uniqueid(16)
195                                         luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
196                                         luci.sauth.write(sid, user)
197                                 end
198                         else
199                                 luci.http.status(403, "Forbidden")
200                                 return
201                         end
202                 end
203         end
204
205         if track.setgroup then
206                 luci.sys.process.setgroup(track.setgroup)
207         end
208
209         if track.setuser then
210                 luci.sys.process.setuser(track.setuser)
211         end
212
213         if c and type(c.target) == "function" then
214                 context.dispatched = c
215                 stat, mod = luci.util.copcall(require, c.module)
216                 if stat then
217                         luci.util.updfenv(c.target, mod)
218                 end
219                 
220                 stat, err = luci.util.copcall(c.target, unpack(args))
221                 if not stat then
222                         error500(err)
223                 end
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         index = {}
264
265         local cache = nil 
266         
267         local controllers = luci.util.combine(
268                 luci.fs.glob(path .. "*" .. suffix) or {},
269                 luci.fs.glob(path .. "*/*" .. suffix) or {}
270         )
271         
272         if indexcache then
273                 cache = luci.fs.mtime(indexcache)
274                 
275                 if not cache then
276                         luci.fs.mkdir(indexcache)
277                         luci.fs.chmod(indexcache, "a=,u=rwx")
278                         cache = luci.fs.mtime(indexcache)
279                 end
280         end
281
282         for i,c in ipairs(controllers) do
283                 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
284                 local cachefile
285                 local stime
286                 local ctime
287                 
288                 if cache then
289                         cachefile = indexcache .. "/" .. module
290                         stime = luci.fs.mtime(c) or 0
291                         ctime = luci.fs.mtime(cachefile) or 0
292                 end
293                 
294                 if not cache or stime > ctime then 
295                         stat, mod = luci.util.copcall(require, module)
296         
297                         if stat and mod and type(mod.index) == "function" then
298                                 index[module] = mod.index
299                                 
300                                 if cache then
301                                         luci.fs.writefile(cachefile, luci.util.get_bytecode(mod.index))
302                                 end
303                         end
304                 else
305                         index[module] = loadfile(cachefile)
306                 end
307         end
308 end
309
310 --- Create the dispatching tree from the index.
311 -- Build the index before if it does not exist yet.
312 function createtree()
313         if not index then
314                 createindex()
315         end
316         
317         context.tree = {nodes={}}
318         require("luci.i18n")
319                 
320         -- Load default translation
321         luci.i18n.loadc("default")
322         
323         local scope = luci.util.clone(_G)
324         for k,v in pairs(luci.dispatcher) do
325                 if type(v) == "function" then
326                         scope[k] = v
327                 end
328         end
329
330         for k, v in pairs(index) do
331                 scope._NAME = k
332                 setfenv(v, scope)
333
334                 local stat, err = luci.util.copcall(v)
335                 if not stat then
336                         error500("createtree failed: " .. k .. ": " .. err)
337                         luci.http.close()
338                         os.exit(1)
339                 end
340         end
341 end
342
343 --- Clone a node of the dispatching tree to another position.
344 -- @param       path    Virtual path destination
345 -- @param       clone   Virtual path source
346 -- @param       title   Destination node title (optional)
347 -- @param       order   Destination node order value (optional)
348 -- @return                      Dispatching tree node
349 function assign(path, clone, title, order)
350         local obj  = node(unpack(path))
351         obj.nodes  = nil
352         obj.module = nil
353         
354         obj.title = title
355         obj.order = order
356         
357         local c = context.tree
358         for k, v in ipairs(clone) do
359                 if not c.nodes[v] then
360                         c.nodes[v] = {nodes={}}
361                 end
362
363                 c = c.nodes[v]
364         end
365         
366         setmetatable(obj, {__index = c})
367         
368         return obj
369 end
370
371 --- Create a new dispatching node and define common parameters.
372 -- @param       path    Virtual path
373 -- @param       target  Target function to call when dispatched. 
374 -- @param       title   Destination node title
375 -- @param       order   Destination node order value (optional)
376 -- @return                      Dispatching tree node
377 function entry(path, target, title, order)
378         local c = node(unpack(path))
379         
380         c.target = target
381         c.title  = title
382         c.order  = order
383         c.module = getfenv(2)._NAME
384
385         return c
386 end
387
388 --- Fetch or create a new dispatching node.
389 -- @param       ...             Virtual path
390 -- @return                      Dispatching tree node
391 function node(...)
392         local c = context.tree
393         arg.n = nil
394
395         for k,v in ipairs(arg) do
396                 if not c.nodes[v] then
397                         c.nodes[v] = {nodes={}, auto=true}
398                 end
399
400                 c = c.nodes[v]
401         end
402
403         c.module = getfenv(2)._NAME
404         c.path = arg
405         c.auto = nil
406
407         return c
408 end
409
410 -- Subdispatchers --
411
412 --- Create a redirect to another dispatching node.
413 -- @param       ...             Virtual path destination
414 function alias(...)
415         local req = arg
416         return function()
417                 dispatch(req)
418         end
419 end
420
421 --- Rewrite the first x path values of the request.
422 -- @param       n               Number of path values to replace
423 -- @param       ...             Virtual path to replace removed path values with
424 function rewrite(n, ...)
425         local req = arg
426         return function()
427                 for i=1,n do 
428                         table.remove(context.path, 1)
429                 end
430                 
431                 for i,r in ipairs(req) do
432                         table.insert(context.path, i, r)
433                 end
434                 
435                 dispatch()
436         end
437 end
438
439 --- Create a function-call dispatching target.
440 -- @param       name    Target function of local controller 
441 -- @param       ...             Additional parameters passed to the function
442 function call(name, ...)
443         local argv = {...}
444         return function() return getfenv()[name](unpack(argv)) end
445 end
446
447 --- Create a template render dispatching target.
448 -- @param       name    Template to be rendered
449 function template(name)
450         require("luci.template")
451         return function() luci.template.render(name) end
452 end
453
454 --- Create a CBI model dispatching target.
455 -- @param       model   CBI model tpo be rendered
456 function cbi(model)
457         require("luci.cbi")
458         require("luci.template")
459
460         return function(...)
461                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
462                 if not stat then
463                         error500(maps)
464                         return true
465                 end
466
467                 for i, res in ipairs(maps) do
468                         local stat, err = luci.util.copcall(res.parse, res)
469                         if not stat then
470                                 error500(err)
471                                 return true
472                         end
473                 end
474
475                 luci.template.render("cbi/header")
476                 for i, res in ipairs(maps) do
477                         res:render()
478                 end
479                 luci.template.render("cbi/footer")
480         end
481 end
482
483 --- Create a CBI form model dispatching target.
484 -- @param       model   CBI form model tpo be rendered
485 function form(model)
486         require("luci.cbi")
487         require("luci.template")
488
489         return function(...)
490                 local stat, maps = luci.util.copcall(luci.cbi.load, model, ...)
491                 if not stat then
492                         error500(maps)
493                         return true
494                 end
495
496                 for i, res in ipairs(maps) do
497                         local stat, err = luci.util.copcall(res.parse, res)
498                         if not stat then
499                                 error500(err)
500                                 return true
501                         end
502                 end
503
504                 luci.template.render("header")
505                 for i, res in ipairs(maps) do
506                         res:render()
507                 end
508                 luci.template.render("footer")
509         end
510 end