rpc_users.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2015-present The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Test multiple RPC users."""
6
7 from test_framework.test_framework import BitcoinTestFramework
8 from test_framework.util import (
9 assert_equal,
10 str_to_b64str,
11 )
12
13 import http.client
14 import os
15 import platform
16 import urllib.parse
17 import subprocess
18 from random import SystemRandom
19 import string
20 import sys
21 from typing import Optional
22
23
24 def call_with_auth(node, user, password, method="getbestblockhash"):
25 url = urllib.parse.urlparse(node.url)
26 headers = {"Authorization": "Basic " + str_to_b64str('{}:{}'.format(user, password))}
27
28 conn = http.client.HTTPConnection(url.hostname, url.port)
29 conn.connect()
30 conn.request('POST', '/', f'{{"method": "{method}"}}', headers)
31 resp = conn.getresponse()
32 conn.close()
33 return resp
34
35
36 class HTTPBasicsTest(BitcoinTestFramework):
37 def set_test_params(self):
38 self.num_nodes = 2
39
40 def conf_setup(self):
41 #Append rpcauth to bitcoin.conf before initialization
42 self.rtpassword = "cA773lm788buwYe4g4WT+05pKyNruVKjQ25x3n0DQcM="
43 rpcauth = "rpcauth=rt:93648e835a54c573682c2eb19f882535$7681e9c5b74bdd85e78166031d2058e1069b3ed7ed967c93fc63abba06f31144"
44
45 self.rpcuser = "rpcuser💻"
46 self.rpcpassword = "rpcpassword🔑"
47
48 gen_rpcauth = self.config["environment"]["RPCAUTH"]
49
50 # Generate RPCAUTH with specified password
51 self.rt2password = "8/F3uMDw4KSEbw96U3CA1C4X05dkHDN2BPFjTgZW4KI="
52 p = subprocess.Popen([sys.executable, gen_rpcauth, 'rt2', self.rt2password], stdout=subprocess.PIPE, text=True)
53 lines = p.stdout.read().splitlines()
54 rpcauth2 = lines[1]
55
56 # Generate RPCAUTH without specifying password
57 self.user = ''.join(SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10))
58 p = subprocess.Popen([sys.executable, gen_rpcauth, self.user], stdout=subprocess.PIPE, text=True)
59 lines = p.stdout.read().splitlines()
60 rpcauth3 = lines[1]
61 self.password = lines[3]
62
63 self.stop_nodes()
64 with open(self.nodes[0].datadir_path / "bitcoin.conf", "a") as f:
65 f.write(rpcauth + "\n")
66 f.write(rpcauth2 + "\n")
67 f.write(rpcauth3 + "\n")
68 with open(self.nodes[1].datadir_path / "bitcoin.conf", "a") as f:
69 f.write("rpcuser={}\n".format(self.rpcuser))
70 f.write("rpcpassword={}\n".format(self.rpcpassword))
71 self.start_nodes()
72
73 def test_auth(self, node, user, password):
74 self.log.info('Correct...')
75 assert_equal(200, call_with_auth(node, user, password).status)
76
77 self.log.info('Wrong...')
78 assert_equal(401, call_with_auth(node, user, password + 'wrong').status)
79
80 self.log.info('Wrong...')
81 assert_equal(401, call_with_auth(node, user + 'wrong', password).status)
82
83 self.log.info('Wrong...')
84 assert_equal(401, call_with_auth(node, user + 'wrong', password + 'wrong').status)
85
86 def test_rpccookieperms(self):
87 p = {"owner": 0o600, "group": 0o640, "all": 0o644}
88
89 if platform.system() == 'Windows':
90 self.log.info(f"Skip cookie file permissions checks as OS detected as: {platform.system()=}")
91 return
92
93 self.log.info('Check cookie file permissions can be set using -rpccookieperms')
94
95 cookie_file_path = self.nodes[1].chain_path / '.cookie'
96 PERM_BITS_UMASK = 0o777
97
98 def test_perm(perm: Optional[str]):
99 if not perm:
100 perm = 'owner'
101 self.restart_node(1)
102 else:
103 self.restart_node(1, extra_args=[f"-rpccookieperms={perm}"])
104
105 file_stat = os.stat(cookie_file_path)
106 actual_perms = file_stat.st_mode & PERM_BITS_UMASK
107 expected_perms = p[perm]
108 assert_equal(expected_perms, actual_perms)
109
110 # Remove any leftover rpc{user|password} config options from previous tests
111 self.stop_node(1)
112 self.nodes[1].replace_in_config([("rpcuser", "#rpcuser"), ("rpcpassword", "#rpcpassword")])
113
114 self.log.info('Check default cookie permission')
115 test_perm(None)
116
117 self.log.info('Check custom cookie permissions')
118 for perm in ["owner", "group", "all"]:
119 test_perm(perm)
120
121 def test_norpccookiefile(self, node0_cookie_path):
122 assert self.nodes[0].is_node_stopped(), "We expect previous test to stopped the node"
123 assert not node0_cookie_path.exists()
124
125 self.log.info('Starting with -norpccookiefile')
126 # Start, but don't wait for RPC connection as TestNode.wait_for_rpc_connection() requires the cookie.
127 with self.nodes[0].busy_wait_for_debug_log([b'init message: Done loading']):
128 self.nodes[0].start(extra_args=["-norpccookiefile"])
129 assert not node0_cookie_path.exists()
130
131 self.log.info('Testing user/password authentication still works without cookie file')
132 assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword).status)
133 # After confirming that we could log in, check that cookie file does not exist.
134 assert not node0_cookie_path.exists()
135
136 # Need to shut down in slightly unorthodox way since cookie auth can't be used
137 assert_equal(200, call_with_auth(self.nodes[0], "rt", self.rtpassword, method="stop").status)
138 self.nodes[0].wait_until_stopped()
139
140 def run_test(self):
141 self.conf_setup()
142 self.log.info('Check correctness of the rpcauth config option')
143 url = urllib.parse.urlparse(self.nodes[0].url)
144
145 self.test_auth(self.nodes[0], url.username, url.password)
146 self.test_auth(self.nodes[0], 'rt', self.rtpassword)
147 self.test_auth(self.nodes[0], 'rt2', self.rt2password)
148 self.test_auth(self.nodes[0], self.user, self.password)
149
150 self.log.info('Check correctness of the rpcuser/rpcpassword config options')
151 url = urllib.parse.urlparse(self.nodes[1].url)
152
153 self.test_auth(self.nodes[1], self.rpcuser, self.rpcpassword)
154
155 init_error = 'Error: Unable to start HTTP server. See debug log for details.'
156
157 self.log.info('Check -rpcauth are validated')
158 self.log.info('Empty -rpcauth are treated as error')
159 self.stop_node(0)
160 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth'])
161 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth='])
162 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=""'])
163 self.log.info('Check malformed -rpcauth')
164 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo'])
165 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar'])
166 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo:bar:baz'])
167 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar:baz'])
168 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=foo$bar$baz'])
169
170 self.log.info('Check interactions between blank and non-blank rpcauth')
171 # pw = bitcoin
172 rpcauth_user1 = '-rpcauth=user1:6dd184e5e69271fdd69103464630014f$eb3d7ce67c4d1ff3564270519b03b636c0291012692a5fa3dd1d2075daedd07b'
173 rpcauth_user2 = '-rpcauth=user2:57b2f77c919eece63cfa46c2f06e46ae$266b63902f99f97eeaab882d4a87f8667ab84435c3799f2ce042ef5a994d620b'
174 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=[rpcauth_user1, rpcauth_user2, '-rpcauth='])
175 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=[rpcauth_user1, '-rpcauth=', rpcauth_user2])
176 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error, extra_args=['-rpcauth=', rpcauth_user1, rpcauth_user2])
177
178 self.log.info('Check -norpcauth disables previous -rpcauth params')
179 self.restart_node(0, extra_args=[rpcauth_user1, rpcauth_user2, '-norpcauth'])
180 assert_equal(401, call_with_auth(self.nodes[0], 'user1', 'bitcoin').status)
181 assert_equal(401, call_with_auth(self.nodes[0], 'rt', self.rtpassword).status)
182 self.stop_node(0)
183
184 self.log.info('Check that failure to write cookie file will abort the node gracefully')
185 cookie_path = self.nodes[0].chain_path / ".cookie"
186 cookie_path_tmp = self.nodes[0].chain_path / ".cookie.tmp"
187 cookie_path_tmp.mkdir()
188 self.nodes[0].assert_start_raises_init_error(expected_msg=init_error)
189 cookie_path_tmp.rmdir()
190 assert not cookie_path.exists()
191 self.restart_node(0)
192 assert cookie_path.exists()
193 self.stop_node(0)
194
195 self.test_rpccookieperms()
196
197 self.test_norpccookiefile(cookie_path)
198
199 if __name__ == '__main__':
200 HTTPBasicsTest(__file__).main()
201