Skip to content

Commit 5afa9f8

Browse files
committed
backport of rpc config handling from python-bitcointx
1 parent ebc85bf commit 5afa9f8

File tree

2 files changed

+184
-39
lines changed

2 files changed

+184
-39
lines changed

bitcoin/rpc.py

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,27 @@ class InWarmupError(JSONRPCError):
115115
RPC_ERROR_CODE = -28
116116

117117

118+
def split_hostport(hostport):
119+
r = hostport.rsplit(':', maxsplit=1)
120+
if len(r) == 1:
121+
return (hostport, None)
122+
123+
maybe_host, maybe_port = r
124+
125+
if ':' in maybe_host:
126+
if not (maybe_host.startswith('[') and maybe_host.endswith(']')):
127+
return (hostport, None)
128+
129+
if not maybe_port.isdigit():
130+
return (hostport, None)
131+
132+
port = int(maybe_port)
133+
if port > 0 and port < 0x10000:
134+
return (maybe_host, port)
135+
136+
return (hostport, None)
137+
138+
118139
class BaseProxy(object):
119140
"""Base JSON-RPC proxy class. Contains only private methods; do not use
120141
directly."""
@@ -123,6 +144,7 @@ def __init__(self,
123144
service_url=None,
124145
service_port=None,
125146
btc_conf_file=None,
147+
btc_conf_file_contents=None,
126148
timeout=DEFAULT_HTTP_TIMEOUT,
127149
connection=None):
128150

