Browse Source

lightningd/jsonrpc: Add a 'rpc_command' hook

The 'rpc_command' hook allows a plugin to take over any RPC command.
It sends the complete JSONRPC request to the plugin, which can then respond
with :
- {'continue'}: executes the command normally
- {'replace': {a_jsonrpc_request}}: replaces the request made
- {'return': {'result': {}}}: send a custom response
- {'return': {'error': {}}}: send a custom error

This way, a plugin can modify (/reimplement) or restrict the usage of
any of `lightningd`'s commands.

Changelog-Added: Plugin: A new plugin hook, `rpc_command` allows a plugin to take over `lightningd` for any RPC command.
travis-debug
darosior 6 years ago
committed by Christian Decker
parent
commit
a0df49718a
  1. 157
      lightningd/jsonrpc.c
  2. 4
      lightningd/test/run-jsonrpc.c

157
lightningd/jsonrpc.c

@ -39,11 +39,14 @@
#include <lightningd/log.h>
#include <lightningd/memdump.h>
#include <lightningd/options.h>
#include <lightningd/plugin_hook.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <wallet/db.h>
/* Dummy structure. */
struct command_result {
@ -575,6 +578,130 @@ struct json_stream *json_stream_fail(struct command *cmd,
return r;
}
static struct command_result *command_exec(struct json_connection *jcon,
struct command *cmd,
const char *buffer,
const jsmntok_t *request,
const jsmntok_t *params)
{
struct command_result *res;
res = cmd->json_cmd->dispatch(cmd, buffer, request, params);
assert(res == &param_failed
|| res == &complete
|| res == &pending
|| res == &unknown);
/* If they didn't complete it, they must call command_still_pending.
* If they completed it, it's freed already. */
if (res == &pending)
assert(cmd->pending);
list_for_each(&jcon->commands, cmd, list)
assert(cmd->pending);
return res;
}
/* A plugin hook to take over (fail/alter) RPC commands */
struct rpc_command_hook_payload {
struct command *cmd;
const char *buffer;
const jsmntok_t *request;
};
static void rpc_command_hook_serialize(struct rpc_command_hook_payload *p,
struct json_stream *s)
{
json_object_start(s, "rpc_command");
json_add_tok(s, "rpc_command", p->request, p->buffer);
json_object_end(s);
}
static void
rpc_command_hook_callback(struct rpc_command_hook_payload *p,
const char *buffer, const jsmntok_t *resulttok)
{
const jsmntok_t *tok, *method, *params, *custom_return, *tok_continue;
struct json_stream *response;
bool exec;
params = json_get_member(p->buffer, p->request, "params");
/* If no plugin registered, just continue command execution. Same if
* the registered plugin tells us to do so. */
if (buffer == NULL)
return was_pending(command_exec(p->cmd->jcon, p->cmd, p->buffer,
p->request, params));
else {
tok_continue = json_get_member(buffer, resulttok, "continue");
if (tok_continue && json_to_bool(buffer, tok_continue, &exec) && exec)
return was_pending(command_exec(p->cmd->jcon, p->cmd, p->buffer,
p->request, params));
}
/* If the registered plugin did not respond with continue,
* it wants either to replace the request... */
tok = json_get_member(buffer, resulttok, "replace");
if (tok) {
method = json_get_member(buffer, tok, "method");
params = json_get_member(buffer, tok, "params");
if (!method || !params)
return was_pending(command_fail(p->cmd, JSONRPC2_INVALID_REQUEST,
"Bad response to 'rpc_command' hook: "
"the 'replace' object must contain a "
"'method' and a 'params' field."));
p->cmd->json_cmd = find_cmd(p->cmd->ld->jsonrpc, buffer, method);
return was_pending(command_exec(p->cmd->jcon, p->cmd, buffer,
method, params));
}
/* ...or return a custom JSONRPC response. */
tok = json_get_member(buffer, resulttok, "return");
if (tok) {
custom_return = json_get_member(buffer, tok, "result");
if (custom_return) {
response = json_start(p->cmd);
json_add_tok(response, "result", custom_return, buffer);
json_object_compat_end(response);
return was_pending(command_raw_complete(p->cmd, response));
}
custom_return = json_get_member(buffer, tok, "error");
if (custom_return) {
int code;
const char *errmsg;
if (!json_to_int(buffer, json_get_member(buffer, custom_return, "code"),
&code))
return was_pending(command_fail(p->cmd, JSONRPC2_INVALID_REQUEST,
"Bad response to 'rpc_command' hook: "
"'error' object does not contain a code."));
errmsg = json_strdup(tmpctx, buffer,
json_get_member(buffer, custom_return, "message"));
if (!errmsg)
return was_pending(command_fail(p->cmd, JSONRPC2_INVALID_REQUEST,
"Bad response to 'rpc_command' hook: "
"'error' object does not contain a message."));
response = json_stream_fail_nodata(p->cmd, code, errmsg);
return was_pending(command_failed(p->cmd, response));
}
}
was_pending(command_fail(p->cmd, JSONRPC2_INVALID_REQUEST,
"Bad response to 'rpc_command' hook."));
}
REGISTER_PLUGIN_HOOK(rpc_command, rpc_command_hook_callback,
struct rpc_command_hook_payload *,
rpc_command_hook_serialize,
struct rpc_command_hook_payload *);
static void call_rpc_command_hook(struct rpc_command_hook_payload *p)
{
plugin_hook_call_rpc_command(p->cmd->ld, p, p);
}
/* We return struct command_result so command_fail return value has a natural
* sink; we don't actually use the result. */
static struct command_result *
@ -582,7 +709,7 @@ parse_request(struct json_connection *jcon, const jsmntok_t tok[])
{
const jsmntok_t *method, *id, *params;
struct command *c;
struct command_result *res;
struct rpc_command_hook_payload *rpc_hook;
if (tok[0].type != JSMN_OBJECT) {
json_command_malformed(jcon, "null",
@ -641,22 +768,18 @@ parse_request(struct json_connection *jcon, const jsmntok_t tok[])
jcon->buffer + method->start);
}
db_begin_transaction(jcon->ld->wallet->db);
res = c->json_cmd->dispatch(c, jcon->buffer, tok, params);
db_commit_transaction(jcon->ld->wallet->db);
assert(res == &param_failed
|| res == &complete
|| res == &pending
|| res == &unknown);
/* If they didn't complete it, they must call command_still_pending.
* If they completed it, it's freed already. */
if (res == &pending)
assert(c->pending);
list_for_each(&jcon->commands, c, list)
assert(c->pending);
return res;
rpc_hook = tal(c, struct rpc_command_hook_payload);
rpc_hook->cmd = c;
/* Duplicate since we might outlive the connection */
rpc_hook->buffer = tal_dup_arr(rpc_hook, char, jcon->buffer,
tal_count(jcon->buffer), 0);
rpc_hook->request = tal_dup_arr(rpc_hook, const jsmntok_t, tok,
tal_count(tok), 0);
/* Prevent a race between was_pending and still_pending */
new_reltimer(c->ld->timers, rpc_hook, time_from_msec(1),
call_rpc_command_hook, rpc_hook);
return command_still_pending(c);
}
/* Mutual recursion */

4
lightningd/test/run-jsonrpc.c

@ -102,6 +102,10 @@ struct command_result *param_tok(struct command *cmd UNNEEDED, const char *name
const char *buffer UNNEEDED, const jsmntok_t * tok UNNEEDED,
const jsmntok_t **out UNNEEDED)
{ fprintf(stderr, "param_tok called!\n"); abort(); }
/* Generated stub for plugin_hook_call_ */
void plugin_hook_call_(struct lightningd *ld UNNEEDED, const struct plugin_hook *hook UNNEEDED,
void *payload UNNEEDED, void *cb_arg UNNEEDED)
{ fprintf(stderr, "plugin_hook_call_ called!\n"); abort(); }
/* AUTOGENERATED MOCKS END */
bool deprecated_apis;

Loading…
Cancel
Save