---[[
-LuCI - Dispatcher
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
+-- 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 <steven@midlink.org>
-
-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 util = require "luci.util"
i18n = require "luci.i18n"
_M.fs = fs
-authenticator = {}
-
-- Index table
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
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 (
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
end
---- Send a 404 error code and render the "error404" template if available.
--- @param message Custom error message (optional)
--- @return false
function error404(message)
http.status(404, "Not Found")
message = message or "Not Found"
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)
util.perror(message)
if not context.template_header_sent then
return false
end
-function authenticator.htmlauth(validator, accs, default)
- local user = http.formvalue("luci_username")
- local pass = 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)
http.context.request = request
local r = {}
context.request = r
- context.urltoken = {}
local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
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()
--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 or request_val == ""))
+ 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
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
ctx.args = args
ctx.requestargs = ctx.requestargs or args
local n
- local token = ctx.urltoken
local preq = {}
local freq = {}
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),
util.pcdata(tostring( val
resource = luci.config.main.resourcebase;
ifattr = function(...) return _ifattr(...) end;
attr = function(...) return _ifattr(true, ...) end;
+ url = build_url;
}, {__index=function(table, key)
if key == "controller" then
return build_url()
elseif key == "REQUEST_URI" then
return build_url(unpack(ctx.requestpath))
+ elseif key == "token" then
+ return ctx.authtoken
else
return rawget(table, key) or _G[key]
end
"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 = 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 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(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 = sys.uniqueid(16)
- util.ubus("session", "set", {
- ubus_rpc_session = sdat.ubus_rpc_session,
- values = {
- user = user,
- token = token,
- section = 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 = {}
- if sess then
- http.header("Set-Cookie", "sysauth=" .. sess.."; path="..build_url())
- ctx.authsession = sess
- ctx.authuser = user
- end
- end
- else
http.status(403, "Forbidden")
+ tmpl.render(track.sysauth_template or "sysauth", {
+ duser = default_user,
+ fuser = user
+ })
+
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
end
end
---- Generate the dispatching index using the native file-cache based strategy.
function createindex()
local controllers = { }
local base = "%s/controller/" % util.libpath()
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
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,
}
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
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))
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({...})
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(...)
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(...)
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
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"] = "1" },
+ config = config,
+ model = model,
+ target = _cbi
+ }
end
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
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"] = "1" },
+ 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)