@@ -951,6 +951,100 @@ async def _wrap_create_connection(
951
951
except OSError as exc :
952
952
raise client_error (req .connection_key , exc ) from exc
953
953
954
+ def _warn_about_tls_in_tls (
955
+ self ,
956
+ underlying_transport : asyncio .Transport ,
957
+ req : "ClientRequest" ,
958
+ ) -> None :
959
+ """Issue a warning if the requested URL has HTTPS scheme."""
960
+ if req .request_info .url .scheme != "https" :
961
+ return
962
+
963
+ asyncio_supports_tls_in_tls = getattr (
964
+ underlying_transport ,
965
+ "_start_tls_compatible" ,
966
+ False ,
967
+ )
968
+
969
+ if asyncio_supports_tls_in_tls :
970
+ return
971
+
972
+ warnings .warn (
973
+ "An HTTPS request is being sent through an HTTPS proxy. "
974
+ "This support for TLS in TLS is known to be disabled "
975
+ "in the stdlib asyncio. This is why you'll probably see "
976
+ "an error in the log below.\n \n "
977
+ "It is possible to enable it via monkeypatching under "
978
+ "Python 3.7 or higher. For more details, see:\n "
979
+ "* https://bugs.python.org/issue37179\n "
980
+ "* https://github.com/python/cpython/pull/28073\n \n "
981
+ "You can temporarily patch this as follows:\n "
982
+ "* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n "
983
+ "* https://github.com/aio-libs/aiohttp/discussions/6044\n " ,
984
+ RuntimeWarning ,
985
+ source = self ,
986
+ # Why `4`? At least 3 of the calls in the stack originate
987
+ # from the methods in this class.
988
+ stacklevel = 3 ,
989
+ )
990
+
991
+ async def _start_tls_connection (
992
+ self ,
993
+ underlying_transport : asyncio .Transport ,
994
+ req : "ClientRequest" ,
995
+ timeout : "ClientTimeout" ,
996
+ client_error : Type [Exception ] = ClientConnectorError ,
997
+ ) -> Tuple [asyncio .BaseTransport , ResponseHandler ]:
998
+ """Wrap the raw TCP transport with TLS."""
999
+ tls_proto = self ._factory () # Create a brand new proto for TLS
1000
+
1001
+ # Safety of the `cast()` call here is based on the fact that
1002
+ # internally `_get_ssl_context()` only returns `None` when
1003
+ # `req.is_ssl()` evaluates to `False` which is never gonna happen
1004
+ # in this code path. Of course, it's rather fragile
1005
+ # maintainability-wise but this is to be solved separately.
1006
+ sslcontext = cast (ssl .SSLContext , self ._get_ssl_context (req ))
1007
+
1008
+ try :
1009
+ async with ceil_timeout (timeout .sock_connect ):
1010
+ try :
1011
+ tls_transport = await self ._loop .start_tls (
1012
+ underlying_transport ,
1013
+ tls_proto ,
1014
+ sslcontext ,
1015
+ server_hostname = req .host ,
1016
+ ssl_handshake_timeout = timeout .total ,
1017
+ )
1018
+ except BaseException :
1019
+ # We need to close the underlying transport since
1020
+ # `start_tls()` probably failed before it had a
1021
+ # chance to do this:
1022
+ underlying_transport .close ()
1023
+ raise
1024
+ except cert_errors as exc :
1025
+ raise ClientConnectorCertificateError (req .connection_key , exc ) from exc
1026
+ except ssl_errors as exc :
1027
+ raise ClientConnectorSSLError (req .connection_key , exc ) from exc
1028
+ except OSError as exc :
1029
+ raise client_error (req .connection_key , exc ) from exc
1030
+ except TypeError as type_err :
1031
+ # Example cause looks like this:
1032
+ # TypeError: transport <asyncio.sslproto._SSLProtocolTransport
1033
+ # object at 0x7f760615e460> is not supported by start_tls()
1034
+
1035
+ raise ClientConnectionError (
1036
+ "Cannot initialize a TLS-in-TLS connection to host "
1037
+ f"{ req .host !s} :{ req .port :d} through an underlying connection "
1038
+ f"to an HTTPS proxy { req .proxy !s} ssl:{ req .ssl or 'default' } "
1039
+ f"[{ type_err !s} ]"
1040
+ ) from type_err
1041
+ else :
1042
+ tls_proto .connection_made (
1043
+ tls_transport
1044
+ ) # Kick the state machine of the new TLS protocol
1045
+
1046
+ return tls_transport , tls_proto
1047
+
954
1048
async def _create_direct_connection (
955
1049
self ,
956
1050
req : "ClientRequest" ,
@@ -1028,7 +1122,7 @@ def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
1028
1122
1029
1123
async def _create_proxy_connection (
1030
1124
self , req : "ClientRequest" , traces : List ["Trace" ], timeout : "ClientTimeout"
1031
- ) -> Tuple [asyncio .Transport , ResponseHandler ]:
1125
+ ) -> Tuple [asyncio .BaseTransport , ResponseHandler ]:
1032
1126
headers = {} # type: Dict[str, str]
1033
1127
if req .proxy_headers is not None :
1034
1128
headers = req .proxy_headers # type: ignore[assignment]
@@ -1063,7 +1157,8 @@ async def _create_proxy_connection(
1063
1157
proxy_req .headers [hdrs .PROXY_AUTHORIZATION ] = auth
1064
1158
1065
1159
if req .is_ssl ():
1066
- sslcontext = self ._get_ssl_context (req )
1160
+ self ._warn_about_tls_in_tls (transport , req )
1161
+
1067
1162
# For HTTPS requests over HTTP proxy
1068
1163
# we must notify proxy to tunnel connection
1069
1164
# so we send CONNECT command:
@@ -1083,7 +1178,11 @@ async def _create_proxy_connection(
1083
1178
try :
1084
1179
protocol = conn ._protocol
1085
1180
assert protocol is not None
1086
- protocol .set_response_params ()
1181
+
1182
+ # read_until_eof=True will ensure the connection isn't closed
1183
+ # once the response is received and processed allowing
1184
+ # START_TLS to work on the connection below.
1185
+ protocol .set_response_params (read_until_eof = True )
1087
1186
resp = await proxy_resp .start (conn )
1088
1187
except BaseException :
1089
1188
proxy_resp .close ()
@@ -1104,21 +1203,19 @@ async def _create_proxy_connection(
1104
1203
message = message ,
1105
1204
headers = resp .headers ,
1106
1205
)
1107
- rawsock = transport .get_extra_info ("socket" , default = None )
1108
- if rawsock is None :
1109
- raise RuntimeError ("Transport does not expose socket instance" )
1110
- # Duplicate the socket, so now we can close proxy transport
1111
- rawsock = rawsock .dup ()
1112
- finally :
1206
+ except BaseException :
1207
+ # It shouldn't be closed in `finally` because it's fed to
1208
+ # `loop.start_tls()` and the docs say not to touch it after
1209
+ # passing there.
1113
1210
transport .close ()
1211
+ raise
1114
1212
1115
- transport , proto = await self ._wrap_create_connection (
1116
- self ._factory ,
1117
- timeout = timeout ,
1118
- ssl = sslcontext ,
1119
- sock = rawsock ,
1120
- server_hostname = req .host ,
1213
+ return await self ._start_tls_connection (
1214
+ # Access the old transport for the last time before it's
1215
+ # closed and forgotten forever:
1216
+ transport ,
1121
1217
req = req ,
1218
+ timeout = timeout ,
1122
1219
)
1123
1220
finally :
1124
1221
proxy_resp .close ()
0 commit comments