Files
plugins/commando/commando.py
Rusty Russell 5d062830ba commando: reckless? Try going commando!
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
2021-09-08 10:11:58 +09:30

157 lines
4.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Commando is a plugin to allow one node to control another. You use
"commando" to send commands, and the 'commando-writer' and
'commando-reader' to allow nodes to send you commands.
The formats are:
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
import json
import textwrap
import random
plugin = Plugin()
# "YOLO"!
COMMANDO_CMD = 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
def split_cmd(cmdstr):
"""Interprets JSON and method and params"""
cmd = json.loads(cmdstr)
return cmd['method'], cmd.get('params')
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_result(plugin, peer_id, idnum, res):
# We can only send 64k in a message.
parts = textwrap.wrap(json.dumps(res), 65000)
for p in parts[:-1]:
send_msg(plugin, peer_id, COMMANDO_REPLY_CONTINUES, idnum, p)
send_msg(plugin, peer_id, COMMANDO_REPLY_TERM, idnum, parts[-1])
def exec_command(plugin, peer_id, idnum, method, params):
"""Run an arbitrary command and message back 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 exec_read_command(plugin, peer_id, idnum, method, params):
"""Run a list or get command and message back the result"""
if method.startswith('list') or method.startswith('get'):
exec_command(plugin, peer_id, idnum, method, params)
else:
send_result(plugin, peer_id, idnum, {'error': "Not permitted"})
@plugin.hook('custommsg')
def on_custommsg(peer_id, payload, plugin, **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:
if peer_id in plugin.writers:
exec_command(plugin, peer_id, idnum, *split_cmd(data))
elif peer_id in plugin.readers:
exec_read_command(plugin, peer_id, idnum, *split_cmd(data))
elif mtype == COMMANDO_REPLY_CONTINUES:
if idnum in plugin.reqs:
plugin.reqs[idnum].buf += data
elif mtype == COMMANDO_REPLY_TERM:
if idnum in plugin.reqs:
plugin.reqs[idnum].buf += data
finished = plugin.reqs[idnum]
del plugin.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'])
return {'result': 'continue'}
@plugin.async_method("commando")
def commando(plugin, request, peer_id, method, params=None):
"""Send a command to node_id, and wait for a response"""
res = {'method': method}
if params:
res['params'] = params
while True:
idnum = random.randint(0, 2**64)
if idnum not in plugin.reqs:
break
plugin.reqs[idnum] = CommandResponse(request)
send_msg(plugin, peer_id, COMMANDO_CMD, idnum, json.dumps(res))
@plugin.init()
def init(options, configuration, plugin):
plugin.writers = options['commando_writer']
plugin.readers = options['commando_reader']
plugin.reqs = {}
# dev-sendcustommsg was renamed to sendcustommsg for 0.10.1
try:
plugin.rpc.help('sendcustommsg')
plugin.msgcmd = 'sendcustommsg'
except RpcError:
plugin.msgcmd = 'dev-sendcustommsg'
plugin.log("Initialized with readers {}, writers {}"
.format(plugin.readers, plugin.writers))
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 commands?",
default=[],
multi=True)
plugin.run()