From 441ace5ec40091d49cc524ef5839d057046df8b1 Mon Sep 17 00:00:00 2001 From: nolash Date: Mon, 6 Sep 2021 15:47:03 +0200 Subject: [PATCH] Add http server to signer daemon --- crypto_dev_signer/runnable/signer.py | 140 ++++++++++++++++++++++++--- requirements.txt | 1 + setup.py | 2 +- tests/test_helper.py | 91 ----------------- tests/test_sign.py | 26 +++-- 5 files changed, 143 insertions(+), 117 deletions(-) delete mode 100644 tests/test_helper.py diff --git a/crypto_dev_signer/runnable/signer.py b/crypto_dev_signer/runnable/signer.py index 0f2d8ba..54c55b1 100755 --- a/crypto_dev_signer/runnable/signer.py +++ b/crypto_dev_signer/runnable/signer.py @@ -8,6 +8,11 @@ import json import logging import argparse from urllib.parse import urlparse +from http.server import ( + HTTPServer, + BaseHTTPRequestHandler, + ) + # external imports import confini @@ -72,6 +77,7 @@ logg.info('using dsn {}'.format(dsn)) logg.info('using socket {}'.format(config.get('SIGNER_SOCKET_PATH'))) re_http = r'^http' +re_tcp = r'^tcp' re_unix = r'^ipc' class MissingSecretError(BaseException): @@ -177,7 +183,7 @@ 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(s) + start_server_socket(s) def start_server_unix(socket_path): @@ -196,15 +202,30 @@ def start_server_unix(socket_path): 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(s) + start_server_socket(s) -def start_server(s): - s.listen(10) - logg.debug('server started') - while True: - (csock, caddr) = s.accept() - d = csock.recv(4096) +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) @@ -212,23 +233,105 @@ def start_server(s): logg.debug('{}'.format(d.decode('utf-8'))) except Exception as e: logg.exception('input error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(None, JSONRPCParseError)).encode('utf-8')) - csock.close() - continue + 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') - csock.send(j) except ValueError as e: # TODO: handle cases to give better error context to caller logg.exception('process error {}'.format(e)) - csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) + 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)) - csock.send(json.dumps(jsonrpc_error(j['id'], JSONRPCServerError)).encode('utf-8')) + 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() @@ -255,15 +358,20 @@ def main(): try: arg = json.loads(sys.argv[1]) except: - logg.info('no json rpc command detected, starting socket server') + logg.info('no json rpc command detected, starting socket server {}'.format(socket_url)) scheme = 'ipc' if socket_url.scheme != '': scheme = socket_url.scheme - if re.match(re_http, socket_url.scheme): + if re.match(re_tcp, socket_url.scheme): 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): + socket_spec = socket_url.netloc.split(':') + host = socket_spec[0] + port = int(socket_spec[1]) + start_server_http((host, port)) else: start_server_unix(socket_url.path) sys.exit(0) diff --git a/requirements.txt b/requirements.txt index d7cb654..7b319e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ cryptography==3.2.1 pysha3==1.0.2 rlp==2.0.1 json-rpc==1.13.0 +jsonrpc-std~=0.0.1a2 confini>=0.3.6rc3,<0.5.0 coincurve==15.0.0 hexathon~=0.0.1a7 diff --git a/setup.py b/setup.py index 6233dfe..84b4f58 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ f.close() setup( name="crypto-dev-signer", - version="0.4.15a1", + version="0.4.15a2", description="A signer and keystore daemon and library for cryptocurrency software development", author="Louis Holbrook", author_email="dev@holbrook.no", diff --git a/tests/test_helper.py b/tests/test_helper.py deleted file mode 100644 index 0e46438..0000000 --- a/tests/test_helper.py +++ /dev/null @@ -1,91 +0,0 @@ -# standard imports -import unittest -import logging -import os - -# local imports -from crypto_dev_signer.keystore.dict import DictKeystore -from crypto_dev_signer.eth.signer import ReferenceSigner -from crypto_dev_signer.helper import TxExecutor -#from crypto_dev_signer.eth.helper import EthTxExecutor -from crypto_dev_signer.encoding import to_checksum_address - -logging.basicConfig(level=logging.DEBUG) -logg = logging.getLogger() - -script_dir = os.path.realpath(os.path.dirname(__file__)) - - -class MockEthTxBackend: - - def dispatcher(self, tx): - logg.debug('sender {}'.format(tx)) - return os.urandom(32) - - def reporter(self, tx): - logg.debug('reporter {}'.format(tx)) - - def verifier(self, rcpt): - logg.debug('reporter {}'.format(rcpt)) - - def fee_price_helper(self): - return 21 - - def fee_helper(self, tx): - logg.debug('fee helper tx {}'.format(tx)) - return 2 - - def builder(self, tx): - return tx - - def builder_two(self, tx): - tx['value'] = 10243 - tx['to'] = to_checksum_address('0x' + os.urandom(20).hex()) - tx['data'] = '0x' - if tx.get('feePrice') != None: - tx['gasPrice'] = tx['feePrice'] - del tx['feePrice'] - if tx.get('feeUnits') != None: - tx['gas'] = tx['feeUnits'] - del tx['feeUnits'] - return tx - - -class TestHelper(unittest.TestCase): - - def setUp(self): - logg.debug('setup') - self.db = DictKeystore() - - keystore_filename = 'UTC--2021-01-08T18-37-01.187235289Z--00a329c0648769a73afac7f9381e08fb43dbea72' - keystore_filepath = os.path.join(script_dir, 'testdata', keystore_filename) - - self.address_hex = self.db.import_keystore_file(keystore_filepath, '') - self.signer = ReferenceSigner(self.db) - - - def tearDown(self): - pass - - - def test_helper(self): - backend = MockEthTxBackend() - executor = TxExecutor(self.address_hex, self.signer, backend.builder, backend.dispatcher, backend.reporter, 666, 13, backend.fee_helper, backend.fee_price_helper, backend.verifier) - - tx_ish = {'from': self.address_hex} - executor.sign_and_send([backend.builder_two]) - - -# def test_eth_helper(self): -# backend = MockEthTxBackend() -# w3 = web3.Web3(web3.Web3.HTTPProvider('http://localhost:8545')) -# executor = EthTxExecutor(w3, self.address_hex, self.signer, 1337) -# -# tx_ish = {'from': self.address_hex} -# #executor.sign_and_send([backend.builder, backend.builder_two]) -# with self.assertRaises(ValueError): -# executor.sign_and_send([backend.builder_two]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_sign.py b/tests/test_sign.py index 334c334..1ec4c63 100644 --- a/tests/test_sign.py +++ b/tests/test_sign.py @@ -1,10 +1,12 @@ -#!/usr/bin/python - +# standard imports import unittest import logging +import copy +# external imports from rlp import encode as rlp_encode +# local imports from crypto_dev_signer.eth.signer import ReferenceSigner from crypto_dev_signer.eth.transaction import EIP155Transaction @@ -23,15 +25,22 @@ tx_ints = { } tx_hexs = { - 'nonce': '0x0', + 'nonce': '0x', 'from': "0xEB014f8c8B418Db6b45774c326A0E64C78914dC0", - 'gasPrice': "0x4a817c800", + 'gasPrice': "0x04a817c800", 'gas': "0x5208", 'to': '0x3535353535353535353535353535353535353535', - 'value': "0x3e8", - 'data': "deadbeef", + 'value': "0x03e8", + 'data': "0xdeadbeef", } +tx_hexs_r = copy.copy(tx_hexs) +tx_hexs_r['r'] = '0x' +tx_hexs_r['s'] = '0x' +tx_hexs_r['v'] = '0x01' +del tx_hexs_r['from'] + + class pkGetter: def __init__(self, pk): @@ -68,14 +77,14 @@ class TestSign(unittest.TestCase): t = EIP155Transaction(tx_ints, 0) self.assertRegex(t.__class__.__name__, "Transaction") s = t.serialize() - self.assertEqual('{}'.format(s), "{'nonce': '0x', 'gasPrice': '0x04a817c800', 'gas': '0x5208', 'to': '0x3535353535353535353535353535353535353535', 'value': '0x03e8', 'data': '0xdeadbeef', 'v': '0x01', 'r': '0x', 's': '0x'}") + self.assertDictEqual(s, tx_hexs_r) r = t.rlp_serialize() self.assertEqual(r.hex(), 'ea808504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef018080') t = EIP155Transaction(tx_hexs, 0) self.assertRegex(t.__class__.__name__, "Transaction") s = t.serialize() - self.assertEqual('{}'.format(s), "{'nonce': '0x', 'gasPrice': '0x04a817c800', 'gas': '0x5208', 'to': '0x3535353535353535353535353535353535353535', 'value': '0x03e8', 'data': '0xdeadbeef', 'v': '0x01', 'r': '0x', 's': '0x'}") + self.assertDictEqual(s, tx_hexs_r) r = t.rlp_serialize() self.assertEqual(r.hex(), 'ea808504a817c8008252089435353535353535353535353535353535353535358203e884deadbeef018080') @@ -91,7 +100,6 @@ class TestSign(unittest.TestCase): s = ReferenceSigner(self.pk_getter) z = s.sign_ethereum_message(tx_ints['from'], '666f6f') z = s.sign_ethereum_message(tx_ints['from'], b'foo') - logg.debug('zzz {}'.format(str(z))) if __name__ == '__main__':