@@ -132,6 +154,15 @@ def __init__(self,
132154
self.__conn = None
133155
authpair = None
134156

157+
network_id = 'main'
158+
extraname = ''
159+
if bitcoin.params.NAME == 'testnet':
160+
network_id = 'test'
161+
extraname = 'testnet3'
162+
elif bitcoin.params.NAME == 'regtest':
163+
network_id = 'regtest'
164+
extraname = 'regtest'
165+
135166
if service_url is None:
136167
# Figure out the path to the bitcoin.conf file
137168
if btc_conf_file is None:
@@ -145,44 +176,81 @@ def __init__(self,
145176

146177
# Bitcoin Core accepts empty rpcuser, not specified in btc_conf_file
147178
conf = {'rpcuser': ""}
148-
149-
# Extract contents of bitcoin.conf to build service_url
150-
try:
151-
with open(btc_conf_file, 'r') as fd:
152-
for line in fd.readlines():
153-
if '#' in line:
154-
line = line[:line.index('#')]
155-
if '=' not in line:
156-
continue
157-
k, v = line.split('=', 1)
158-
conf[k.strip()] = v.strip()
159-
160-
# Treat a missing bitcoin.conf as though it were empty
161-
except FileNotFoundError:
162-
pass
179+
section = ''
180+
181+
def process_line(line: str) -> None:
182+
nonlocal section
183+
184+
if '#' in line:
185+
line = line[:line.index('#')]
186+
line = line.strip()
187+
if not line:
188+
return
189+
if line[0] == '[' and line[-1] == ']':
190+
section = line[1:-1] + '.'
191+
return
192+
if '=' not in line:
193+
return
194+
k, v = line.split('=', 1)
195+
conf[section + k.strip()] = v.strip()
196+
197+
if btc_conf_file_contents is not None:
198+
buf = btc_conf_file_contents
199+
while '\n' in buf:
200+
line, buf = buf.split('\n', 1)
201+
process_line(line)
202+
else:
203+
# Extract contents of bitcoin.conf to build service_url
204+
try:
205+
with open(btc_conf_file, 'r') as fd:
206+
for line in fd.readlines():
207+
process_line(line)
208+
# Treat a missing bitcoin.conf as though it were empty
209+
except FileNotFoundError:
210+
pass
163211

164212
if service_port is None:
165213
service_port = bitcoin.params.RPC_PORT
166-
conf['rpcport'] = int(conf.get('rpcport', service_port))
167-
conf['rpchost'] = conf.get('rpcconnect', 'localhost')
168-
169-
service_url = ('%s://%s:%d' %
170-
('http', conf['rpchost'], conf['rpcport']))
171-
172-
cookie_dir = conf.get('datadir', os.path.dirname(btc_conf_file))
173-
if bitcoin.params.NAME != "mainnet":
174-
cookie_dir = os.path.join(cookie_dir, bitcoin.params.NAME)
175-
cookie_file = os.path.join(cookie_dir, ".cookie")
176-
try:
177-
with open(cookie_file, 'r') as fd:
178-
authpair = fd.read()
179-
except IOError as err:
180-
if 'rpcpassword' in conf:
181-
authpair = "%s:%s" % (conf['rpcuser'], conf['rpcpassword'])
182214

215+
(host, port) = split_hostport(
216+
conf.get(network_id + '.rpcconnect',
217+
conf.get('rpcconnect', 'localhost')))
218+
219+
port = int(conf.get(network_id + '.rpcport',
220+
conf.get('rpcport', port or service_port)))
221+
service_url = ('%s://%s:%d' % ('http', host, port))
222+
223+
cookie_dir = conf.get(network_id + '.datadir',
224+
conf.get('datadir',
225+
None if btc_conf_file is None
226+
else os.path.dirname(btc_conf_file)))
227+
io_err = None
228+
if cookie_dir is not None:
229+
cookie_dir = os.path.join(cookie_dir, extraname)
230+
cookie_file = os.path.join(cookie_dir, ".cookie")
231+
try:
232+
with open(cookie_file, 'r') as fd:
233+
authpair = fd.read()
234+
except IOError as err:
235+
io_err = err
236+
237+
if authpair is None:
238+
if network_id + '.rpcpassword' in conf:
239+
authpair = "%s:%s" % (
240+
conf.get(network_id + '.rpcuser', ''),
241+
conf[network_id + '.rpcpassword'])
242+
elif 'rpcpassword' in conf:
243+
authpair = "%s:%s" % (conf.get('rpcuser', ''),
244+
conf['rpcpassword'])
245+
elif io_err is None:
246+
raise ValueError(
247+
'Cookie dir is not known and rpcpassword is not '
248+
'specified in btc_conf_file_contents')
183249
else:
184-
raise ValueError('Cookie file unusable (%s) and rpcpassword not specified in the configuration file: %r' % (err, btc_conf_file))
185-
250+
raise ValueError(
251+
'Cookie file unusable (%s) and rpcpassword '
252+
'not specified in the configuration file: %r'
253+
% (io_err, btc_conf_file))
186254
else:
187255
url = urlparse.urlparse(service_url)
188256
authpair = "%s:%s" % (url.username, url.password)

bitcoin/tests/test_rpc.py

Lines changed: 83 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,93 @@
1212
from __future__ import absolute_import, division, print_function, unicode_literals
1313

1414
import unittest
15+
import base64
1516

16-
from bitcoin.rpc import Proxy
17+
from bitcoin import SelectParams
18+
from bitcoin.rpc import Proxy, split_hostport
1719

1820
class Test_RPC(unittest.TestCase):
19-
# Tests disabled, see discussion below.
20-
# "Looks like your unit tests won't work if Bitcoin Core isn't running;
21-
# maybe they in turn need to check that and disable the test if core isn't available?"
22-
# https://github.com/petertodd/python-bitcoinlib/pull/10
23-
pass
2421

22+
def tearDown(self):
23+
SelectParams('mainnet')
24+
25+
def test_split_hostport(self):
26+
def T(hostport, expected_pair):
27+
(host, port) = split_hostport(hostport)
28+
self.assertEqual((host, port), expected_pair)
29+
30+
T('localhost', ('localhost', None))
31+
T('localhost:123', ('localhost', 123))
32+
T('localhost:0', ('localhost:0', None))
33+
T('localhost:88888', ('localhost:88888', None))
34+
T('lo.cal.host:123', ('lo.cal.host', 123))
35+
T('lo.cal.host:123_', ('lo.cal.host:123_', None))
36+
T('lo:cal:host:123', ('lo:cal:host:123', None))
37+
T('local:host:123', ('local:host:123', None))
38+
T('[1a:2b:3c]:491', ('[1a:2b:3c]', 491))
39+
# split_hostport doesn't care what's in square brackets
40+
T('[local:host]:491', ('[local:host]', 491))
41+
T('[local:host]:491934', ('[local:host]:491934', None))
42+
T('.[local:host]:491', ('.[local:host]:491', None))
43+
T('[local:host].:491', ('[local:host].:491', None))
44+
T('[local:host]:p491', ('[local:host]:p491', None))
45+
46+
def test_parse_config(self):
47+
conf_file_contents = """
48+
listen=1
49+
server=1
50+
51+
rpcpassword=somepass # should be overriden
52+
53+
regtest.rpcport = 8123
54+
55+
rpcport = 8888
56+
57+
[main]
58+
rpcuser=someuser1
59+
rpcpassword=somepass1
60+
rpcconnect=127.0.0.10
61+
62+
[test]
63+
rpcpassword=somepass2
64+
rpcconnect=127.0.0.11
65+
rpcport = 9999
66+
67+
[regtest]
68+
rpcuser=someuser3
69+
rpcpassword=somepass3
70+
rpcconnect=127.0.0.12
71+
"""
72+
73+
rpc = Proxy(btc_conf_file_contents=conf_file_contents)
74+
self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.10:8888')
75+
authpair = "someuser1:somepass1"
76+
authhdr = "Basic " + base64.b64encode(authpair.encode('utf8')
77+
).decode('utf8')
78+
self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8'))
79+
80+
SelectParams('testnet')
81+
82+
rpc = Proxy(btc_conf_file_contents=conf_file_contents)
83+
self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.11:9999')
84+
authpair = ":somepass2" # no user specified
85+
authhdr = "Basic " + base64.b64encode(authpair.encode('utf8')
86+
).decode('utf8')
87+
self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8'))
88+
89+
SelectParams('regtest')
90+
91+
rpc = Proxy(btc_conf_file_contents=conf_file_contents)
92+
self.assertEqual(rpc._BaseProxy__service_url, 'http://127.0.0.12:8123')
93+
authpair = "someuser3:somepass3"
94+
authhdr = "Basic " + base64.b64encode(authpair.encode('utf8')
95+
).decode('utf8')
96+
self.assertEqual(rpc._BaseProxy__auth_header, authhdr.encode('utf-8'))
97+
98+
# The following Tests are disabled, see discussion below.
99+
# "Looks like your unit tests won't work if Bitcoin Core isn't running;
100+
# maybe they in turn need to check that and disable the test if core isn't available?"
101+
# https://github.com/petertodd/python-bitcoinlib/pull/10
25102
# def test_can_validate(self):
26103
# working_address = '1CB2fxLGAZEzgaY4pjr4ndeDWJiz3D3AT7'
27104
# p = Proxy()

0 commit comments

Comments
 (0)