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 local function render()
79 local template = require "luci.template"
80 template.render("error404")
83 if not util.copcall(render) then
84 http.prepare_content("text/plain")
91 function error500(message)
93 if not context.template_header_sent then
94 http.status(500, "Internal Server Error")
95 http.prepare_content("text/plain")
98 require("luci.template")
99 if not util.copcall(luci.template.render, "error500", {message=message}) then
100 http.prepare_content("text/plain")
107 function httpdispatch(request, prefix)
108 http.context.request = request
113 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
116 for _, node in ipairs(prefix) do
122 for node in pathinfo:gmatch("[^/%z]+") do
126 local stat, err = util.coxpcall(function()
127 dispatch(context.request)
132 --context._disable_memtrace()
135 local function require_post_security(target)
136 if type(target) == "table" then
137 if type(target.post) == "table" then
138 local param_name, required_val, request_val
140 for param_name, required_val in pairs(target.post) do
141 request_val = http.formvalue(param_name)
143 if (type(required_val) == "string" and
144 request_val ~= required_val) or
145 (required_val == true and request_val == nil)
154 return (target.post == true)
160 function test_post_security()
161 if http.getenv("REQUEST_METHOD") ~= "POST" then
162 http.status(405, "Method Not Allowed")
163 http.header("Allow", "POST")
167 if http.formvalue("token") ~= context.authtoken then
168 http.status(403, "Forbidden")
169 luci.template.render("csrftoken")
176 local function session_retrieve(sid, allowed_users)
177 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
179 if type(sdat) == "table" and
180 type(sdat.values) == "table" and
181 type(sdat.values.token) == "string" and
182 (not allowed_users or
183 util.contains(allowed_users, sdat.values.username))
185 uci:set_session_id(sid)
186 return sid, sdat.values
192 local function session_setup(user, pass, allowed_users)
193 if util.contains(allowed_users, user) then
194 local login = util.ubus("session", "login", {
197 timeout = tonumber(luci.config.sauth.sessiontime)
200 local rp = context.requestpath
201 and table.concat(context.requestpath, "/") or ""
203 if type(login) == "table" and
204 type(login.ubus_rpc_session) == "string"
206 util.ubus("session", "set", {
207 ubus_rpc_session = login.ubus_rpc_session,
208 values = { token = sys.uniqueid(16) }
211 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
212 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
214 return session_retrieve(login.ubus_rpc_session)
217 io.stderr:write("luci: failed login on /%s for %s from %s\n"
218 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
224 function dispatch(request)
225 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
229 local conf = require "luci.config"
231 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
233 local i18n = require "luci.i18n"
234 local lang = conf.main.lang or "auto"
235 if lang == "auto" then
236 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
237 for aclang in aclang:gmatch("[%w_-]+") do
238 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
239 if country and culture then
240 local cc = "%s_%s" %{ country, culture:lower() }
241 if conf.languages[cc] then
244 elseif conf.languages[country] then
248 elseif conf.languages[aclang] then
254 if lang == "auto" then
257 i18n.setlanguage(lang)
268 ctx.requestargs = ctx.requestargs or args
273 for i, s in ipairs(request) do
282 util.update(track, c)
290 for j=n+1, #request do
291 args[#args+1] = request[j]
292 freq[#freq+1] = request[j]
296 ctx.requestpath = ctx.requestpath or freq
300 i18n.loadc(track.i18n)
303 -- Init template engine
304 if (c and c.index) or not track.notemplate then
305 local tpl = require("luci.template")
306 local media = track.mediaurlbase or luci.config.main.mediaurlbase
307 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
309 for name, theme in pairs(luci.config.themes) do
310 if name:sub(1,1) ~= "." and pcall(tpl.Template,
311 "themes/%s/header" % fs.basename(theme)) then
315 assert(media, "No valid theme found")
318 local function _ifattr(cond, key, val)
320 local env = getfenv(3)
321 local scope = (type(env.self) == "table") and env.self
322 if type(val) == "table" then
323 if not next(val) then
326 val = util.serialize_json(val)
329 return string.format(
330 ' %s="%s"', tostring(key),
331 util.pcdata(tostring( val
332 or (type(env[key]) ~= "function" and env[key])
333 or (scope and type(scope[key]) ~= "function" and scope[key])
341 tpl.context.viewns = setmetatable({
343 include = function(name) tpl.Template(name):render(getfenv(2)) end;
344 translate = i18n.translate;
345 translatef = i18n.translatef;
346 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
347 striptags = util.striptags;
348 pcdata = util.pcdata;
350 theme = fs.basename(media);
351 resource = luci.config.main.resourcebase;
352 ifattr = function(...) return _ifattr(...) end;
353 attr = function(...) return _ifattr(true, ...) end;
355 }, {__index=function(tbl, key)
356 if key == "controller" then
358 elseif key == "REQUEST_URI" then
359 return build_url(unpack(ctx.requestpath))
360 elseif key == "FULL_REQUEST_URI" then
361 local url = { http.getenv("SCRIPT_NAME") or "" , http.getenv("PATH_INFO") }
362 local query = http.getenv("QUERY_STRING")
363 if query and #query > 0 then
367 return table.concat(url, "")
368 elseif key == "token" then
371 return rawget(tbl, key) or _G[key]
376 track.dependent = (track.dependent ~= false)
377 assert(not track.dependent or not track.auto,
378 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
379 "has no parent node so the access to this location has been denied.\n" ..
380 "This is a software bug, please report this message at " ..
381 "https://github.com/openwrt/luci/issues"
384 if track.sysauth and not ctx.authsession then
385 local authen = track.sysauth_authenticator
386 local _, sid, sdat, default_user, allowed_users
388 if type(authen) == "string" and authen ~= "htmlauth" then
389 error500("Unsupported authenticator %q configured" % authen)
393 if type(track.sysauth) == "table" then
394 default_user, allowed_users = nil, track.sysauth
396 default_user, allowed_users = track.sysauth, { track.sysauth }
399 if type(authen) == "function" then
400 _, sid = authen(sys.user.checkpasswd, allowed_users)
402 sid = http.getcookie("sysauth")
405 sid, sdat = session_retrieve(sid, allowed_users)
407 if not (sid and sdat) and authen == "htmlauth" then
408 local user = http.getenv("HTTP_AUTH_USER")
409 local pass = http.getenv("HTTP_AUTH_PASS")
411 if user == nil and pass == nil then
412 user = http.formvalue("luci_username")
413 pass = http.formvalue("luci_password")
416 sid, sdat = session_setup(user, pass, allowed_users)
419 local tmpl = require "luci.template"
423 http.status(403, "Forbidden")
424 tmpl.render(track.sysauth_template or "sysauth", {
425 duser = default_user,
432 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
433 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
435 http.redirect(build_url(unpack(ctx.requestpath)))
438 if not sid or not sdat then
439 http.status(403, "Forbidden")
443 ctx.authsession = sid
444 ctx.authtoken = sdat.token
445 ctx.authuser = sdat.username
448 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
449 luci.http.status(200, "OK")
450 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
451 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
455 if c and require_post_security(c.target) then
456 if not test_post_security(c) then
461 if track.setgroup then
462 sys.process.setgroup(track.setgroup)
465 if track.setuser then
466 sys.process.setuser(track.setuser)
471 if type(c.target) == "function" then
473 elseif type(c.target) == "table" then
474 target = c.target.target
478 if c and (c.index or type(target) == "function") then
480 ctx.requested = ctx.requested or ctx.dispatched
483 if c and c.index then
484 local tpl = require "luci.template"
486 if util.copcall(tpl.render, "indexer", {}) then
491 if type(target) == "function" then
492 util.copcall(function()
493 local oldenv = getfenv(target)
494 local module = require(c.module)
495 local env = setmetatable({}, {__index=
498 return rawget(tbl, key) or module[key] or oldenv[key]
505 if type(c.target) == "table" then
506 ok, err = util.copcall(target, c.target, unpack(args))
508 ok, err = util.copcall(target, unpack(args))
511 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
512 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
513 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
516 if not root or not root.target then
517 error404("No root node was registered, this usually happens if no module was installed.\n" ..
518 "Install luci-mod-admin-full and retry. " ..
519 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
521 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
522 "If this url belongs to an extension, make sure it is properly installed.\n" ..
523 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
528 function createindex()
529 local controllers = { }
530 local base = "%s/controller/" % util.libpath()
533 for path in (fs.glob("%s*.lua" % base) or function() end) do
534 controllers[#controllers+1] = path
537 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
538 controllers[#controllers+1] = path
542 local cachedate = fs.stat(indexcache, "mtime")
545 for _, obj in ipairs(controllers) do
546 local omtime = fs.stat(obj, "mtime")
547 realdate = (omtime and omtime > realdate) and omtime or realdate
550 if cachedate > realdate and sys.process.info("uid") == 0 then
552 sys.process.info("uid") == fs.stat(indexcache, "uid")
553 and fs.stat(indexcache, "modestr") == "rw-------",
554 "Fatal: Indexcache is not sane!"
557 index = loadfile(indexcache)()
565 for _, path in ipairs(controllers) do
566 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
567 local mod = require(modname)
569 "Invalid controller file found\n" ..
570 "The file '" .. path .. "' contains an invalid module line.\n" ..
571 "Please verify whether the module name is set to '" .. modname ..
572 "' - It must correspond to the file path!")
574 local idx = mod.index
575 assert(type(idx) == "function",
576 "Invalid controller file found\n" ..
577 "The file '" .. path .. "' contains no index() function.\n" ..
578 "Please make sure that the controller contains a valid " ..
579 "index function and verify the spelling!")
585 local f = nixio.open(indexcache, "w", 600)
586 f:writeall(util.get_bytecode(index))
591 -- Build the index before if it does not exist yet.
592 function createtree()
598 local tree = {nodes={}, inreq=true}
601 ctx.treecache = setmetatable({}, {__mode="v"})
605 -- Load default translation
606 require "luci.i18n".loadc("base")
608 local scope = setmetatable({}, {__index = luci.dispatcher})
610 for k, v in pairs(index) do
616 local function modisort(a,b)
617 return modi[a].order < modi[b].order
620 for _, v in util.spairs(modi, modisort) do
621 scope._NAME = v.module
622 setfenv(v.func, scope)
629 function modifier(func, order)
630 context.modifiers[#context.modifiers+1] = {
638 function assign(path, clone, title, order)
639 local obj = node(unpack(path))
646 setmetatable(obj, {__index = _create_node(clone)})
651 function entry(path, target, title, order)
652 local c = node(unpack(path))
657 c.module = getfenv(2)._NAME
662 -- enabling the node.
664 return _create_node({...})
668 local c = _create_node({...})
670 c.module = getfenv(2)._NAME
677 local i, path = nil, {}
678 for i = 1, select('#', ...) do
679 local name, arg = nil, tostring(select(i, ...))
680 for name in arg:gmatch("[^/]+") do
685 for i = #path, 1, -1 do
686 local node = context.treecache[table.concat(path, ".", 1, i)]
687 if node and (i == #path or node.leaf) then
688 return node, build_url(unpack(path))
693 function _create_node(path)
698 local name = table.concat(path, ".")
699 local c = context.treecache[name]
702 local last = table.remove(path)
703 local parent = _create_node(path)
705 c = {nodes={}, auto=true}
706 -- the node is "in request" if the request path matches
707 -- at least up to the length of the node path
708 if parent.inreq and context.path[#path+1] == last then
711 parent.nodes[last] = c
712 context.treecache[name] = c
719 function _firstchild()
720 local path = { unpack(context.path) }
721 local name = table.concat(path, ".")
722 local node = context.treecache[name]
725 if node and node.nodes and next(node.nodes) then
727 for k, v in pairs(node.nodes) do
729 (v.order or 100) < (node.nodes[lowest].order or 100)
736 assert(lowest ~= nil,
737 "The requested node contains no childs, unable to redispatch")
739 path[#path+1] = lowest
743 function firstchild()
744 return { type = "firstchild", target = _firstchild }
750 for _, r in ipairs({...}) do
758 function rewrite(n, ...)
761 local dispatched = util.clone(context.dispatched)
764 table.remove(dispatched, 1)
767 for i, r in ipairs(req) do
768 table.insert(dispatched, i, r)
771 for _, r in ipairs({...}) do
772 dispatched[#dispatched+1] = r
780 local function _call(self, ...)
781 local func = getfenv()[self.name]
783 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
785 assert(type(func) == "function",
786 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
787 'of type "' .. type(func) .. '".')
789 if #self.argv > 0 then
790 return func(unpack(self.argv), ...)
796 function call(name, ...)
797 return {type = "call", argv = {...}, name = name, target = _call}
800 function post_on(params, name, ...)
811 return post_on(true, ...)
815 local _template = function(self, ...)
816 require "luci.template".render(self.view)
819 function template(name)
820 return {type = "template", view = name, target = _template}
824 local function _cbi(self, ...)
825 local cbi = require "luci.cbi"
826 local tpl = require "luci.template"
827 local http = require "luci.http"
829 local config = self.config or {}
830 local maps = cbi.load(self.model, ...)
835 for i, res in ipairs(maps) do
836 if util.instanceof(res, cbi.SimpleForm) then
837 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
840 io.stderr:write("please change %s to use the form() action instead.\n"
841 % table.concat(context.request, "/"))
845 local cstate = res:parse()
846 if cstate and (not state or cstate < state) then
851 local function _resolve_path(path)
852 return type(path) == "table" and build_url(unpack(path)) or path
855 if config.on_valid_to and state and state > 0 and state < 2 then
856 http.redirect(_resolve_path(config.on_valid_to))
860 if config.on_changed_to and state and state > 1 then
861 http.redirect(_resolve_path(config.on_changed_to))
865 if config.on_success_to and state and state > 0 then
866 http.redirect(_resolve_path(config.on_success_to))
870 if config.state_handler then
871 if not config.state_handler(state, maps) then
876 http.header("X-CBI-State", state or 0)
878 if not config.noheader then
879 tpl.render("cbi/header", {state = state})
884 local applymap = false
885 local pageaction = true
886 local parsechain = { }
888 local is_rollback, time_remaining = uci:rollback_pending()
890 for i, res in ipairs(maps) do
891 if res.apply_needed and res.parsechain then
893 for _, c in ipairs(res.parsechain) do
894 parsechain[#parsechain+1] = c
900 redirect = redirect or res.redirect
903 if res.pageaction == false then
908 messages = messages or { }
909 messages[#messages+1] = res.message
913 for i, res in ipairs(maps) do
917 confirmmap = (is_rollback and time_remaining or nil),
920 pageaction = pageaction,
921 parsechain = parsechain
925 if not config.nofooter then
926 tpl.render("cbi/footer", {
928 pageaction = pageaction,
931 autoapply = config.autoapply
936 function cbi(model, config)
939 post = { ["cbi.submit"] = true },
947 local function _arcombine(self, ...)
949 local target = #argv > 0 and self.targets[2] or self.targets[1]
950 setfenv(target.target, self.env)
951 target:target(unpack(argv))
954 function arcombine(trg1, trg2)
955 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
959 local function _form(self, ...)
960 local cbi = require "luci.cbi"
961 local tpl = require "luci.template"
962 local http = require "luci.http"
964 local maps = luci.cbi.load(self.model, ...)
968 for i, res in ipairs(maps) do
969 local cstate = res:parse()
970 if cstate and (not state or cstate < state) then
975 http.header("X-CBI-State", state or 0)
977 for i, res in ipairs(maps) do
986 post = { ["cbi.submit"] = true },
992 translate = i18n.translate
994 -- This function does not actually translate the given argument but
995 -- is used by build/i18n-scan.pl to find translatable entries.