luci-base: add ubus-http gateway

Add an admin/ubus route mimicking the native uhttpd-mod-ubus protocol.

The main difference to the native protocol is that this gateway requires
no additional per-object/procedure ACL setup on the router side and that
it is located under the same prefix as LuCI itself, allowing the reuse
of the session login cookie.

This route is meant to be a transitional mechanism until client side
RPC calls are eventually migrated to uhttpd-mod-ubus completely.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2019-02-07 18:40:36 +01:00
parent 558f8c3b2a
commit c89bbd50fd
2 changed files with 226 additions and 31 deletions

View file

@ -433,8 +433,6 @@
__init__: function(env) {
Object.assign(this.env, env);
document.addEventListener('DOMContentLoaded', this.setupDOM.bind(this));
document.addEventListener('poll-start', function(ev) {
document.querySelectorAll('[id^="xhr_poll_status"]').forEach(function(e) {
e.style.display = (e.id == 'xhr_poll_status_off') ? 'none' : '';
@ -447,6 +445,17 @@
});
});
var domReady = new Promise(function(resolveFn, rejectFn) {
document.addEventListener('DOMContentLoaded', resolveFn);
});
Promise.all([
domReady,
this.require('ui')
]).then(this.setupDOM.bind(this)).catch(function(error) {
alert('LuCI class loading error:\n' + error);
});
originalCBIInit = window.cbi_init;
window.cbi_init = function() {};
},
@ -582,38 +591,32 @@
/* DOM setup */
setupDOM: function(ev) {
Promise.all([
L.require('ui')
]).then(function() {
Request.addInterceptor(function(res) {
if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
return;
Request.addInterceptor(function(res) {
if (res.status != 403 || res.headers.get('X-LuCI-Login-Required') != 'yes')
return;
Request.poll.stop();
Request.poll.stop();
L.ui.showModal(_('Session expired'), [
E('div', { class: 'alert-message warning' },
_('A new login is required since the authentication session expired.')),
E('div', { class: 'right' },
E('div', {
class: 'btn primary',
click: function() {
var loc = window.location;
window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
}
}, _('To login…')))
]);
L.ui.showModal(_('Session expired'), [
E('div', { class: 'alert-message warning' },
_('A new login is required since the authentication session expired.')),
E('div', { class: 'right' },
E('div', {
class: 'btn primary',
click: function() {
var loc = window.location;
window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
}
}, _('To login…')))
]);
L.error('AuthenticationError', 'Session expired');
});
originalCBIInit();
Request.poll.start();
document.dispatchEvent(new CustomEvent('luci-loaded'));
}).catch(function(error) {
alert('LuCI class loading error:\n' + error);
L.error('AuthenticationError', 'Session expired');
});
originalCBIInit();
Request.poll.start();
document.dispatchEvent(new CustomEvent('luci-loaded'));
},
env: {},
@ -923,7 +926,87 @@
}),
Class: Class,
Request: Request
Request: Request,
view: Class.extend({
__name__: 'LuCI.View',
__init__: function() {
var mc = document.getElementById('maincontent');
L.dom.content(mc, E('div', { 'class': 'spinning' }, _('Loading view…')));
return Promise.resolve(this.load())
.then(L.bind(this.render, this))
.then(L.bind(function(nodes) {
var mc = document.getElementById('maincontent');
L.dom.content(mc, nodes);
L.dom.append(mc, this.addFooter());
}, this));
},
load: function() {},
render: function() {},
handleSave: function(ev) {
var tasks = [];
document.getElementById('maincontent')
.querySelectorAll('.cbi-map').forEach(function(map) {
tasks.push(L.dom.callClassMethod(map, 'save'));
});
return Promise.all(tasks);
},
handleSaveApply: function(ev) {
return this.handleSave(ev).then(function() {
L.ui.changes.apply(true);
});
},
handleReset: function(ev) {
var tasks = [];
document.getElementById('maincontent')
.querySelectorAll('.cbi-map').forEach(function(map) {
tasks.push(L.dom.callClassMethod(map, 'reset'));
});
return Promise.all(tasks);
},
addFooter: function() {
var footer = E([]),
mc = document.getElementById('maincontent');
if (mc.querySelector('.cbi-map')) {
footer.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
E('input', {
'class': 'cbi-button cbi-button-apply',
'type': 'button',
'value': _('Save & Apply'),
'click': L.bind(this.handleSaveApply, this)
}), ' ',
E('input', {
'class': 'cbi-button cbi-button-save',
'type': 'submit',
'value': _('Save'),
'click': L.bind(this.handleSave, this)
}), ' ',
E('input', {
'class': 'cbi-button cbi-button-reset',
'type': 'button',
'value': _('Reset'),
'click': L.bind(this.handleReset, this)
})
]));
}
return footer;
}
})
});
XHR = Class.extend({

View file

@ -88,6 +88,9 @@ function index()
page = entry({"admin", "translations"}, call("action_translations"), nil)
page.leaf = true
page = entry({"admin", "ubus"}, call("action_ubus"), nil)
page.leaf = true
-- Logout is last
entry({"admin", "logout"}, call("action_logout"), _("Logout"), 999)
end
@ -129,6 +132,115 @@ function action_translations(lang)
http.write_json(i18n.dump())
end
local function ubus_reply(id, data, code, errmsg)
local reply = { jsonrpc = "2.0", id = id }
if errmsg then
reply.error = {
code = code,
message = errmsg
}
else
reply.result = { code, data }
end
return reply
end
local ubus_types = {
nil,
"array",
"object",
"string",
nil, -- INT64
"number",
nil, -- INT16,
"boolean",
"double"
}
local function ubus_request(req)
if type(req) ~= "table" or type(req.method) ~= "string" or type(req.params) ~= "table" or
#req.params < 2 or req.jsonrpc ~= "2.0" or req.id == nil then
return ubus_reply(req.id, nil, -32600, "Invalid request")
elseif req.method == "call" then
local sid, obj, fun, arg =
req.params[1], req.params[2], req.params[3], req.params[4] or {}
if type(arg) ~= "table" or arg.ubus_rpc_session ~= nil then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
if sid == "00000000000000000000000000000000" then
sid = luci.dispatcher.context.authsession
end
arg.ubus_rpc_session = sid
local res, code = luci.util.ubus(obj, fun, arg)
return ubus_reply(req.id, res, code or 0)
elseif req.method == "list" then
if type(params) ~= "table" or #params == 0 then
local objs = { luci.util.ubus() }
return ubus_reply(req.id, objs, 0)
else
local n, rv = nil, {}
for n = 1, #params do
if type(params[n]) ~= "string" then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
local sig = luci.util.ubus(params[n])
if sig and type(sig) == "table" then
rv[params[n]] = {}
local m, p
for m, p in pairs(sig) do
if type(p) == "table" then
rv[params[n]][m] = {}
local pn, pt
for pn, pt in pairs(p) do
rv[params[n]][m][pn] = ubus_types[pt] or "unknown"
end
end
end
end
end
return ubus_reply(req.id, rv, 0)
end
end
return ubus_reply(req.id, nil, -32601, "Method not found")
end
function action_ubus()
local parser = require "luci.jsonc".new()
luci.http.context.request:setfilehandler(function(_, s) parser:parse(s or "") end)
luci.http.context.request:content()
local json = parser:get()
if json == nil or type(json) ~= "table" then
luci.http.prepare_content("application/json")
luci.http.write_json(ubus_reply(nil, nil, -32700, "Parse error"))
return
end
local response
if #json == 0 then
response = ubus_request(json)
else
response = {}
local _, request
for _, request in ipairs(json) do
response[_] = ubus_request(request)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json(response)
end
function lease_status()
local s = require "luci.tools.status"