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 error500("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)"))
517 if not root or not root.target then
518 error404("No root node was registered, this usually happens if no module was installed.\n" ..
519 "Install luci-mod-admin-full and retry. " ..
520 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
522 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
523 "If this url belongs to an extension, make sure it is properly installed.\n" ..
524 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
529 function createindex()
530 local controllers = { }
531 local base = "%s/controller/" % util.libpath()
534 for path in (fs.glob("%s*.lua" % base) or function() end) do
535 controllers[#controllers+1] = path
538 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
539 controllers[#controllers+1] = path
543 local cachedate = fs.stat(indexcache, "mtime")
546 for _, obj in ipairs(controllers) do
547 local omtime = fs.stat(obj, "mtime")
548 realdate = (omtime and omtime > realdate) and omtime or realdate
551 if cachedate > realdate and sys.process.info("uid") == 0 then
553 sys.process.info("uid") == fs.stat(indexcache, "uid")
554 and fs.stat(indexcache, "modestr") == "rw-------",
555 "Fatal: Indexcache is not sane!"
558 index = loadfile(indexcache)()
566 for _, path in ipairs(controllers) do
567 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
568 local mod = require(modname)
570 "Invalid controller file found\n" ..
571 "The file '" .. path .. "' contains an invalid module line.\n" ..
572 "Please verify whether the module name is set to '" .. modname ..
573 "' - It must correspond to the file path!")
575 local idx = mod.index
576 assert(type(idx) == "function",
577 "Invalid controller file found\n" ..
578 "The file '" .. path .. "' contains no index() function.\n" ..
579 "Please make sure that the controller contains a valid " ..
580 "index function and verify the spelling!")
586 local f = nixio.open(indexcache, "w", 600)
587 f:writeall(util.get_bytecode(index))
592 -- Build the index before if it does not exist yet.
593 function createtree()
599 local tree = {nodes={}, inreq=true}
602 ctx.treecache = setmetatable({}, {__mode="v"})
606 -- Load default translation
607 require "luci.i18n".loadc("base")
609 local scope = setmetatable({}, {__index = luci.dispatcher})
611 for k, v in pairs(index) do
617 local function modisort(a,b)
618 return modi[a].order < modi[b].order
621 for _, v in util.spairs(modi, modisort) do
622 scope._NAME = v.module
623 setfenv(v.func, scope)
630 function modifier(func, order)
631 context.modifiers[#context.modifiers+1] = {
639 function assign(path, clone, title, order)
640 local obj = node(unpack(path))
647 setmetatable(obj, {__index = _create_node(clone)})
652 function entry(path, target, title, order)
653 local c = node(unpack(path))
658 c.module = getfenv(2)._NAME
663 -- enabling the node.
665 return _create_node({...})
669 local c = _create_node({...})
671 c.module = getfenv(2)._NAME
678 local i, path = nil, {}
679 for i = 1, select('#', ...) do
680 local name, arg = nil, tostring(select(i, ...))
681 for name in arg:gmatch("[^/]+") do
686 for i = #path, 1, -1 do
687 local node = context.treecache[table.concat(path, ".", 1, i)]
688 if node and (i == #path or node.leaf) then
689 return node, build_url(unpack(path))
694 function _create_node(path)
699 local name = table.concat(path, ".")
700 local c = context.treecache[name]
703 local last = table.remove(path)
704 local parent = _create_node(path)
706 c = {nodes={}, auto=true}
707 -- the node is "in request" if the request path matches
708 -- at least up to the length of the node path
709 if parent.inreq and context.path[#path+1] == last then
712 parent.nodes[last] = c
713 context.treecache[name] = c
720 function _firstchild()
721 local path = { unpack(context.path) }
722 local name = table.concat(path, ".")
723 local node = context.treecache[name]
726 if node and node.nodes and next(node.nodes) then
728 for k, v in pairs(node.nodes) do
730 (v.order or 100) < (node.nodes[lowest].order or 100)
737 assert(lowest ~= nil,
738 "The requested node contains no childs, unable to redispatch")
740 path[#path+1] = lowest
744 function firstchild()
745 return { type = "firstchild", target = _firstchild }
751 for _, r in ipairs({...}) do
759 function rewrite(n, ...)
762 local dispatched = util.clone(context.dispatched)
765 table.remove(dispatched, 1)
768 for i, r in ipairs(req) do
769 table.insert(dispatched, i, r)
772 for _, r in ipairs({...}) do
773 dispatched[#dispatched+1] = r
781 local function _call(self, ...)
782 local func = getfenv()[self.name]
784 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
786 assert(type(func) == "function",
787 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
788 'of type "' .. type(func) .. '".')
790 if #self.argv > 0 then
791 return func(unpack(self.argv), ...)
797 function call(name, ...)
798 return {type = "call", argv = {...}, name = name, target = _call}
801 function post_on(params, name, ...)
812 return post_on(true, ...)
816 local _template = function(self, ...)
817 require "luci.template".render(self.view)
820 function template(name)
821 return {type = "template", view = name, target = _template}
825 local function _cbi(self, ...)
826 local cbi = require "luci.cbi"
827 local tpl = require "luci.template"
828 local http = require "luci.http"
830 local config = self.config or {}
831 local maps = cbi.load(self.model, ...)
836 for i, res in ipairs(maps) do
837 if util.instanceof(res, cbi.SimpleForm) then
838 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
841 io.stderr:write("please change %s to use the form() action instead.\n"
842 % table.concat(context.request, "/"))
846 local cstate = res:parse()
847 if cstate and (not state or cstate < state) then
852 local function _resolve_path(path)
853 return type(path) == "table" and build_url(unpack(path)) or path
856 if config.on_valid_to and state and state > 0 and state < 2 then
857 http.redirect(_resolve_path(config.on_valid_to))
861 if config.on_changed_to and state and state > 1 then
862 http.redirect(_resolve_path(config.on_changed_to))
866 if config.on_success_to and state and state > 0 then
867 http.redirect(_resolve_path(config.on_success_to))
871 if config.state_handler then
872 if not config.state_handler(state, maps) then
877 http.header("X-CBI-State", state or 0)
879 if not config.noheader then
880 tpl.render("cbi/header", {state = state})
885 local applymap = false
886 local pageaction = true
887 local parsechain = { }
889 local is_rollback, time_remaining = uci:rollback_pending()
891 for i, res in ipairs(maps) do
892 if res.apply_needed and res.parsechain then
894 for _, c in ipairs(res.parsechain) do
895 parsechain[#parsechain+1] = c
901 redirect = redirect or res.redirect
904 if res.pageaction == false then
909 messages = messages or { }
910 messages[#messages+1] = res.message
914 for i, res in ipairs(maps) do
918 confirmmap = (is_rollback and time_remaining or nil),
921 pageaction = pageaction,
922 parsechain = parsechain
926 if not config.nofooter then
927 tpl.render("cbi/footer", {
929 pageaction = pageaction,
932 autoapply = config.autoapply
937 function cbi(model, config)
940 post = { ["cbi.submit"] = true },
948 local function _arcombine(self, ...)
950 local target = #argv > 0 and self.targets[2] or self.targets[1]
951 setfenv(target.target, self.env)
952 target:target(unpack(argv))
955 function arcombine(trg1, trg2)
956 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
960 local function _form(self, ...)
961 local cbi = require "luci.cbi"
962 local tpl = require "luci.template"
963 local http = require "luci.http"
965 local maps = luci.cbi.load(self.model, ...)
969 for i, res in ipairs(maps) do
970 local cstate = res:parse()
971 if cstate and (not state or cstate < state) then
976 http.header("X-CBI-State", state or 0)
978 for i, res in ipairs(maps) do
987 post = { ["cbi.submit"] = true },
993 translate = i18n.translate
995 -- This function does not actually translate the given argument but
996 -- is used by build/i18n-scan.pl to find translatable entries.