X-Git-Url: http://git.archive.openwrt.org/?p=project%2Fluci.git;a=blobdiff_plain;f=modules%2Fluci-base%2Fluasrc%2Fdispatcher.lua;h=91b86679fd739bc5ba67deb47258a9c46c09d0c5;hp=4bbd58f9dbb32729ef5ae8ac42b44bce5183394a;hb=b1b5723516a5ad11174c531c1c792dec5b753303;hpb=199c8cbc4c32506ecfe89850615b88a3f0276dd3 diff --git a/modules/luci-base/luasrc/dispatcher.lua b/modules/luci-base/luasrc/dispatcher.lua index 4bbd58f9d..91b86679f 100644 --- a/modules/luci-base/luasrc/dispatcher.lua +++ b/modules/luci-base/luasrc/dispatcher.lua @@ -1,33 +1,9 @@ ---[[ -LuCI - Dispatcher +-- Copyright 2008 Steven Barth +-- Copyright 2008-2015 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. -Description: -The request dispatcher and module dispatcher generators - -FileId: -$Id$ - -License: -Copyright 2008 Steven Barth - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -]]-- - ---- LuCI web dispatcher. local fs = require "nixio.fs" local sys = require "luci.sys" -local init = require "luci.init" local util = require "luci.util" local http = require "luci.http" local nixio = require "nixio", require "nixio.util" @@ -38,8 +14,6 @@ uci = require "luci.model.uci" i18n = require "luci.i18n" _M.fs = fs -authenticator = {} - -- Index table local index = nil @@ -47,21 +21,10 @@ local index = nil local fi ---- Build the URL relative to the server webroot from given virtual path. --- @param ... Virtual path --- @return Relative URL function build_url(...) local path = {...} local url = { http.getenv("SCRIPT_NAME") or "" } - local k, v - for k, v in pairs(context.urltoken) do - url[#url+1] = "/;" - url[#url+1] = http.urlencode(k) - url[#url+1] = "=" - url[#url+1] = http.urlencode(v) - end - local p for _, p in ipairs(path) do if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then @@ -70,12 +33,13 @@ function build_url(...) end end + if #path == 0 then + url[#url+1] = "/" + end + return table.concat(url, "") end ---- Check whether a dispatch node shall be visible --- @param node Dispatch node --- @return Boolean indicating whether the node should be visible function node_visible(node) if node then return not ( @@ -88,9 +52,6 @@ function node_visible(node) return false end ---- Return a sorted table of visible childs within a given node --- @param node Dispatch node --- @return Ordered table of child node names function node_childs(node) local rv = { } if node then @@ -110,64 +71,39 @@ function node_childs(node) end ---- Send a 404 error code and render the "error404" template if available. --- @param message Custom error message (optional) --- @return false function error404(message) - luci.http.status(404, "Not Found") + http.status(404, "Not Found") message = message or "Not Found" require("luci.template") - if not luci.util.copcall(luci.template.render, "error404") then - luci.http.prepare_content("text/plain") - luci.http.write(message) + if not util.copcall(luci.template.render, "error404") then + http.prepare_content("text/plain") + http.write(message) end return false end ---- Send a 500 error code and render the "error500" template if available. --- @param message Custom error message (optional)# --- @return false function error500(message) - luci.util.perror(message) + util.perror(message) if not context.template_header_sent then - luci.http.status(500, "Internal Server Error") - luci.http.prepare_content("text/plain") - luci.http.write(message) + http.status(500, "Internal Server Error") + http.prepare_content("text/plain") + http.write(message) else require("luci.template") - if not luci.util.copcall(luci.template.render, "error500", {message=message}) then - luci.http.prepare_content("text/plain") - luci.http.write(message) + if not util.copcall(luci.template.render, "error500", {message=message}) then + http.prepare_content("text/plain") + http.write(message) end end return false end -function authenticator.htmlauth(validator, accs, default) - local user = luci.http.formvalue("luci_username") - local pass = luci.http.formvalue("luci_password") - - if user and validator(user, pass) then - return user - end - - require("luci.i18n") - require("luci.template") - context.path = {} - luci.template.render("sysauth", {duser=default, fuser=user}) - return false - -end - ---- Dispatch an HTTP request. --- @param request LuCI HTTP Request object function httpdispatch(request, prefix) - luci.http.context.request = request + http.context.request = request local r = {} context.request = r - context.urltoken = {} local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) @@ -177,31 +113,107 @@ function httpdispatch(request, prefix) end end - local tokensok = true for node in pathinfo:gmatch("[^/]+") do - local tkey, tval - if tokensok then - tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)") - end - if tkey then - context.urltoken[tkey] = tval - else - tokensok = false - r[#r+1] = node - end + r[#r+1] = node end local stat, err = util.coxpcall(function() dispatch(context.request) end, error500) - luci.http.close() + http.close() --context._disable_memtrace() end ---- Dispatches a LuCI virtual path. --- @param request Virtual path +local function require_post_security(target) + if type(target) == "table" then + if type(target.post) == "table" then + local param_name, required_val, request_val + + for param_name, required_val in pairs(target.post) do + request_val = http.formvalue(param_name) + + if (type(required_val) == "string" and + request_val ~= required_val) or + (required_val == true and request_val == nil) + then + return false + end + end + + return true + end + + return (target.post == true) + end + + return false +end + +function test_post_security() + if http.getenv("REQUEST_METHOD") ~= "POST" then + http.status(405, "Method Not Allowed") + http.header("Allow", "POST") + return false + end + + if http.formvalue("token") ~= context.authtoken then + http.status(403, "Forbidden") + luci.template.render("csrftoken") + return false + end + + return true +end + +local function session_retrieve(sid, allowed_users) + local sdat = util.ubus("session", "get", { ubus_rpc_session = sid }) + + if type(sdat) == "table" and + type(sdat.values) == "table" and + type(sdat.values.token) == "string" and + (not allowed_users or + util.contains(allowed_users, sdat.values.username)) + then + return sid, sdat.values + end + + return nil, nil +end + +local function session_setup(user, pass, allowed_users) + if util.contains(allowed_users, user) then + local login = util.ubus("session", "login", { + username = user, + password = pass, + timeout = tonumber(luci.config.sauth.sessiontime) + }) + + local rp = context.requestpath + and table.concat(context.requestpath, "/") or "" + + if type(login) == "table" and + type(login.ubus_rpc_session) == "string" + then + util.ubus("session", "set", { + ubus_rpc_session = login.ubus_rpc_session, + values = { token = sys.uniqueid(16) } + }) + + io.stderr:write("luci: accepted login on /%s for %s from %s\n" + %{ rp, user, http.getenv("REMOTE_ADDR") or "?" }) + + return session_retrieve(login.ubus_rpc_session) + end + + io.stderr:write("luci: failed login on /%s for %s from %s\n" + %{ rp, user, http.getenv("REMOTE_ADDR") or "?" }) + end + + return nil, nil +end + function dispatch(request) --context._disable_memtrace = require "luci.debug".trap_memtrace("l") local ctx = context @@ -211,18 +223,31 @@ function dispatch(request) assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'") + local i18n = require "luci.i18n" local lang = conf.main.lang or "auto" if lang == "auto" then local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" - for lpat in aclang:gmatch("[%w-]+") do - lpat = lpat and lpat:gsub("-", "_") - if conf.languages[lpat] then - lang = lpat + for aclang in aclang:gmatch("[%w_-]+") do + local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") + if country and culture then + local cc = "%s_%s" %{ country, culture:lower() } + if conf.languages[cc] then + lang = cc + break + elseif conf.languages[country] then + lang = country + break + end + elseif conf.languages[aclang] then + lang = aclang break end end end - require "luci.i18n".setlanguage(lang) + if lang == "auto" then + lang = i18n.default + end + i18n.setlanguage(lang) local c = ctx.tree local stat @@ -235,7 +260,6 @@ function dispatch(request) ctx.args = args ctx.requestargs = ctx.requestargs or args local n - local token = ctx.urltoken local preq = {} local freq = {} @@ -288,9 +312,16 @@ function dispatch(request) if cond then local env = getfenv(3) local scope = (type(env.self) == "table") and env.self + if type(val) == "table" then + if not next(val) then + return '' + else + val = util.serialize_json(val) + end + end return string.format( ' %s="%s"', tostring(key), - luci.util.pcdata(tostring( val + util.pcdata(tostring( val or (type(env[key]) ~= "function" and env[key]) or (scope and type(scope[key]) ~= "function" and scope[key]) or "" )) @@ -301,7 +332,7 @@ function dispatch(request) end tpl.context.viewns = setmetatable({ - write = luci.http.write; + write = http.write; include = function(name) tpl.Template(name):render(getfenv(2)) end; translate = i18n.translate; translatef = i18n.translatef; @@ -313,13 +344,24 @@ function dispatch(request) resource = luci.config.main.resourcebase; ifattr = function(...) return _ifattr(...) end; attr = function(...) return _ifattr(true, ...) end; - }, {__index=function(table, key) + url = build_url; + }, {__index=function(tbl, key) if key == "controller" then return build_url() elseif key == "REQUEST_URI" then return build_url(unpack(ctx.requestpath)) + elseif key == "FULL_REQUEST_URI" then + local url = { http.getenv("SCRIPT_NAME"), http.getenv("PATH_INFO") } + local query = http.getenv("QUERY_STRING") + if query and #query > 0 then + url[#url+1] = "?" + url[#url+1] = query + end + return table.concat(url, "") + elseif key == "token" then + return ctx.authtoken else - return rawget(table, key) or _G[key] + return rawget(tbl, key) or _G[key] end end}) end @@ -329,85 +371,83 @@ function dispatch(request) "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " .. "has no parent node so the access to this location has been denied.\n" .. "This is a software bug, please report this message at " .. - "http://luci.subsignal.org/trac/newticket" + "https://github.com/openwrt/luci/issues" ) - if track.sysauth then - local authen = type(track.sysauth_authenticator) == "function" - and track.sysauth_authenticator - or authenticator[track.sysauth_authenticator] + if track.sysauth and not ctx.authsession then + local authen = track.sysauth_authenticator + local _, sid, sdat, default_user, allowed_users - local def = (type(track.sysauth) == "string") and track.sysauth - local accs = def and {track.sysauth} or track.sysauth - local sess = ctx.authsession - local verifytoken = false - if not sess then - sess = luci.http.getcookie("sysauth") - sess = sess and sess:match("^[a-f0-9]*$") - verifytoken = true + if type(authen) == "string" and authen ~= "htmlauth" then + error500("Unsupported authenticator %q configured" % authen) + return end - local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values - local user + if type(track.sysauth) == "table" then + default_user, allowed_users = nil, track.sysauth + else + default_user, allowed_users = track.sysauth, { track.sysauth } + end - if sdat then - if not verifytoken or ctx.urltoken.stok == sdat.token then - user = sdat.user - end + if type(authen) == "function" then + _, sid = authen(sys.user.checkpasswd, allowed_users) else - local eu = http.getenv("HTTP_AUTH_USER") - local ep = http.getenv("HTTP_AUTH_PASS") - if eu and ep and luci.sys.user.checkpasswd(eu, ep) then - authen = function() return eu end - end + sid = http.getcookie("sysauth") end - if not util.contains(accs, user) then - if authen then - ctx.urltoken.stok = nil - local user, sess = authen(luci.sys.user.checkpasswd, accs, def) - if not user or not util.contains(accs, user) then - return - else - if not sess then - local sdat = util.ubus("session", "create", { timeout = luci.config.sauth.sessiontime }) - if sdat then - local token = luci.sys.uniqueid(16) - util.ubus("session", "set", { - ubus_rpc_session = sdat.ubus_rpc_session, - values = { - user = user, - token = token, - section = luci.sys.uniqueid(16) - } - }) - sess = sdat.ubus_rpc_session - ctx.urltoken.stok = token - end - end + sid, sdat = session_retrieve(sid, allowed_users) + + if not (sid and sdat) and authen == "htmlauth" then + local user = http.getenv("HTTP_AUTH_USER") + local pass = http.getenv("HTTP_AUTH_PASS") + + if user == nil and pass == nil then + user = http.formvalue("luci_username") + pass = http.formvalue("luci_password") + end + + sid, sdat = session_setup(user, pass, allowed_users) + + if not sid then + local tmpl = require "luci.template" + + context.path = {} + + http.status(403, "Forbidden") + tmpl.render(track.sysauth_template or "sysauth", { + duser = default_user, + fuser = user + }) - if sess then - luci.http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url()) - ctx.authsession = sess - ctx.authuser = user - end - end - else - luci.http.status(403, "Forbidden") return end - else - ctx.authsession = sess - ctx.authuser = user + + http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sid, build_url() }) + http.redirect(build_url(unpack(ctx.requestpath))) + end + + if not sid or not sdat then + http.status(403, "Forbidden") + return + end + + ctx.authsession = sid + ctx.authtoken = sdat.token + ctx.authuser = sdat.username + end + + if c and require_post_security(c.target) then + if not test_post_security(c) then + return end end if track.setgroup then - luci.sys.process.setgroup(track.setgroup) + sys.process.setgroup(track.setgroup) end if track.setuser then - luci.sys.process.setuser(track.setuser) + sys.process.setuser(track.setuser) end local target = nil @@ -469,46 +509,17 @@ function dispatch(request) end end ---- Generate the dispatching index using the best possible strategy. function createindex() - local path = luci.util.libpath() .. "/controller/" - local suff = { ".lua", ".lua.gz" } - - if luci.util.copcall(require, "luci.fastindex") then - createindex_fastindex(path, suff) - else - createindex_plain(path, suff) - end -end - ---- Generate the dispatching index using the fastindex C-indexer. --- @param path Controller base directory --- @param suffixes Controller file suffixes -function createindex_fastindex(path, suffixes) - index = {} - - if not fi then - fi = luci.fastindex.new("index") - for _, suffix in ipairs(suffixes) do - fi.add(path .. "*" .. suffix) - fi.add(path .. "*/*" .. suffix) - end - end - fi.scan() + local controllers = { } + local base = "%s/controller/" % util.libpath() + local _, path - for k, v in pairs(fi.indexes) do - index[v[2]] = v[1] + for path in (fs.glob("%s*.lua" % base) or function() end) do + controllers[#controllers+1] = path end -end ---- Generate the dispatching index using the native file-cache based strategy. --- @param path Controller base directory --- @param suffixes Controller file suffixes -function createindex_plain(path, suffixes) - local controllers = { } - for _, suffix in ipairs(suffixes) do - nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers) - nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers) + for path in (fs.glob("%s*/*.lua" % base) or function() end) do + controllers[#controllers+1] = path end if indexcache then @@ -520,7 +531,7 @@ function createindex_plain(path, suffixes) realdate = (omtime and omtime > realdate) and omtime or realdate end - if cachedate > realdate then + if cachedate > realdate and sys.process.info("uid") == 0 then assert( sys.process.info("uid") == fs.stat(indexcache, "uid") and fs.stat(indexcache, "modestr") == "rw-------", @@ -535,23 +546,19 @@ function createindex_plain(path, suffixes) index = {} - for i,c in ipairs(controllers) do - local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".") - for _, suffix in ipairs(suffixes) do - modname = modname:gsub(suffix.."$", "") - end - + for _, path in ipairs(controllers) do + local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".") local mod = require(modname) assert(mod ~= true, "Invalid controller file found\n" .. - "The file '" .. c .. "' contains an invalid module line.\n" .. + "The file '" .. path .. "' contains an invalid module line.\n" .. "Please verify whether the module name is set to '" .. modname .. "' - It must correspond to the file path!") local idx = mod.index assert(type(idx) == "function", "Invalid controller file found\n" .. - "The file '" .. c .. "' contains no index() function.\n" .. + "The file '" .. path .. "' contains no index() function.\n" .. "Please make sure that the controller contains a valid " .. "index function and verify the spelling!") @@ -565,7 +572,6 @@ function createindex_plain(path, suffixes) end end ---- Create the dispatching tree from the index. -- Build the index before if it does not exist yet. function createtree() if not index then @@ -604,9 +610,6 @@ function createtree() return tree end ---- Register a tree modifier. --- @param func Modifier function --- @param order Modifier order value (optional) function modifier(func, order) context.modifiers[#context.modifiers+1] = { func = func, @@ -616,12 +619,6 @@ function modifier(func, order) } end ---- Clone a node of the dispatching tree to another position. --- @param path Virtual path destination --- @param clone Virtual path source --- @param title Destination node title (optional) --- @param order Destination node order value (optional) --- @return Dispatching tree node function assign(path, clone, title, order) local obj = node(unpack(path)) obj.nodes = nil @@ -635,12 +632,6 @@ function assign(path, clone, title, order) return obj end ---- Create a new dispatching node and define common parameters. --- @param path Virtual path --- @param target Target function to call when dispatched. --- @param title Destination node title --- @param order Destination node order value (optional) --- @return Dispatching tree node function entry(path, target, title, order) local c = node(unpack(path)) @@ -652,17 +643,11 @@ function entry(path, target, title, order) return c end ---- Fetch or create a dispatching node without setting the target module or -- enabling the node. --- @param ... Virtual path --- @return Dispatching tree node function get(...) return _create_node({...}) end ---- Fetch or create a new dispatching node. --- @param ... Virtual path --- @return Dispatching tree node function node(...) local c = _create_node({...}) @@ -672,6 +657,23 @@ function node(...) return c end +function lookup(...) + local i, path = nil, {} + for i = 1, select('#', ...) do + local name, arg = nil, tostring(select(i, ...)) + for name in arg:gmatch("[^/]+") do + path[#path+1] = name + end + end + + for i = #path, 1, -1 do + local node = context.treecache[table.concat(path, ".", 1, i)] + if node and (i == #path or node.leaf) then + return node, build_url(unpack(path)) + end + end +end + function _create_node(path) if #path == 0 then return context.tree @@ -722,13 +724,10 @@ function _firstchild() dispatch(path) end ---- Alias the first (lowest order) page automatically function firstchild() return { type = "firstchild", target = _firstchild } end ---- Create a redirect to another dispatching node. --- @param ... Virtual path destination function alias(...) local req = {...} return function(...) @@ -740,9 +739,6 @@ function alias(...) end end ---- Rewrite the first x path values of the request. --- @param n Number of path values to replace --- @param ... Virtual path to replace removed path values with function rewrite(n, ...) local req = {...} return function(...) @@ -781,20 +777,29 @@ local function _call(self, ...) end end ---- Create a function-call dispatching target. --- @param name Target function of local controller --- @param ... Additional parameters passed to the function function call(name, ...) return {type = "call", argv = {...}, name = name, target = _call} end +function post_on(params, name, ...) + return { + type = "call", + post = params, + argv = { ... }, + name = name, + target = _call + } +end + +function post(...) + return post_on(true, ...) +end + local _template = function(self, ...) require "luci.template".render(self.view) end ---- Create a template render dispatching target. --- @param name Template to be rendered function template(name) return {type = "template", view = name, target = _template} end @@ -810,7 +815,16 @@ local function _cbi(self, ...) local state = nil + local i, res for i, res in ipairs(maps) do + if util.instanceof(res, cbi.SimpleForm) then + io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n" + % self.model) + + io.stderr:write("please change %s to use the form() action instead.\n" + % table.concat(context.request, "/")) + end + res.flow = config local cstate = res:parse() if cstate and (not state or cstate < state) then @@ -900,10 +914,14 @@ local function _cbi(self, ...) end end ---- Create a CBI model dispatching target. --- @param model CBI model to be rendered function cbi(model, config) - return {type = "cbi", config = config, model = model, target = _cbi} + return { + type = "cbi", + post = { ["cbi.submit"] = true }, + config = config, + model = model, + target = _cbi + } end @@ -914,9 +932,6 @@ local function _arcombine(self, ...) target:target(unpack(argv)) end ---- Create a combined dispatching target for non argv and argv requests. --- @param trg1 Overview Target --- @param trg2 Detail Target function arcombine(trg1, trg2) return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}} end @@ -930,6 +945,7 @@ local function _form(self, ...) local maps = luci.cbi.load(self.model, ...) local state = nil + local i, res for i, res in ipairs(maps) do local cstate = res:parse() if cstate and (not state or cstate < state) then @@ -945,19 +961,17 @@ local function _form(self, ...) tpl.render("footer") end ---- Create a CBI form model dispatching target. --- @param model CBI form model tpo be rendered function form(model) - return {type = "cbi", model = model, target = _form} + return { + type = "cbi", + post = { ["cbi.submit"] = true }, + model = model, + target = _form + } end ---- Access the luci.i18n translate() api. --- @class function --- @name translate --- @param text Text to translate translate = i18n.translate ---- No-op function used to mark translation entries for menu labels. -- This function does not actually translate the given argument but -- is used by build/i18n-scan.pl to find translatable entries. function _(text)