Factor out components of cli daemon runnable to cli module

This commit is contained in:
nolash 2021-09-06 21:34:09 +02:00
parent 787d92c4ee
commit 6dda9fa764
Signed by: lash
GPG Key ID: 21D2E7BB88C2A746
8 changed files with 319 additions and 271 deletions

View File

View File

View File

@ -0,0 +1,122 @@
# standard imports
import json
import logging
# external imports
from jsonrpc.exceptions import (
JSONRPCServerError,
JSONRPCParseError,
JSONRPCInvalidParams,
)
# local imports
from crypto_dev_signer.eth.transaction import EIP155Transaction
from crypto_dev_signer.error import (
UnknownAccountError,
SignerError,
)
from crypto_dev_signer.cli.jsonrpc import jsonrpc_ok
from .jsonrpc import (
jsonrpc_error,
is_valid_json,
)
logg = logging.getLogger(__name__)
#methods = {
# 'personal_newAccount': personal_new_account,
# 'personal_signTransaction': personal_signTransaction,
# 'eth_signTransaction': eth_signTransaction,
# 'eth_sign': eth_sign,
# }
class SignRequestHandler:
keystore = None
signer = None
def process_input(self, j):
rpc_id = j['id']
m = j['method']
p = j['params']
return (rpc_id, getattr(self, m)(p)) #methods[m](p))
def handle_jsonrpc(self, d):
j = None
try:
j = json.loads(d)
is_valid_json(j)
logg.debug('{}'.format(d.decode('utf-8')))
except Exception as e:
logg.exception('input error {}'.format(e))
j = json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')
raise SignerError(j)
try:
(rpc_id, r) = self.process_input(j)
r = jsonrpc_ok(rpc_id, r)
j = json.dumps(r).encode('utf-8')
except ValueError as e:
# TODO: handle cases to give better error context to caller
logg.exception('process error {}'.format(e))
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
raise SignerError(j)
except UnknownAccountError as e:
logg.exception('process unknown account error {}'.format(e))
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
raise SignerError(j)
return j
def personal_newAccount(self, p):
password = p
if p.__class__.__name__ != 'str':
if p.__class__.__name__ != 'list':
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
logg.error('foo {}'.format(p))
if len(p) != 1:
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
if p[0].__class__.__name__ != 'str':
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
password = p[0]
r = self.keystore.new(password)
return add_0x(r)
# TODO: move to translation module ("personal" rpc namespace is node-specific)
def personal_signTransaction(self, p):
logg.debug('got {} to sign'.format(p[0]))
t = EIP155Transaction(p[0], p[0]['nonce'], p[0]['chainId'])
raw_signed_tx = self.signer.sign_transaction_to_rlp(t, p[1])
o = {
'raw': '0x' + raw_signed_tx.hex(),
'tx': t.serialize(),
}
logg.debug('signed {}'.format(o))
return o
def eth_signTransaction(self, tx):
o = personal_signTransaction([tx[0], ''])
return o['raw']
def eth_sign(self, p):
logg.debug('got message {} to sign'.format(p[1]))
message_type = type(p[1]).__name__
if message_type != 'str':
raise ValueError('invalid message format, must be {}, not {}'.format(message_type))
z = self.signer.sign_ethereum_message(p[0], p[1][2:])
return str(z)

View File

