Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport of rpc config handling from python-bitcointx #219

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 101 additions & 33 deletions bitcoin/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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):

Expand All @@ -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:
Expand All @@ -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)
Expand Down
91 changes: 85 additions & 6 deletions bitcoin/tests/test_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down