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): class NetworkError(Exception):
pass 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 re
import os import os
import sys import sys
import stat
import socket
import json import json
import logging import logging
import argparse import argparse
from urllib.parse import urlparse from urllib.parse import urlparse
from http.server import (
HTTPServer,
BaseHTTPRequestHandler,
)
# external imports # external imports
import confini import confini
@ -20,10 +13,10 @@ from jsonrpc.exceptions import *
from hexathon import add_0x from hexathon import add_0x
# local imports # local imports
from crypto_dev_signer.cli.cmd import *
from crypto_dev_signer.eth.signer import ReferenceSigner 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.keystore.reference import ReferenceKeystore
from crypto_dev_signer.error import UnknownAccountError from crypto_dev_signer.cli.handle import SignRequestHandler
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
@ -80,266 +73,12 @@ re_http = r'^http'
re_tcp = r'^tcp' re_tcp = r'^tcp'
re_unix = r'^ipc' re_unix = r'^ipc'
class MissingSecretError(BaseException): class MissingSecretError(Exception):
def __init__(self, message):
super(MissingSecretError, self).__init__(message)
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]
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 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'' def main():
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') secret_hex = config.get('SIGNER_SECRET')
if secret_hex == None: if secret_hex == None:
raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable') raise MissingSecretError('please provide a valid hex value for the SIGNER_SECRET configuration variable')
@ -348,12 +87,9 @@ def init():
kw = { kw = {
'symmetric_key': secret, 'symmetric_key': secret,
} }
db = ReferenceKeystore(dsn, **kw) SignRequestHandler.keystore = ReferenceKeystore(dsn, **kw)
signer = ReferenceSigner(db) SignRequestHandler.signer = ReferenceSigner(SignRequestHandler.keystore)
def main():
init()
arg = None arg = None
try: try:
arg = json.loads(sys.argv[1]) arg = json.loads(sys.argv[1])
@ -363,16 +99,19 @@ def main():
if socket_url.scheme != '': if socket_url.scheme != '':
scheme = socket_url.scheme scheme = socket_url.scheme
if re.match(re_tcp, 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(':') socket_spec = socket_url.netloc.split(':')
host = socket_spec[0] host = socket_spec[0]
port = int(socket_spec[1]) port = int(socket_spec[1])
start_server_tcp((host, port)) start_server_tcp((host, port))
elif re.match(re_http, socket_url.scheme): elif re.match(re_http, socket_url.scheme):
from crypto_dev_signer.cli.http import start_server_http
socket_spec = socket_url.netloc.split(':') socket_spec = socket_url.netloc.split(':')
host = socket_spec[0] host = socket_spec[0]
port = int(socket_spec[1]) port = int(socket_spec[1])
start_server_http((host, port)) start_server_http((host, port))
else: else:
from crypto_dev_signer.cli.socket import start_server_unix
start_server_unix(socket_url.path) start_server_unix(socket_url.path)
sys.exit(0) sys.exit(0)