Rename directory 'archive' to 'Unmaintained'

This commit is contained in:
fmhoeger
2024-02-01 15:40:46 -06:00
committed by mergify[bot]
parent 7cbfcaf025
commit e538e3d559
82 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
# Commando plugin
Commando has been **included in Core Lightning as first class C plugin**.
It has been actively developed since and has more cool new features added
than listed below.
Checkout latest updates on commando at:
https://docs.corelightning.org/docs/commando &
https://docs.corelightning.org/reference/lightning-commando
------------------------------------------------------------------------------------------------------
# Archived Commando python plugin
This plugin allows other nodes to send your node commands, and allows you
to send them to other nodes. The nodes must be authorized, and must be
directly connected.
Motto: Reckless? Try going commando!
## Installation
This plugin requires the runes library; and to use runes requires
datastore support. You can either use a lightningd version after
0.10.1, or the [datastore plugin](https://github.com/lightningd/plugins/blob/datastore/README.md).
For general plugin installation instructions see the repos main
[README.md](https://github.com/lightningd/plugins/blob/master/README.md#Installation)
## Options and Commands
There are two configuration options, which can be specified multiple
times:
* --commando-reader: a node id which can execute (most) `list` and `get` / `summary` commands
* --commando-writer: a node id which can execute any commands.
You can do this for static access lists, no runes necessary. You would
normally put "commando-writer" (or "commando-reader") lines in your
config file.
For quick testing, you can use this fairly awkward command to start the
plugin dynamically, with a reader by node id:
lightning-cli plugin subcommand=start plugin=`pwd`/commando.py commando_reader=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518
### Using Commando to Control A Node
Once the node has authorized you can run the `commando` command to send it a
command, like this example which sends a `stop` message to 022d...
lightning-cli commando 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59 stop
For more advanced authorization, you can create **runes** which permit
restricted access, and send them along with commands.
### Creating Runes
If you have datastore support (see the [datastore
plugin](https://github.com/lightningd/plugins/blob/datastore/README.md),
you can also create a "rune": anyone who has the rune can use it to
execute the commands it allows.
- `commando-rune` by itself gives a "full access" rune.
- `commando-rune restrictions=readonly` gives a rune which is restricted to get,
list and summary commands.
For example, say we have peer
0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62,
and we want to allow peer
0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518 to
run read-only commands:
0336...$ lightning-cli commando-rune restrictions=readonly
{
"rune": "ZN7IkVe8S0fO7htvQ23mCMQ-QGzFTvn0OZPqucp881Vjb21tYW5kXmxpc3R8Y29tbWFuZF5nZXR8Y29tbWFuZD1zdW1tYXJ5JmNvbW1hbmQvZ2V0c2hhcmVkc2VjcmV0"
}
We could hand that rune out to anyone, to access.
### Using Runes
You use a rune a peer gives you with the `rune` parameter to `commando`, eg:
02be...$ lightning-cli commando 0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62 listchannels {} j2fEW43Y8Ie7d0oGt9pPxaIcl6RP6MjRGC1mgxKuUDxpZD0wMmJlODVlNzA4MjFlNmNjZjIxNDlmMWE3YmY1ZTM0ZDc3OTAwMGY3MjgxNTQ1MDhjYzkwNzJlNGU5MDE4MmNkZDI=
Or, using keyword parameters:
02be...$ lightning-cli commando peer_id=0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62 method=listchannels rune=j2fEW43Y8Ie7d0oGt9pPxaIcl6RP6MjRGC1mgxKuUDxpZD0wMmJlODVlNzA4MjFlNmNjZjIxNDlmMWE3YmY1ZTM0ZDc3OTAwMGY3MjgxNTQ1MDhjYzkwNzJlNGU5MDE4MmNkZDI=
It's more common to set your peer to persistently cache the rune as the default for whenever you issue a command, using `commando-cacherune`:
02be...$ lightning-cli commando 0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62 commando-cacherune {} j2fEW43Y8Ie7d0oGt9pPxaIcl6RP6MjRGC1mgxKuUDxpZD0wMmJlODVlNzA4MjFlNmNjZjIxNDlmMWE3YmY1ZTM0ZDc3OTAwMGY3MjgxNTQ1MDhjYzkwNzJlNGU5MDE4MmNkZDI=
02be...$ lightning-cli commando 0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62 listpeers
### Restricting Runes
There's a [runes library](https://github.com/rustyrussell/runes/): which lets you add restrictions, but for
convenience the `commando-rune` can also add them, like so:
- `commandorune RUNE RESTRICTION...`
Each RESTRICTION is a string: fieldname, followed by a condition, followed by a
value. It can either be a single string, or an array of strings.
Valid fieldnames are:
* **id**: what peer is allowed to use it.
* **time**: time in seconds since 1970, as returned by `date +%s` or Python `int(time.time())`.
* **version**: what version of c-lightning is running.
* **command**: the command they are trying to run (e.g. "listpeers")
* **parr0**..**parrN**: the parameters if specified using a JSON array
* **pnameNAME**..: the parameters (by name) if specified using a JSON object ('-' and other punctuation are removed from NAME).
conditions are listed in the [runes documentation](https://github.com/rustyrussell/runes/blob/v0.3.1/README.md#rune-language):
* `!`: Pass if field is missing (value ignored)
* `=`: Pass if exists and exactly equals
* `^`: Pass if exists and begins with
* `$`: Pass if exists and ends with
* `~`: Pass if exists and contains
* `<`: Pass if exists, is a valid decimal (may be signed), and numerically less than
* `>`: Pass if exists, is a valid decimal (may be signed), and numerically greater than
* `}`: Pass if exists and lexicograpically greater than (or longer)
* `{`: Pass if exists and lexicograpically less than (or shorter)
* `#`: Always pass: no condition, this is a comment.
Say we have peer
0336efaa22b8ba77ae721a25d589e1c5f2486073dd2f041add32a23316150e8b62,
and we want to allow peer
0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518 to
run listpeers on itself. This is actually three restrictions:
1. id=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518,
since it must be the one initiating the command.
2. method=listpeers, since that's the only command it can run.
3. pnameid=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518 OR
parr0=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518;
we let them specify parameters by name or array, so allow both.
We can add these restrictions one at a time, or specify them all at
once. By default, we start with the master rune, which has no
restrictions:
0336...$ lightning-cli commando-rune restrictions='["id=02be85e70821e6ccf2149f1a7bf5e34d779000f728154508cc9072e4e90182cdd2","method=listpeers","pnameid=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518|parr0=0266e4598d1d3c415f572a8488830b60f7e744ed9235eb0b1ba93283b315c03518"]'
{
"rune": "-As0gqymadZpgTnm9fBoDVtrjPmpwPrKmCQWUcqlouJpZD0wMmJlODVlNzA4MjFlNmNjZjIxNDlmMWE3YmY1ZTM0ZDc3OTAwMGY3MjgxNTQ1MDhjYzkwNzJlNGU5MDE4MmNkZDImbWV0aG9kPWxpc3RwZWVycyZwbmFtZWlkPTAyNjZlNDU5OGQxZDNjNDE1ZjU3MmE4NDg4ODMwYjYwZjdlNzQ0ZWQ5MjM1ZWIwYjFiYTkzMjgzYjMxNWMwMzUxOHxwYXJyMD0wMjY2ZTQ1OThkMWQzYzQxNWY1NzJhODQ4ODgzMGI2MGY3ZTc0NGVkOTIzNWViMGIxYmE5MzI4M2IzMTVjMDM1MTg="
}
We can publish this on Twitter and it doesn't matter, since it only
works for that one peer.
### Temporary Runes to Authorize Yourself
This creates a rune which can only be used to create another rune for
a specific nodeid, for (as of this writing!) the next 60 seconds:
lightning-cli commando-rune restrictions='["method=commando-rune","pnamerestrictions^id=|parr1^id=","time<1627886935"]'
That rune only allows them to rune "commando-rune" with an "id="
restriction, within the given time; useful to place in a QR code to
allow self-authorization.
### Huge Commands and Responses
Commands larger than about 64k are split into multiple parts; command
responses similarly. To avoid a Denial of Service, commands must be
less than about 1MB in size: that's increased to 10MB if the peer has
successfully used `commando-cacherune`.

426
Unmaintained/commando/commando.py Executable file
View File

@@ -0,0 +1,426 @@
#!/usr/bin/env python3
"""Commando is a plugin to allow one node to control another. You use
"commando" to send commands, with 'method', 'params' and optional
'rune' which authorizes it.
Additionally, you can use "commando-rune" to create/add restrictions to
existing runes (you can also use the runes.py library).
Rather than handing a rune every time, peers can do "commando-cacherune"
to make it the persistent default for their peer_id.
The formats are:
type:4C4D - execute this command (with more coming)
type:4C4F - execute this command
type:594B - reply (with more coming)
type:594D - last reply
Each one is an 8 byte id (to link replies to command), followed by JSON.
"""
from pyln.client import Plugin, RpcError # type: ignore
import json
import textwrap
import time
import random
import secrets
import string
import runes # type: ignore
import multiprocessing
from typing import Dict, Tuple, Optional
plugin = Plugin()
# "YOLO"!
COMMANDO_CMD_CONTINUES = 0x4c4d
COMMANDO_CMD_TERM = 0x4c4f
# Replies are split across multiple CONTINUES, then TERM.
COMMANDO_REPLY_CONTINUES = 0x594b
COMMANDO_REPLY_TERM = 0x594d
class CommandResponse:
def __init__(self, req):
self.buf = bytes()
self.req = req
class InReq:
def __init__(self, idnum):
self.idnum = idnum
self.buf = b''
self.discard = False
def append(self, data):
if not self.discard:
self.buf += data
def start_discard(self):
self.buf = b''
self.discard = True
def split_cmd(cmdstr):
"""Interprets JSON and method and params"""
cmd = json.loads(cmdstr)
return cmd['method'], cmd.get('params', {}), cmd.get('rune')
def send_msg(plugin, peer_id, msgtype, idnum, contents):
"""Messages are form [8-byte-id][data]"""
msg = (msgtype.to_bytes(2, 'big')
+ idnum.to_bytes(8, 'big')
+ bytes(contents, encoding='utf8'))
plugin.rpc.call(plugin.msgcmd, {'node_id': peer_id, 'msg': msg.hex()})
def send_msgs(plugin, peer_id, idnum, obj, msgtype_cont, msgtype_term):
# We can only send 64k in a message, but there is 10 byte overhead
# in the message header; 65000 is safe.
parts = textwrap.wrap(json.dumps(obj), 65000)
for p in parts[:-1]:
send_msg(plugin, peer_id, msgtype_cont, idnum, p)
send_msg(plugin, peer_id, msgtype_term, idnum, parts[-1])
def send_result(plugin, peer_id, idnum, res):
send_msgs(plugin, peer_id, idnum, res,
COMMANDO_REPLY_CONTINUES, COMMANDO_REPLY_TERM)
def send_request(plugin, peer_id, idnum, req):
send_msgs(plugin, peer_id, idnum, req,
COMMANDO_CMD_CONTINUES, COMMANDO_CMD_TERM)
def is_rune_valid(plugin, runestr) -> Tuple[Optional[runes.Rune], str]:
"""Is this runestring valid, and authorized for us?"""
try:
rune = runes.Rune.from_base64(runestr)
except: # noqa: E722
return None, 'Malformed base64 string'
if not plugin.masterrune.is_rune_authorized(rune):
return None, 'Invalid rune string'
return rune, ''
def check_rune(plugin, node_id, runestr, command, params) -> Tuple[bool, str]:
"""If we have a runestr, check it's valid and conditions met"""
# If they don't specify a rune, we use any previous for this peer
if runestr is None:
runestr = plugin.peer_runes.get(node_id)
if runestr is None:
# Finally, try reader-writer lists
if node_id in plugin.writers:
runestr = plugin.masterrune.to_base64()
elif node_id in plugin.readers:
runestr = add_reader_restrictions(plugin.masterrune.copy())
if runestr is None:
return False, 'No rune'
commando_dict = {'time': int(time.time()),
'id': node_id,
'version': plugin.version,
'method': command}
# FIXME: This doesn't work well with complex params (it makes them str())
if isinstance(params, list):
for i, p in enumerate(params):
commando_dict['parr{}'.format(i)] = p
else:
for k, v in params.items():
# Cannot have punctuation in fieldnames, so remove.
for c in string.punctuation:
k = k.replace(c, '')
commando_dict['pname{}'.format(k)] = v
return plugin.masterrune.check_with_reason(runestr, commando_dict)
def do_cacherune(plugin, peer_id, runestr):
if not plugin.have_datastore:
return {'error': 'No datastore available: try datastore.py?'}
if runestr is None:
return {'error': 'No rune set?'}
rune, whynot = is_rune_valid(plugin, runestr)
if not rune:
return {'error': whynot}
plugin.peer_runes[peer_id] = runestr
save_peer_rune(plugin, peer_id, runestr)
return {'result': {'rune': runestr}}
def command_run(plugin, peer_id, idnum, method, params):
"""Function to run a command and write the result"""
try:
res = {'result': plugin.rpc.call(method, params)}
except RpcError as e:
res = {'error': e.error}
send_result(plugin, peer_id, idnum, res)
def try_command(plugin, peer_id, idnum, method, params, runestr):
"""Run an arbitrary command and message back the result"""
# You can always set your rune, even if *that rune* wouldn't
# allow it!
if method == 'commando-cacherune':
res = do_cacherune(plugin, peer_id, runestr)
else:
ok, failstr = check_rune(plugin, peer_id, runestr, method, params)
if not ok:
res = {'error': 'Not authorized: ' + failstr}
elif method in plugin.methods:
# Don't try to call indirectly into ourselves; we deadlock!
# But commando-rune is useful, so hardcode that.
if method == "commando-rune":
if isinstance(params, list):
res = {'result': commando_rune(plugin, *params)}
else:
res = {'result': commando_rune(plugin, **params)}
else:
res = {'error': 'FIXME: Refusing to call inside ourselves'}
else:
# The subprocess does send_result itself: pyln-client doesn't
# support async RPC yet.
multiprocessing.Process(target=command_run,
args=(plugin, peer_id, idnum, method, params)).start()
return
send_result(plugin, peer_id, idnum, res)
@plugin.async_hook('custommsg')
def on_custommsg(peer_id, payload, plugin, request, **kwargs):
pbytes = bytes.fromhex(payload)
mtype = int.from_bytes(pbytes[:2], "big")
idnum = int.from_bytes(pbytes[2:10], "big")
data = pbytes[10:]
if mtype == COMMANDO_CMD_CONTINUES:
if peer_id not in plugin.in_reqs or idnum != plugin.in_reqs[peer_id].idnum:
plugin.in_reqs[peer_id] = InReq(idnum)
plugin.in_reqs[peer_id].append(data)
# If you have cached a rune, give 10MB, otherwise 1MB.
# We can have hundreds of these things...
max_cmdlen = 1000000
if peer_id in plugin.peer_runes:
max_cmdlen *= 10
if len(plugin.in_reqs[peer_id].buf) > max_cmdlen:
plugin.in_reqs[peer_id].start_discard()
elif mtype == COMMANDO_CMD_TERM:
# Prepend any prior data from COMMANDO_CMD_CONTINUES:
if peer_id in plugin.in_reqs:
data = plugin.in_reqs[peer_id].buf + data
discard = plugin.in_reqs[peer_id].discard
del plugin.in_reqs[peer_id]
# Were we ignoring this for being too long? Error out now.
if discard:
send_result(plugin, peer_id, idnum,
{'error': "Command too long"})
request.set_result({'result': 'continue'})
return
method, params, runestr = split_cmd(data)
try_command(plugin, peer_id, idnum, method, params, runestr)
elif mtype == COMMANDO_REPLY_CONTINUES:
if idnum in plugin.out_reqs:
plugin.out_reqs[idnum].buf += data
elif mtype == COMMANDO_REPLY_TERM:
if idnum in plugin.out_reqs:
plugin.out_reqs[idnum].buf += data
finished = plugin.out_reqs[idnum]
del plugin.out_reqs[idnum]
try:
ret = json.loads(finished.buf.decode())
except Exception as e:
# Bad response
finished.req.set_exception(e)
return {'result': 'continue'}
if 'error' in ret:
# Pass through error
finished.req.set_exception(RpcError('commando', {},
ret['error']))
else:
# Pass through result
finished.req.set_result(ret['result'])
request.set_result({'result': 'continue'})
@plugin.subscribe('disconnect')
def on_disconnect(id, plugin, request, **kwargs):
if id in plugin.in_reqs:
del plugin.in_reqs[id]
@plugin.async_method("commando")
def commando(plugin, request, peer_id, method, params=None, rune=None):
"""Send a command to node_id, and wait for a response"""
res = {'method': method}
if params:
res['params'] = params
if rune:
res['rune'] = rune
while True:
idnum = random.randint(0, 2**64)
if idnum not in plugin.out_reqs:
break
plugin.out_reqs[idnum] = CommandResponse(request)
send_request(plugin, peer_id, idnum, res)
@plugin.method("commando-cacherune")
def commando_cacherune(plugin, rune):
"""Sets the rune given to the persistent rune for this peer_id"""
# This is intercepted by commando runner, above.
raise RpcError('commando-cacherune', {},
'Must be called as a remote commando call')
def add_reader_restrictions(rune: runes.Rune) -> str:
"""Let them execute list or get, but not getsharesecret!"""
# Allow list*, get* or summary.
rune.add_restriction(runes.Restriction.from_str('method^list'
'|method^get'
'|method=summary'))
# But not getsharesecret!
rune.add_restriction(runes.Restriction.from_str('method/getsharedsecret'))
# And not listdatastore!
rune.add_restriction(runes.Restriction.from_str('method/listdatastore'))
return rune.to_base64()
def save_peer_rune(plugin, peer_id, runestr) -> None:
assert plugin.have_datastore
plugin.rpc.datastore(key=['commando', 'peer_runes', peer_id],
string=runestr,
mode='create-or-replace')
def load_peer_runes(plugin) -> Dict[str, str]:
if not plugin.have_datastore:
return {}
peer_runes = {}
entries = plugin.rpc.listdatastore(key=['commando', 'peer_runes'])
for entry in entries['datastore']:
peer_runes[entry['key'][2]] = entry['string']
return peer_runes
@plugin.method("commando-rune")
def commando_rune(plugin, rune=None, restrictions=[]):
"""Create a rune, (or derive from {rune}) with the given
{restrictions} array (or string), or 'readonly'"""
if not plugin.have_datastore:
raise RpcError('commando-rune', {},
'No datastore available: try datastore.py?')
if rune is None:
this_rune = plugin.masterrune.copy()
this_rune.add_restriction(runes.Restriction.unique_id(plugin.rune_counter))
else:
this_rune, whynot = is_rune_valid(plugin, rune)
if this_rune is None:
raise RpcError('commando-rune', {'rune': rune}, whynot)
if restrictions == 'readonly':
add_reader_restrictions(this_rune)
elif isinstance(restrictions, str):
this_rune.add_restriction(runes.Restriction.from_str(restrictions))
else:
for r in restrictions:
this_rune.add_restriction(runes.Restriction.from_str(r))
# Now we've succeeded, update rune_counter.
if rune is None:
plugin.rpc.datastore(key=['commando', 'rune_counter'],
string=str(plugin.rune_counter + 1),
mode='must-replace',
generation=plugin.rune_counter_generation)
plugin.rune_counter += 1
plugin.rune_counter_generation += 1
return {'rune': this_rune.to_base64()}
@plugin.init()
def init(options, configuration, plugin):
plugin.out_reqs = {}
plugin.in_reqs = {}
plugin.writers = options['commando-writer']
plugin.readers = options['commando-reader']
plugin.version = plugin.rpc.getinfo()['version']
# dev-sendcustommsg was renamed to sendcustommsg for 0.10.1
try:
plugin.rpc.help('sendcustommsg')
plugin.msgcmd = 'sendcustommsg'
except RpcError:
plugin.msgcmd = 'dev-sendcustommsg'
# Unfortunately, on startup it can take a while for
# the datastore to be loaded (as it's actually a second plugin,
# loaded by the first.
end = time.time() + 10
secret = None
while time.time() < end:
try:
secret = plugin.rpc.listdatastore(['commando', 'secret'])['datastore']
except RpcError:
time.sleep(1)
else:
break
if secret is None:
# Use a throwaway secret
secret = secrets.token_bytes()
plugin.have_datastore = False
plugin.peer_runes = {}
plugin.log("Initialized without rune support"
" (needs datastore.py plugin)",
level="info")
else:
plugin.have_datastore = True
if secret == []:
plugin.log("Creating initial rune secret", level='unusual')
secret = secrets.token_bytes()
plugin.rpc.datastore(key=['commando', 'secret'], hex=secret.hex())
plugin.rune_counter = 0
plugin.rune_counter_generation = 0
plugin.rpc.datastore(key=['commando', 'rune_counter'], string=str(0))
else:
secret = bytes.fromhex(secret[0]['hex'])
counter = plugin.rpc.listdatastore(['commando', 'rune_counter'])['datastore'][0]
plugin.rune_counter = int(counter['string'])
plugin.rune_counter_generation = int(counter['generation'])
plugin.log("Initialized with rune support: {} runes so far".format(plugin.rune_counter),
level="info")
plugin.masterrune = runes.MasterRune(secret)
plugin.peer_runes = load_peer_runes(plugin)
plugin.add_option('commando-writer',
description="What nodeid can do all commands?",
default=[],
multi=True)
plugin.add_option('commando-reader',
description="What nodeid can do list/get/summary commands?",
default=[],
multi=True)
plugin.run()

View File

@@ -0,0 +1,2 @@
runes>=0.4
pyln-client>=0.10.1

View File

@@ -0,0 +1,325 @@
import os
from pyln.testing.fixtures import * # type: ignore
from pyln.client import RpcError # type: ignore
import pytest
import json
import runes # type: ignore
import commando
import time
plugin_path = os.path.join(os.path.dirname(__file__), "commando.py")
datastore_path = os.path.join(os.path.dirname(os.path.dirname(__file__)),
"datastore", "datastore.py")
def test_add_reader_restrictions():
mrune = runes.MasterRune(bytes(32))
runestr = commando.add_reader_restrictions(mrune.copy())
assert mrune.check_with_reason(runestr, {'method': 'listfoo'}) == (True, '')
assert mrune.check_with_reason(runestr, {'method': 'getfoo'}) == (True, '')
assert mrune.check_with_reason(runestr, {'method': 'getsharedsecret'}) == (False, 'method: = getsharedsecret')
assert mrune.check_with_reason(runestr, {'method': 'summary'}) == (True, '')
assert mrune.check_with_reason(runestr, {'method': 'fail'}) == (False, 'method: does not start with list AND method: does not start with get AND method: != summary')
def test_commando(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=True)
l1.rpc.plugin_start(plugin_path, commando_reader=l2.info['id'])
l2.rpc.plugin_start(plugin_path)
# This works
res = l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'listpeers'})
assert len(res['peers']) == 1
assert res['peers'][0]['id'] == l2.info['id']
res = l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'listpeers',
'params': {'id': l2.info['id']}})
assert len(res['peers']) == 1
assert res['peers'][0]['id'] == l2.info['id']
# This fails
with pytest.raises(RpcError, match='method: does not start with list AND method: does not start with get AND method: != summary'):
l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'withdraw'})
# As a writer, anything goes.
l1.rpc.plugin_stop(plugin_path)
l1.rpc.plugin_start(plugin_path, commando_writer=l2.info['id'])
with pytest.raises(RpcError, match='missing required parameter'):
l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'withdraw'})
ret = l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'ping',
'params': {'id': l2.info['id']}})
assert 'totlen' in ret
# Now, this will go over a single message!
ret = l2.rpc.call(method='commando',
payload={'peer_id': l1.info['id'],
'method': 'getlog',
'params': {'level': 'io'}})
assert len(json.dumps(ret)) > 65535
def test_commando_rune(node_factory):
l1, l2, l3 = node_factory.line_graph(3, fundchannel=False,
opts={'plugin': [plugin_path,
datastore_path]})
l1.daemon.logsearch_start = 0
l1.daemon.wait_for_log("Initialized with rune support")
l2.daemon.logsearch_start = 0
l2.daemon.wait_for_log("Initialized with rune support")
l3.daemon.logsearch_start = 0
l3.daemon.wait_for_log("Initialized with rune support")
wrune = l2.rpc.commando_rune()['rune']
rrune = l2.rpc.commando_rune(restrictions='readonly')['rune']
# This works
res = l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': rrune,
'method': 'listpeers'})
assert len(res['peers']) == 2
# This fails (no rune!)
with pytest.raises(RpcError, match='Not authorized'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'withdraw'})
# This fails (ro rune!)
with pytest.raises(RpcError, match='Not authorized'):
res = l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': rrune,
'method': 'withdraw'})
# This would succeed, except missing param)
with pytest.raises(RpcError, match='missing required parameter'):
res = l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': wrune,
'method': 'withdraw'})
# We can subrune and use that rune explicitly.
lcrune = l2.rpc.commando_rune(rrune, 'method=listchannels')['rune']
with pytest.raises(RpcError, match='Not authorized'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listpeers'})
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels'})
# Only allow it to list l3's channels (by source, second param)
lcrune = l2.rpc.commando_rune(rrune, ['method=listchannels',
'pnamesource=' + l3.info['id']
+ '|' + 'parr1=' + l3.info['id']])['rune']
# Needs rune!
with pytest.raises(RpcError, match='Not authorized'):
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': [None, l3.info['id']]})
# Command wrong
with pytest.raises(RpcError, match='Not authorized.*method'):
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'withdraw'})
# Params missing
with pytest.raises(RpcError, match='Not authorized.*missing'):
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels'})
# Param wrong (array)
with pytest.raises(RpcError, match='Not authorized.*parr1'):
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels',
'params': [None, l2.info['id']]})
# Param wrong (obj)
with pytest.raises(RpcError, match='Not authorized.*pnamesource'):
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels',
'params': {'source': l2.info['id']}})
# Param right (array)
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels',
'params': [None, l3.info['id']]})
# Param right (obj)
l3.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'rune': lcrune,
'method': 'listchannels',
'params': {'source': l3.info['id']}})
def test_commando_cacherune(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False,
opts={'plugin': [plugin_path,
datastore_path]})
restrictions = ['method=listchannels',
'pnamesource={id}|parr1={id}'.format(id=l1.info['id'])]
lcrune = l2.rpc.commando_rune(restrictions=restrictions)['rune']
# You can't set it, it needs to be via commando!
with pytest.raises(RpcError,
match='Must be called as a remote commando call'):
l1.rpc.commando_cacherune(lcrune)
l1.rpc.commando(peer_id=l2.info['id'],
method='commando-cacherune',
rune=lcrune)
# Param wrong (array)
with pytest.raises(RpcError, match='Not authorized.*parr1'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': [None, l2.info['id']]})
# Param wrong (obj)
with pytest.raises(RpcError, match='Not authorized.*pnamesource'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': {'source': l2.info['id']}})
# Param right (array)
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': [None, l1.info['id']]})
# Param right (obj)
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': {'source': l1.info['id']}})
# Still works after restart!
l2.restart()
l1.rpc.connect(l2.info['id'], 'localhost', l2.port)
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'params': {'source': l1.info['id']}})
def test_rune_time(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False,
opts={'plugin': [plugin_path,
datastore_path]})
rune = l1.rpc.commando_rune(restrictions=["method=commando-rune",
"pnamerestrictions^id=|parr1^id=",
"time<{}"
.format(int(time.time()) + 15)])['rune']
# l2 has to obey restrictions
with pytest.raises(RpcError, match='Not authorized.*pnamerestrictions'):
l2.rpc.commando(peer_id=l1.info['id'], method='commando-rune', rune=rune)
with pytest.raises(RpcError, match='Not authorized.*pnamerestrictions'):
l2.rpc.commando(peer_id=l1.info['id'], method='commando-rune', rune=rune,
params={'restrictions': 'id<{}'.format(l2.info['id'])})
# By name
rune2 = l2.rpc.commando(peer_id=l1.info['id'],
method='commando-rune',
rune=rune,
params={'restrictions': 'id={}'.format(l2.info['id'])})
# By position
rune2a = l2.rpc.commando(peer_id=l1.info['id'],
method='commando-rune',
rune=rune,
params=[None, 'id={}'.format(l2.info['id'])])
# r2a ID will be 1 greater than r2 ID
r2 = runes.Rune.from_base64(rune2['rune'])
r2a = runes.Rune.from_base64(rune2a['rune'])
assert len(r2.restrictions) == len(r2a.restrictions)
assert r2a.restrictions[0].alternatives == [runes.Alternative(r2.restrictions[0].alternatives[0].field,
r2.restrictions[0].alternatives[0].cond,
str(int(r2.restrictions[0].alternatives[0].value) + 1))]
for r2_r, r2a_r in zip(r2.restrictions[1:], r2a.restrictions[1:]):
assert r2_r == r2a_r
time.sleep(16)
with pytest.raises(RpcError, match='Not authorized.*time'):
l2.rpc.commando(peer_id=l1.info['id'],
method='commando-rune',
rune=rune,
params={'restrictions': 'id={}'.format(l2.info['id'])})
def test_readonly(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False,
opts={'plugin': [plugin_path,
datastore_path]})
rrune = l2.rpc.commando_rune(restrictions='readonly')['rune']
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'listchannels',
'rune': rrune,
'params': {'source': l1.info['id']}})
with pytest.raises(RpcError, match='Not authorized.* = getsharedsecret'):
l1.rpc.commando(peer_id=l2.info['id'],
rune=rrune,
method='getsharedsecret')
with pytest.raises(RpcError, match='Not authorized.* = listdatastore'):
l1.rpc.commando(peer_id=l2.info['id'],
rune=rrune,
method='listdatastore')
def test_megacmd(node_factory):
l1, l2 = node_factory.line_graph(2, fundchannel=False,
opts={'plugin': [plugin_path,
datastore_path]})
rrune = l2.rpc.commando_rune(restrictions='readonly')['rune']
# Proof that it got the rune: fails with "Unknown command" not "Not authorized"
with pytest.raises(RpcError, match='Unknown command'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'get' + 'x' * 130000,
'rune': rrune,
'params': {}})
with pytest.raises(RpcError, match='Command too long'):
l1.rpc.call(method='commando',
payload={'peer_id': l2.info['id'],
'method': 'get' + 'x' * 1100000,
'rune': rrune,
'params': {}})