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"
24 function build_url(...)
26 local url = { http.getenv("SCRIPT_NAME") or "" }
29 for _, p in ipairs(path) do
30 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
40 return table.concat(url, "")
43 function node_visible(node)
46 (not node.title or #node.title == 0) or
47 (not node.target or node.hidden == true) or
48 (type(node.target) == "table" and node.target.type == "firstchild" and
49 (type(node.nodes) ~= "table" or not next(node.nodes)))
55 function node_childs(node)
59 for k, v in util.spairs(node.nodes,
61 return (node.nodes[a].order or 100)
62 < (node.nodes[b].order or 100)
65 if node_visible(v) then
74 function error404(message)
75 http.status(404, "Not Found")
76 message = message or "Not Found"
78 require("luci.template")
79 if not util.copcall(luci.template.render, "error404") then
80 http.prepare_content("text/plain")
86 function error500(message)
88 if not context.template_header_sent then
89 http.status(500, "Internal Server Error")
90 http.prepare_content("text/plain")
93 require("luci.template")
94 if not util.copcall(luci.template.render, "error500", {message=message}) then
95 http.prepare_content("text/plain")
102 function httpdispatch(request, prefix)
103 http.context.request = request
108 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
111 for _, node in ipairs(prefix) do
116 for node in pathinfo:gmatch("[^/]+") do
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
126 --context._disable_memtrace()
129 local function require_post_security(target)
130 if type(target) == "table" then
131 if type(target.post) == "table" then
132 local param_name, required_val, request_val
134 for param_name, required_val in pairs(target.post) do
135 request_val = http.formvalue(param_name)
137 if (type(required_val) == "string" and
138 request_val ~= required_val) or
139 (required_val == true and
140 (request_val == nil or request_val == ""))
149 return (target.post == true)
155 function test_post_security()
156 if http.getenv("REQUEST_METHOD") ~= "POST" then
157 http.status(405, "Method Not Allowed")
158 http.header("Allow", "POST")
162 if http.formvalue("token") ~= context.authtoken then
163 http.status(403, "Forbidden")
164 luci.template.render("csrftoken")
171 local function session_retrieve(sid, allowed_users)
172 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
174 if type(sdat) == "table" and
175 type(sdat.values) == "table" and
176 type(sdat.values.token) == "string" and
177 (not allowed_users or
178 util.contains(allowed_users, sdat.values.username))
180 return sid, sdat.values
186 local function session_setup(user, pass, allowed_users)
187 if util.contains(allowed_users, user) then
188 local login = util.ubus("session", "login", {
191 timeout = tonumber(luci.config.sauth.sessiontime)
194 local rp = context.requestpath
195 and table.concat(context.requestpath, "/") or ""
197 if type(login) == "table" and
198 type(login.ubus_rpc_session) == "string"
200 util.ubus("session", "set", {
201 ubus_rpc_session = login.ubus_rpc_session,
202 values = { token = sys.uniqueid(16) }
205 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
206 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
208 return session_retrieve(login.ubus_rpc_session)
211 io.stderr:write("luci: failed login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
218 function dispatch(request)
219 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
223 local conf = require "luci.config"
225 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
227 local i18n = require "luci.i18n"
228 local lang = conf.main.lang or "auto"
229 if lang == "auto" then
230 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
231 for aclang in aclang:gmatch("[%w_-]+") do
232 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
233 if country and culture then
234 local cc = "%s_%s" %{ country, culture:lower() }
235 if conf.languages[cc] then
238 elseif conf.languages[country] then
242 elseif conf.languages[aclang] then
248 if lang == "auto" then
251 i18n.setlanguage(lang)
262 ctx.requestargs = ctx.requestargs or args
267 for i, s in ipairs(request) do
276 util.update(track, c)
284 for j=n+1, #request do
285 args[#args+1] = request[j]
286 freq[#freq+1] = request[j]
290 ctx.requestpath = ctx.requestpath or freq
294 i18n.loadc(track.i18n)
297 -- Init template engine
298 if (c and c.index) or not track.notemplate then
299 local tpl = require("luci.template")
300 local media = track.mediaurlbase or luci.config.main.mediaurlbase
301 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
303 for name, theme in pairs(luci.config.themes) do
304 if name:sub(1,1) ~= "." and pcall(tpl.Template,
305 "themes/%s/header" % fs.basename(theme)) then
309 assert(media, "No valid theme found")
312 local function _ifattr(cond, key, val)
314 local env = getfenv(3)
315 local scope = (type(env.self) == "table") and env.self
316 if type(val) == "table" then
317 if not next(val) then
320 val = util.serialize_json(val)
323 return string.format(
324 ' %s="%s"', tostring(key),
325 util.pcdata(tostring( val
326 or (type(env[key]) ~= "function" and env[key])
327 or (scope and type(scope[key]) ~= "function" and scope[key])
335 tpl.context.viewns = setmetatable({
337 include = function(name) tpl.Template(name):render(getfenv(2)) end;
338 translate = i18n.translate;
339 translatef = i18n.translatef;
340 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
341 striptags = util.striptags;
342 pcdata = util.pcdata;
344 theme = fs.basename(media);
345 resource = luci.config.main.resourcebase;
346 ifattr = function(...) return _ifattr(...) end;
347 attr = function(...) return _ifattr(true, ...) end;
349 }, {__index=function(tbl, key)
350 if key == "controller" then
352 elseif key == "REQUEST_URI" then
353 return build_url(unpack(ctx.requestpath))
354 elseif key == "FULL_REQUEST_URI" then
355 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
356 local query = http.getenv("QUERY_STRING")
357 if query and #query > 0 then
361 return table.concat(url, "")
362 elseif key == "token" then
365 return rawget(tbl, key) or _G[key]
370 track.dependent = (track.dependent ~= false)
371 assert(not track.dependent or not track.auto,
372 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
373 "has no parent node so the access to this location has been denied.\n" ..
374 "This is a software bug, please report this message at " ..
375 "https://github.com/openwrt/luci/issues"
378 if track.sysauth and not ctx.authsession then
379 local authen = track.sysauth_authenticator
380 local _, sid, sdat, default_user, allowed_users
382 if type(authen) == "string" and authen ~= "htmlauth" then
383 error500("Unsupported authenticator %q configured" % authen)
387 if type(track.sysauth) == "table" then
388 default_user, allowed_users = nil, track.sysauth
390 default_user, allowed_users = track.sysauth, { track.sysauth }
393 if type(authen) == "function" then
394 _, sid = authen(sys.user.checkpasswd, allowed_users)
396 sid = http.getcookie("sysauth")
399 sid, sdat = session_retrieve(sid, allowed_users)
401 if not (sid and sdat) and authen == "htmlauth" then
402 local user = http.getenv("HTTP_AUTH_USER")
403 local pass = http.getenv("HTTP_AUTH_PASS")
405 if user == nil and pass == nil then
406 user = http.formvalue("luci_username")
407 pass = http.formvalue("luci_password")
410 sid, sdat = session_setup(user, pass, allowed_users)
413 local tmpl = require "luci.template"
417 http.status(403, "Forbidden")
418 tmpl.render(track.sysauth_template or "sysauth", {
419 duser = default_user,
426 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
427 http.redirect(build_url(unpack(ctx.requestpath)))
430 if not sid or not sdat then
431 http.status(403, "Forbidden")
435 ctx.authsession = sid
436 ctx.authtoken = sdat.token
437 ctx.authuser = sdat.username
440 if c and require_post_security(c.target) then
441 if not test_post_security(c) then
446 if track.setgroup then
447 sys.process.setgroup(track.setgroup)
450 if track.setuser then
451 sys.process.setuser(track.setuser)
456 if type(c.target) == "function" then
458 elseif type(c.target) == "table" then
459 target = c.target.target
463 if c and (c.index or type(target) == "function") then
465 ctx.requested = ctx.requested or ctx.dispatched
468 if c and c.index then
469 local tpl = require "luci.template"
471 if util.copcall(tpl.render, "indexer", {}) then
476 if type(target) == "function" then
477 util.copcall(function()
478 local oldenv = getfenv(target)
479 local module = require(c.module)
480 local env = setmetatable({}, {__index=
483 return rawget(tbl, key) or module[key] or oldenv[key]
490 if type(c.target) == "table" then
491 ok, err = util.copcall(target, c.target, unpack(args))
493 ok, err = util.copcall(target, unpack(args))
496 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
497 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
498 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
501 if not root or not root.target then
502 error404("No root node was registered, this usually happens if no module was installed.\n" ..
503 "Install luci-mod-admin-full and retry. " ..
504 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
506 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
507 "If this url belongs to an extension, make sure it is properly installed.\n" ..
508 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
513 function createindex()
514 local controllers = { }
515 local base = "%s/controller/" % util.libpath()
518 for path in (fs.glob("%s*.lua" % base) or function() end) do
519 controllers[#controllers+1] = path
522 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
523 controllers[#controllers+1] = path
527 local cachedate = fs.stat(indexcache, "mtime")
530 for _, obj in ipairs(controllers) do
531 local omtime = fs.stat(obj, "mtime")
532 realdate = (omtime and omtime > realdate) and omtime or realdate
535 if cachedate > realdate and sys.process.info("uid") == 0 then
537 sys.process.info("uid") == fs.stat(indexcache, "uid")
538 and fs.stat(indexcache, "modestr") == "rw-------",
539 "Fatal: Indexcache is not sane!"
542 index = loadfile(indexcache)()
550 for _, path in ipairs(controllers) do
551 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
552 local mod = require(modname)
554 "Invalid controller file found\n" ..
555 "The file '" .. path .. "' contains an invalid module line.\n" ..
556 "Please verify whether the module name is set to '" .. modname ..
557 "' - It must correspond to the file path!")
559 local idx = mod.index
560 assert(type(idx) == "function",
561 "Invalid controller file found\n" ..
562 "The file '" .. path .. "' contains no index() function.\n" ..
563 "Please make sure that the controller contains a valid " ..
564 "index function and verify the spelling!")
570 local f = nixio.open(indexcache, "w", 600)
571 f:writeall(util.get_bytecode(index))
576 -- Build the index before if it does not exist yet.
577 function createtree()
583 local tree = {nodes={}, inreq=true}
586 ctx.treecache = setmetatable({}, {__mode="v"})
590 -- Load default translation
591 require "luci.i18n".loadc("base")
593 local scope = setmetatable({}, {__index = luci.dispatcher})
595 for k, v in pairs(index) do
601 local function modisort(a,b)
602 return modi[a].order < modi[b].order
605 for _, v in util.spairs(modi, modisort) do
606 scope._NAME = v.module
607 setfenv(v.func, scope)
614 function modifier(func, order)
615 context.modifiers[#context.modifiers+1] = {
623 function assign(path, clone, title, order)
624 local obj = node(unpack(path))
631 setmetatable(obj, {__index = _create_node(clone)})
636 function entry(path, target, title, order)
637 local c = node(unpack(path))
642 c.module = getfenv(2)._NAME
647 -- enabling the node.
649 return _create_node({...})
653 local c = _create_node({...})
655 c.module = getfenv(2)._NAME
661 function _create_node(path)
666 local name = table.concat(path, ".")
667 local c = context.treecache[name]
670 local last = table.remove(path)
671 local parent = _create_node(path)
673 c = {nodes={}, auto=true}
674 -- the node is "in request" if the request path matches
675 -- at least up to the length of the node path
676 if parent.inreq and context.path[#path+1] == last then
679 parent.nodes[last] = c
680 context.treecache[name] = c
687 function _firstchild()
688 local path = { unpack(context.path) }
689 local name = table.concat(path, ".")
690 local node = context.treecache[name]
693 if node and node.nodes and next(node.nodes) then
695 for k, v in pairs(node.nodes) do
697 (v.order or 100) < (node.nodes[lowest].order or 100)
704 assert(lowest ~= nil,
705 "The requested node contains no childs, unable to redispatch")
707 path[#path+1] = lowest
711 function firstchild()
712 return { type = "firstchild", target = _firstchild }
718 for _, r in ipairs({...}) do
726 function rewrite(n, ...)
729 local dispatched = util.clone(context.dispatched)
732 table.remove(dispatched, 1)
735 for i, r in ipairs(req) do
736 table.insert(dispatched, i, r)
739 for _, r in ipairs({...}) do
740 dispatched[#dispatched+1] = r
748 local function _call(self, ...)
749 local func = getfenv()[self.name]
751 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
753 assert(type(func) == "function",
754 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
755 'of type "' .. type(func) .. '".')
757 if #self.argv > 0 then
758 return func(unpack(self.argv), ...)
764 function call(name, ...)
765 return {type = "call", argv = {...}, name = name, target = _call}
768 function post_on(params, name, ...)
779 return post_on(true, ...)
783 local _template = function(self, ...)
784 require "luci.template".render(self.view)
787 function template(name)
788 return {type = "template", view = name, target = _template}
792 local function _cbi(self, ...)
793 local cbi = require "luci.cbi"
794 local tpl = require "luci.template"
795 local http = require "luci.http"
797 local config = self.config or {}
798 local maps = cbi.load(self.model, ...)
802 for i, res in ipairs(maps) do
804 local cstate = res:parse()
805 if cstate and (not state or cstate < state) then
810 local function _resolve_path(path)
811 return type(path) == "table" and build_url(unpack(path)) or path
814 if config.on_valid_to and state and state > 0 and state < 2 then
815 http.redirect(_resolve_path(config.on_valid_to))
819 if config.on_changed_to and state and state > 1 then
820 http.redirect(_resolve_path(config.on_changed_to))
824 if config.on_success_to and state and state > 0 then
825 http.redirect(_resolve_path(config.on_success_to))
829 if config.state_handler then
830 if not config.state_handler(state, maps) then
835 http.header("X-CBI-State", state or 0)
837 if not config.noheader then
838 tpl.render("cbi/header", {state = state})
843 local applymap = false
844 local pageaction = true
845 local parsechain = { }
847 for i, res in ipairs(maps) do
848 if res.apply_needed and res.parsechain then
850 for _, c in ipairs(res.parsechain) do
851 parsechain[#parsechain+1] = c
857 redirect = redirect or res.redirect
860 if res.pageaction == false then
865 messages = messages or { }
866 messages[#messages+1] = res.message
870 for i, res in ipairs(maps) do
876 pageaction = pageaction,
877 parsechain = parsechain
881 if not config.nofooter then
882 tpl.render("cbi/footer", {
884 pageaction = pageaction,
887 autoapply = config.autoapply
892 function cbi(model, config)
895 post = { ["cbi.submit"] = true },
903 local function _arcombine(self, ...)
905 local target = #argv > 0 and self.targets[2] or self.targets[1]
906 setfenv(target.target, self.env)
907 target:target(unpack(argv))
910 function arcombine(trg1, trg2)
911 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
915 local function _form(self, ...)
916 local cbi = require "luci.cbi"
917 local tpl = require "luci.template"
918 local http = require "luci.http"
920 local maps = luci.cbi.load(self.model, ...)
923 for i, res in ipairs(maps) do
924 local cstate = res:parse()
925 if cstate and (not state or cstate < state) then
930 http.header("X-CBI-State", state or 0)
932 for i, res in ipairs(maps) do
941 post = { ["cbi.submit"] = true },
947 translate = i18n.translate
949 -- This function does not actually translate the given argument but
950 -- is used by build/i18n-scan.pl to find translatable entries.