diff --git a/common/json_tok.c b/common/json_tok.c index eab00fa36..8668df60b 100644 --- a/common/json_tok.c +++ b/common/json_tok.c @@ -78,6 +78,13 @@ struct command_result *param_string(struct command *cmd, const char *name, return NULL; } +struct command_result *param_ignore(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + const void *unused) +{ + return NULL; +} + struct command_result *param_label(struct command *cmd, const char *name, const char * buffer, const jsmntok_t *tok, struct json_escape **label) diff --git a/common/json_tok.h b/common/json_tok.h index 8473eda88..feb158536 100644 --- a/common/json_tok.h +++ b/common/json_tok.h @@ -85,4 +85,8 @@ struct command_result *param_tok(struct command *cmd, const char *name, const char *buffer, const jsmntok_t * tok, const jsmntok_t **out); +/* Ignore the token. Not usually used. */ +struct command_result *param_ignore(struct command *cmd, const char *name, + const char *buffer, const jsmntok_t *tok, + const void *unused); #endif /* LIGHTNING_COMMON_JSON_TOK_H */ diff --git a/common/param.c b/common/param.c index 4d184eb95..46485477e 100644 --- a/common/param.c +++ b/common/param.c @@ -263,6 +263,50 @@ static struct command_result *param_arr(struct command *cmd, const char *buffer, "Expected array or object for params"); } +#include + +const char *param_subcommand(struct command *cmd, const char *buffer, + const jsmntok_t tokens[], + const char *name, ...) +{ + va_list ap; + struct param *params = tal_arr(cmd, struct param, 0); + const char *arg, **names = tal_arr(tmpctx, const char *, 1); + const char *subcmd; + + param_add(¶ms, "subcommand", true, (void *)param_string, &subcmd); + names[0] = name; + va_start(ap, name); + while ((arg = va_arg(ap, const char *)) != NULL) + tal_arr_expand(&names, arg); + va_end(ap); + + if (command_usage_only(cmd)) { + char *usage = tal_strdup(cmd, "subcommand"); + for (size_t i = 0; i < tal_count(names); i++) + tal_append_fmt(&usage, "%c%s", + i == 0 ? '=' : '|', names[i]); + command_set_usage(cmd, usage); + return NULL; + } + + /* Check it's valid */ + if (param_arr(cmd, buffer, tokens, params, true) != NULL) { + return NULL; + } + + /* Check it's one of the known ones. */ + for (size_t i = 0; i < tal_count(names); i++) + if (streq(subcmd, names[i])) + return subcmd; + + /* We really do ignore this. */ + if (command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Unknown subcommand '%s'", subcmd)) + ; + return NULL; +} + bool param(struct command *cmd, const char *buffer, const jsmntok_t tokens[], ...) { diff --git a/common/param.h b/common/param.h index d0f160095..f4e2b40d2 100644 --- a/common/param.h +++ b/common/param.h @@ -58,13 +58,25 @@ typedef struct command_result *(*param_cbx)(struct command *cmd, const jsmntok_t *tok, void **arg); +/** + * Parse the first json value. + * + * name...: NULL-terminated array of valid values. + * + * Returns subcommand: if it returns NULL if you should return + * command_param_failed() immediately. + */ +const char *param_subcommand(struct command *cmd, const char *buffer, + const jsmntok_t tokens[], + const char *name, ...) LAST_ARG_NULL; + /* * Add a required parameter. */ #define p_req(name, cbx, arg) \ name"", \ true, \ - (cbx), \ + (param_cbx)(cbx), \ (arg) + 0*sizeof((cbx)((struct command *)NULL, \ (const char *)NULL, \ (const char *)NULL, \ @@ -77,7 +89,7 @@ typedef struct command_result *(*param_cbx)(struct command *cmd, #define p_opt(name, cbx, arg) \ name"", \ false, \ - (cbx), \ + (param_cbx)(cbx), \ ({ *arg = NULL; \ (arg) + 0*sizeof((cbx)((struct command *)NULL, \ (const char *)NULL, \ @@ -91,7 +103,7 @@ typedef struct command_result *(*param_cbx)(struct command *cmd, #define p_opt_def(name, cbx, arg, def) \ name"", \ false, \ - (cbx), \ + (param_cbx)(cbx), \ ({ (*arg) = tal((cmd), typeof(**arg)); \ (**arg) = (def); \ (arg) + 0*sizeof((cbx)((struct command *)NULL, \ diff --git a/lightningd/jsonrpc.c b/lightningd/jsonrpc.c index 44cf375a3..11629cbbf 100644 --- a/lightningd/jsonrpc.c +++ b/lightningd/jsonrpc.c @@ -269,49 +269,63 @@ static void slowcmd_start(struct slowcmd *sc) slowcmd_finish, sc); } -static struct command_result *json_slowcmd(struct command *cmd, - const char *buffer, - const jsmntok_t *obj UNUSED, - const jsmntok_t *params) +static struct command_result *json_dev(struct command *cmd UNUSED, + const char *buffer, + const jsmntok_t *obj UNNEEDED, + const jsmntok_t *params) { - struct slowcmd *sc = tal(cmd, struct slowcmd); + const char *subcmd; - sc->cmd = cmd; - if (!param(cmd, buffer, params, - p_opt_def("msec", param_number, &sc->msec, 1000), - NULL)) + subcmd = param_subcommand(cmd, buffer, params, + "crash", "rhash", "slowcmd", NULL); + if (!subcmd) return command_param_failed(); - new_reltimer(cmd->ld->timers, sc, time_from_msec(0), slowcmd_start, sc); - return command_still_pending(cmd); + if (streq(subcmd, "crash")) { + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + NULL)) + return command_param_failed(); + fatal("Crash at user request"); + } else if (streq(subcmd, "slowcmd")) { + struct slowcmd *sc = tal(cmd, struct slowcmd); + + sc->cmd = cmd; + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + p_opt_def("msec", param_number, &sc->msec, 1000), + NULL)) + return command_param_failed(); + + new_reltimer(cmd->ld->timers, sc, time_from_msec(0), + slowcmd_start, sc); + return command_still_pending(cmd); + } else { + assert(streq(subcmd, "rhash")); + struct json_stream *response; + struct sha256 *secret; + + if (!param(cmd, buffer, params, + p_req("subcommand", param_ignore, cmd), + p_req("secret", param_sha256, &secret), + NULL)) + return command_param_failed(); + + /* Hash in place. */ + sha256(secret, secret, sizeof(*secret)); + response = json_stream_success(cmd); + json_add_hex(response, "rhash", secret, sizeof(*secret)); + return command_success(cmd, response); + } } -static const struct json_command dev_slowcmd_command = { - "dev-slowcmd", +static const struct json_command dev_command = { + "dev", "developer", - json_slowcmd, - "Torture test for slow commands, optional {msec}" + json_dev, + "Developer command test multiplexer" }; -AUTODATA(json_command, &dev_slowcmd_command); - -static struct command_result *json_crash(struct command *cmd UNUSED, - const char *buffer, - const jsmntok_t *obj UNNEEDED, - const jsmntok_t *params) -{ - if (!param(cmd, buffer, params, NULL)) - return command_param_failed(); - - fatal("Crash at user request"); -} - -static const struct json_command dev_crash_command = { - "dev-crash", - "developer", - json_crash, - "Crash lightningd by calling fatal()" -}; -AUTODATA(json_command, &dev_crash_command); +AUTODATA(json_command, &dev_command); #endif /* DEVELOPER */ static size_t num_cmdlist; diff --git a/lightningd/test/run-jsonrpc.c b/lightningd/test/run-jsonrpc.c index 55c8d6420..74fdd55f3 100644 --- a/lightningd/test/run-jsonrpc.c +++ b/lightningd/test/run-jsonrpc.c @@ -70,6 +70,11 @@ struct command_result *param_feerate_estimate(struct command *cmd UNNEEDED, u32 **feerate_per_kw UNNEEDED, enum feerate feerate UNNEEDED) { fprintf(stderr, "param_feerate_estimate called!\n"); abort(); } +/* Generated stub for param_ignore */ +struct command_result *param_ignore(struct command *cmd UNNEEDED, const char *name UNNEEDED, + const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, + const void *unused UNNEEDED) +{ fprintf(stderr, "param_ignore called!\n"); abort(); } /* Generated stub for param_number */ struct command_result *param_number(struct command *cmd UNNEEDED, const char *name UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, @@ -80,6 +85,11 @@ struct command_result *param_sha256(struct command *cmd UNNEEDED, const char *na const char *buffer UNNEEDED, const jsmntok_t *tok UNNEEDED, struct sha256 **hash UNNEEDED) { fprintf(stderr, "param_sha256 called!\n"); abort(); } +/* Generated stub for param_subcommand */ +const char *param_subcommand(struct command *cmd UNNEEDED, const char *buffer UNNEEDED, + const jsmntok_t tokens[] UNNEEDED, + const char *name UNNEEDED, ...) +{ fprintf(stderr, "param_subcommand called!\n"); abort(); } /* Generated stub for param_tok */ struct command_result *param_tok(struct command *cmd UNNEEDED, const char *name UNNEEDED, const char *buffer UNNEEDED, const jsmntok_t * tok UNNEEDED, diff --git a/tests/test_misc.py b/tests/test_misc.py index b0235bf53..35a87a73a 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -506,14 +506,14 @@ def test_multiplexed_rpc(node_factory): # Neighbouring ones may be in or out of order. commands = [ - b'{"id":1,"jsonrpc":"2.0","method":"dev-slowcmd","params":[2000]}', - b'{"id":1,"jsonrpc":"2.0","method":"dev-slowcmd","params":[2000]}', - b'{"id":2,"jsonrpc":"2.0","method":"dev-slowcmd","params":[1500]}', - b'{"id":2,"jsonrpc":"2.0","method":"dev-slowcmd","params":[1500]}', - b'{"id":3,"jsonrpc":"2.0","method":"dev-slowcmd","params":[1000]}', - b'{"id":3,"jsonrpc":"2.0","method":"dev-slowcmd","params":[1000]}', - b'{"id":4,"jsonrpc":"2.0","method":"dev-slowcmd","params":[500]}', - b'{"id":4,"jsonrpc":"2.0","method":"dev-slowcmd","params":[500]}' + b'{"id":1,"jsonrpc":"2.0","method":"dev","params":["slowcmd",2000]}', + b'{"id":1,"jsonrpc":"2.0","method":"dev","params":["slowcmd",2000]}', + b'{"id":2,"jsonrpc":"2.0","method":"dev","params":["slowcmd",1500]}', + b'{"id":2,"jsonrpc":"2.0","method":"dev","params":["slowcmd",1500]}', + b'{"id":3,"jsonrpc":"2.0","method":"dev","params":["slowcmd",1000]}', + b'{"id":3,"jsonrpc":"2.0","method":"dev","params":["slowcmd",1000]}', + b'{"id":4,"jsonrpc":"2.0","method":"dev","params":["slowcmd",500]}', + b'{"id":4,"jsonrpc":"2.0","method":"dev","params":["slowcmd",500]}' ] sock.sendall(b'\n'.join(commands)) @@ -1260,3 +1260,54 @@ def test_bitcoind_fail_first(node_factory, bitcoind, executor): l1.daemon.rpcproxy.mock_rpc('estimatesmartfee', None) f.result() + + +@unittest.skipIf(not DEVELOPER, "needs dev command") +def test_dev_demux(node_factory): + l1 = node_factory.get_node(may_fail=True, allow_broken_log=True) + + # Check should work. + l1.rpc.check(command_to_check='dev', subcommand='crash') + l1.rpc.check(command_to_check='dev', subcommand='slowcmd', msec=1000) + l1.rpc.check(command_to_check='dev', subcommand='rhash', secret='00' * 32) + with pytest.raises(RpcError, match=r'Unknown subcommand'): + l1.rpc.check(command_to_check='dev', subcommand='foobar') + with pytest.raises(RpcError, match=r'unknown parameter'): + l1.rpc.check(command_to_check='dev', subcommand='crash', unk=1) + with pytest.raises(RpcError, match=r"'msec' should be an integer"): + l1.rpc.check(command_to_check='dev', subcommand='slowcmd', msec='aaa') + with pytest.raises(RpcError, match=r'missing required parameter'): + l1.rpc.check(command_to_check='dev', subcommand='rhash') + with pytest.raises(RpcError, match=r'missing required parameter'): + l1.rpc.check(command_to_check='dev') + + # Non-check failures should fail, in both object and array form. + with pytest.raises(RpcError, match=r'Unknown subcommand'): + l1.rpc.call('dev', {'subcommand': 'foobar'}) + with pytest.raises(RpcError, match=r'Unknown subcommand'): + l1.rpc.call('dev', ['foobar']) + with pytest.raises(RpcError, match=r'unknown parameter'): + l1.rpc.call('dev', {'subcommand': 'crash', 'unk': 1}) + with pytest.raises(RpcError, match=r'too many parameters'): + l1.rpc.call('dev', ['crash', 1]) + with pytest.raises(RpcError, match=r"'msec' should be an integer"): + l1.rpc.call('dev', {'subcommand': 'slowcmd', 'msec': 'aaa'}) + with pytest.raises(RpcError, match=r"'msec' should be an integer"): + l1.rpc.call('dev', ['slowcmd', 'aaa']) + with pytest.raises(RpcError, match=r'missing required parameter'): + l1.rpc.call('dev', {'subcommand': 'rhash'}) + with pytest.raises(RpcError, match=r'missing required parameter'): + l1.rpc.call('dev', ['rhash']) + with pytest.raises(RpcError, match=r'missing required parameter'): + l1.rpc.call('dev') + + # Help should list them all. + assert 'subcommand=crash|rhash|slowcmd' in l1.rpc.help('dev')['help'][0]['command'] + + # These work + assert l1.rpc.call('dev', ['slowcmd', '7'])['msec'] == 7 + assert l1.rpc.call('dev', {'subcommand': 'slowcmd', 'msec': '7'})['msec'] == 7 + assert l1.rpc.call('dev', {'subcommand': 'rhash', 'secret': '00' * 32})['rhash'] == '66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925' + + with pytest.raises(RpcError): + l1.rpc.call('dev', {'subcommand': 'crash'})