commit 6dda9fa764871b5559217370f4cb542c841b83f6
parent 787d92c4eec149389b42051578df12e2429c5eb6
Author: nolash <dev@holbrook.no>
Date: Mon, 6 Sep 2021 21:34:09 +0200
Factor out components of cli daemon runnable to cli module
Diffstat:
8 files changed, 319 insertions(+), 271 deletions(-)
diff --git a/crypto_dev_signer/cli/__init__.py b/crypto_dev_signer/cli/__init__.py
diff --git a/crypto_dev_signer/cli/cmd.py b/crypto_dev_signer/cli/cmd.py
diff --git a/crypto_dev_signer/cli/handle.py b/crypto_dev_signer/cli/handle.py
@@ -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)
+
diff --git a/crypto_dev_signer/cli/http.py b/crypto_dev_signer/cli/http.py
@@ -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
+
+
diff --git a/crypto_dev_signer/cli/jsonrpc.py b/crypto_dev_signer/cli/jsonrpc.py
@@ -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
+
+
+
diff --git a/crypto_dev_signer/cli/socket.py b/crypto_dev_signer/cli/socket.py
@@ -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)
diff --git a/crypto_dev_signer/error.py b/crypto_dev_signer/error.py
@@ -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
+
+
+
diff --git a/crypto_dev_signer/runnable/signer.py b/crypto_dev_signer/runnable/signer.py
@@ -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)
-
-
-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,
- }
-
+class MissingSecretError(Exception):
+ pass
-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 main():
-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)