|
8 | 8 | from test_framework.util import assert_equal, str_to_b64str
|
9 | 9 |
|
10 | 10 | import http.client
|
| 11 | +import time |
11 | 12 | import urllib.parse
|
12 | 13 |
|
13 | 14 | class HTTPBasicsTest (BitcoinTestFramework):
|
@@ -105,5 +106,136 @@ def run_test(self):
|
105 | 106 | assert_equal(out1.status, http.client.BAD_REQUEST)
|
106 | 107 |
|
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 | + |
108 | 240 | if __name__ == '__main__':
|
109 | 241 | HTTPBasicsTest(__file__).main()
|
0 commit comments