mirror of
https://github.com/openwrt/luci.git
synced 2025-05-15 15:12:18 +00:00
Convert existing JavaScript code in LuCI base to utilize ES6 standard features such as spread arguments, arrow functions, object method declarations etc. This makes the code somewhat easier to follow and slightly smaller. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
486 lines
15 KiB
JavaScript
486 lines
15 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require request';
|
|
|
|
let rpcRequestID = 1;
|
|
let rpcSessionID = L.env.sessionid ?? '00000000000000000000000000000000';
|
|
let rpcBaseURL = L.url('admin/ubus');
|
|
const rpcInterceptorFns = [];
|
|
|
|
/**
|
|
* @class rpc
|
|
* @memberof LuCI
|
|
* @hideconstructor
|
|
* @classdesc
|
|
*
|
|
* The `LuCI.rpc` class provides high level ubus JSON-RPC abstractions
|
|
* and means for listing and invoking remove RPC methods.
|
|
*/
|
|
return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
|
|
/* privates */
|
|
call(req, cb, nobatch) {
|
|
let q = '';
|
|
|
|
if (Array.isArray(req)) {
|
|
if (req.length == 0)
|
|
return Promise.resolve([]);
|
|
|
|
for (let i = 0; i < req.length; i++)
|
|
if (req[i].params)
|
|
q += '%s%s.%s'.format(
|
|
q ? ';' : '/',
|
|
req[i].params[1],
|
|
req[i].params[2]
|
|
);
|
|
}
|
|
|
|
return request.post(rpcBaseURL + q, req, {
|
|
timeout: (L.env.rpctimeout ?? 20) * 1000,
|
|
nobatch,
|
|
credentials: true
|
|
}).then(cb, cb);
|
|
},
|
|
|
|
parseCallReply(req, res) {
|
|
let msg = null;
|
|
|
|
if (res instanceof Error)
|
|
return req.reject(res);
|
|
|
|
try {
|
|
if (!res.ok)
|
|
L.raise('RPCError', 'RPC call to %s/%s failed with HTTP error %d: %s',
|
|
req.object, req.method, res.status, res.statusText || '?');
|
|
|
|
msg = res.json();
|
|
}
|
|
catch (e) {
|
|
return req.reject(e);
|
|
}
|
|
|
|
/*
|
|
* The interceptor args are intentionally swapped.
|
|
* Response is passed as first arg to align with Request class interceptors
|
|
*/
|
|
Promise.all(rpcInterceptorFns.map(fn => fn(msg, req)))
|
|
.then(this.handleCallReply.bind(this, req, msg))
|
|
.catch(req.reject);
|
|
},
|
|
|
|
handleCallReply(req, msg) {
|
|
const type = Object.prototype.toString;
|
|
let ret = null;
|
|
|
|
try {
|
|
/* verify message frame */
|
|
if (!L.isObject(msg) || msg.jsonrpc != '2.0')
|
|
L.raise('RPCError', 'RPC call to %s/%s returned invalid message frame',
|
|
req.object, req.method);
|
|
|
|
/* check error condition */
|
|
if (L.isObject(msg.error) && msg.error.code && msg.error.message)
|
|
L.raise('RPCError', 'RPC call to %s/%s failed with error %d: %s',
|
|
req.object, req.method, msg.error.code, msg.error.message || '?');
|
|
}
|
|
catch (e) {
|
|
return req.reject(e);
|
|
}
|
|
|
|
if (!req.object && !req.method) {
|
|
ret = msg.result;
|
|
}
|
|
else if (Array.isArray(msg.result)) {
|
|
if (req.raise && msg.result[0] !== 0)
|
|
L.raise('RPCError', 'RPC call to %s/%s failed with ubus code %d: %s',
|
|
req.object, req.method, msg.result[0], this.getStatusText(msg.result[0]));
|
|
|
|
ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
|
|
}
|
|
|
|
if (req.expect) {
|
|
for (const key in req.expect) {
|
|
if (ret != null && key != '')
|
|
ret = ret[key];
|
|
|
|
if (ret == null || type.call(ret) != type.call(req.expect[key]))
|
|
ret = req.expect[key];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* apply filter */
|
|
if (typeof(req.filter) == 'function') {
|
|
req.priv[0] = ret;
|
|
req.priv[1] = req.params;
|
|
ret = req.filter.apply(this, req.priv);
|
|
}
|
|
|
|
req.resolve(ret);
|
|
},
|
|
|
|
/**
|
|
* Lists available remote ubus objects or the method signatures of
|
|
* specific objects.
|
|
*
|
|
* This function has two signatures and is sensitive to the number of
|
|
* arguments passed to it:
|
|
* - `list()` -
|
|
* Returns an array containing the names of all remote `ubus` objects
|
|
* - `list("objname", ...)`
|
|
* Returns method signatures for each given `ubus` object name.
|
|
*
|
|
* @param {...string} [objectNames]
|
|
* If any object names are given, this function will return the method
|
|
* signatures of each given object.
|
|
*
|
|
* @returns {Promise<Array<string>|Object<string, Object<string, Object<string, string>>>>}
|
|
* When invoked without arguments, this function will return a promise
|
|
* resolving to an array of `ubus` object names. When invoked with one or
|
|
* more arguments, a promise resolving to an object describing the method
|
|
* signatures of each requested `ubus` object name will be returned.
|
|
*/
|
|
list(...args) {
|
|
const msg = {
|
|
jsonrpc: '2.0',
|
|
id: rpcRequestID++,
|
|
method: 'list',
|
|
params: args.length ? args : undefined
|
|
};
|
|
|
|
return new Promise(L.bind(function(resolve, reject) {
|
|
/* store request info */
|
|
const req = { resolve, reject };
|
|
|
|
/* call rpc */
|
|
this.call(msg, this.parseCallReply.bind(this, req));
|
|
}, this));
|
|
},
|
|
|
|
/**
|
|
* @typedef {Object} DeclareOptions
|
|
* @memberof LuCI.rpc
|
|
*
|
|
* @property {string} object
|
|
* The name of the remote `ubus` object to invoke.
|
|
*
|
|
* @property {string} method
|
|
* The name of the remote `ubus` method to invoke.
|
|
*
|
|
* @property {string[]} [params]
|
|
* Lists the named parameters expected by the remote `ubus` RPC method.
|
|
* The arguments passed to the resulting generated method call function
|
|
* will be mapped to named parameters in the order they appear in this
|
|
* array.
|
|
*
|
|
* Extraneous parameters passed to the generated function will not be
|
|
* sent to the remote procedure but are passed to the
|
|
* {@link LuCI.rpc~filterFn filter function} if one is specified.
|
|
*
|
|
* Examples:
|
|
* - `params: [ "foo", "bar" ]` -
|
|
* When the resulting call function is invoked with `fn(true, false)`,
|
|
* the corresponding args object sent to the remote procedure will be
|
|
* `{ foo: true, bar: false }`.
|
|
* - `params: [ "test" ], filter: function(reply, args, extra) { ... }` -
|
|
* When the resulting generated function is invoked with
|
|
* `fn("foo", "bar", "baz")` then `{ "test": "foo" }` will be sent as
|
|
* argument to the remote procedure and the filter function will be
|
|
* invoked with `filterFn(reply, [ "foo" ], "bar", "baz")`
|
|
*
|
|
* @property {Object<string,*>} [expect]
|
|
* Describes the expected return data structure. The given object is
|
|
* supposed to contain a single key selecting the value to use from
|
|
* the returned `ubus` reply object. The value of the sole key within
|
|
* the `expect` object is used to infer the expected type of the received
|
|
* `ubus` reply data.
|
|
*
|
|
* If the received data does not contain `expect`'s key, or if the
|
|
* type of the data differs from the type of the value in the expect
|
|
* object, the expect object's value is returned as default instead.
|
|
*
|
|
* The key in the `expect` object may be an empty string (`''`) in which
|
|
* case the entire reply object is selected instead of one of its subkeys.
|
|
*
|
|
* If the `expect` option is omitted, the received reply will be returned
|
|
* as-is, regardless of its format or type.
|
|
*
|
|
* Examples:
|
|
* - `expect: { '': { error: 'Invalid response' } }` -
|
|
* This requires the entire `ubus` reply to be a plain JavaScript
|
|
* object. If the reply isn't an object but e.g. an array or a numeric
|
|
* error code instead, it will get replaced with
|
|
* `{ error: 'Invalid response' }` instead.
|
|
* - `expect: { results: [] }` -
|
|
* This requires the received `ubus` reply to be an object containing
|
|
* a key `results` with an array as value. If the received reply does
|
|
* not contain such a key, or if `reply.results` points to a non-array
|
|
* value, the empty array (`[]`) will be used instead.
|
|
* - `expect: { success: false }` -
|
|
* This requires the received `ubus` reply to be an object containing
|
|
* a key `success` with a boolean value. If the reply does not contain
|
|
* `success` or if `reply.success` is not a boolean value, `false` will
|
|
* be returned as default instead.
|
|
*
|
|
* @property {LuCI.rpc~filterFn} [filter]
|
|
* Specifies an optional filter function which is invoked to transform the
|
|
* received reply data before it is returned to the caller.
|
|
*
|
|
* @property {boolean} [reject=false]
|
|
* If set to `true`, non-zero ubus call status codes are treated as fatal
|
|
* error and lead to the rejection of the call promise. The default
|
|
* behaviour is to resolve with the call return code value instead.
|
|
*/
|
|
|
|
/**
|
|
* The filter function is invoked to transform a received `ubus` RPC call
|
|
* reply before returning it to the caller.
|
|
*
|
|
* @callback LuCI.rpc~filterFn
|
|
*
|
|
* @param {*} data
|
|
* The received `ubus` reply data or a subset of it as described in the
|
|
* `expect` option of the RPC call declaration. In case of remote call
|
|
* errors, `data` is numeric `ubus` error code instead.
|
|
*
|
|
* @param {Array<*>} args
|
|
* The arguments the RPC method has been invoked with.
|
|
*
|
|
* @param {...*} extraArgs
|
|
* All extraneous arguments passed to the RPC method exceeding the number
|
|
* of arguments describes in the RPC call declaration.
|
|
*
|
|
* @return {*}
|
|
* The return value of the filter function will be returned to the caller
|
|
* of the RPC method as-is.
|
|
*/
|
|
|
|
/**
|
|
* The generated invocation function is returned by
|
|
* {@link LuCI.rpc#declare rpc.declare()} and encapsulates a single
|
|
* RPC method call.
|
|
*
|
|
* Calling this function will execute a remote `ubus` HTTP call request
|
|
* using the arguments passed to it as arguments and return a promise
|
|
* resolving to the received reply values.
|
|
*
|
|
* @callback LuCI.rpc~invokeFn
|
|
*
|
|
* @param {...*} params
|
|
* The parameters to pass to the remote procedure call. The given
|
|
* positional arguments will be named to named RPC parameters according
|
|
* to the names specified in the `params` array of the method declaration.
|
|
*
|
|
* Any additional parameters exceeding the amount of arguments in the
|
|
* `params` declaration are passed as private extra arguments to the
|
|
* declared filter function.
|
|
*
|
|
* @return {Promise<*>}
|
|
* Returns a promise resolving to the result data of the remote `ubus`
|
|
* RPC method invocation, optionally substituted and filtered according
|
|
* to the `expect` and `filter` declarations.
|
|
*/
|
|
|
|
/**
|
|
* Describes a remote RPC call procedure and returns a function
|
|
* implementing it.
|
|
*
|
|
* @param {LuCI.rpc.DeclareOptions} options
|
|
* If any object names are given, this function will return the method
|
|
* signatures of each given object.
|
|
*
|
|
* @returns {LuCI.rpc~invokeFn}
|
|
* Returns a new function implementing the method call described in
|
|
* `options`.
|
|
*/
|
|
declare(options) {
|
|
return Function.prototype.bind.call(function(rpc, options, ...args) {
|
|
return new Promise((resolve, reject) => {
|
|
/* build parameter object */
|
|
let p_off = 0;
|
|
const params = { };
|
|
if (Array.isArray(options.params))
|
|
for (p_off = 0; p_off < options.params.length; p_off++)
|
|
params[options.params[p_off]] = args[p_off];
|
|
|
|
/* all remaining arguments are private args */
|
|
const priv = [ undefined, undefined ];
|
|
for (; p_off < args.length; p_off++)
|
|
priv.push(args[p_off]);
|
|
|
|
/* store request info */
|
|
const req = {
|
|
expect: options.expect,
|
|
filter: options.filter,
|
|
resolve,
|
|
reject,
|
|
params,
|
|
priv,
|
|
object: options.object,
|
|
method: options.method,
|
|
raise: options.reject
|
|
};
|
|
|
|
/* build message object */
|
|
const msg = {
|
|
jsonrpc: '2.0',
|
|
id: rpcRequestID++,
|
|
method: 'call',
|
|
params: [
|
|
rpcSessionID,
|
|
options.object,
|
|
options.method,
|
|
params
|
|
]
|
|
};
|
|
|
|
/* call rpc */
|
|
rpc.call(msg, rpc.parseCallReply.bind(rpc, req), options.nobatch);
|
|
});
|
|
}, this, this, options);
|
|
},
|
|
|
|
/**
|
|
* Returns the current RPC session id.
|
|
*
|
|
* @returns {string}
|
|
* Returns the 32 byte session ID string used for authenticating remote
|
|
* requests.
|
|
*/
|
|
getSessionID() {
|
|
return rpcSessionID;
|
|
},
|
|
|
|
/**
|
|
* Set the RPC session id to use.
|
|
*
|
|
* @param {string} sid
|
|
* Sets the 32 byte session ID string used for authenticating remote
|
|
* requests.
|
|
*/
|
|
setSessionID(sid) {
|
|
rpcSessionID = sid;
|
|
},
|
|
|
|
/**
|
|
* Returns the current RPC base URL.
|
|
*
|
|
* @returns {string}
|
|
* Returns the RPC URL endpoint to issue requests against.
|
|
*/
|
|
getBaseURL() {
|
|
return rpcBaseURL;
|
|
},
|
|
|
|
/**
|
|
* Set the RPC base URL to use.
|
|
*
|
|
* @param {string} url
|
|
* Sets the RPC URL endpoint to issue requests against.
|
|
*/
|
|
setBaseURL(url) {
|
|
rpcBaseURL = url;
|
|
},
|
|
|
|
/**
|
|
* Translates a numeric `ubus` error code into a human readable
|
|
* description.
|
|
*
|
|
* @param {number} statusCode
|
|
* The numeric status code.
|
|
*
|
|
* @returns {string}
|
|
* Returns the textual description of the code.
|
|
*/
|
|
getStatusText(statusCode) {
|
|
switch (statusCode) {
|
|
case 0: return _('Command OK');
|
|
case 1: return _('Invalid command');
|
|
case 2: return _('Invalid argument');
|
|
case 3: return _('Method not found');
|
|
case 4: return _('Resource not found');
|
|
case 5: return _('No data received');
|
|
case 6: return _('Permission denied');
|
|
case 7: return _('Request timeout');
|
|
case 8: return _('Not supported');
|
|
case 9: return _('Unspecified error');
|
|
case 10: return _('Connection lost');
|
|
default: return _('Unknown error code');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Registered interceptor functions are invoked before the standard reply
|
|
* parsing and handling logic.
|
|
*
|
|
* By returning rejected promises, interceptor functions can cause the
|
|
* invocation function to fail, regardless of the received reply.
|
|
*
|
|
* Interceptors may also modify their message argument in-place to
|
|
* rewrite received replies before they're processed by the standard
|
|
* response handling code.
|
|
*
|
|
* A common use case for such functions is to detect failing RPC replies
|
|
* due to expired authentication in order to trigger a new login.
|
|
*
|
|
* @callback LuCI.rpc~interceptorFn
|
|
*
|
|
* @param {*} msg
|
|
* The unprocessed, JSON decoded remote RPC method call reply.
|
|
*
|
|
* Since interceptors run before the standard parsing logic, the reply
|
|
* data is not verified for correctness or filtered according to
|
|
* `expect` and `filter` specifications in the declarations.
|
|
*
|
|
* @param {Object} req
|
|
* The related request object which is an extended variant of the
|
|
* declaration object, allowing access to internals of the invocation
|
|
* function such as `filter`, `expect` or `params` values.
|
|
*
|
|
* @return {Promise<*>|*}
|
|
* Interceptor functions may return a promise to defer response
|
|
* processing until some delayed work completed. Any values the returned
|
|
* promise resolves to are ignored.
|
|
*
|
|
* When the returned promise rejects with an error, the invocation
|
|
* function will fail too, forwarding the error to the caller.
|
|
*/
|
|
|
|
/**
|
|
* Registers a new interceptor function.
|
|
*
|
|
* @param {LuCI.rpc~interceptorFn} interceptorFn
|
|
* The interceptor function to register.
|
|
*
|
|
* @returns {LuCI.rpc~interceptorFn}
|
|
* Returns the given function value.
|
|
*/
|
|
addInterceptor(interceptorFn) {
|
|
if (typeof(interceptorFn) == 'function')
|
|
rpcInterceptorFns.push(interceptorFn);
|
|
|
|
return interceptorFn;
|
|
},
|
|
|
|
/**
|
|
* Removes a registered interceptor function.
|
|
*
|
|
* @param {LuCI.rpc~interceptorFn} interceptorFn
|
|
* The interceptor function to remove.
|
|
*
|
|
* @returns {boolean}
|
|
* Returns `true` if the given function has been removed or `false`
|
|
* if it has not been found.
|
|
*/
|
|
removeInterceptor(interceptorFn) {
|
|
const oldlen = rpcInterceptorFns.length;
|
|
let i = oldlen;
|
|
|
|
while (i--)
|
|
if (rpcInterceptorFns[i] === interceptorFn)
|
|
rpcInterceptorFns.splice(i, 1);
|
|
|
|
return (rpcInterceptorFns.length < oldlen);
|
|
}
|
|
});
|