luci-base: add ubus-http gateway
authorJo-Philipp Wich <jo@mein.io>
Thu, 7 Feb 2019 17:40:36 +0000 (18:40 +0100)
committerJo-Philipp Wich <jo@mein.io>
Sun, 7 Jul 2019 13:25:49 +0000 (15:25 +0200)
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>
modules/luci-base/htdocs/luci-static/resources/luci.js
modules/luci-base/luasrc/controller/admin/index.lua

index 1653bcfce20e929528dc1aa84306fad796ae0796..07e8d8c88731c4ea7086c9be7d9d9ba7be1ba21c 100644 (file)
                __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' : '';
                                });
                        });
 
+                       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() {};
                },
 
                /* 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();
+
+                               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…')))
+                               ]);
 
-                                       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.error('AuthenticationError', 'Session expired');
-                               });
+                               L.error('AuthenticationError', 'Session expired');
+                       });
 
-                               originalCBIInit();
-                               Request.poll.start();
+                       originalCBIInit();
+                       Request.poll.start();
 
-                               document.dispatchEvent(new CustomEvent('luci-loaded'));
-                       }).catch(function(error) {
-                               alert('LuCI class loading error:\n' + error);
-                       });
+                       document.dispatchEvent(new CustomEvent('luci-loaded'));
                },
 
                env: {},
                }),
 
                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({
index 1f7db0cb38db474e17d5641a703e57f9747e6487..259c34eee81b4afd4214252acf8fcb2df1b25f02 100644 (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"