1
0
Fork 0
mirror of https://github.com/Ysurac/openmptcprouter-feeds.git synced 2025-02-13 11:01:50 +00:00
openmptcprouter-feeds/luci-base/luasrc/dispatcher.lua
2018-07-31 17:18:00 +02:00

1003 lines
22 KiB
Lua

-- 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.
local fs = require "nixio.fs"
local sys = require "luci.sys"
local util = require "luci.util"
local http = require "luci.http"
local nixio = require "nixio", require "nixio.util"
module("luci.dispatcher", package.seeall)
context = util.threadlocal()
uci = require "luci.model.uci"
i18n = require "luci.i18n"
_M.fs = fs
-- Index table
local index = nil
-- Fastindex
local fi
function build_url(...)
local path = {...}
local url = { http.getenv("SCRIPT_NAME") or "" }
local p
for _, p in ipairs(path) do
if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
url[#url+1] = "/"
url[#url+1] = p
end
end
if #path == 0 then
url[#url+1] = "/"
end
return table.concat(url, "")
end
function node_visible(node)
if node then
return not (
(not node.title or #node.title == 0) or
(not node.target or node.hidden == true) or
(type(node.target) == "table" and node.target.type == "firstchild" and
(type(node.nodes) ~= "table" or not next(node.nodes)))
)
end
return false
end
function node_childs(node)
local rv = { }
if node then
local k, v
for k, v in util.spairs(node.nodes,
function(a, b)
return (node.nodes[a].order or 100)
< (node.nodes[b].order or 100)
end)
do
if node_visible(v) then
rv[#rv+1] = k
end
end
end
return rv
end
function error404(message)
http.status(404, "Not Found")
message = message or "Not Found"
local function render()
local template = require "luci.template"
template.render("error404")
end
if not util.copcall(render) then
http.prepare_content("text/plain")
http.write(message)
end
return false
end
function error500(message)
util.perror(message)
if not context.template_header_sent then
http.status(500, "Internal Server Error")
http.prepare_content("text/plain")
http.write(message)
else
require("luci.template")
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 httpdispatch(request, prefix)
http.context.request = request
local r = {}
context.request = r
local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
if prefix then
for _, node in ipairs(prefix) do
r[#r+1] = node
end
end
local node
for node in pathinfo:gmatch("[^/%z]+") do
r[#r+1] = node
end
local stat, err = util.coxpcall(function()
dispatch(context.request)
end, error500)
http.close()
--context._disable_memtrace()
end
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
uci:set_session_id(sid)
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
ctx.path = request
local conf = require "luci.config"
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 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
if lang == "auto" then
lang = i18n.default
end
i18n.setlanguage(lang)
local c = ctx.tree
local stat
if not c then
c = createtree()
end
local track = {}
local args = {}
ctx.args = args
ctx.requestargs = ctx.requestargs or args
local n
local preq = {}
local freq = {}
for i, s in ipairs(request) do
preq[#preq+1] = s
freq[#freq+1] = s
c = c.nodes[s]
n = i
if not c then
break
end
util.update(track, c)
if c.leaf then
break
end
end
if c and c.leaf then
for j=n+1, #request do
args[#args+1] = request[j]
freq[#freq+1] = request[j]
end
end
ctx.requestpath = ctx.requestpath or freq
ctx.path = preq
if track.i18n then
i18n.loadc(track.i18n)
end
-- Init template engine
if (c and c.index) or not track.notemplate then
local tpl = require("luci.template")
local media = track.mediaurlbase or luci.config.main.mediaurlbase
if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
media = nil
for name, theme in pairs(luci.config.themes) do
if name:sub(1,1) ~= "." and pcall(tpl.Template,
"themes/%s/header" % fs.basename(theme)) then
media = theme
end
end
assert(media, "No valid theme found")
end
local function _ifattr(cond, key, val)
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
or (type(env[key]) ~= "function" and env[key])
or (scope and type(scope[key]) ~= "function" and scope[key])
or "" ))
)
else
return ''
end
end
tpl.context.viewns = setmetatable({
write = http.write;
include = function(name) tpl.Template(name):render(getfenv(2)) end;
translate = i18n.translate;
translatef = i18n.translatef;
export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
striptags = util.striptags;
pcdata = util.pcdata;
media = media;
theme = fs.basename(media);
resource = luci.config.main.resourcebase;
ifattr = function(...) return _ifattr(...) end;
attr = function(...) return _ifattr(true, ...) end;
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") or "", 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(tbl, key) or _G[key]
end
end})
end
track.dependent = (track.dependent ~= false)
assert(not track.dependent or not track.auto,
"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 " ..
"https://github.com/openwrt/luci/issues"
)
if track.sysauth and not ctx.authsession then
local authen = track.sysauth_authenticator
local _, sid, sdat, default_user, allowed_users
if type(authen) == "string" and authen ~= "htmlauth" then
error500("Unsupported authenticator %q configured" % authen)
return
end
if type(track.sysauth) == "table" then
default_user, allowed_users = nil, track.sysauth
else
default_user, allowed_users = track.sysauth, { track.sysauth }
end
if type(authen) == "function" then
_, sid = authen(sys.user.checkpasswd, allowed_users)
else
sid = http.getcookie("sysauth")
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
})
return
end
http.header("Set-Cookie", 'sysauth=%s; path=%s; HttpOnly%s' %{
sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or ""
})
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 track.cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then
luci.http.status(200, "OK")
luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*")
luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
return
end
if c and require_post_security(c.target) then
if not test_post_security(c) then
return
end
end
if track.setgroup then
sys.process.setgroup(track.setgroup)
end
if track.setuser then
sys.process.setuser(track.setuser)
end
local target = nil
if c then
if type(c.target) == "function" then
target = c.target
elseif type(c.target) == "table" then
target = c.target.target
end
end
if c and (c.index or type(target) == "function") then
ctx.dispatched = c
ctx.requested = ctx.requested or ctx.dispatched
end
if c and c.index then
local tpl = require "luci.template"
if util.copcall(tpl.render, "indexer", {}) then
return true
end
end
if type(target) == "function" then
util.copcall(function()
local oldenv = getfenv(target)
local module = require(c.module)
local env = setmetatable({}, {__index=
function(tbl, key)
return rawget(tbl, key) or module[key] or oldenv[key]
end})
setfenv(target, env)
end)
local ok, err
if type(c.target) == "table" then
ok, err = util.copcall(target, c.target, unpack(args))
else
ok, err = util.copcall(target, unpack(args))
end
if not ok then
error500("Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
" dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
"The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
end
else
local root = node()
if not root or not root.target then
error404("No root node was registered, this usually happens if no module was installed.\n" ..
"Install luci-mod-admin-full and retry. " ..
"If the module is already installed, try removing the /tmp/luci-indexcache file.")
else
error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
"If this url belongs to an extension, make sure it is properly installed.\n" ..
"If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
end
end
end
function createindex()
local controllers = { }
local base = "%s/controller/" % util.libpath()
local _, path
for path in (fs.glob("%s*.lua" % base) or function() end) do
controllers[#controllers+1] = path
end
for path in (fs.glob("%s*/*.lua" % base) or function() end) do
controllers[#controllers+1] = path
end
if indexcache then
local cachedate = fs.stat(indexcache, "mtime")
if cachedate then
local realdate = 0
for _, obj in ipairs(controllers) do
local omtime = fs.stat(obj, "mtime")
realdate = (omtime and omtime > realdate) and omtime or realdate
end
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-------",
"Fatal: Indexcache is not sane!"
)
index = loadfile(indexcache)()
return index
end
end
end
index = {}
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 '" .. 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 '" .. path .. "' contains no index() function.\n" ..
"Please make sure that the controller contains a valid " ..
"index function and verify the spelling!")
index[modname] = idx
end
if indexcache then
local f = nixio.open(indexcache, "w", 600)
f:writeall(util.get_bytecode(index))
f:close()
end
end
-- Build the index before if it does not exist yet.
function createtree()
if not index then
createindex()
end
local ctx = context
local tree = {nodes={}, inreq=true}
local modi = {}
ctx.treecache = setmetatable({}, {__mode="v"})
ctx.tree = tree
ctx.modifiers = modi
-- Load default translation
require "luci.i18n".loadc("base")
local scope = setmetatable({}, {__index = luci.dispatcher})
for k, v in pairs(index) do
scope._NAME = k
setfenv(v, scope)
v()
end
local function modisort(a,b)
return modi[a].order < modi[b].order
end
for _, v in util.spairs(modi, modisort) do
scope._NAME = v.module
setfenv(v.func, scope)
v.func()
end
return tree
end
function modifier(func, order)
context.modifiers[#context.modifiers+1] = {
func = func,
order = order or 0,
module
= getfenv(2)._NAME
}
end
function assign(path, clone, title, order)
local obj = node(unpack(path))
obj.nodes = nil
obj.module = nil
obj.title = title
obj.order = order
setmetatable(obj, {__index = _create_node(clone)})
return obj
end
function entry(path, target, title, order)
local c = node(unpack(path))
c.target = target
c.title = title
c.order = order
c.module = getfenv(2)._NAME
return c
end
-- enabling the node.
function get(...)
return _create_node({...})
end
function node(...)
local c = _create_node({...})
c.module = getfenv(2)._NAME
c.auto = nil
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
end
local name = table.concat(path, ".")
local c = context.treecache[name]
if not c then
local last = table.remove(path)
local parent = _create_node(path)
c = {nodes={}, auto=true, inreq=true}
local _, n
for _, n in ipairs(path) do
if context.path[_] ~= n then
c.inreq = false
break
end
end
c.inreq = c.inreq and (context.path[#path + 1] == last)
parent.nodes[last] = c
context.treecache[name] = c
end
return c
end
-- Subdispatchers --
function _firstchild()
local path = { unpack(context.path) }
local name = table.concat(path, ".")
local node = context.treecache[name]
local lowest
if node and node.nodes and next(node.nodes) then
local k, v
for k, v in pairs(node.nodes) do
if not lowest or
(v.order or 100) < (node.nodes[lowest].order or 100)
then
lowest = k
end
end
end
assert(lowest ~= nil,
"The requested node contains no childs, unable to redispatch")
path[#path+1] = lowest
dispatch(path)
end
function firstchild()
return { type = "firstchild", target = _firstchild }
end
function alias(...)
local req = {...}
return function(...)
for _, r in ipairs({...}) do
req[#req+1] = r
end
dispatch(req)
end
end
function rewrite(n, ...)
local req = {...}
return function(...)
local dispatched = util.clone(context.dispatched)
for i=1,n do
table.remove(dispatched, 1)
end
for i, r in ipairs(req) do
table.insert(dispatched, i, r)
end
for _, r in ipairs({...}) do
dispatched[#dispatched+1] = r
end
dispatch(dispatched)
end
end
local function _call(self, ...)
local func = getfenv()[self.name]
assert(func ~= nil,
'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
assert(type(func) == "function",
'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
'of type "' .. type(func) .. '".')
if #self.argv > 0 then
return func(unpack(self.argv), ...)
else
return func(...)
end
end
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
function template(name)
return {type = "template", view = name, target = _template}
end
local function _cbi(self, ...)
local cbi = require "luci.cbi"
local tpl = require "luci.template"
local http = require "luci.http"
local config = self.config or {}
local maps = cbi.load(self.model, ...)
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
state = cstate
end
end
local function _resolve_path(path)
return type(path) == "table" and build_url(unpack(path)) or path
end
if config.on_valid_to and state and state > 0 and state < 2 then
http.redirect(_resolve_path(config.on_valid_to))
return
end
if config.on_changed_to and state and state > 1 then
http.redirect(_resolve_path(config.on_changed_to))
return
end
if config.on_success_to and state and state > 0 then
http.redirect(_resolve_path(config.on_success_to))
return
end
if config.state_handler then
if not config.state_handler(state, maps) then
return
end
end
http.header("X-CBI-State", state or 0)
if not config.noheader then
tpl.render("cbi/header", {state = state})
end
local redirect
local messages
local applymap = false
local pageaction = true
local parsechain = { }
for i, res in ipairs(maps) do
if res.apply_needed and res.parsechain then
local c
for _, c in ipairs(res.parsechain) do
parsechain[#parsechain+1] = c
end
applymap = true
end
if res.redirect then
redirect = redirect or res.redirect
end
if res.pageaction == false then
pageaction = false
end
if res.message then
messages = messages or { }
messages[#messages+1] = res.message
end
end
for i, res in ipairs(maps) do
res:render({
firstmap = (i == 1),
redirect = redirect,
messages = messages,
pageaction = pageaction,
parsechain = parsechain
})
end
if not config.nofooter then
tpl.render("cbi/footer", {
flow = config,
pageaction = pageaction,
redirect = redirect,
state = state,
autoapply = config.autoapply,
trigger_apply = applymap
})
end
end
function cbi(model, config)
return {
type = "cbi",
post = { ["cbi.submit"] = true },
config = config,
model = model,
target = _cbi
}
end
local function _arcombine(self, ...)
local argv = {...}
local target = #argv > 0 and self.targets[2] or self.targets[1]
setfenv(target.target, self.env)
target:target(unpack(argv))
end
function arcombine(trg1, trg2)
return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
end
local function _form(self, ...)
local cbi = require "luci.cbi"
local tpl = require "luci.template"
local http = require "luci.http"
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
state = cstate
end
end
http.header("X-CBI-State", state or 0)
tpl.render("header")
for i, res in ipairs(maps) do
res:render()
end
tpl.render("footer")
end
function form(model)
return {
type = "cbi",
post = { ["cbi.submit"] = true },
model = model,
target = _form
}
end
translate = i18n.translate
-- This function does not actually translate the given argument but
-- is used by build/i18n-scan.pl to find translatable entries.
function _(text)
return text
end