From a0df49718afe791d24ee1f12d5fce181b8a9f579 Mon Sep 17 00:00:00 2001 From: darosior Date: Fri, 27 Sep 2019 09:50:50 +0200 Subject: [PATCH] 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. --- lightningd/jsonrpc.c | 155 ++++++++++++++++++++++++++++++---- lightningd/test/run-jsonrpc.c | 4 + 2 files changed, 143 insertions(+), 16 deletions(-) diff --git a/lightningd/jsonrpc.c b/lightningd/jsonrpc.c index a4fcc4e40..82c445f3e 100644 --- a/lightningd/jsonrpc.c +++ b/lightningd/jsonrpc.c @@ -39,11 +39,14 @@ #include #include #include +#include #include #include #include #include #include +#include + /* 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 == ¶m_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); + 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); - assert(res == ¶m_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; + return command_still_pending(c); } /* Mutual recursion */ diff --git a/lightningd/test/run-jsonrpc.c b/lightningd/test/run-jsonrpc.c index c2e7f88b6..e59857522 100644 --- a/lightningd/test/run-jsonrpc.c +++ b/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;