/ test / functional / interface_http.py
interface_http.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2014-2021 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()