Rework the apply confirmation mechanism to be session agnostic in order to circumvent cross domain restrictions which prevent the JS code from issuing apply confirm requests in some cases, e.g. when changing the LAN IP. Confirmation calls may now be done from unauthenticated pages, as long as a matching confirmation token is sent along with the request. The reasoning behind this is that there is little security impact in confirming pending apply sessions, especially since those sessions can only be initiated while being authenticated. After this change, LuCI will now launch a confirmation process on every rendered page when a rollback is pending. The confirmation will happen regardless of whether the user is logged in or not, or if the current page is a CBI form or static template. A confirmation request now also requires a random one-time token which is rendered along with the confirmation JavaScript code in order to succeed. This token is not meant to provide security but to ensure that the confirm was triggered from an interactive browser session and not some background HTTP requests that happened to end up in the admin ui. As a consequence, the different apply/confirm/rollback code paths in CBI maps and the UCI change/revert pages have been consolidated into one common implementation residing in the common global theme agnostic footer template. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
534 lines
11 KiB
Lua
534 lines
11 KiB
Lua
-- Copyright 2008 Steven Barth <steven@midlink.org>
|
|
-- Licensed to the public under the Apache License 2.0.
|
|
|
|
local os = require "os"
|
|
local util = require "luci.util"
|
|
local table = require "table"
|
|
|
|
|
|
local setmetatable, rawget, rawset = setmetatable, rawget, rawset
|
|
local require, getmetatable, assert = require, getmetatable, assert
|
|
local error, pairs, ipairs, select = error, pairs, ipairs, select
|
|
local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack
|
|
|
|
-- The typical workflow for UCI is: Get a cursor instance from the
|
|
-- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.),
|
|
-- save the changes to the staging area via Cursor.save and finally
|
|
-- Cursor.commit the data to the actual config files.
|
|
-- LuCI then needs to Cursor.apply the changes so deamons etc. are
|
|
-- reloaded.
|
|
module "luci.model.uci"
|
|
|
|
local ERRSTR = {
|
|
"Invalid command",
|
|
"Invalid argument",
|
|
"Method not found",
|
|
"Entry not found",
|
|
"No data",
|
|
"Permission denied",
|
|
"Timeout",
|
|
"Not supported",
|
|
"Unknown error",
|
|
"Connection failed"
|
|
}
|
|
|
|
local session_id = nil
|
|
|
|
local function call(cmd, args)
|
|
if type(args) == "table" and session_id then
|
|
args.ubus_rpc_session = session_id
|
|
end
|
|
return util.ubus("uci", cmd, args)
|
|
end
|
|
|
|
|
|
function cursor()
|
|
return _M
|
|
end
|
|
|
|
function cursor_state()
|
|
return _M
|
|
end
|
|
|
|
function substate(self)
|
|
return self
|
|
end
|
|
|
|
|
|
function get_confdir(self)
|
|
return "/etc/config"
|
|
end
|
|
|
|
function get_savedir(self)
|
|
return "/tmp/.uci"
|
|
end
|
|
|
|
function get_session_id(self)
|
|
return session_id
|
|
end
|
|
|
|
function set_confdir(self, directory)
|
|
return false
|
|
end
|
|
|
|
function set_savedir(self, directory)
|
|
return false
|
|
end
|
|
|
|
function set_session_id(self, id)
|
|
session_id = id
|
|
return true
|
|
end
|
|
|
|
|
|
function load(self, config)
|
|
return true
|
|
end
|
|
|
|
function save(self, config)
|
|
return true
|
|
end
|
|
|
|
function unload(self, config)
|
|
return true
|
|
end
|
|
|
|
|
|
function changes(self, config)
|
|
local rv = call("changes", { config = config })
|
|
local res = {}
|
|
|
|
if type(rv) == "table" and type(rv.changes) == "table" then
|
|
local package, changes
|
|
for package, changes in pairs(rv.changes) do
|
|
res[package] = {}
|
|
|
|
local _, change
|
|
for _, change in ipairs(changes) do
|
|
local operation, section, option, value = unpack(change)
|
|
if option and operation ~= "add" then
|
|
res[package][section] = res[package][section] or { }
|
|
|
|
if operation == "list-add" then
|
|
local v = res[package][section][option]
|
|
if type(v) == "table" then
|
|
v[#v+1] = value or ""
|
|
elseif v ~= nil then
|
|
res[package][section][option] = { v, value }
|
|
else
|
|
res[package][section][option] = { value }
|
|
end
|
|
else
|
|
res[package][section][option] = value or ""
|
|
end
|
|
else
|
|
res[package][section] = res[package][section] or {}
|
|
res[package][section][".type"] = option or ""
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
|
|
function revert(self, config)
|
|
local _, err = call("revert", { config = config })
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
function commit(self, config)
|
|
local _, err = call("commit", { config = config })
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
function apply(self, rollback)
|
|
local _, err
|
|
|
|
if rollback then
|
|
local sys = require "luci.sys"
|
|
local conf = require "luci.config"
|
|
local timeout = tonumber(conf and conf.apply and conf.apply.rollback or 30) or 0
|
|
|
|
_, err = call("apply", {
|
|
timeout = (timeout > 30) and timeout or 30,
|
|
rollback = true
|
|
})
|
|
|
|
if not err then
|
|
local now = os.time()
|
|
local token = sys.uniqueid(16)
|
|
|
|
util.ubus("session", "set", {
|
|
ubus_rpc_session = "00000000000000000000000000000000",
|
|
values = {
|
|
rollback = {
|
|
token = token,
|
|
session = session_id,
|
|
timeout = now + timeout
|
|
}
|
|
}
|
|
})
|
|
|
|
return token
|
|
end
|
|
else
|
|
_, err = call("changes", {})
|
|
|
|
if not err then
|
|
if type(_) == "table" and type(_.changes) == "table" then
|
|
local k, v
|
|
for k, v in pairs(_.changes) do
|
|
_, err = call("commit", { config = k })
|
|
if err then
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not err then
|
|
_, err = call("apply", { rollback = false })
|
|
end
|
|
end
|
|
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
function confirm(self, token)
|
|
local is_pending, time_remaining, rollback_sid, rollback_token = self:rollback_pending()
|
|
|
|
if is_pending then
|
|
if token ~= rollback_token then
|
|
return false, "Permission denied"
|
|
end
|
|
|
|
local _, err = util.ubus("uci", "confirm", {
|
|
ubus_rpc_session = rollback_sid
|
|
})
|
|
|
|
if not err then
|
|
util.ubus("session", "set", {
|
|
ubus_rpc_session = "00000000000000000000000000000000",
|
|
values = { rollback = {} }
|
|
})
|
|
end
|
|
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
return false, "No data"
|
|
end
|
|
|
|
function rollback(self)
|
|
local is_pending, time_remaining, rollback_sid = self:rollback_pending()
|
|
|
|
if is_pending then
|
|
local _, err = util.ubus("uci", "rollback", {
|
|
ubus_rpc_session = rollback_sid
|
|
})
|
|
|
|
if not err then
|
|
util.ubus("session", "set", {
|
|
ubus_rpc_session = "00000000000000000000000000000000",
|
|
values = { rollback = {} }
|
|
})
|
|
end
|
|
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
return false, "No data"
|
|
end
|
|
|
|
function rollback_pending(self)
|
|
local rv, err = util.ubus("session", "get", {
|
|
ubus_rpc_session = "00000000000000000000000000000000",
|
|
keys = { "rollback" }
|
|
})
|
|
|
|
local now = os.time()
|
|
|
|
if type(rv) == "table" and
|
|
type(rv.values) == "table" and
|
|
type(rv.values.rollback) == "table" and
|
|
type(rv.values.rollback.token) == "string" and
|
|
type(rv.values.rollback.session) == "string" and
|
|
type(rv.values.rollback.timeout) == "number" and
|
|
rv.values.rollback.timeout > now
|
|
then
|
|
return true,
|
|
rv.values.rollback.timeout - now,
|
|
rv.values.rollback.session,
|
|
rv.values.rollback.token
|
|
end
|
|
|
|
return false, ERRSTR[err]
|
|
end
|
|
|
|
|
|
function foreach(self, config, stype, callback)
|
|
if type(callback) == "function" then
|
|
local rv, err = call("get", {
|
|
config = config,
|
|
type = stype
|
|
})
|
|
|
|
if type(rv) == "table" and type(rv.values) == "table" then
|
|
local sections = { }
|
|
local res = false
|
|
local index = 1
|
|
|
|
local _, section
|
|
for _, section in pairs(rv.values) do
|
|
section[".index"] = section[".index"] or index
|
|
sections[index] = section
|
|
index = index + 1
|
|
end
|
|
|
|
table.sort(sections, function(a, b)
|
|
return a[".index"] < b[".index"]
|
|
end)
|
|
|
|
for _, section in ipairs(sections) do
|
|
local continue = callback(section)
|
|
res = true
|
|
if continue == false then
|
|
break
|
|
end
|
|
end
|
|
return res
|
|
else
|
|
return false, ERRSTR[err] or "No data"
|
|
end
|
|
else
|
|
return false, "Invalid argument"
|
|
end
|
|
end
|
|
|
|
local function _get(self, operation, config, section, option)
|
|
if section == nil then
|
|
return nil
|
|
elseif type(option) == "string" and option:byte(1) ~= 46 then
|
|
local rv, err = call(operation, {
|
|
config = config,
|
|
section = section,
|
|
option = option
|
|
})
|
|
|
|
if type(rv) == "table" then
|
|
return rv.value or nil
|
|
elseif err then
|
|
return false, ERRSTR[err]
|
|
else
|
|
return nil
|
|
end
|
|
elseif option == nil then
|
|
local values = self:get_all(config, section)
|
|
if values then
|
|
return values[".type"], values[".name"]
|
|
else
|
|
return nil
|
|
end
|
|
else
|
|
return false, "Invalid argument"
|
|
end
|
|
end
|
|
|
|
function get(self, ...)
|
|
return _get(self, "get", ...)
|
|
end
|
|
|
|
function get_state(self, ...)
|
|
return _get(self, "state", ...)
|
|
end
|
|
|
|
function get_all(self, config, section)
|
|
local rv, err = call("get", {
|
|
config = config,
|
|
section = section
|
|
})
|
|
|
|
if type(rv) == "table" and type(rv.values) == "table" then
|
|
return rv.values
|
|
elseif err then
|
|
return false, ERRSTR[err]
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
function get_bool(self, ...)
|
|
local val = self:get(...)
|
|
return (val == "1" or val == "true" or val == "yes" or val == "on")
|
|
end
|
|
|
|
function get_first(self, config, stype, option, default)
|
|
local rv = default
|
|
|
|
self:foreach(config, stype, function(s)
|
|
local val = not option and s[".name"] or s[option]
|
|
|
|
if type(default) == "number" then
|
|
val = tonumber(val)
|
|
elseif type(default) == "boolean" then
|
|
val = (val == "1" or val == "true" or
|
|
val == "yes" or val == "on")
|
|
end
|
|
|
|
if val ~= nil then
|
|
rv = val
|
|
return false
|
|
end
|
|
end)
|
|
|
|
return rv
|
|
end
|
|
|
|
function get_list(self, config, section, option)
|
|
if config and section and option then
|
|
local val = self:get(config, section, option)
|
|
return (type(val) == "table" and val or { val })
|
|
end
|
|
return { }
|
|
end
|
|
|
|
|
|
function section(self, config, stype, name, values)
|
|
local rv, err = call("add", {
|
|
config = config,
|
|
type = stype,
|
|
name = name,
|
|
values = values
|
|
})
|
|
|
|
if type(rv) == "table" then
|
|
return rv.section
|
|
elseif err then
|
|
return false, ERRSTR[err]
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
function add(self, config, stype)
|
|
return self:section(config, stype)
|
|
end
|
|
|
|
function set(self, config, section, option, ...)
|
|
if select('#', ...) == 0 then
|
|
local sname, err = self:section(config, option, section)
|
|
return (not not sname), err
|
|
else
|
|
local _, err = call("set", {
|
|
config = config,
|
|
section = section,
|
|
values = { [option] = select(1, ...) }
|
|
})
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
end
|
|
|
|
function set_list(self, config, section, option, value)
|
|
if section == nil or option == nil then
|
|
return false
|
|
elseif value == nil or (type(value) == "table" and #value == 0) then
|
|
return self:delete(config, section, option)
|
|
elseif type(value) == "table" then
|
|
return self:set(config, section, option, value)
|
|
else
|
|
return self:set(config, section, option, { value })
|
|
end
|
|
end
|
|
|
|
function tset(self, config, section, values)
|
|
local _, err = call("set", {
|
|
config = config,
|
|
section = section,
|
|
values = values
|
|
})
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
function reorder(self, config, section, index)
|
|
local sections
|
|
|
|
if type(section) == "string" and type(index) == "number" then
|
|
local pos = 0
|
|
|
|
sections = { }
|
|
|
|
self:foreach(config, nil, function(s)
|
|
if pos == index then
|
|
pos = pos + 1
|
|
end
|
|
|
|
if s[".name"] ~= section then
|
|
pos = pos + 1
|
|
sections[pos] = s[".name"]
|
|
else
|
|
sections[index + 1] = section
|
|
end
|
|
end)
|
|
elseif type(section) == "table" then
|
|
sections = section
|
|
else
|
|
return false, "Invalid argument"
|
|
end
|
|
|
|
local _, err = call("order", {
|
|
config = config,
|
|
sections = sections
|
|
})
|
|
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
|
|
function delete(self, config, section, option)
|
|
local _, err = call("delete", {
|
|
config = config,
|
|
section = section,
|
|
option = option
|
|
})
|
|
return (err == nil), ERRSTR[err]
|
|
end
|
|
|
|
function delete_all(self, config, stype, comparator)
|
|
local _, err
|
|
if type(comparator) == "table" then
|
|
_, err = call("delete", {
|
|
config = config,
|
|
type = stype,
|
|
match = comparator
|
|
})
|
|
elseif type(comparator) == "function" then
|
|
local rv = call("get", {
|
|
config = config,
|
|
type = stype
|
|
})
|
|
|
|
if type(rv) == "table" and type(rv.values) == "table" then
|
|
local sname, section
|
|
for sname, section in pairs(rv.values) do
|
|
if comparator(section) then
|
|
_, err = call("delete", {
|
|
config = config,
|
|
section = sname
|
|
})
|
|
end
|
|
end
|
|
end
|
|
elseif comparator == nil then
|
|
_, err = call("delete", {
|
|
config = config,
|
|
type = stype
|
|
})
|
|
else
|
|
return false, "Invalid argument"
|
|
end
|
|
|
|
return (err == nil), ERRSTR[err]
|
|
end
|