diff --git a/crypto_dev_signer/cli/__init__.py b/crypto_dev_signer/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crypto_dev_signer/cli/cmd.py b/crypto_dev_signer/cli/cmd.py new file mode 100644 index 0000000..e69de29 diff --git a/crypto_dev_signer/cli/handle.py b/crypto_dev_signer/cli/handle.py new file mode 100644 index 0000000..f029617 --- /dev/null +++ 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 new file mode 100644 index 0000000..1ae2d72 --- /dev/null +++ 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 new file mode 100644 index 0000000..434c170 --- /dev/null +++ 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 new file mode 100644 index 0000000..2331d51 --- /dev/null +++ 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 index 9022a3d..2724ee6 100644 --- 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 index 54c55b1..6eac26c 100755 --- 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) +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)