@ -0,0 +1,85 @@
# standard imports
import logging
# external imports
from http.server import (
HTTPServer,
BaseHTTPRequestHandler,
)
# local imports
from .handle import SignRequestHandler
from crypto_dev_signer.error import SignerError
logg = logging.getLogger(__name__)
def start_server_http(spec):
httpd = HTTPServer(spec, HTTPSignRequestHandler)
logg.debug('starting http server {}'.format(spec))
httpd.serve_forever()
class HTTPSignRequestHandler(SignRequestHandler, BaseHTTPRequestHandler):
def do_POST(self):
if self.headers.get('Content-Type') != 'application/json':
self.send_response(400, 'me read json only')
self.end_headers()
return
try:
if 'application/json' not in self.headers.get('Accept').split(','):
self.send_response(400, 'me json only speak')
self.end_headers()
return
except AttributeError:
pass
l = self.headers.get('Content-Length')
try:
l = int(l)
except ValueError:
self.send_response(400, 'content length must be integer')
self.end_headers()
return
if l > 4096:
self.send_response(400, 'too much information')
self.end_headers()
return
if l < 0:
self.send_response(400, 'you are too negative')
self.end_headers()
return
b = b''
c = 0
while c < l:
d = self.rfile.read(l-c)
if d == None:
break
b += d
c += len(d)
if c > 4096:
self.send_response(413, 'i should slap you around for lying about your size')
self.end_headers()
return
try:
r = self.handle_jsonrpc(d)
except SignerError as e:
r = e.to_jsonrpc()
l = len(r)
self.send_response(200, 'You are the Keymaster')
self.send_header('Content-Length', str(l))
self.send_header('Cache-Control', 'no-cache')
self.send_header('Content-Type', 'application/json')
self.end_headers()
c = 0
while c < l:
n = self.wfile.write(r[c:])
c += n

View File

@ -0,0 +1,30 @@
# local imports
from crypto_dev_signer.error import UnknownAccountError
def jsonrpc_error(rpc_id, err):
return {
'jsonrpc': '2.0',
'id': rpc_id,
'error': {
'code': err.CODE,
'message': err.MESSAGE,
},
}
def jsonrpc_ok(rpc_id, response):
return {
'jsonrpc': '2.0',
'id': rpc_id,
'result': response,
}
def is_valid_json(j):
if j.get('id') == 'None':
raise ValueError('id missing')
return True

View File

@ -0,0 +1,58 @@
# standard imports
import os
import logging
import socket
import stat
# local imports
from crypto_dev_signer.error import SignerError
from .handle import SignRequestHandler
logg = logging.getLogger(__name__)
def start_server_socket(s):
s.listen(10)
logg.debug('server started')
handler = SignRequestHandler()
while True:
(csock, caddr) = s.accept()
d = csock.recv(4096)
r = None
try:
r = handler.handle_jsonrpc(d)
except SignerError as e:
r = e.to_jsonrpc()
csock.send(r)
csock.close()
s.close()
os.unlink(socket_path)
def start_server_tcp(spec):
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
s.bind(spec)
logg.debug('created tcp socket {}'.format(spec))
start_server_socket(s)
def start_server_unix(socket_path):
socket_dir = os.path.dirname(socket_path)
try:
fi = os.stat(socket_dir)
if not stat.S_ISDIR:
RuntimeError('socket path {} is not a directory'.format(socket_dir))
except FileNotFoundError:
os.mkdir(socket_dir)
try:
os.unlink(socket_path)
except FileNotFoundError:
pass
s = socket.socket(family = socket.AF_UNIX, type = socket.SOCK_STREAM)
s.bind(socket_path)
logg.debug('created unix ipc socket {}'.format(socket_path))
start_server_socket(s)

View File

@ -8,3 +8,17 @@ class TransactionRevertError(Exception):
class NetworkError(Exception):
pass
class SignerError(Exception):
def __init__(self, s):
super(SignerError, self).__init__(s)
self.jsonrpc_error = s
def to_jsonrpc(self):
return self.jsonrpc_error

View File

