interface_http.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2014-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 the RPC HTTP basics.""" 6 7 from test_framework.test_framework import BitcoinTestFramework 8 from test_framework.util import assert_equal, str_to_b64str 9 10 import http.client 11 import time 12 import urllib.parse 13 14 class HTTPBasicsTest (BitcoinTestFramework): 15 def set_test_params(self): 16 self.num_nodes = 3 17 self.supports_cli = False 18 19 def setup_network(self): 20 self.setup_nodes() 21 22 def run_test(self): 23 24 ################################################# 25 # lowlevel check for http persistent connection # 26 ################################################# 27 url = urllib.parse.urlparse(self.nodes[0].url) 28 authpair = f'{url.username}:{url.password}' 29 headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} 30 31 conn = http.client.HTTPConnection(url.hostname, url.port) 32 conn.connect() 33 conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) 34 out1 = conn.getresponse().read() 35 assert b'"error":null' in out1 36 assert conn.sock is not None #according to http/1.1 connection must still be open! 37 38 #send 2nd request without closing connection 39 conn.request('POST', '/', '{"method": "getchaintips"}', headers) 40 out1 = conn.getresponse().read() 41 assert b'"error":null' in out1 #must also response with a correct json-rpc message 42 assert conn.sock is not None #according to http/1.1 connection must still be open! 43 conn.close() 44 45 #same should be if we add keep-alive because this should be the std. behaviour 46 headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection": "keep-alive"} 47 48 conn = http.client.HTTPConnection(url.hostname, url.port) 49 conn.connect() 50 conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) 51 out1 = conn.getresponse().read() 52 assert b'"error":null' in out1 53 assert conn.sock is not None #according to http/1.1 connection must still be open! 54 55 #send 2nd request without closing connection 56 conn.request('POST', '/', '{"method": "getchaintips"}', headers) 57 out1 = conn.getresponse().read() 58 assert b'"error":null' in out1 #must also response with a correct json-rpc message 59 assert conn.sock is not None #according to http/1.1 connection must still be open! 60 conn.close() 61 62 #now do the same with "Connection: close" 63 headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", "Connection":"close"} 64 65 conn = http.client.HTTPConnection(url.hostname, url.port) 66 conn.connect() 67 conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) 68 out1 = conn.getresponse().read() 69 assert b'"error":null' in out1 70 assert conn.sock is None #now the connection must be closed after the response 71 72 #node1 (2nd node) is running with disabled keep-alive option 73 urlNode1 = urllib.parse.urlparse(self.nodes[1].url) 74 authpair = f'{urlNode1.username}:{urlNode1.password}' 75 headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} 76 77 conn = http.client.HTTPConnection(urlNode1.hostname, urlNode1.port) 78 conn.connect() 79 conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) 80 out1 = conn.getresponse().read() 81 assert b'"error":null' in out1 82 83 #node2 (third node) is running with standard keep-alive parameters which means keep-alive is on 84 urlNode2 = urllib.parse.urlparse(self.nodes[2].url) 85 authpair = f'{urlNode2.username}:{urlNode2.password}' 86 headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} 87 88 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 89 conn.connect() 90 conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) 91 out1 = conn.getresponse().read() 92 assert b'"error":null' in out1 93 assert conn.sock is not None #connection must be closed because bitcoind should use keep-alive by default 94 95 # Check excessive request size 96 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 97 conn.connect() 98 conn.request('GET', f'/{"x"*1000}', '', headers) 99 out1 = conn.getresponse() 100 assert_equal(out1.status, http.client.NOT_FOUND) 101 102 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 103 conn.connect() 104 conn.request('GET', f'/{"x"*10000}', '', headers) 105 out1 = conn.getresponse() 106 assert_equal(out1.status, http.client.BAD_REQUEST) 107 108 109 self.log.info("Check pipelining") 110 # Requests are responded to in order they were received 111 # See https://www.rfc-editor.org/rfc/rfc7230#section-6.3.2 112 tip_height = self.nodes[2].getblockcount() 113 114 req = "POST / HTTP/1.1\r\n" 115 req += f'Authorization: Basic {str_to_b64str(authpair)}\r\n' 116 117 # First request will take a long time to process 118 body1 = f'{{"method": "waitforblockheight", "params": [{tip_height + 1}]}}' 119 req1 = req 120 req1 += f'Content-Length: {len(body1)}\r\n\r\n' 121 req1 += body1 122 123 # Second request will process very fast 124 body2 = '{"method": "getblockcount"}' 125 req2 = req 126 req2 += f'Content-Length: {len(body2)}\r\n\r\n' 127 req2 += body2 128 # Get the underlying socket from HTTP connection so we can send something unusual 129 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 130 conn.connect() 131 sock = conn.sock 132 sock.settimeout(5) 133 # Send two requests in a row. The first will block the second indefinitely 134 sock.sendall(req1.encode("utf-8")) 135 sock.sendall(req2.encode("utf-8")) 136 try: 137 # The server should not respond to the fast, second request 138 # until the (very) slow first request has been handled: 139 res = sock.recv(1024) 140 assert False 141 except TimeoutError: 142 pass 143 144 # Use a separate http connection to generate a block 145 self.generate(self.nodes[2], 1, sync_fun=self.no_op) 146 147 # Wait for two responses to be received 148 res = b"" 149 while res.count(b"result") != 2: 150 res += sock.recv(1024) 151 152 # waitforblockheight was responded to first, and then getblockcount 153 # which includes the block added after the request was made 154 chunks = res.split(b'"result":') 155 assert chunks[1].startswith(b'{"hash":') 156 assert chunks[2].startswith(bytes(f'{tip_height + 1}', 'utf8')) 157 158 159 self.log.info("Check HTTP request encoded with chunked transfer") 160 headers_chunked = headers.copy() 161 headers_chunked.update({"Transfer-encoding": "chunked"}) 162 body_chunked = [ 163 b'{"method": "submitblock", "params": ["', 164 b'0' * 1000000, 165 b'1' * 1000000, 166 b'2' * 1000000, 167 b'3' * 1000000, 168 b'"]}' 169 ] 170 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 171 conn.connect() 172 conn.request( 173 method='POST', 174 url='/', 175 body=iter(body_chunked), 176 headers=headers_chunked, 177 encode_chunked=True) 178 out1 = conn.getresponse().read() 179 assert_equal(out1, b'{"result":"high-hash","error":null}\n') 180 181 182 self.log.info("Check -rpcservertimeout") 183 # The test framework typically reuses a single persistent HTTP connection 184 # for all RPCs to a TestNode. Because we are setting -rpcservertimeout 185 # so low on this one node, its connection will quickly timeout and get dropped by 186 # the server. Negating this setting will force the AuthServiceProxy 187 # for this node to create a fresh new HTTP connection for every command 188 # called for the remainder of this test. 189 self.nodes[2].reuse_http_connections = False 190 191 self.restart_node(2, extra_args=["-rpcservertimeout=2"]) 192 # This is the amount of time the server will wait for a client to 193 # send a complete request. Test it by sending an incomplete but 194 # so-far otherwise well-formed HTTP request, and never finishing it. 195 196 # Copied from http_incomplete_test_() in regress_http.c in libevent. 197 # A complete request would have an additional "\r\n" at the end. 198 http_request = "GET /test1 HTTP/1.1\r\nHost: somehost\r\n" 199 200 # Get the underlying socket from HTTP connection so we can send something unusual 201 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 202 conn.connect() 203 sock = conn.sock 204 sock.sendall(http_request.encode("utf-8")) 205 # Wait for response, but expect a timeout disconnection after 1 second 206 start = time.time() 207 res = sock.recv(1024) 208 stop = time.time() 209 # Server disconnected with EOF 210 assert_equal(res, b"") 211 # Server disconnected within an acceptable range of time: 212 # not immediately, and not too far over the configured duration. 213 # This allows for some jitter in the test between client and server. 214 duration = stop - start 215 assert duration <= 4, f"Server disconnected too slow: {duration} > 4" 216 assert duration >= 1, f"Server disconnected too fast: {duration} < 1" 217 218 # The connection is definitely closed. 219 got_expected_error = False 220 try: 221 conn.request('GET', '/') 222 conn.getresponse() 223 # macos/linux windows 224 except (ConnectionResetError, ConnectionAbortedError): 225 got_expected_error = True 226 assert got_expected_error 227 228 # Sanity check 229 http_request = "GET /test2 HTTP/1.1\r\nHost: somehost\r\n\r\n" 230 conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) 231 conn.connect() 232 sock = conn.sock 233 sock.sendall(http_request.encode("utf-8")) 234 res = sock.recv(1024) 235 assert res.startswith(b"HTTP/1.1 404 Not Found") 236 # still open 237 conn.request('GET', '/') 238 conn.getresponse() 239 240 if __name__ == '__main__': 241 HTTPBasicsTest(__file__).main()