2020import socket
2121import ssl
2222import sys
23+ import threading
2324from typing import (
2425 TYPE_CHECKING ,
2526 Any ,
4647 from pymongo .pyopenssl_context import _sslConn
4748 from pymongo .typings import _Address
4849
50+
51+ class _SSLSessionCache :
52+ """Thread-safe cache for a single TLS session per pool, enabling session resumption."""
53+
54+ __slots__ = ("_session" , "_lock" )
55+
56+ def __init__ (self ) -> None :
57+ self ._session : Optional [Any ] = None
58+ self ._lock = threading .Lock ()
59+
60+ def get (self ) -> Optional [Any ]:
61+ with self ._lock :
62+ return self ._session
63+
64+ def set (self , session : Any ) -> None :
65+ with self ._lock :
66+ self ._session = session
67+
68+
69+ def _get_ssl_session (ssl_sock : Any ) -> Optional [Any ]:
70+ """Return the TLS session from an SSL socket, handling both PyOpenSSL and stdlib ssl."""
71+ if hasattr (ssl_sock , "get_session" ):
72+ return ssl_sock .get_session ()
73+ return getattr (ssl_sock , "session" , None )
74+
75+
4976try :
5077 from fcntl import F_GETFD , F_SETFD , FD_CLOEXEC , fcntl
5178
@@ -298,7 +325,9 @@ async def _async_configured_socket(
298325
299326
300327async def _configured_protocol_interface (
301- address : _Address , options : PoolOptions
328+ address : _Address ,
329+ options : PoolOptions ,
330+ ssl_session_cache : Optional [_SSLSessionCache ] = None , # noqa: ARG001
302331) -> AsyncNetworkingInterface :
303332 """Given (host, port) and PoolOptions, return a configured AsyncNetworkingInterface.
304333
@@ -470,7 +499,11 @@ def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.
470499 return ssl_sock
471500
472501
473- def _configured_socket_interface (address : _Address , options : PoolOptions ) -> NetworkingInterface :
502+ def _configured_socket_interface (
503+ address : _Address ,
504+ options : PoolOptions ,
505+ ssl_session_cache : Optional [_SSLSessionCache ] = None ,
506+ ) -> NetworkingInterface :
474507 """Given (host, port) and PoolOptions, return a NetworkingInterface wrapping a configured socket.
475508
476509 Can raise socket.error, ConnectionFailure, or _CertificateError.
@@ -485,13 +518,14 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
485518 return NetworkingInterface (sock )
486519
487520 host = address [0 ]
521+ session = ssl_session_cache .get () if ssl_session_cache is not None else None
488522 try :
489523 # We have to pass hostname / ip address to wrap_socket
490524 # to use SSLContext.check_hostname.
491525 if _has_sni (True ):
492- ssl_sock = ssl_context .wrap_socket (sock , server_hostname = host )
526+ ssl_sock = ssl_context .wrap_socket (sock , server_hostname = host , session = session )
493527 else :
494- ssl_sock = ssl_context .wrap_socket (sock )
528+ ssl_sock = ssl_context .wrap_socket (sock , session = session )
495529 except _CertificateError :
496530 sock .close ()
497531 # Raise _CertificateError directly like we do after match_hostname
@@ -515,5 +549,10 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
515549 ssl_sock .close ()
516550 raise
517551
552+ if ssl_session_cache is not None :
553+ new_session = _get_ssl_session (ssl_sock )
554+ if new_session is not None :
555+ ssl_session_cache .set (new_session )
556+
518557 ssl_sock .settimeout (options .socket_timeout )
519558 return NetworkingInterface (ssl_sock )
0 commit comments