mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-03-03 18:57:06 +01:00
commando: require runes for operation.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
ae4856df70
commit
8688daf937
2 changed files with 157 additions and 7 deletions
|
@ -1,8 +1,10 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <ccan/array_size/array_size.h>
|
#include <ccan/array_size/array_size.h>
|
||||||
|
#include <ccan/json_escape/json_escape.h>
|
||||||
#include <ccan/json_out/json_out.h>
|
#include <ccan/json_out/json_out.h>
|
||||||
#include <ccan/rune/rune.h>
|
#include <ccan/rune/rune.h>
|
||||||
#include <ccan/tal/str/str.h>
|
#include <ccan/tal/str/str.h>
|
||||||
|
#include <ccan/time/time.h>
|
||||||
#include <common/json_param.h>
|
#include <common/json_param.h>
|
||||||
#include <common/json_stream.h>
|
#include <common/json_stream.h>
|
||||||
#include <common/memleak.h>
|
#include <common/memleak.h>
|
||||||
|
@ -175,14 +177,96 @@ static void commando_error(struct commando *incoming,
|
||||||
send_response(NULL, NULL, NULL, reply);
|
send_response(NULL, NULL, NULL, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *check_rune(struct commando *incoming,
|
struct cond_info {
|
||||||
|
const struct node_id *peer;
|
||||||
|
const char *buf;
|
||||||
|
const jsmntok_t *method;
|
||||||
|
const jsmntok_t *params;
|
||||||
|
STRMAP(const jsmntok_t *) cached_params;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const char *check_condition(const tal_t *ctx,
|
||||||
|
const struct rune *rune,
|
||||||
|
const struct rune_altern *alt,
|
||||||
|
struct cond_info *cinfo)
|
||||||
|
{
|
||||||
|
const jsmntok_t *ptok;
|
||||||
|
|
||||||
|
if (streq(alt->fieldname, "time")) {
|
||||||
|
return rune_alt_single_int(ctx, alt, time_now().ts.tv_sec);
|
||||||
|
} else if (streq(alt->fieldname, "id")) {
|
||||||
|
const char *id = node_id_to_hexstr(tmpctx, cinfo->peer);
|
||||||
|
return rune_alt_single_str(ctx, alt, id, strlen(id));
|
||||||
|
} else if (streq(alt->fieldname, "method")) {
|
||||||
|
return rune_alt_single_str(ctx, alt,
|
||||||
|
cinfo->buf + cinfo->method->start,
|
||||||
|
cinfo->method->end - cinfo->method->start);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rest are params looksup: generate this once! */
|
||||||
|
if (cinfo->params) {
|
||||||
|
/* Note: we require that params be an obj! */
|
||||||
|
const jsmntok_t *t;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
json_for_each_obj(i, t, cinfo->params) {
|
||||||
|
char *pmemname = tal_fmt(tmpctx,
|
||||||
|
"pname%.*s",
|
||||||
|
t->end - t->start,
|
||||||
|
cinfo->buf + t->start);
|
||||||
|
size_t off = strlen("pname");
|
||||||
|
/* Remove punctuation! */
|
||||||
|
for (size_t n = off; pmemname[n]; n++) {
|
||||||
|
if (cispunct(pmemname[n]))
|
||||||
|
continue;
|
||||||
|
pmemname[off++] = pmemname[n];
|
||||||
|
}
|
||||||
|
pmemname[off++] = '\0';
|
||||||
|
strmap_add(&cinfo->cached_params, pmemname, t+1);
|
||||||
|
}
|
||||||
|
cinfo->params = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ptok = strmap_get(&cinfo->cached_params, alt->fieldname);
|
||||||
|
if (!ptok)
|
||||||
|
return rune_alt_single_missing(ctx, alt);
|
||||||
|
|
||||||
|
return rune_alt_single_str(ctx, alt,
|
||||||
|
cinfo->buf + ptok->start,
|
||||||
|
ptok->end - ptok->start);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *check_rune(const tal_t *ctx,
|
||||||
|
struct commando *incoming,
|
||||||
|
const struct node_id *peer,
|
||||||
const char *buf,
|
const char *buf,
|
||||||
const jsmntok_t *method,
|
const jsmntok_t *method,
|
||||||
const jsmntok_t *params,
|
const jsmntok_t *params,
|
||||||
const jsmntok_t *rune)
|
const jsmntok_t *runetok)
|
||||||
{
|
{
|
||||||
/* FIXME! */
|
struct rune *rune;
|
||||||
return NULL;
|
struct cond_info cinfo;
|
||||||
|
const char *err;
|
||||||
|
|
||||||
|
if (!runetok)
|
||||||
|
return "Missing rune";
|
||||||
|
|
||||||
|
rune = rune_from_base64n(tmpctx, buf + runetok->start,
|
||||||
|
runetok->end - runetok->start);
|
||||||
|
if (!rune)
|
||||||
|
return "Invalid rune";
|
||||||
|
|
||||||
|
cinfo.peer = peer;
|
||||||
|
cinfo.buf = buf;
|
||||||
|
cinfo.method = method;
|
||||||
|
cinfo.params = params;
|
||||||
|
strmap_init(&cinfo.cached_params);
|
||||||
|
err = rune_test(ctx, master_rune, rune, check_condition, &cinfo);
|
||||||
|
/* Just in case they manage to make us speak non-JSON, escape! */
|
||||||
|
if (err)
|
||||||
|
err = json_escape(ctx, take(err))->s;
|
||||||
|
strmap_clear(&cinfo.cached_params);
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void try_command(struct node_id *peer,
|
static void try_command(struct node_id *peer,
|
||||||
|
@ -223,7 +307,7 @@ static void try_command(struct node_id *peer,
|
||||||
}
|
}
|
||||||
rune = json_get_member(buf, toks, "rune");
|
rune = json_get_member(buf, toks, "rune");
|
||||||
|
|
||||||
failmsg = check_rune(incoming, buf, method, params, rune);
|
failmsg = check_rune(tmpctx, incoming, peer, buf, method, params, rune);
|
||||||
if (failmsg) {
|
if (failmsg) {
|
||||||
commando_error(incoming, COMMANDO_ERROR_REMOTE_AUTH,
|
commando_error(incoming, COMMANDO_ERROR_REMOTE_AUTH,
|
||||||
"Not authorized: %s", failmsg);
|
"Not authorized: %s", failmsg);
|
||||||
|
|
|
@ -2557,17 +2557,18 @@ def test_commando(node_factory, executor):
|
||||||
with pytest.raises(concurrent.futures.TimeoutError):
|
with pytest.raises(concurrent.futures.TimeoutError):
|
||||||
fut.result(10)
|
fut.result(10)
|
||||||
|
|
||||||
l1.rpc.commando_rune()
|
rune = l1.rpc.commando_rune()['rune']
|
||||||
|
|
||||||
# This works
|
# This works
|
||||||
res = l2.rpc.call(method='commando',
|
res = l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'listpeers'})
|
'method': 'listpeers'})
|
||||||
assert len(res['peers']) == 1
|
assert len(res['peers']) == 1
|
||||||
assert res['peers'][0]['id'] == l2.info['id']
|
assert res['peers'][0]['id'] == l2.info['id']
|
||||||
|
|
||||||
res = l2.rpc.call(method='commando',
|
res = l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'listpeers',
|
'method': 'listpeers',
|
||||||
'params': {'id': l2.info['id']}})
|
'params': {'id': l2.info['id']}})
|
||||||
assert len(res['peers']) == 1
|
assert len(res['peers']) == 1
|
||||||
|
@ -2576,16 +2577,19 @@ def test_commando(node_factory, executor):
|
||||||
with pytest.raises(RpcError, match='missing required parameter'):
|
with pytest.raises(RpcError, match='missing required parameter'):
|
||||||
l2.rpc.call(method='commando',
|
l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'withdraw'})
|
'method': 'withdraw'})
|
||||||
|
|
||||||
with pytest.raises(RpcError, match='unknown parameter: foobar'):
|
with pytest.raises(RpcError, match='unknown parameter: foobar'):
|
||||||
l2.rpc.call(method='commando',
|
l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
'method': 'invoice',
|
'method': 'invoice',
|
||||||
|
'rune': rune,
|
||||||
'params': {'foobar': 1}})
|
'params': {'foobar': 1}})
|
||||||
|
|
||||||
ret = l2.rpc.call(method='commando',
|
ret = l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'ping',
|
'method': 'ping',
|
||||||
'params': {'id': l2.info['id']}})
|
'params': {'id': l2.info['id']}})
|
||||||
assert 'totlen' in ret
|
assert 'totlen' in ret
|
||||||
|
@ -2593,6 +2597,7 @@ def test_commando(node_factory, executor):
|
||||||
# Now, reply will go over a multiple messages!
|
# Now, reply will go over a multiple messages!
|
||||||
ret = l2.rpc.call(method='commando',
|
ret = l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'getlog',
|
'method': 'getlog',
|
||||||
'params': {'level': 'io'}})
|
'params': {'level': 'io'}})
|
||||||
|
|
||||||
|
@ -2601,6 +2606,7 @@ def test_commando(node_factory, executor):
|
||||||
# Command will go over multiple messages.
|
# Command will go over multiple messages.
|
||||||
ret = l2.rpc.call(method='commando',
|
ret = l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'invoice',
|
'method': 'invoice',
|
||||||
'params': {'amount_msat': 'any',
|
'params': {'amount_msat': 'any',
|
||||||
'label': 'label',
|
'label': 'label',
|
||||||
|
@ -2613,6 +2619,7 @@ def test_commando(node_factory, executor):
|
||||||
with pytest.raises(RpcError, match='No connection to first peer found') as exc_info:
|
with pytest.raises(RpcError, match='No connection to first peer found') as exc_info:
|
||||||
l2.rpc.call(method='commando',
|
l2.rpc.call(method='commando',
|
||||||
payload={'peer_id': l1.info['id'],
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune,
|
||||||
'method': 'sendpay',
|
'method': 'sendpay',
|
||||||
'params': {'route': [{'amount_msat': 1000,
|
'params': {'route': [{'amount_msat': 1000,
|
||||||
'id': l1.info['id'],
|
'id': l1.info['id'],
|
||||||
|
@ -2653,3 +2660,62 @@ def test_commando_rune(node_factory):
|
||||||
rune5 = l1.rpc.commando_rune(rune4['rune'], "pnamelevel!|pnamelevel/io")
|
rune5 = l1.rpc.commando_rune(rune4['rune'], "pnamelevel!|pnamelevel/io")
|
||||||
assert rune5['rune'] == 'Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8='
|
assert rune5['rune'] == 'Dw2tzGCoUojAyT0JUw7fkYJYqExpEpaDRNTkyvWKoJY9MyZpZF4wMjJkMjIzNjIwYTM1OWE0N2ZmNyZtZXRob2Q9bGlzdHBlZXJzJnBuYW1lbGV2ZWwhfHBuYW1lbGV2ZWwvaW8='
|
||||||
assert rune5['unique_id'] == '3'
|
assert rune5['unique_id'] == '3'
|
||||||
|
|
||||||
|
# Replace rune3 with a more useful timestamp!
|
||||||
|
expiry = int(time.time()) + 15
|
||||||
|
rune3 = l1.rpc.commando_rune(restrictions="time<{}".format(expiry))
|
||||||
|
successes = ((rune1, "listpeers", {}),
|
||||||
|
(rune2, "listpeers", {}),
|
||||||
|
(rune2, "getinfo", {}),
|
||||||
|
(rune2, "getinfo", {}),
|
||||||
|
(rune3, "getinfo", {}),
|
||||||
|
(rune4, "listpeers", {}),
|
||||||
|
(rune5, "listpeers", {'id': l2.info['id']}),
|
||||||
|
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'broken'}))
|
||||||
|
failures = ((rune2, "withdraw", {}),
|
||||||
|
(rune2, "plugin", {'subcommand': 'list'}),
|
||||||
|
(rune3, "getinfo", {}),
|
||||||
|
(rune4, "listnodes", {}),
|
||||||
|
(rune5, "listpeers", {'id': l2.info['id'], 'level': 'io'}))
|
||||||
|
|
||||||
|
for rune, cmd, params in successes:
|
||||||
|
l2.rpc.call(method='commando',
|
||||||
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune['rune'],
|
||||||
|
'method': cmd,
|
||||||
|
'params': params})
|
||||||
|
|
||||||
|
while time.time() < expiry:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
for rune, cmd, params in failures:
|
||||||
|
print("{} {}".format(cmd, params))
|
||||||
|
with pytest.raises(RpcError, match='Not authorized:') as exc_info:
|
||||||
|
l2.rpc.call(method='commando',
|
||||||
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune['rune'],
|
||||||
|
'method': cmd,
|
||||||
|
'params': params})
|
||||||
|
assert exc_info.value.error['code'] == 0x4c51
|
||||||
|
|
||||||
|
# rune5 can only be used by l2:
|
||||||
|
l3 = node_factory.get_node()
|
||||||
|
l3.connect(l1)
|
||||||
|
with pytest.raises(RpcError, match='Not authorized:') as exc_info:
|
||||||
|
l3.rpc.call(method='commando',
|
||||||
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune5['rune'],
|
||||||
|
'method': "listpeers",
|
||||||
|
'params': {}})
|
||||||
|
assert exc_info.value.error['code'] == 0x4c51
|
||||||
|
|
||||||
|
# Remote doesn't allow array parameters.
|
||||||
|
l2.rpc.check_request_schemas = False
|
||||||
|
with pytest.raises(RpcError, match='Params must be object') as exc_info:
|
||||||
|
l2.rpc.call(method='commando',
|
||||||
|
payload={'peer_id': l1.info['id'],
|
||||||
|
'rune': rune5['rune'],
|
||||||
|
'method': "listpeers",
|
||||||
|
'params': [l2.info['id'], 'io']})
|
||||||
|
assert exc_info.value.error['code'] == 0x4c50
|
||||||
|
l2.rpc.check_request_schemas = True
|
||||||
|
|
Loading…
Add table
Reference in a new issue