luci/modules/luci-base-ucode/ucode/controller/admin/index.uc
Jo-Philipp Wich ded8ccf93e luci-base-ucode: add initial ucode based LuCI runtime
This commits introduces an initial ucode based LuCI runtime. It supports
JSON menu files as used by Lua based LuCI and the template, call, view and
alias dispatch targets.

It is able to render a basic LuCI installation without errors. An embedded
Lua VM is lazily loaded when Lua based resources are encountered, such as
`*.htm` templates or server side Lua call targets.

When a template is requested, the ucode runtime first tries to render an
`/usr/share/ucode/luci/template/${path}.uc` ucode template and falls back
to rendering the corresponding `/usr/lib/lua/luci/view/${path}.htm` Lua
template in case no suitable ucode replacement is found. This allows for
gradual migration of existing Lua based tmeplates to ucode.

Furthermore, a set of stripped down LuCI libraries is shipped in the
`/usr/lib/lua/luci/ucodebridge/` directory. Those libraries provide
compatibility shims for the current Lua API towards Lua templates and Lua
based server side actions while utilizing the ucode request runtime state
internally.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
2022-10-25 01:03:37 +02:00

158 lines
3.9 KiB
Ucode

// Copyright 2022 Jo-Philipp Wich <jo@mein.io>
// Licensed to the public under the Apache License 2.0.
import { load_catalog, change_catalog, get_translations } from 'luci.core';
const ubus_types = [
null,
'array',
'object',
'string',
null, // INT64
'number',
null, // INT16,
'boolean',
'double'
];
function ubus_reply(id, data, code, errmsg) {
const reply = { jsonrpc: '2.0', id };
if (errmsg)
reply.error = { code, message: errmsg };
else if (type(code) == 'object')
reply.result = code;
else
reply.result = [ code, data ];
return reply;
}
function ubus_access(sid, obj, fun) {
return (ubus.call('session', 'access', {
ubus_rpc_session: sid,
scope: 'ubus',
object: obj,
function: fun
})?.access == true);
}
function ubus_request(req) {
if (type(req?.method) != 'string' || req?.jsonrpc != '2.0' || req?.id == null)
return ubus_reply(null, null, -32600, 'Invalid request');
if (req.method == 'call') {
if (type(req?.params) != 'array' || length(req.params) < 3)
return ubus_reply(null, null, -32600, 'Invalid parameters');
let sid = req.params[0],
obj = req.params[1],
fun = req.params[2],
arg = req.params[3] ?? {};
if (type(arg) != 'object' || exists(arg, 'ubus_rpc_session'))
return ubus_reply(req.id, null, -32602, 'Invalid parameters');
if (sid == '00000000000000000000000000000000' && ctx.authsession)
sid = ctx.authsession;
if (!ubus_access(sid, obj, fun))
return ubus_reply(req.id, null, -32002, 'Access denied');
arg.ubus_rpc_session = sid;
// clear error
ubus.error();
const res = ubus.call(obj, fun, arg);
return ubus_reply(req.id, res, ubus.error(true) ?? 0);
}
if (req.method == 'list') {
if (req?.params == null || (type(req.params) == 'array' && length(req.params) == 0)) {
return ubus_reply(req.id, null, ubus.list());
}
else if (type(req.params) == 'array') {
const rv = {};
for (let param in req.params) {
if (type(param) != 'string')
return ubus_reply(req.id, null, -32602, 'Invalid parameters');
for (let m, p in ubus.list(param)?.[0]) {
for (let pn, pt in p) {
rv[param] ??= {};
rv[param][m] ??= {};
rv[param][m][pn] = ubus_types[pt] ?? 'unknown';
}
}
}
return ubus_reply(req.id, null, rv);
}
else {
return ubus_reply(req.id, null, -32602, 'Invalid parameters')
}
}
return ubus_reply(req.id, null, -32601, 'Method not found')
}
return {
action_ubus: function() {
let request;
try { request = json(http.content()); }
catch { request = null; }
http.prepare_content('application/json; charset=UTF-8');
if (type(request) == 'object')
http.write_json(ubus_request(request));
else if (type(request) == 'array')
http.write_json(map(request, ubus_request));
else
http.write_json(ubus_reply(null, null, -32700, 'Parse error'))
},
action_translations: function(reqlang) {
if (reqlang != null && reqlang != dispatcher.lang) {
load_catalog(reqlang, '/usr/lib/lua/luci/i18n');
change_catalog(reqlang);
}
http.prepare_content('application/javascript; charset=UTF-8');
http.write('window.TR={');
get_translations((key, val) => http.write(sprintf('"%08x":%J,', key, val)));
http.write('};');
},
action_logout: function() {
const url = dispatcher.build_url();
if (ctx.authsession) {
ubus.call('session', 'destroy', { ubus_rpc_session: ctx.authsession });
if (http.getenv('HTTPS') == 'on')
http.header('Set-Cookie', `sysauth_https=; expires=Thu, 01 Jan 1970 01:00:00 GMT; path=${url}`);
http.header('Set-Cookie', `sysauth_http=; expires=Thu, 01 Jan 1970 01:00:00 GMT; path=${url}`);
}
http.redirect(url);
},
action_menu: function() {
const session = dispatcher.is_authenticated({ methods: [ 'cookie:sysauth_https', 'cookie:sysauth_http' ] });
const menu = dispatcher.menu_json(session?.acls ?? {}) ?? {};
http.prepare_content('application/json; charset=UTF-8');
http.write_json(menu);
}
};