@ -2,17 +2,10 @@
import re
import os
import sys
import stat
import socket
import json
import logging
import argparse
from urllib.parse import urlparse
from http.server import (
HTTPServer,
BaseHTTPRequestHandler,
)
# external imports
import confini
@ -20,10 +13,10 @@ from jsonrpc.exceptions import *
from hexathon import add_0x
# local imports
from crypto_dev_signer.cli.cmd import *
from crypto_dev_signer.eth.signer import ReferenceSigner
from crypto_dev_signer.eth.transaction import EIP155Transaction
from crypto_dev_signer.keystore.reference import ReferenceKeystore
from crypto_dev_signer.error import UnknownAccountError
from crypto_dev_signer.cli.handle import SignRequestHandler
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
@ -80,266 +73,12 @@ re_http = r'^http'
re_tcp = r'^tcp'
re_unix = r'^ipc'
class MissingSecretError(BaseException):
def __init__(self, message):
super(MissingSecretError, self).__init__(message)
class MissingSecretError(Exception):
pass
def personal_new_account(p):
password = p
if p.__class__.__name__ != 'str':
if p.__class__.__name__ != 'list':
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
logg.error('foo {}'.format(p))
if len(p) != 1:
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
if p[0].__class__.__name__ != 'str':
e = JSONRPCInvalidParams()
e.data = 'parameter must be list containing one string'
raise ValueError(e)
password = p[0]
def main():
r = db.new(password)
return add_0x(r)
# TODO: move to translation module ("personal" rpc namespace is node-specific)
def personal_signTransaction(p):
logg.debug('got {} to sign'.format(p[0]))
#t = EIP155Transaction(p[0], p[0]['nonce'], 8995)
t = EIP155Transaction(p[0], p[0]['nonce'], p[0]['chainId'])
# z = signer.sign_transaction(t, p[1])
# raw_signed_tx = t.rlp_serialize()
raw_signed_tx = signer.sign_transaction_to_rlp(t, p[1])
o = {
'raw': '0x' + raw_signed_tx.hex(),
'tx': t.serialize(),
}
logg.debug('signed {}'.format(o))
return o
def eth_signTransaction(tx):
o = personal_signTransaction([tx[0], ''])
return o['raw']
def eth_sign(p):
logg.debug('got message {} to sign'.format(p[1]))
message_type = type(p[1]).__name__
if message_type != 'str':
raise ValueError('invalid message format, must be {}, not {}'.format(message_type))
z = signer.sign_ethereum_message(p[0], p[1][2:])
return str(z)
methods = {
'personal_newAccount': personal_new_account,
'personal_signTransaction': personal_signTransaction,
'eth_signTransaction': eth_signTransaction,
'eth_sign': eth_sign,
}
def jsonrpc_error(rpc_id, err):
return {
'jsonrpc': '2.0',
'id': rpc_id,
'error': {
'code': err.CODE,
'message': err.MESSAGE,
},
}
def jsonrpc_ok(rpc_id, response):
return {
'jsonrpc': '2.0',
'id': rpc_id,
'result': response,
}
def is_valid_json(j):
if j.get('id') == 'None':
raise ValueError('id missing')
return True
def process_input(j):
rpc_id = j['id']
m = j['method']
p = j['params']
return (rpc_id, methods[m](p))
def start_server_tcp(spec):
s = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
s.bind(spec)
logg.debug('created tcp socket {}'.format(spec))
start_server_socket(s)
def start_server_unix(socket_path):
socket_dir = os.path.dirname(socket_path)
try:
fi = os.stat(socket_dir)
if not stat.S_ISDIR:
RuntimeError('socket path {} is not a directory'.format(socket_dir))
except FileNotFoundError:
os.mkdir(socket_dir)
try:
os.unlink(socket_path)
except FileNotFoundError:
pass
s = socket.socket(family = socket.AF_UNIX, type = socket.SOCK_STREAM)
s.bind(socket_path)
logg.debug('created unix ipc socket {}'.format(socket_path))
start_server_socket(s)
def start_server_http(spec):
httpd = HTTPServer(spec, HTTPSignRequestHandler)
logg.debug('starting http server {}'.format(spec))
httpd.serve_forever()
class SignerError(Exception):
def __init__(self, s):
super(SignerError, self).__init__(s)
self.jsonrpc_error = s
def to_jsonrpc(self):
return self.jsonrpc_error
class SignRequestHandler:
def handle_jsonrpc(self, d):
j = None
try:
j = json.loads(d)
is_valid_json(j)
logg.debug('{}'.format(d.decode('utf-8')))
except Exception as e:
logg.exception('input error {}'.format(e))
j = json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')
raise SignerError(j)
try:
(rpc_id, r) = process_input(j)
r = jsonrpc_ok(rpc_id, r)
j = json.dumps(r).encode('utf-8')
except ValueError as e:
# TODO: handle cases to give better error context to caller
logg.exception('process error {}'.format(e))
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
raise SignerError(j)
except UnknownAccountError as e:
logg.exception('process unknown account error {}'.format(e))
j = json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')
raise SignerError(j)
return j
class HTTPSignRequestHandler(BaseHTTPRequestHandler, SignRequestHandler):
def do_POST(self):
if self.headers.get('Content-Type') != 'application/json':
self.send_response(400, 'me read json only')
self.end_headers()
return
try:
if 'application/json' not in self.headers.get('Accept').split(','):
self.send_response(400, 'me json only speak')
self.end_headers()
return
except AttributeError:
pass
l = self.headers.get('Content-Length')
try:
l = int(l)
except ValueError:
self.send_response(400, 'content length must be integer')
self.end_headers()
return
if l > 4096:
self.send_response(400, 'too much information')
self.end_headers()
return
if l < 0:
self.send_response(400, 'you are too negative')
self.end_headers()
return
b = b''
c = 0
while c < l:
d = self.rfile.read(l-c)
if d == None:
break
b += d
c += len(d)
if c > 4096:
self.send_response(413, 'i should slap you around for lying about your size')
self.end_headers()
return
try:
r = self.handle_jsonrpc(d)
except SignerError as e:
r = e.to_jsonrpc()
#b = json.dumps(r).encode('utf-8')
l = len(r)
self.send_response(200, 'You are the Keymaster')
self.send_header('Content-Length', str(l))
self.send_header('Cache-Control', 'no-cache')
self.send_header('Content-Type', 'application/json')
self.end_headers()
c = 0
while c < l:
n = self.wfile.write(r[c:])
c += n
def start_server_socket(s):
s.listen(10)
logg.debug('server started')
handler = SignRequestHandler()
while True:
(csock, caddr) = s.accept()
d = csock.recv(4096)
r = None
try:
r = handler.handle_jsonrpc(d)
except SignerError as e:
r = e.to_jsonrpc()
csock.send(r)
csock.close()
s.close()
os.unlink(socket_path)
def init():
global db, signer
secret_hex = config.get('SIGNER_SECRET')
if secret_hex == None:
raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable')
@ -348,12 +87,9 @@ def init():
kw = {
'symmetric_key': secret,
}
db = ReferenceKeystore(dsn, **kw)
signer = ReferenceSigner(db)
SignRequestHandler.keystore = ReferenceKeystore(dsn, **kw)
SignRequestHandler.signer = ReferenceSigner(SignRequestHandler.keystore)
def main():
init()
arg = None
try:
arg = json.loads(sys.argv[1])
@ -363,16 +99,19 @@ def main():
if socket_url.scheme != '':
scheme = socket_url.scheme
if re.match(re_tcp, socket_url.scheme):
from crypto_dev_signer.cli.socket import start_server_tcp
socket_spec = socket_url.netloc.split(':')
host = socket_spec[0]
port = int(socket_spec[1])
start_server_tcp((host, port))
elif re.match(re_http, socket_url.scheme):
from crypto_dev_signer.cli.http import start_server_http
socket_spec = socket_url.netloc.split(':')
host = socket_spec[0]
port = int(socket_spec[1])
start_server_http((host, port))
else:
from crypto_dev_signer.cli.socket import start_server_unix
start_server_unix(socket_url.path)
sys.exit(0)