1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
26 function build_url(...)
28 local url = { http.getenv("SCRIPT_NAME") or "" }
31 for _, p in ipairs(path) do
32 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
42 return table.concat(url, "")
45 function node_visible(node)
48 (not node.title or #node.title == 0) or
49 (not node.target or node.hidden == true) or
50 (type(node.target) == "table" and node.target.type == "firstchild" and
51 (type(node.nodes) ~= "table" or not next(node.nodes)))
57 function node_childs(node)
61 for k, v in util.spairs(node.nodes,
63 return (node.nodes[a].order or 100)
64 < (node.nodes[b].order or 100)
67 if node_visible(v) then
76 function error404(message)
77 http.status(404, "Not Found")
78 message = message or "Not Found"
80 require("luci.template")
81 if not util.copcall(luci.template.render, "error404") then
82 http.prepare_content("text/plain")
88 function error500(message)
90 if not context.template_header_sent then
91 http.status(500, "Internal Server Error")
92 http.prepare_content("text/plain")
95 require("luci.template")
96 if not util.copcall(luci.template.render, "error500", {message=message}) then
97 http.prepare_content("text/plain")
104 function authenticator.htmlauth(validator, accs, default)
105 local user = http.formvalue("luci_username")
106 local pass = http.formvalue("luci_password")
108 if user and validator(user, pass) then
113 require("luci.template")
115 http.status(403, "Forbidden")
116 luci.template.render("sysauth", {duser=default, fuser=user})
122 function httpdispatch(request, prefix)
123 http.context.request = request
128 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
131 for _, node in ipairs(prefix) do
136 for node in pathinfo:gmatch("[^/]+") do
140 local stat, err = util.coxpcall(function()
141 dispatch(context.request)
146 --context._disable_memtrace()
149 local function require_post_security(target)
150 if type(target) == "table" then
151 if type(target.post) == "table" then
152 local param_name, required_val, request_val
154 for param_name, required_val in pairs(target.post) do
155 request_val = http.formvalue(param_name)
157 if (type(required_val) == "string" and
158 request_val ~= required_val) or
159 (required_val == true and
160 (request_val == nil or request_val == ""))
169 return (target.post == true)
175 function test_post_security()
176 if http.getenv("REQUEST_METHOD") ~= "POST" then
177 http.status(405, "Method Not Allowed")
178 http.header("Allow", "POST")
182 if http.formvalue("token") ~= context.authtoken then
183 http.status(403, "Forbidden")
184 luci.template.render("csrftoken")
191 function dispatch(request)
192 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
196 local conf = require "luci.config"
198 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
200 local lang = conf.main.lang or "auto"
201 if lang == "auto" then
202 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
203 for lpat in aclang:gmatch("[%w-]+") do
204 lpat = lpat and lpat:gsub("-", "_")
205 if conf.languages[lpat] then
211 require "luci.i18n".setlanguage(lang)
222 ctx.requestargs = ctx.requestargs or args
227 for i, s in ipairs(request) do
236 util.update(track, c)
244 for j=n+1, #request do
245 args[#args+1] = request[j]
246 freq[#freq+1] = request[j]
250 ctx.requestpath = ctx.requestpath or freq
254 i18n.loadc(track.i18n)
257 -- Init template engine
258 if (c and c.index) or not track.notemplate then
259 local tpl = require("luci.template")
260 local media = track.mediaurlbase or luci.config.main.mediaurlbase
261 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
263 for name, theme in pairs(luci.config.themes) do
264 if name:sub(1,1) ~= "." and pcall(tpl.Template,
265 "themes/%s/header" % fs.basename(theme)) then
269 assert(media, "No valid theme found")
272 local function _ifattr(cond, key, val)
274 local env = getfenv(3)
275 local scope = (type(env.self) == "table") and env.self
276 return string.format(
277 ' %s="%s"', tostring(key),
278 util.pcdata(tostring( val
279 or (type(env[key]) ~= "function" and env[key])
280 or (scope and type(scope[key]) ~= "function" and scope[key])
288 tpl.context.viewns = setmetatable({
290 include = function(name) tpl.Template(name):render(getfenv(2)) end;
291 translate = i18n.translate;
292 translatef = i18n.translatef;
293 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
294 striptags = util.striptags;
295 pcdata = util.pcdata;
297 theme = fs.basename(media);
298 resource = luci.config.main.resourcebase;
299 ifattr = function(...) return _ifattr(...) end;
300 attr = function(...) return _ifattr(true, ...) end;
302 }, {__index=function(table, key)
303 if key == "controller" then
305 elseif key == "REQUEST_URI" then
306 return build_url(unpack(ctx.requestpath))
307 elseif key == "token" then
310 return rawget(table, key) or _G[key]
315 track.dependent = (track.dependent ~= false)
316 assert(not track.dependent or not track.auto,
317 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
318 "has no parent node so the access to this location has been denied.\n" ..
319 "This is a software bug, please report this message at " ..
320 "http://luci.subsignal.org/trac/newticket"
323 if track.sysauth then
324 local authen = type(track.sysauth_authenticator) == "function"
325 and track.sysauth_authenticator
326 or authenticator[track.sysauth_authenticator]
328 local def = (type(track.sysauth) == "string") and track.sysauth
329 local accs = def and {track.sysauth} or track.sysauth
330 local sess = ctx.authsession
332 sess = http.getcookie("sysauth")
333 sess = sess and sess:match("^[a-f0-9]*$")
336 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
343 local eu = http.getenv("HTTP_AUTH_USER")
344 local ep = http.getenv("HTTP_AUTH_PASS")
345 if eu and ep and sys.user.checkpasswd(eu, ep) then
346 authen = function() return eu end
350 if not util.contains(accs, user) then
352 local user, sess = authen(sys.user.checkpasswd, accs, def)
354 if not user or not util.contains(accs, user) then
358 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
360 token = sys.uniqueid(16)
361 util.ubus("session", "set", {
362 ubus_rpc_session = sdat.ubus_rpc_session,
366 section = sys.uniqueid(16)
369 sess = sdat.ubus_rpc_session
373 if sess and token then
374 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
376 ctx.authsession = sess
377 ctx.authtoken = token
380 http.redirect(build_url(unpack(ctx.requestpath)))
384 http.status(403, "Forbidden")
388 ctx.authsession = sess
389 ctx.authtoken = token
394 if c and require_post_security(c.target) then
395 if not test_post_security(c) then
400 if track.setgroup then
401 sys.process.setgroup(track.setgroup)
404 if track.setuser then
405 -- trigger ubus connection before dropping root privs
408 sys.process.setuser(track.setuser)
413 if type(c.target) == "function" then
415 elseif type(c.target) == "table" then
416 target = c.target.target
420 if c and (c.index or type(target) == "function") then
422 ctx.requested = ctx.requested or ctx.dispatched
425 if c and c.index then
426 local tpl = require "luci.template"
428 if util.copcall(tpl.render, "indexer", {}) then
433 if type(target) == "function" then
434 util.copcall(function()
435 local oldenv = getfenv(target)
436 local module = require(c.module)
437 local env = setmetatable({}, {__index=
440 return rawget(tbl, key) or module[key] or oldenv[key]
447 if type(c.target) == "table" then
448 ok, err = util.copcall(target, c.target, unpack(args))
450 ok, err = util.copcall(target, unpack(args))
453 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
454 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
455 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
458 if not root or not root.target then
459 error404("No root node was registered, this usually happens if no module was installed.\n" ..
460 "Install luci-mod-admin-full and retry. " ..
461 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
463 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
464 "If this url belongs to an extension, make sure it is properly installed.\n" ..
465 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
470 function createindex()
471 local controllers = { }
472 local base = "%s/controller/" % util.libpath()
475 for path in (fs.glob("%s*.lua" % base) or function() end) do
476 controllers[#controllers+1] = path
479 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
480 controllers[#controllers+1] = path
484 local cachedate = fs.stat(indexcache, "mtime")
487 for _, obj in ipairs(controllers) do
488 local omtime = fs.stat(obj, "mtime")
489 realdate = (omtime and omtime > realdate) and omtime or realdate
492 if cachedate > realdate and sys.process.info("uid") == 0 then
494 sys.process.info("uid") == fs.stat(indexcache, "uid")
495 and fs.stat(indexcache, "modestr") == "rw-------",
496 "Fatal: Indexcache is not sane!"
499 index = loadfile(indexcache)()
507 for _, path in ipairs(controllers) do
508 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
509 local mod = require(modname)
511 "Invalid controller file found\n" ..
512 "The file '" .. path .. "' contains an invalid module line.\n" ..
513 "Please verify whether the module name is set to '" .. modname ..
514 "' - It must correspond to the file path!")
516 local idx = mod.index
517 assert(type(idx) == "function",
518 "Invalid controller file found\n" ..
519 "The file '" .. path .. "' contains no index() function.\n" ..
520 "Please make sure that the controller contains a valid " ..
521 "index function and verify the spelling!")
527 local f = nixio.open(indexcache, "w", 600)
528 f:writeall(util.get_bytecode(index))
533 -- Build the index before if it does not exist yet.
534 function createtree()
540 local tree = {nodes={}, inreq=true}
543 ctx.treecache = setmetatable({}, {__mode="v"})
547 -- Load default translation
548 require "luci.i18n".loadc("base")
550 local scope = setmetatable({}, {__index = luci.dispatcher})
552 for k, v in pairs(index) do
558 local function modisort(a,b)
559 return modi[a].order < modi[b].order
562 for _, v in util.spairs(modi, modisort) do
563 scope._NAME = v.module
564 setfenv(v.func, scope)
571 function modifier(func, order)
572 context.modifiers[#context.modifiers+1] = {
580 function assign(path, clone, title, order)
581 local obj = node(unpack(path))
588 setmetatable(obj, {__index = _create_node(clone)})
593 function entry(path, target, title, order)
594 local c = node(unpack(path))
599 c.module = getfenv(2)._NAME
604 -- enabling the node.
606 return _create_node({...})
610 local c = _create_node({...})
612 c.module = getfenv(2)._NAME
618 function _create_node(path)
623 local name = table.concat(path, ".")
624 local c = context.treecache[name]
627 local last = table.remove(path)
628 local parent = _create_node(path)
630 c = {nodes={}, auto=true}
631 -- the node is "in request" if the request path matches
632 -- at least up to the length of the node path
633 if parent.inreq and context.path[#path+1] == last then
636 parent.nodes[last] = c
637 context.treecache[name] = c
644 function _firstchild()
645 local path = { unpack(context.path) }
646 local name = table.concat(path, ".")
647 local node = context.treecache[name]
650 if node and node.nodes and next(node.nodes) then
652 for k, v in pairs(node.nodes) do
654 (v.order or 100) < (node.nodes[lowest].order or 100)
661 assert(lowest ~= nil,
662 "The requested node contains no childs, unable to redispatch")
664 path[#path+1] = lowest
668 function firstchild()
669 return { type = "firstchild", target = _firstchild }
675 for _, r in ipairs({...}) do
683 function rewrite(n, ...)
686 local dispatched = util.clone(context.dispatched)
689 table.remove(dispatched, 1)
692 for i, r in ipairs(req) do
693 table.insert(dispatched, i, r)
696 for _, r in ipairs({...}) do
697 dispatched[#dispatched+1] = r
705 local function _call(self, ...)
706 local func = getfenv()[self.name]
708 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
710 assert(type(func) == "function",
711 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
712 'of type "' .. type(func) .. '".')
714 if #self.argv > 0 then
715 return func(unpack(self.argv), ...)
721 function call(name, ...)
722 return {type = "call", argv = {...}, name = name, target = _call}
725 function post_on(params, name, ...)
736 return post_on(true, ...)
740 local _template = function(self, ...)
741 require "luci.template".render(self.view)
744 function template(name)
745 return {type = "template", view = name, target = _template}
749 local function _cbi(self, ...)
750 local cbi = require "luci.cbi"
751 local tpl = require "luci.template"
752 local http = require "luci.http"
754 local config = self.config or {}
755 local maps = cbi.load(self.model, ...)
759 for i, res in ipairs(maps) do
761 local cstate = res:parse()
762 if cstate and (not state or cstate < state) then
767 local function _resolve_path(path)
768 return type(path) == "table" and build_url(unpack(path)) or path
771 if config.on_valid_to and state and state > 0 and state < 2 then
772 http.redirect(_resolve_path(config.on_valid_to))
776 if config.on_changed_to and state and state > 1 then
777 http.redirect(_resolve_path(config.on_changed_to))
781 if config.on_success_to and state and state > 0 then
782 http.redirect(_resolve_path(config.on_success_to))
786 if config.state_handler then
787 if not config.state_handler(state, maps) then
792 http.header("X-CBI-State", state or 0)
794 if not config.noheader then
795 tpl.render("cbi/header", {state = state})
800 local applymap = false
801 local pageaction = true
802 local parsechain = { }
804 for i, res in ipairs(maps) do
805 if res.apply_needed and res.parsechain then
807 for _, c in ipairs(res.parsechain) do
808 parsechain[#parsechain+1] = c
814 redirect = redirect or res.redirect
817 if res.pageaction == false then
822 messages = messages or { }
823 messages[#messages+1] = res.message
827 for i, res in ipairs(maps) do
833 pageaction = pageaction,
834 parsechain = parsechain
838 if not config.nofooter then
839 tpl.render("cbi/footer", {
841 pageaction = pageaction,
844 autoapply = config.autoapply
849 function cbi(model, config)
852 post = { ["cbi.submit"] = "1" },
860 local function _arcombine(self, ...)
862 local target = #argv > 0 and self.targets[2] or self.targets[1]
863 setfenv(target.target, self.env)
864 target:target(unpack(argv))
867 function arcombine(trg1, trg2)
868 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
872 local function _form(self, ...)
873 local cbi = require "luci.cbi"
874 local tpl = require "luci.template"
875 local http = require "luci.http"
877 local maps = luci.cbi.load(self.model, ...)
880 for i, res in ipairs(maps) do
881 local cstate = res:parse()
882 if cstate and (not state or cstate < state) then
887 http.header("X-CBI-State", state or 0)
889 for i, res in ipairs(maps) do
898 post = { ["cbi.submit"] = "1" },
904 translate = i18n.translate
906 -- This function does not actually translate the given argument but
907 -- is used by build/i18n-scan.pl to find translatable entries.