|
9 | 9 | from test.support import socket_helper
|
10 | 10 | from test.support import threading_helper
|
11 | 11 | from test.support import warnings_helper
|
| 12 | +import re |
12 | 13 | import socket
|
13 | 14 | import select
|
| 15 | +import struct |
14 | 16 | import time
|
15 | 17 | import datetime
|
16 | 18 | import gc
|
| 19 | +import http.client |
17 | 20 | import os
|
18 | 21 | import errno
|
19 | 22 | import pprint
|
@@ -4904,6 +4907,218 @@ def sni_cb(sock, servername, ctx):
|
4904 | 4907 | s.connect((HOST, server.port))
|
4905 | 4908 |
|
4906 | 4909 |
|
| 4910 | +def set_socket_so_linger_on_with_zero_timeout(sock): |
| 4911 | + sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)) |
| 4912 | + |
| 4913 | + |
| 4914 | +class TestPreHandshakeClose(unittest.TestCase): |
| 4915 | + """Verify behavior of close sockets with received data before to the handshake. |
| 4916 | + """ |
| 4917 | + |
| 4918 | + class SingleConnectionTestServerThread(threading.Thread): |
| 4919 | + |
| 4920 | + def __init__(self, *, name, call_after_accept): |
| 4921 | + self.call_after_accept = call_after_accept |
| 4922 | + self.received_data = b'' # set by .run() |
| 4923 | + self.wrap_error = None # set by .run() |
| 4924 | + self.listener = None # set by .start() |
| 4925 | + self.port = None # set by .start() |
| 4926 | + super().__init__(name=name) |
| 4927 | + |
| 4928 | + def __enter__(self): |
| 4929 | + self.start() |
| 4930 | + return self |
| 4931 | + |
| 4932 | + def __exit__(self, *args): |
| 4933 | + try: |
| 4934 | + if self.listener: |
| 4935 | + self.listener.close() |
| 4936 | + except OSError: |
| 4937 | + pass |
| 4938 | + self.join() |
| 4939 | + self.wrap_error = None # avoid dangling references |
| 4940 | + |
| 4941 | + def start(self): |
| 4942 | + self.ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) |
| 4943 | + self.ssl_ctx.verify_mode = ssl.CERT_REQUIRED |
| 4944 | + self.ssl_ctx.load_verify_locations(cafile=ONLYCERT) |
| 4945 | + self.ssl_ctx.load_cert_chain(certfile=ONLYCERT, keyfile=ONLYKEY) |
| 4946 | + self.listener = socket.socket() |
| 4947 | + self.port = socket_helper.bind_port(self.listener) |
| 4948 | + self.listener.settimeout(2.0) |
| 4949 | + self.listener.listen(1) |
| 4950 | + super().start() |
| 4951 | + |
| 4952 | + def run(self): |
| 4953 | + conn, address = self.listener.accept() |
| 4954 | + self.listener.close() |
| 4955 | + with conn: |
| 4956 | + if self.call_after_accept(conn): |
| 4957 | + return |
| 4958 | + try: |
| 4959 | + tls_socket = self.ssl_ctx.wrap_socket(conn, server_side=True) |
| 4960 | + except OSError as err: # ssl.SSLError inherits from OSError |
| 4961 | + self.wrap_error = err |
| 4962 | + else: |
| 4963 | + try: |
| 4964 | + self.received_data = tls_socket.recv(400) |
| 4965 | + except OSError: |
| 4966 | + pass # closed, protocol error, etc. |
| 4967 | + |
| 4968 | + def non_linux_skip_if_other_okay_error(self, err): |
| 4969 | + if sys.platform == "linux": |
| 4970 | + return # Expect the full test setup to always work on Linux. |
| 4971 | + if (isinstance(err, ConnectionResetError) or |
| 4972 | + (isinstance(err, OSError) and err.errno == errno.EINVAL) or |
| 4973 | + re.search('wrong.version.number', getattr(err, "reason", ""), re.I)): |
| 4974 | + # On Windows the TCP RST leads to a ConnectionResetError |
| 4975 | + # (ECONNRESET) which Linux doesn't appear to surface to userspace. |
| 4976 | + # If wrap_socket() winds up on the "if connected:" path and doing |
| 4977 | + # the actual wrapping... we get an SSLError from OpenSSL. Typically |
| 4978 | + # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario |
| 4979 | + # we're specifically trying to test. The way this test is written |
| 4980 | + # is known to work on Linux. We'll skip it anywhere else that it |
| 4981 | + # does not present as doing so. |
| 4982 | + self.skipTest(f"Could not recreate conditions on {sys.platform}:" |
| 4983 | + f" {err=}") |
| 4984 | + # If maintaining this conditional winds up being a problem. |
| 4985 | + # just turn this into an unconditional skip anything but Linux. |
| 4986 | + # The important thing is that our CI has the logic covered. |
| 4987 | + |
| 4988 | + def test_preauth_data_to_tls_server(self): |
| 4989 | + server_accept_called = threading.Event() |
| 4990 | + ready_for_server_wrap_socket = threading.Event() |
| 4991 | + |
| 4992 | + def call_after_accept(unused): |
| 4993 | + server_accept_called.set() |
| 4994 | + if not ready_for_server_wrap_socket.wait(2.0): |
| 4995 | + raise RuntimeError("wrap_socket event never set, test may fail.") |
| 4996 | + return False # Tell the server thread to continue. |
| 4997 | + |
| 4998 | + server = self.SingleConnectionTestServerThread( |
| 4999 | + call_after_accept=call_after_accept, |
| 5000 | + name="preauth_data_to_tls_server") |
| 5001 | + server.__enter__() # starts it |
| 5002 | + self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it. |
| 5003 | + |
| 5004 | + with socket.socket() as client: |
| 5005 | + client.connect(server.listener.getsockname()) |
| 5006 | + # This forces an immediate connection close via RST on .close(). |
| 5007 | + set_socket_so_linger_on_with_zero_timeout(client) |
| 5008 | + client.setblocking(False) |
| 5009 | + |
| 5010 | + server_accept_called.wait() |
| 5011 | + client.send(b"DELETE /data HTTP/1.0\r\n\r\n") |
| 5012 | + client.close() # RST |
| 5013 | + |
| 5014 | + ready_for_server_wrap_socket.set() |
| 5015 | + server.join() |
| 5016 | + wrap_error = server.wrap_error |
| 5017 | + self.assertEqual(b"", server.received_data) |
| 5018 | + self.assertIsInstance(wrap_error, OSError) # All platforms. |
| 5019 | + self.non_linux_skip_if_other_okay_error(wrap_error) |
| 5020 | + self.assertIsInstance(wrap_error, ssl.SSLError) |
| 5021 | + self.assertIn("before TLS handshake with data", wrap_error.args[1]) |
| 5022 | + self.assertIn("before TLS handshake with data", wrap_error.reason) |
| 5023 | + self.assertNotEqual(0, wrap_error.args[0]) |
| 5024 | + self.assertIsNone(wrap_error.library, msg="attr must exist") |
| 5025 | + |
| 5026 | + def test_preauth_data_to_tls_client(self): |
| 5027 | + client_can_continue_with_wrap_socket = threading.Event() |
| 5028 | + |
| 5029 | + def call_after_accept(conn_to_client): |
| 5030 | + # This forces an immediate connection close via RST on .close(). |
| 5031 | + set_socket_so_linger_on_with_zero_timeout(conn_to_client) |
| 5032 | + conn_to_client.send( |
| 5033 | + b"HTTP/1.0 307 Temporary Redirect\r\n" |
| 5034 | + b"Location: https://example.com/someone-elses-server\r\n" |
| 5035 | + b"\r\n") |
| 5036 | + conn_to_client.close() # RST |
| 5037 | + client_can_continue_with_wrap_socket.set() |
| 5038 | + return True # Tell the server to stop. |
| 5039 | + |
| 5040 | + server = self.SingleConnectionTestServerThread( |
| 5041 | + call_after_accept=call_after_accept, |
| 5042 | + name="preauth_data_to_tls_client") |
| 5043 | + server.__enter__() # starts it |
| 5044 | + self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it. |
| 5045 | + |
| 5046 | + # Redundant; call_after_accept sets SO_LINGER on the accepted conn. |
| 5047 | + set_socket_so_linger_on_with_zero_timeout(server.listener) |
| 5048 | + |
| 5049 | + with socket.socket() as client: |
| 5050 | + client.connect(server.listener.getsockname()) |
| 5051 | + if not client_can_continue_with_wrap_socket.wait(2.0): |
| 5052 | + self.fail("test server took too long.") |
| 5053 | + ssl_ctx = ssl.create_default_context() |
| 5054 | + try: |
| 5055 | + tls_client = ssl_ctx.wrap_socket( |
| 5056 | + client, server_hostname="localhost") |
| 5057 | + except OSError as err: # SSLError inherits from OSError |
| 5058 | + wrap_error = err |
| 5059 | + received_data = b"" |
| 5060 | + else: |
| 5061 | + wrap_error = None |
| 5062 | + received_data = tls_client.recv(400) |
| 5063 | + tls_client.close() |
| 5064 | + |
| 5065 | + server.join() |
| 5066 | + self.assertEqual(b"", received_data) |
| 5067 | + self.assertIsInstance(wrap_error, OSError) # All platforms. |
| 5068 | + self.non_linux_skip_if_other_okay_error(wrap_error) |
| 5069 | + self.assertIsInstance(wrap_error, ssl.SSLError) |
| 5070 | + self.assertIn("before TLS handshake with data", wrap_error.args[1]) |
| 5071 | + self.assertIn("before TLS handshake with data", wrap_error.reason) |
| 5072 | + self.assertNotEqual(0, wrap_error.args[0]) |
| 5073 | + self.assertIsNone(wrap_error.library, msg="attr must exist") |
| 5074 | + |
| 5075 | + def test_https_client_non_tls_response_ignored(self): |
| 5076 | + |
| 5077 | + server_responding = threading.Event() |
| 5078 | + |
| 5079 | + class SynchronizedHTTPSConnection(http.client.HTTPSConnection): |
| 5080 | + def connect(self): |
| 5081 | + http.client.HTTPConnection.connect(self) |
| 5082 | + # Wait for our fault injection server to have done its thing. |
| 5083 | + if not server_responding.wait(1.0) and support.verbose: |
| 5084 | + sys.stdout.write("server_responding event never set.") |
| 5085 | + self.sock = self._context.wrap_socket( |
| 5086 | + self.sock, server_hostname=self.host) |
| 5087 | + |
| 5088 | + def call_after_accept(conn_to_client): |
| 5089 | + # This forces an immediate connection close via RST on .close(). |
| 5090 | + set_socket_so_linger_on_with_zero_timeout(conn_to_client) |
| 5091 | + conn_to_client.send( |
| 5092 | + b"HTTP/1.0 402 Payment Required\r\n" |
| 5093 | + b"\r\n") |
| 5094 | + conn_to_client.close() # RST |
| 5095 | + server_responding.set() |
| 5096 | + return True # Tell the server to stop. |
| 5097 | + |
| 5098 | + server = self.SingleConnectionTestServerThread( |
| 5099 | + call_after_accept=call_after_accept, |
| 5100 | + name="non_tls_http_RST_responder") |
| 5101 | + server.__enter__() # starts it |
| 5102 | + self.addCleanup(server.__exit__) # ... & unittest.TestCase stops it. |
| 5103 | + # Redundant; call_after_accept sets SO_LINGER on the accepted conn. |
| 5104 | + set_socket_so_linger_on_with_zero_timeout(server.listener) |
| 5105 | + |
| 5106 | + connection = SynchronizedHTTPSConnection( |
| 5107 | + f"localhost", |
| 5108 | + port=server.port, |
| 5109 | + context=ssl.create_default_context(), |
| 5110 | + timeout=2.0, |
| 5111 | + ) |
| 5112 | + # There are lots of reasons this raises as desired, long before this |
| 5113 | + # test was added. Sending the request requires a successful TLS wrapped |
| 5114 | + # socket; that fails if the connection is broken. It may seem pointless |
| 5115 | + # to test this. It serves as an illustration of something that we never |
| 5116 | + # want to happen... properly not happening. |
| 5117 | + with self.assertRaises(OSError) as err_ctx: |
| 5118 | + connection.request("HEAD", "/test", headers={"Host": "localhost"}) |
| 5119 | + response = connection.getresponse() |
| 5120 | + |
| 5121 | + |
4907 | 5122 | def setUpModule():
|
4908 | 5123 | if support.verbose:
|
4909 | 5124 | plats = {
|
|
0 commit comments