From 7c1881f6fad2c09ceeb55ec60c45c5cdbfda7200 Mon Sep 17 00:00:00 2001 From: Dmitry Petukhov Date: Thu, 26 Dec 2019 20:30:39 +0500 Subject: [PATCH] backport of rpc config handling from python-bitcointx --- bitcoin/rpc.py | 134 ++++++++++++++++++++++++++++---------- bitcoin/tests/test_rpc.py | 91 ++++++++++++++++++++++++-- 2 files changed, 186 insertions(+), 39 deletions(-) diff --git a/bitcoin/rpc.py b/bitcoin/rpc.py index 51f24ac9..29c280c2 100644 --- a/bitcoin/rpc.py +++ b/bitcoin/rpc.py @@ -115,6 +115,27 @@ class InWarmupError(JSONRPCError): RPC_ERROR_CODE = -28 +def split_hostport(hostport): + r = hostport.rsplit(':', maxsplit=1) + if len(r) == 1: + return (hostport, None) + + maybe_host, maybe_port = r + + if ':' in maybe_host: + if not (maybe_host.startswith('[') and maybe_host.endswith(']')): + return (hostport, None) + + if not maybe_port.isdigit(): + return (hostport, None) + + port = int(maybe_port) + if port > 0 and port < 0x10000: + return (maybe_host, port) + + return (hostport, None) + + class BaseProxy(object): """Base JSON-RPC proxy class. Contains only private methods; do not use directly.""" @@ -123,6 +144,7 @@ def __init__(self, service_url=None, service_port=None, btc_conf_file=None, + btc_conf_file_contents=None, timeout=DEFAULT_HTTP_TIMEOUT, connection=None): @@ -132,6 +154,15 @@ def __init__(self, self.__conn = None authpair = None + network_id = 'main' + extraname = '' + if bitcoin.params.NAME == 'testnet': + network_id = 'test' + extraname = 'testnet3' + elif bitcoin.params.NAME == 'regtest': + network_id = 'regtest' + extraname = 'regtest' + if service_url is None: # Figure out the path to the bitcoin.conf file if btc_conf_file is None: @@ -145,44 +176,81 @@ def __init__(self, # Bitcoin Core accepts empty rpcuser, not specified in btc_conf_file conf = {'rpcuser': ""} - - # Extract contents of bitcoin.conf to build service_url - try: - with open(btc_conf_file, 'r') as fd: - for line in fd.readlines(): - if '#' in line: - line = line[:line.index('#')] - if '=' not in line: - continue - k, v = line.split('=', 1) - conf[k.strip()] = v.strip() - - # Treat a missing bitcoin.conf as though it were empty - except FileNotFoundError: - pass + section = '' + + def process_line(line: str) -> None: + nonlocal section + + if '#' in line: + line = line[:line.index('#')] + line = line.strip() + if not line: + return + if line[0] == '[' and line[-1] == ']': + section = line[1:-1] + '.' + return + if '=' not in line: + return + k, v = line.split('=', 1) + conf[section + k.strip()] = v.strip() + + if btc_conf_file_contents is not None: + buf = btc_conf_file_contents + while '\n' in buf: + line, buf = buf.split('\n', 1) + process_line(line) + else: + # Extract contents of bitcoin.conf to build service_url + try: + with open(btc_conf_file, 'r') as fd: + for line in fd.readlines(): + process_line(line) + # Treat a missing bitcoin.conf as though it were empty + except FileNotFoundError: + pass if service_port is None: service_port = bitcoin.params.RPC_PORT - conf['rpcport'] = int(conf.get('rpcport', service_port)) - conf['rpchost'] = conf.get('rpcconnect', 'localhost') - - service_url = ('%s://%s:%d' % - ('http', conf['rpchost'], conf['rpcport'])) - - cookie_dir = conf.get('datadir', os.path.dirname(btc_conf_file)) - if bitcoin.params.NAME != "mainnet": - cookie_dir = os.path.join(cookie_dir, bitcoin.params.NAME) - cookie_file = os.path.join(cookie_dir, ".cookie") - try: - with open(cookie_file, 'r') as fd: - authpair = fd.read() - except IOError as err: - if 'rpcpassword' in conf: - authpair = "%s:%s" % (conf['rpcuser'], conf['rpcpassword']) + (host, port) = split_hostport( + conf.get(network_id + '.rpcconnect', + conf.get('rpcconnect', 'localhost'))) + + port = int(conf.get(network_id + '.rpcport', + conf.get('rpcport', port or service_port))) + service_url = ('%s://%s:%d' % ('http', host, port)) + + cookie_dir = conf.get(network_id + '.datadir', + conf.get('datadir', + None if btc_conf_file is None + else os.path.dirname(btc_conf_file))) + io_err = None + if cookie_dir is not None: + cookie_dir = os.path.join(cookie_dir, extraname) + cookie_file = os.path.join(cookie_dir, ".cookie") + try: + with open(cookie_file, 'r') as fd: + authpair = fd.read() + except IOError as err: + io_err = err + + if authpair is None: + if network_id + '.rpcpassword' in conf: + authpair = "%s:%s" % ( + conf.get(network_id + '.rpcuser', ''), + conf[network_id + '.rpcpassword']) + elif 'rpcpassword' in conf: + authpair = "%s:%s" % (conf.get('rpcuser', ''), + conf['rpcpassword']) + elif io_err is None: + raise ValueError( + 'Cookie dir is not known and rpcpassword is not ' + 'specified in btc_conf_file_contents') else: - raise ValueError('Cookie file unusable (%s) and rpcpassword not specified in the configuration file: %r' % (err, btc_conf_file)) - + raise ValueError( + 'Cookie file unusable (%s) and rpcpassword ' + 'not specified in the configuration file: %r' + % (io_err, btc_conf_file)) else: url = urlparse.urlparse(service_url) authpair = "%s:%s" % (url.username, url.password) diff --git a/bitcoin/tests/test_rpc.py b/bitcoin/tests/test_rpc.py index 07d1f964..8399d983 100644 --- a/bitcoin/tests/test_rpc.py +++ b/bitcoin/tests/test_rpc.py @@ -12,16 +12,95 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest +import base64 -from bitcoin.rpc import Proxy +from bitcoin import SelectParams +from bitcoin.rpc import Proxy, split_hostport class Test_RPC(unittest.TestCase): - # Tests disabled, see discussion below. - # "Looks like your unit tests won't work if Bitcoin Core isn't running; - # maybe they in turn need to check that and disable the test if core isn't available?" - # https://github.com/petertodd/python-bitcoinlib/pull/10 - pass + def tearDown(self): + SelectParams('mainnet') + + def test_split_hostport(self): + def T(hostport, expected_pair): + (host, port) = split_hostport(hostport) + self.assertEqual((host, port), expected_pair) + + T('localhost', ('localhost', None)) + T('localhost:123', ('localhost', 123)) + T('localhost:0', ('localhost:0', None)) + T('localhost:88888', ('localhost:88888', None)) + T('lo.cal.host:123', ('lo.cal.host', 123)) + T('lo.cal.host:123_', ('lo.cal.host:123_', None)) + T('lo:cal:host:123', ('lo:cal:host:123', None)) + T('local:host:123', ('local:host:123', None)) + T('[1a:2b:3c]:491', ('[1a:2b:3c]', 491)) + # split_hostport doesn't care what's in square brackets + T('[local:host]:491', ('[local:host]', 491)) + T('[local:host]:491934', ('[local:host]:491934', None)) + T('.[local:host]:491', ('.[local:host]:491', None)) + T('[local:host].:491', ('[local:host].:491', None)) + T('[local:host]:p491', ('[local:host]:p491', None)) + + def test_parse_config(self): + conf_file_contents = """ + listen=1 + server=1 + + rpcpassword=somepass # should be overriden + + regtest.rpcport = 8123 + + rpcport = 8888 + + [main] + rpcuser=someuser1 + rpcpassword=somepass1 + rpcconnect=127.0.0.10 + + [test] + rpcpassword=somepass2 + rpcconnect=127.0.0.11 + rpcport = 9999 + + [regtest] + rpcuser=someuser3 + rpcpassword=somepass3 + rpcconnect=127.0.0.12 + """ + + rpc = Proxy(btc_conf_file_contents=conf_file_contents) + self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.10:8888') + authpair = "someuser1:somepass1" + authhdr = "Basic " + base64.b64encode(authpair.encode('utf8') + ).decode('utf8') + self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8')) + + SelectParams('testnet') + + rpc = Proxy(btc_conf_file_contents=conf_file_contents) + self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.11:9999') + authpair = ":somepass2" # no user specified + authhdr = "Basic " + base64.b64encode(authpair.encode('utf8') + ).decode('utf8') + self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8')) + + SelectParams('regtest') + + rpc = Proxy(btc_conf_file_contents=conf_file_contents) + self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.12:8123') + authpair = "someuser3:somepass3" + authhdr = "Basic " + base64.b64encode(authpair.encode('utf8') + ).decode('utf8') + self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8')) + + SelectParams('mainnet') + +# The following Tests are disabled, see discussion below. +# "Looks like your unit tests won't work if Bitcoin Core isn't running; +# maybe they in turn need to check that and disable the test if core isn't available?" +# https://github.com/petertodd/python-bitcoinlib/pull/10 # def test_can_validate(self): # working_address = '1CB2fxLGAZEzgaY4pjr4ndeDWJiz3D3AT7' # p = Proxy()