diff --git a/cli/lightning-cli.c b/cli/lightning-cli.c index 83d35422a..dfc2ff00f 100644 --- a/cli/lightning-cli.c +++ b/cli/lightning-cli.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -613,6 +614,29 @@ static void opt_log_stderr_exit_usage(const char *fmt, ...) exit(ERROR_USAGE); } +struct commando { + const char *peer_id; + const char *rune; +}; + +static char *opt_set_commando(const char *arg, struct commando **commando) +{ + size_t idlen = strcspn(arg, ":"); + *commando = tal(NULL, struct commando); + + /* We don't use common/node_id.c here, to keep dependencies minimal */ + if (idlen != PUBKEY_CMPR_LEN * 2) + return "Invalid peer id"; + (*commando)->peer_id = tal_strndup(*commando, arg, idlen); + + if (arg[idlen] == '\0') + (*commando)->rune = NULL; + else + (*commando)->rune = tal_strdup(*commando, arg + idlen + 1); + + return NULL; +} + int main(int argc, char *argv[]) { setup_locale(); @@ -633,6 +657,7 @@ int main(int argc, char *argv[]) enum log_level notification_level = LOG_INFORM; bool last_was_progress = false; char *command = NULL, *filter = NULL; + struct commando *commando = NULL; err_set_progname(argv[0]); jsmn_init(&parser); @@ -665,12 +690,18 @@ int main(int argc, char *argv[]) opt_register_arg("-l|--filter", opt_set_charp, opt_show_charp, &filter, "Set JSON reply filter"); + opt_register_arg("-c|--commando", opt_set_commando, + NULL, &commando, + "Send this as a commando command to nodeid:rune"); opt_register_version(); opt_early_parse(argc, argv, opt_log_stderr_exit_usage); opt_parse(&argc, argv, opt_log_stderr_exit_usage); + /* Make sure this is parented correctly if set! */ + tal_steal(ctx, commando); + method = argv[1]; if (!method) { char *usage = opt_usage(argv[0], NULL); @@ -682,7 +713,7 @@ int main(int argc, char *argv[]) /* Launch a manpage if we have a help command with an argument. We do * not need to have lightningd running in this case. */ - if (streq(method, "help") && format == DEFAULT_FORMAT && argc >= 3) { + if (streq(method, "help") && format == DEFAULT_FORMAT && argc >= 3 && !commando) { command = argv[2]; char *page = tal_fmt(ctx, "lightning-%s", command); @@ -724,16 +755,34 @@ int main(int argc, char *argv[]) else idstr = tal_fmt(ctx, "cli:%s#%i", method, getpid()); - if (notification_level <= LOG_LEVEL_MAX) + /* FIXME: commando should support notifications! */ + if (notification_level <= LOG_LEVEL_MAX && !commando) enable_notifications(fd); cmd = tal_fmt(ctx, "{ \"jsonrpc\" : \"2.0\", \"method\" : \"%s\", \"id\" : \"%s\",", - json_escape(ctx, method)->s, idstr); - if (filter) + commando ? "commando" : json_escape(ctx, method)->s, + idstr); + if (filter && !commando) tal_append_fmt(&cmd, "\"filter\": %s,", filter); tal_append_fmt(&cmd, " \"params\" :"); + if (commando) { + tal_append_fmt(&cmd, "{" + " \"peer_id\": \"%s\"," + " \"method\": \"%s\",", + commando->peer_id, + json_escape(ctx, method)->s); + if (filter) { + tal_append_fmt(&cmd, "\"filter\": %s,", filter); + } + if (commando->rune) { + tal_append_fmt(&cmd, " \"rune\": \"%s\",", + commando->rune); + } + tal_append_fmt(&cmd, " \"params\": "); + } + if (input == DEFAULT_INPUT) { /* Hacky autodetect; only matters if more than single arg */ if (argc > 2 && strchr(argv[2], '=')) @@ -764,6 +813,10 @@ int main(int argc, char *argv[]) tal_append_fmt(&cmd, "] }"); } + /* For commando, "params" we just populated is inside real "params" */ + if (commando) + tal_append_fmt(&cmd, "}"); + toks = json_parse_simple(ctx, cmd, strlen(cmd)); if (toks == NULL) errx(ERROR_USAGE, diff --git a/doc/lightning-cli.1.md b/doc/lightning-cli.1.md index 9a41cf32c..0b1433303 100644 --- a/doc/lightning-cli.1.md +++ b/doc/lightning-cli.1.md @@ -69,7 +69,7 @@ field without parsing JSON. If *LEVEL* is 'none', then never print out notifications. Otherwise, print out notifications of *LEVEL* or above (one of `io`, `debug`, `info` (the default), `unusual` or `broken`: they are prefixed with `# -`. +`. (Note: currently not supported with `--commando`). * **--filter**/**-l**=*JSON* @@ -84,12 +84,21 @@ be changed using `-F`, `-R`, `-J`, `-H` etc. Print version number to standard output and exit. -* **allow-deprecated-apis**=*BOOL* +* **--allow-deprecated-apis**=*BOOL* Enable deprecated options. It defaults to *true*, but you should set it to *false* when testing to ensure that an upgrade won't break your configuration. +* **--commando**/**-c**=**peerid**:**rune** + + Convenience option to indicate that this command should be wrapped +in a `commando` command to be sent to the connected peer with id +`peerid`, using rune `rune`. This also means that any `--filter` is +handed via commando to the remote peer to reduce its output (which it +will do it it is v23.02 or newer), rather than trying to do so +locally. Note that currently `-N` is not supported by commando. + COMMANDS -------- diff --git a/tests/test_misc.py b/tests/test_misc.py index d7ecdca98..3feb87ccf 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1044,6 +1044,121 @@ def test_cli(node_factory): assert [l for l in lines if not re.search(r'^help\[[0-9]*\].', l)] == ['format-hint=simple'] +def test_cli_commando(node_factory): + l1, l2 = node_factory.line_graph(2, fundchannel=False, + opts={'log-level': 'io'}) + rune = l2.rpc.commando_rune()['rune'] + + # Invalid peer id. + val = subprocess.run(['cli/lightning-cli', + '--commando=00', + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + 'help']) + assert val.returncode == 3 + + # Valid peer id, but needs rune! + val = subprocess.run(['cli/lightning-cli', + '--commando={}'.format(l2.info['id']), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + 'help']) + assert val.returncode == 1 + + # This works! + out = subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + 'help']).decode('utf-8') + # Test some known output. + assert 'help [command]\n List available commands, or give verbose help on one {command}' in out + + # Check JSON id is as expected + l1.daemon.wait_for_log(r'jsonrpc#[0-9]*: "cli:help#[0-9]*"\[IN\]') + + # And through l2... + l2.daemon.wait_for_log(r'jsonrpc#[0-9]*: "cli:help#[0-9]*/cln:commando#[0-9]*/commando:help#[0-9]*"\[IN\]') + + # Test keyword input (forced) + out = subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + '-J', '-k', + 'help', 'command=help']).decode('utf-8') + j, _ = json.JSONDecoder().raw_decode(out) + assert 'help [command]' in j['help'][0]['verbose'] + + # Test ordered input (forced) + out = subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + '-J', '-o', + 'help', 'help']).decode('utf-8') + j, _ = json.JSONDecoder().raw_decode(out) + assert 'help [command]' in j['help'][0]['verbose'] + + # Test filtering + out = subprocess.check_output(['cli/lightning-cli', + '-c', '{}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + '-J', '--filter={"help":[{"command":true}]}', + 'help', 'help']).decode('utf-8') + j, _ = json.JSONDecoder().raw_decode(out) + assert j == {'help': [{'command': 'help [command]'}]} + + # Test missing parameters. + try: + # This will error due to missing parameters. + # We want to check if lightningd will crash. + out = subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + '-J', '-o', + 'sendpay']).decode('utf-8') + except Exception: + pass + + # Test it escapes JSON completely in both method and params. + # cli turns " into \", reply turns that into \\\". + out = subprocess.run(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + 'x"[]{}'], + stdout=subprocess.PIPE) + assert 'Unknown command' in out.stdout.decode('utf-8') + + subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + 'invoice', '123000', 'l"[]{}', 'd"[]{}']).decode('utf-8') + # Check label is correct, and also that cli's keyword parsing works. + out = subprocess.check_output(['cli/lightning-cli', + '--commando={}:{}'.format(l2.info['id'], rune), + '--network={}'.format(TEST_NETWORK), + '--lightning-dir={}' + .format(l1.daemon.lightning_dir), + '-k', + 'listinvoices', 'label=l"[]{}']).decode('utf-8') + j = json.loads(out) + assert only_one(j['invoices'])['label'] == 'l"[]{}' + + def test_daemon_option(node_factory): """ Make sure --daemon at least vaguely works!