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 return sid, sdat.values
191 local function session_setup(user, pass, allowed_users)
192 if util.contains(allowed_users, user) then
193 local login = util.ubus("session", "login", {
196 timeout = tonumber(luci.config.sauth.sessiontime)
199 local rp = context.requestpath
200 and table.concat(context.requestpath, "/") or ""
202 if type(login) == "table" and
203 type(login.ubus_rpc_session) == "string"
205 util.ubus("session", "set", {
206 ubus_rpc_session = login.ubus_rpc_session,
207 values = { token = sys.uniqueid(16) }
210 io.stderr:write("luci: accepted login on /%s for %s from %s\n"
211 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
213 return session_retrieve(login.ubus_rpc_session)
216 io.stderr:write("luci: failed login on /%s for %s from %s\n"
217 %{ rp, user, http.getenv("REMOTE_ADDR") or "?" })
223 function dispatch(request)
224 --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
228 local conf = require "luci.config"
230 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
232 local i18n = require "luci.i18n"
233 local lang = conf.main.lang or "auto"
234 if lang == "auto" then
235 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
236 for aclang in aclang:gmatch("[%w_-]+") do
237 local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$")
238 if country and culture then
239 local cc = "%s_%s" %{ country, culture:lower() }
240 if conf.languages[cc] then
243 elseif conf.languages[country] then
247 elseif conf.languages[aclang] then
253 if lang == "auto" then
256 i18n.setlanguage(lang)
267 ctx.requestargs = ctx.requestargs or args
272 for i, s in ipairs(request) do
281 util.update(track, c)
289 for j=n+1, #request do
290 args[#args+1] = request[j]
291 freq[#freq+1] = request[j]
295 ctx.requestpath = ctx.requestpath or freq
299 i18n.loadc(track.i18n)
302 -- Init template engine
303 if (c and c.index) or not track.notemplate then
304 local tpl = require("luci.template")
305 local media = track.mediaurlbase or luci.config.main.mediaurlbase
306 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
308 for name, theme in pairs(luci.config.themes) do
309 if name:sub(1,1) ~= "." and pcall(tpl.Template,
310 "themes/%s/header" % fs.basename(theme)) then
314 assert(media, "No valid theme found")
317 local function _ifattr(cond, key, val)
319 local env = getfenv(3)
320 local scope = (type(env.self) == "table") and env.self
321 if type(val) == "table" then
322 if not next(val) then
325 val = util.serialize_json(val)
328 return string.format(
329 ' %s="%s"', tostring(key),
330 util.pcdata(tostring( val
331 or (type(env[key]) ~= "function" and env[key])
332 or (scope and type(scope[key]) ~= "function" and scope[key])
340 tpl.context.viewns = setmetatable({
342 include = function(name) tpl.Template(name):render(getfenv(2)) end;
343 translate = i18n.translate;
344 translatef = i18n.translatef;
345 export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
346 striptags = util.striptags;
347 pcdata = util.pcdata;
349 theme = fs.basename(media);
350 resource = luci.config.main.resourcebase;
351 ifattr = function(...) return _ifattr(...) end;
352 attr = function(...) return _ifattr(true, ...) end;
354 }, {__index=function(tbl, key)
355 if key == "controller" then
357 elseif key == "REQUEST_URI" then
358 return build_url(unpack(ctx.requestpath))
359 elseif key == "FULL_REQUEST_URI" then
360 local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") }
361 local query = http.getenv("QUERY_STRING")
362 if query and #query > 0 then
366 return table.concat(url, "")
367 elseif key == "token" then
370 return rawget(tbl, key) or _G[key]
375 track.dependent = (track.dependent ~= false)
376 assert(not track.dependent or not track.auto,
377 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
378 "has no parent node so the access to this location has been denied.\n" ..
379 "This is a software bug, please report this message at " ..
380 "https://github.com/openwrt/luci/issues"
383 if track.sysauth and not ctx.authsession then
384 local authen = track.sysauth_authenticator
385 local _, sid, sdat, default_user, allowed_users
387 if type(authen) == "string" and authen ~= "htmlauth" then
388 error500("Unsupported authenticator %q configured" % authen)
392 if type(track.sysauth) == "table" then
393 default_user, allowed_users = nil, track.sysauth
395 default_user, allowed_users = track.sysauth, { track.sysauth }
398 if type(authen) == "function" then
399 _, sid = authen(sys.user.checkpasswd, allowed_users)
401 sid = http.getcookie("sysauth")
404 sid, sdat = session_retrieve(sid, allowed_users)
406 if not (sid and sdat) and authen == "htmlauth" then
407 local user = http.getenv("HTTP_AUTH_USER")
408 local pass = http.getenv("HTTP_AUTH_PASS")
410 if user == nil and pass == nil then
411 user = http.formvalue("luci_username")
412 pass = http.formvalue("luci_password")
415 sid, sdat = session_setup(user, pass, allowed_users)
418 local tmpl = require "luci.template"
422 http.status(403, "Forbidden")
423 tmpl.render(track.sysauth_template or "sysauth", {
424 duser = default_user,
431 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() })
432 http.redirect(build_url(unpack(ctx.requestpath)))
435 if not sid or not sdat then
436 http.status(403, "Forbidden")
440 ctx.authsession = sid
441 ctx.authtoken = sdat.token
442 ctx.authuser = sdat.username
445 if track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
446 luci.http.status(200, "OK")
447 luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
448 luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
452 if c and require_post_security(c.target) then
453 if not test_post_security(c) then
458 if track.setgroup then
459 sys.process.setgroup(track.setgroup)
462 if track.setuser then
463 sys.process.setuser(track.setuser)
468 if type(c.target) == "function" then
470 elseif type(c.target) == "table" then
471 target = c.target.target
475 if c and (c.index or type(target) == "function") then
477 ctx.requested = ctx.requested or ctx.dispatched
480 if c and c.index then
481 local tpl = require "luci.template"
483 if util.copcall(tpl.render, "indexer", {}) then
488 if type(target) == "function" then
489 util.copcall(function()
490 local oldenv = getfenv(target)
491 local module = require(c.module)
492 local env = setmetatable({}, {__index=
495 return rawget(tbl, key) or module[key] or oldenv[key]
502 if type(c.target) == "table" then
503 ok, err = util.copcall(target, c.target, unpack(args))
505 ok, err = util.copcall(target, unpack(args))
508 "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
509 " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
510 "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
513 if not root or not root.target then
514 error404("No root node was registered, this usually happens if no module was installed.\n" ..
515 "Install luci-mod-admin-full and retry. " ..
516 "If the module is already installed, try removing the /tmp/luci-indexcache file.")
518 error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
519 "If this url belongs to an extension, make sure it is properly installed.\n" ..
520 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
525 function createindex()
526 local controllers = { }
527 local base = "%s/controller/" % util.libpath()
530 for path in (fs.glob("%s*.lua" % base) or function() end) do
531 controllers[#controllers+1] = path
534 for path in (fs.glob("%s*/*.lua" % base) or function() end) do
535 controllers[#controllers+1] = path
539 local cachedate = fs.stat(indexcache, "mtime")
542 for _, obj in ipairs(controllers) do
543 local omtime = fs.stat(obj, "mtime")
544 realdate = (omtime and omtime > realdate) and omtime or realdate
547 if cachedate > realdate and sys.process.info("uid") == 0 then
549 sys.process.info("uid") == fs.stat(indexcache, "uid")
550 and fs.stat(indexcache, "modestr") == "rw-------",
551 "Fatal: Indexcache is not sane!"
554 index = loadfile(indexcache)()
562 for _, path in ipairs(controllers) do
563 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
564 local mod = require(modname)
566 "Invalid controller file found\n" ..
567 "The file '" .. path .. "' contains an invalid module line.\n" ..
568 "Please verify whether the module name is set to '" .. modname ..
569 "' - It must correspond to the file path!")
571 local idx = mod.index
572 assert(type(idx) == "function",
573 "Invalid controller file found\n" ..
574 "The file '" .. path .. "' contains no index() function.\n" ..
575 "Please make sure that the controller contains a valid " ..
576 "index function and verify the spelling!")
582 local f = nixio.open(indexcache, "w", 600)
583 f:writeall(util.get_bytecode(index))
588 -- Build the index before if it does not exist yet.
589 function createtree()
595 local tree = {nodes={}, inreq=true}
598 ctx.treecache = setmetatable({}, {__mode="v"})
602 -- Load default translation
603 require "luci.i18n".loadc("base")
605 local scope = setmetatable({}, {__index = luci.dispatcher})
607 for k, v in pairs(index) do
613 local function modisort(a,b)
614 return modi[a].order < modi[b].order
617 for _, v in util.spairs(modi, modisort) do
618 scope._NAME = v.module
619 setfenv(v.func, scope)
626 function modifier(func, order)
627 context.modifiers[#context.modifiers+1] = {
635 function assign(path, clone, title, order)
636 local obj = node(unpack(path))
643 setmetatable(obj, {__index = _create_node(clone)})
648 function entry(path, target, title, order)
649 local c = node(unpack(path))
654 c.module = getfenv(2)._NAME
659 -- enabling the node.
661 return _create_node({...})
665 local c = _create_node({...})
667 c.module = getfenv(2)._NAME
674 local i, path = nil, {}
675 for i = 1, select('#', ...) do
676 local name, arg = nil, tostring(select(i, ...))
677 for name in arg:gmatch("[^/]+") do
682 for i = #path, 1, -1 do
683 local node = context.treecache[table.concat(path, ".", 1, i)]
684 if node and (i == #path or node.leaf) then
685 return node, build_url(unpack(path))
690 function _create_node(path)
695 local name = table.concat(path, ".")
696 local c = context.treecache[name]
699 local last = table.remove(path)
700 local parent = _create_node(path)
702 c = {nodes={}, auto=true}
703 -- the node is "in request" if the request path matches
704 -- at least up to the length of the node path
705 if parent.inreq and context.path[#path+1] == last then
708 parent.nodes[last] = c
709 context.treecache[name] = c
716 function _firstchild()
717 local path = { unpack(context.path) }
718 local name = table.concat(path, ".")
719 local node = context.treecache[name]
722 if node and node.nodes and next(node.nodes) then
724 for k, v in pairs(node.nodes) do
726 (v.order or 100) < (node.nodes[lowest].order or 100)
733 assert(lowest ~= nil,
734 "The requested node contains no childs, unable to redispatch")
736 path[#path+1] = lowest
740 function firstchild()
741 return { type = "firstchild", target = _firstchild }
747 for _, r in ipairs({...}) do
755 function rewrite(n, ...)
758 local dispatched = util.clone(context.dispatched)
761 table.remove(dispatched, 1)
764 for i, r in ipairs(req) do
765 table.insert(dispatched, i, r)
768 for _, r in ipairs({...}) do
769 dispatched[#dispatched+1] = r
777 local function _call(self, ...)
778 local func = getfenv()[self.name]
780 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
782 assert(type(func) == "function",
783 'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
784 'of type "' .. type(func) .. '".')
786 if #self.argv > 0 then
787 return func(unpack(self.argv), ...)
793 function call(name, ...)
794 return {type = "call", argv = {...}, name = name, target = _call}
797 function post_on(params, name, ...)
808 return post_on(true, ...)
812 local _template = function(self, ...)
813 require "luci.template".render(self.view)
816 function template(name)
817 return {type = "template", view = name, target = _template}
821 local function _cbi(self, ...)
822 local cbi = require "luci.cbi"
823 local tpl = require "luci.template"
824 local http = require "luci.http"
826 local config = self.config or {}
827 local maps = cbi.load(self.model, ...)
832 for i, res in ipairs(maps) do
833 if util.instanceof(res, cbi.SimpleForm) then
834 io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
837 io.stderr:write("please change %s to use the form() action instead.\n"
838 % table.concat(context.request, "/"))
842 local cstate = res:parse()
843 if cstate and (not state or cstate < state) then
848 local function _resolve_path(path)
849 return type(path) == "table" and build_url(unpack(path)) or path
852 if config.on_valid_to and state and state > 0 and state < 2 then
853 http.redirect(_resolve_path(config.on_valid_to))
857 if config.on_changed_to and state and state > 1 then
858 http.redirect(_resolve_path(config.on_changed_to))
862 if config.on_success_to and state and state > 0 then
863 http.redirect(_resolve_path(config.on_success_to))
867 if config.state_handler then
868 if not config.state_handler(state, maps) then
873 http.header("X-CBI-State", state or 0)
875 if not config.noheader then
876 tpl.render("cbi/header", {state = state})
881 local applymap = false
882 local pageaction = true
883 local parsechain = { }
885 for i, res in ipairs(maps) do
886 if res.apply_needed and res.parsechain then
888 for _, c in ipairs(res.parsechain) do
889 parsechain[#parsechain+1] = c
895 redirect = redirect or res.redirect
898 if res.pageaction == false then
903 messages = messages or { }
904 messages[#messages+1] = res.message
908 for i, res in ipairs(maps) do
914 pageaction = pageaction,
915 parsechain = parsechain
919 if not config.nofooter then
920 tpl.render("cbi/footer", {
922 pageaction = pageaction,
925 autoapply = config.autoapply
930 function cbi(model, config)
933 post = { ["cbi.submit"] = true },
941 local function _arcombine(self, ...)
943 local target = #argv > 0 and self.targets[2] or self.targets[1]
944 setfenv(target.target, self.env)
945 target:target(unpack(argv))
948 function arcombine(trg1, trg2)
949 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
953 local function _form(self, ...)
954 local cbi = require "luci.cbi"
955 local tpl = require "luci.template"
956 local http = require "luci.http"
958 local maps = luci.cbi.load(self.model, ...)
962 for i, res in ipairs(maps) do
963 local cstate = res:parse()
964 if cstate and (not state or cstate < state) then
969 http.header("X-CBI-State", state or 0)
971 for i, res in ipairs(maps) do
980 post = { ["cbi.submit"] = true },
986 translate = i18n.translate
988 -- This function does not actually translate the given argument but
989 -- is used by build/i18n-scan.pl to find translatable entries.