mirror of
https://github.com/aljazceru/plugins.git
synced 2025-12-23 16:14:20 +01:00
157 lines
4.7 KiB
Python
Executable File
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()
|