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 _ordered_children(node)
44 local name, child, children = nil, nil, {}
46 for name, child in pairs(node.nodes) do
47 children[#children+1] = {
50 order = child.order or 100
54 table.sort(children, function(a, b)
55 if a.order == b.order then
56 return a.name < b.name
58 return a.order < b.order
65 local function dependencies_satisfied(node)
66 if type(node.file_depends) == "table" then
67 for _, file in ipairs(node.file_depends) do
68 local ftype = fs.stat(file, "type")
69 if ftype == "dir" then
71 for e in (fs.dir(file) or function() end) do
77 elseif ftype == nil then
83 if type(node.uci_depends) == "table" then
84 for config, expect_sections in pairs(node.uci_depends) do
85 if type(expect_sections) == "table" then
86 for section, expect_options in pairs(expect_sections) do
87 if type(expect_options) == "table" then
88 for option, expect_value in pairs(expect_options) do
89 local val = uci:get(config, section, option)
90 if expect_value == true and val == nil then
92 elseif type(expect_value) == "string" then
93 if type(val) == "table" then
95 for _, subval in ipairs(val) do
96 if subval == expect_value then
103 elseif val ~= expect_value then
109 local val = uci:get(config, section)
110 if expect_options == true and val == nil then
112 elseif type(expect_options) == "string" and val ~= expect_options then
117 elseif expect_sections == true then
118 if not uci:get_first(config) then
128 function node_visible(node)
131 (not dependencies_satisfied(node)) or
132 (not node.title or #node.title == 0) or
133 (not node.target or node.hidden == true) or
134 (type(node.target) == "table" and node.target.type == "firstchild" and
135 (type(node.nodes) ~= "table" or not next(node.nodes)))
141 function node_childs(node)
145 for _, child in ipairs(_ordered_children(node)) do
146 if node_visible(child.node) then
147 rv[#rv+1] = child.name
155 function error404(message)
156 http.status(404, "Not Found")
157 message = message or "Not Found"
159 local function render()
160 local template = require "luci.template"
161 template.render("error404")
164 if not util.copcall(render) then
165 http.prepare_content("text/plain")
172 function error500(message)
174 if not context.template_header_sent then
175 http.status(500, "Internal Server Error")
176 http.prepare_content("text/plain")
179 require("luci.template")
180 if not util.copcall(luci.template.render, "error500", {message=message}) then
181 http.prepare_content("text/plain")
188 function httpdispatch(request, prefix)
189 http.context.request = request
194 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
197 for _, node in ipairs(prefix) do
203 for node in pathinfo:gmatch("[^/%z]+") do
207 local stat, err = util.coxpcall(function()
208 dispatch(context.request)
213 --context._disable_memtrace()
216 local function require_post_security(target, args)
217 if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then
218 return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args)
221 if type(target) == "table" then
222 if type(target.post) == "table" then
223 local param_name, required_val, request_val
225 for param_name, required_val in pairs(target.post) do
226 request_val = http.formvalue(param_name)
228 if (type(required_val) == "string" and
229 request_val ~= required_val) or
230 (required_val == true and request_val == nil)
239 return (target.post == true)
245 function test_post_security()
246 if http.getenv("REQUEST_METHOD") ~= "POST" then
247 http.status(405, "Method Not Allowed")
248 http.header("Allow", "POST")
252 if http.formvalue("token") ~= context.authtoken then
253 http.status(403, "Forbidden")
254 luci.template.render("csrftoken")
261 local function session_retrieve(sid, allowed_users)
262 local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
264 if type(sdat) == "table" and
265 type(sdat.values) == "table" and
266 type(sdat.values.token) == "string" and
267 (not allowed_users or
268 util.contains(allowed_users, sdat.values.username))
270 uci:set_session_id(sid)
271 return sid, sdat.values
277 local function session_setup(user, pass, allowed_users)
278 if util.contains(allowed_users, user) then
279 local login = util.ubus("session", "login", {
282 timeout = tonumber(luci.config.sauth.sessiontime)
285 local rp = context.requestpath
286 and table.concat(context.requestpath, "/") or ""
288 if type(login) == "table" and
289 type(login.ubus_rpc_session) == "string"
291 util.ubus("session", "set", {
292 ubus_rpc_session = login.ubus_rpc_session,
293 values = { token = sys.uniqueid(16) }
296 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
297 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
299 return session_retrieve(login.ubus_rpc_session)
302 io.stderr:write("luci: failed login on /%s for %s from %s\n"
303 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
309 function dispatch(request)
310 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
314 local conf = require "luci.config"
316 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
318 local i18n = require "luci.i18n"
319 local lang = conf.main.lang or "auto"
320 if lang == "auto" then
321 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
322 for aclang in aclang:gmatch("[%w_-]+") do
323 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
324 if country and culture then
325 local cc = "%s_%s" %{ country, culture:lower() }
326 if conf.languages[cc] then
329 elseif conf.languages[country] then
333 elseif conf.languages[aclang] then
339 if lang == "auto" then
342 i18n.setlanguage(lang)
353 ctx.requestargs = ctx.requestargs or args
358 for i, s in ipairs(request) do
367 util.update(track, c)
375 for j=n+1, #request do
376 args[#args+1] = request[j]
377 freq[#freq+1] = request[j]
381 ctx.requestpath = ctx.requestpath or freq
384 -- Init template engine
385 if (c and c.index) or not track.notemplate then
386 local tpl = require("luci.template")
387 local media = track.mediaurlbase or luci.config.main.mediaurlbase
388 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
390 for name, theme in pairs(luci.config.themes) do
391 if name:sub(1,1) ~= "." and pcall(tpl.Template,
392 "themes/%s/header" % fs.basename(theme)) then
396 assert(media, "No valid theme found")
399 local function _ifattr(cond, key, val, noescape)
401 local env = getfenv(3)
402 local scope = (type(env.self) == "table") and env.self
403 if type(val) == "table" then
404 if not next(val) then
407 val = util.serialize_json(val)
411 val = tostring(val or
412 (type(env[key]) ~= "function" and env[key]) or
413 (scope and type(scope[key]) ~= "function" and scope[key]) or "")
415 if noescape ~= true then
416 val = util.pcdata(val)
419 return string.format(' %s="%s"', tostring(key), val)
425 tpl.context.viewns = setmetatable({
427 include = function(name) tpl.Template(name):render(getfenv(2)) end;
428 translate = i18n.translate;
429 translatef = i18n.translatef;
430 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
431 striptags = util.striptags;
432 pcdata = util.pcdata;
434 theme = fs.basename(media);
435 resource = luci.config.main.resourcebase;
436 ifattr = function(...) return _ifattr(...) end;
437 attr = function(...) return _ifattr(true, ...) end;
439 }, {__index=function(tbl, key)
440 if key == "controller" then
442 elseif key == "REQUEST_URI" then
443 return build_url(unpack(ctx.requestpath))
444 elseif key == "FULL_REQUEST_URI" then
445 local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") }
446 local query = http.getenv("QUERY_STRING")
447 if query and #query > 0 then
451 return table.concat(url, "")
452 elseif key == "token" then
455 return rawget(tbl, key) or _G[key]
460 track.dependent = (track.dependent ~= false)
461 assert(not track.dependent or not track.auto,
462 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
463 "has no parent node so the access to this location has been denied.\n" ..
464 "This is a software bug, please report this message at " ..
465 "https://github.com/openwrt/luci/issues"
468 if track.sysauth and not ctx.authsession then
469 local authen = track.sysauth_authenticator
470 local _, sid, sdat, default_user, allowed_users
472 if type(authen) == "string" and authen ~= "htmlauth" then
473 error500("Unsupported authenticator %q configured" % authen)
477 if type(track.sysauth) == "table" then
478 default_user, allowed_users = nil, track.sysauth
480 default_user, allowed_users = track.sysauth, { track.sysauth }
483 if type(authen) == "function" then
484 _, sid = authen(sys.user.checkpasswd, allowed_users)
486 sid = http.getcookie("sysauth")
489 sid, sdat = session_retrieve(sid, allowed_users)
491 if not (sid and sdat) and authen == "htmlauth" then
492 local user = http.getenv("HTTP_AUTH_USER")
493 local pass = http.getenv("HTTP_AUTH_PASS")
495 if user == nil and pass == nil then
496 user = http.formvalue("luci_username")
497 pass = http.formvalue("luci_password")
500 sid, sdat = session_setup(user, pass, allowed_users)
503 local tmpl = require "luci.template"
507 http.status(403, "Forbidden")
508 http.header("X-LuCI-Login-Required", "yes")
509 tmpl.render(track.sysauth_template or "sysauth", {
510 duser = default_user,
517 http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
518 sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
520 http.redirect(build_url(unpack(ctx.requestpath)))
523 if not sid or not sdat then
524 http.status(403, "Forbidden")
525 http.header("X-LuCI-Login-Required", "yes")
529 ctx.authsession = sid
530 ctx.authtoken = sdat.token
531 ctx.authuser = sdat.username
534 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
535 luci.http.status(200, "OK")
536 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
537 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
541 if c and require_post_security(c.target, args) then
542 if not test_post_security(c) then
547 if track.setgroup then
548 sys.process.setgroup(track.setgroup)
551 if track.setuser then
552 sys.process.setuser(track.setuser)
557 if type(c.target) == "function" then
559 elseif type(c.target) == "table" then
560 target = c.target.target
564 if c and (c.index or type(target) == "function") then
566 ctx.requested = ctx.requested or ctx.dispatched
569 if c and c.index then
570 local tpl = require "luci.template"
572 if util.copcall(tpl.render, "indexer", {}) then
577 if type(target) == "function" then
578 util.copcall(function()
579 local oldenv = getfenv(target)
580 local module = require(c.module)
581 local env = setmetatable({}, {__index=
584 return rawget(tbl, key) or module[key] or oldenv[key]
591 if type(c.target) == "table" then
592 ok, err = util.copcall(target, c.target, unpack(args))
594 ok, err = util.copcall(target, unpack(args))
597 error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
598 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
599 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
603 if not root or not root.target then
604 error404("No root node was registered, this usually happens if no module was installed.\n" ..
605 "Install luci-mod-admin-full and retry. " ..
606 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
608 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
609 "If this url belongs to an extension, make sure it is properly installed.\n" ..
610 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
615 function createindex()
616 local controllers = { }
617 local base = "%s/controller/" % util.libpath()
620 for path in (fs.glob("%s*.lua" % base) or function() end) do
621 controllers[#controllers+1] = path
624 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
625 controllers[#controllers+1] = path
629 local cachedate = fs.stat(indexcache, "mtime")
632 for _, obj in ipairs(controllers) do
633 local omtime = fs.stat(obj, "mtime")
634 realdate = (omtime and omtime > realdate) and omtime or realdate
637 if cachedate > realdate and sys.process.info("uid") == 0 then
639 sys.process.info("uid") == fs.stat(indexcache, "uid")
640 and fs.stat(indexcache, "modestr") == "rw-------",
641 "Fatal: Indexcache is not sane!"
644 index = loadfile(indexcache)()
652 for _, path in ipairs(controllers) do
653 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
654 local mod = require(modname)
656 "Invalid controller file found\n" ..
657 "The file '" .. path .. "' contains an invalid module line.\n" ..
658 "Please verify whether the module name is set to '" .. modname ..
659 "' - It must correspond to the file path!")
661 local idx = mod.index
662 assert(type(idx) == "function",
663 "Invalid controller file found\n" ..
664 "The file '" .. path .. "' contains no index() function.\n" ..
665 "Please make sure that the controller contains a valid " ..
666 "index function and verify the spelling!")
672 local f = nixio.open(indexcache, "w", 600)
673 f:writeall(util.get_bytecode(index))
678 -- Build the index before if it does not exist yet.
679 function createtree()
685 local tree = {nodes={}, inreq=true}
687 ctx.treecache = setmetatable({}, {__mode="v"})
690 local scope = setmetatable({}, {__index = luci.dispatcher})
692 for k, v in pairs(index) do
701 function assign(path, clone, title, order)
702 local obj = node(unpack(path))
709 setmetatable(obj, {__index = _create_node(clone)})
714 function entry(path, target, title, order)
715 local c = node(unpack(path))
720 c.module = getfenv(2)._NAME
725 -- enabling the node.
727 return _create_node({...})
731 local c = _create_node({...})
733 c.module = getfenv(2)._NAME
740 local i, path = nil, {}
741 for i = 1, select('#', ...) do
742 local name, arg = nil, tostring(select(i, ...))
743 for name in arg:gmatch("[^/]+") do
748 for i = #path, 1, -1 do
749 local node = context.treecache[table.concat(path, ".", 1, i)]
750 if node and (i == #path or node.leaf) then
751 return node, build_url(unpack(path))
756 function _create_node(path)
761 local name = table.concat(path, ".")
762 local c = context.treecache[name]
765 local last = table.remove(path)
766 local parent = _create_node(path)
768 c = {nodes={}, auto=true, inreq=true}
771 for _, n in ipairs(path) do
772 if context.path[_] ~= n then
778 c.inreq = c.inreq and (context.path[#path + 1] == last)
780 parent.nodes[last] = c
781 context.treecache[name] = c
789 function _find_eligible_node(root, prefix, deep, types, descend)
790 local children = _ordered_children(root)
792 if not root.leaf and deep ~= nil then
793 local sub_path = { unpack(prefix) }
795 if deep == false then
800 for _, child in ipairs(children) do
801 sub_path[#prefix+1] = child.name
803 local res_path = _find_eligible_node(child.node, sub_path,
814 (type(root.target) == "table" and
815 util.contains(types, root.target.type)))
821 function _find_node(recurse, types)
822 local path = { unpack(context.path) }
823 local name = table.concat(path, ".")
824 local node = context.treecache[name]
826 path = _find_eligible_node(node, path, recurse, types)
831 require "luci.template".render("empty_node_placeholder")
835 function _firstchild()
836 return _find_node(false, nil)
839 function firstchild()
840 return { type = "firstchild", target = _firstchild }
843 function _firstnode()
844 return _find_node(true, { "cbi", "form", "template", "arcombine" })
848 return { type = "firstnode", target = _firstnode }
854 for _, r in ipairs({...}) do
862 function rewrite(n, ...)
865 local dispatched = util.clone(context.dispatched)
868 table.remove(dispatched, 1)
871 for i, r in ipairs(req) do
872 table.insert(dispatched, i, r)
875 for _, r in ipairs({...}) do
876 dispatched[#dispatched+1] = r
884 local function _call(self, ...)
885 local func = getfenv()[self.name]
887 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
889 assert(type(func) == "function",
890 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
891 'of type "' .. type(func) .. '".')
893 if #self.argv > 0 then
894 return func(unpack(self.argv), ...)
900 function call(name, ...)
901 return {type = "call", argv = {...}, name = name, target = _call}
904 function post_on(params, name, ...)
915 return post_on(true, ...)
919 local _template = function(self, ...)
920 require "luci.template".render(self.view)
923 function template(name)
924 return {type = "template", view = name, target = _template}
928 local _view = function(self, ...)
929 require "luci.template".render("view", { view = self.view })
933 return {type = "view", view = name, target = _view}
937 local function _cbi(self, ...)
938 local cbi = require "luci.cbi"
939 local tpl = require "luci.template"
940 local http = require "luci.http"
942 local config = self.config or {}
943 local maps = cbi.load(self.model, ...)
948 for i, res in ipairs(maps) do
949 if util.instanceof(res, cbi.SimpleForm) then
950 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
953 io.stderr:write("please change %s to use the form() action instead.\n"
954 % table.concat(context.request, "/"))
958 local cstate = res:parse()
959 if cstate and (not state or cstate < state) then
964 local function _resolve_path(path)
965 return type(path) == "table" and build_url(unpack(path)) or path
968 if config.on_valid_to and state and state > 0 and state < 2 then
969 http.redirect(_resolve_path(config.on_valid_to))
973 if config.on_changed_to and state and state > 1 then
974 http.redirect(_resolve_path(config.on_changed_to))
978 if config.on_success_to and state and state > 0 then
979 http.redirect(_resolve_path(config.on_success_to))
983 if config.state_handler then
984 if not config.state_handler(state, maps) then
989 http.header("X-CBI-State", state or 0)
991 if not config.noheader then
992 tpl.render("cbi/header", {state = state})
997 local applymap = false
998 local pageaction = true
999 local parsechain = { }
1001 for i, res in ipairs(maps) do
1002 if res.apply_needed and res.parsechain then
1004 for _, c in ipairs(res.parsechain) do
1005 parsechain[#parsechain+1] = c
1010 if res.redirect then
1011 redirect = redirect or res.redirect
1014 if res.pageaction == false then
1019 messages = messages or { }
1020 messages[#messages+1] = res.message
1024 for i, res in ipairs(maps) do
1026 firstmap = (i == 1),
1027 redirect = redirect,
1028 messages = messages,
1029 pageaction = pageaction,
1030 parsechain = parsechain
1034 if not config.nofooter then
1035 tpl.render("cbi/footer", {
1037 pageaction = pageaction,
1038 redirect = redirect,
1040 autoapply = config.autoapply,
1041 trigger_apply = applymap
1046 function cbi(model, config)
1049 post = { ["cbi.submit"] = true },
1057 local function _arcombine(self, ...)
1059 local target = #argv > 0 and self.targets[2] or self.targets[1]
1060 setfenv(target.target, self.env)
1061 target:target(unpack(argv))
1064 function arcombine(trg1, trg2)
1065 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
1069 local function _form(self, ...)
1070 local cbi = require "luci.cbi"
1071 local tpl = require "luci.template"
1072 local http = require "luci.http"
1074 local maps = luci.cbi.load(self.model, ...)
1078 for i, res in ipairs(maps) do
1079 local cstate = res:parse()
1080 if cstate and (not state or cstate < state) then
1085 http.header("X-CBI-State", state or 0)
1086 tpl.render("header")
1087 for i, res in ipairs(maps) do
1090 tpl.render("footer")
1093 function form(model)
1096 post = { ["cbi.submit"] = true },
1102 translate = i18n.translate
1104 -- This function does not actually translate the given argument but
1105 -- is used by build/i18n-scan.pl to find translatable entries.