|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
19 | 19 | import asyncio |
20 | | -import contextlib |
21 | 20 | import functools |
22 | | -import inspect |
23 | 21 | import socket |
24 | | -import ssl |
25 | 22 | import time as time # noqa: PLC0414 # needed in sync version |
26 | 23 | import uuid |
27 | 24 | import weakref |
|
56 | 53 | from bson.codec_options import CodecOptions |
57 | 54 | from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bson |
58 | 55 | from pymongo import _csot, _op_id |
| 56 | +from pymongo._kms_connect_shared import AsyncKMSConnectCallback |
| 57 | +from pymongo.asynchronous._kms_connect import _KMS_CONNECT_TIMEOUT, _connect_kms |
59 | 58 | from pymongo.asynchronous.collection import AsyncCollection |
60 | 59 | from pymongo.asynchronous.cursor import AsyncCursor |
61 | 60 | from pymongo.asynchronous.database import AsyncDatabase |
62 | 61 | from pymongo.asynchronous.mongo_client import AsyncMongoClient |
63 | | -from pymongo.common import CONNECT_TIMEOUT |
64 | 62 | from pymongo.daemon import _spawn_daemon |
65 | 63 | from pymongo.encryption_options import ( |
66 | | - AsyncKMSConnectCallback, |
67 | 64 | AutoEncryptionOpts, |
68 | | - KMSConnectContext, |
69 | 65 | RangeOpts, |
70 | 66 | StringOpts, |
71 | 67 | # Re-exported for backwards compatibility: TextOpts is deprecated but must |
|
94 | 90 | from pymongo.operations import UpdateOne |
95 | 91 | from pymongo.pool_options import PoolOptions |
96 | 92 | from pymongo.pool_shared import ( |
97 | | - _async_configured_socket, |
98 | | - _async_wrap_socket_tls, |
99 | | - _close_late_socket, |
100 | 93 | _raise_connection_failure, |
101 | 94 | ) |
102 | 95 | from pymongo.read_concern import ReadConcern |
|
109 | 102 | if TYPE_CHECKING: |
110 | 103 | from pymongocrypt.mongocrypt import MongoCryptKmsContext |
111 | 104 |
|
112 | | - from pymongo.pyopenssl_context import _sslConn |
113 | | - from pymongo.typings import _Address |
114 | | - |
115 | 105 |
|
116 | 106 | _IS_SYNC = False |
117 | 107 |
|
118 | 108 | _HTTPS_PORT = 443 |
119 | | -_KMS_CONNECT_TIMEOUT = CONNECT_TIMEOUT # CDRIVER-3262 redefined this value to CONNECT_TIMEOUT |
120 | 109 | _MONGOCRYPTD_TIMEOUT_MS = 10000 |
121 | 110 |
|
122 | 111 | _DATA_KEY_OPTS: CodecOptions[dict[str, Any]] = CodecOptions( |
|
127 | 116 | _KEY_VAULT_OPTS = CodecOptions(document_class=RawBSONDocument) |
128 | 117 |
|
129 | 118 |
|
130 | | -def _close_rejected_kms_socket(obj: Any) -> None: |
131 | | - """Close a callback return value that failed validation, best effort. |
132 | | -
|
133 | | - ``_connect_kms`` raises on a contract violation instead of returning the |
134 | | - value, so no caller ever takes ownership of it. Close it here, tolerating |
135 | | - non-socket values and ``close()`` failures. |
136 | | - """ |
137 | | - close = getattr(obj, "close", None) |
138 | | - if callable(close): |
139 | | - with contextlib.suppress(Exception): |
140 | | - close() |
141 | | - |
142 | | - |
143 | | -async def _connect_kms( |
144 | | - address: _Address, |
145 | | - opts: PoolOptions, |
146 | | - kms_connect_callback: Optional[AsyncKMSConnectCallback], |
147 | | - timeout: float, |
148 | | -) -> Union[socket.socket, _sslConn]: |
149 | | - """Connect to a KMS host and perform the TLS handshake over the socket. |
150 | | -
|
151 | | - Uses ``kms_connect_callback`` when one is provided, otherwise connects |
152 | | - directly, and always verifies against ``address`` (the KMS host). |
153 | | - """ |
154 | | - if kms_connect_callback is None: |
155 | | - try: |
156 | | - return await _async_configured_socket(address, opts) |
157 | | - except Exception as exc: |
158 | | - _raise_connection_failure(address, exc, timeout_details=_get_timeout_details(opts)) |
159 | | - |
160 | | - # TLS targets address, not the peer, so verification follows the KMS host. |
161 | | - # A plain callable would block the event loop before we could reject it, |
162 | | - # so check the callback first. |
163 | | - if not _IS_SYNC: |
164 | | - callback_any: Any = kms_connect_callback |
165 | | - is_coro = inspect.iscoroutinefunction(callback_any) |
166 | | - if not is_coro and callable(callback_any): |
167 | | - is_coro = inspect.iscoroutinefunction(callback_any.__call__) |
168 | | - if not is_coro: |
169 | | - raise ConfigurationError( |
170 | | - "kms_connect_callback must be a coroutine function for the async API." |
171 | | - ) |
172 | | - # Any: the sync version's callback returns a plain socket. |
173 | | - result: Any = kms_connect_callback( |
174 | | - KMSConnectContext(host=address[0], port=cast(int, address[1]), timeout=timeout) |
175 | | - ) |
176 | | - remaining = _csot.remaining() |
177 | | - if remaining is None or _IS_SYNC: |
178 | | - # The synchronous API cannot interrupt a callback that has started |
179 | | - # running; honoring the deadline is the callback's contract there. |
180 | | - sock = await result |
181 | | - else: |
182 | | - # CSOT is cooperative: a callback that ignores the timeout could block |
183 | | - # past the deadline. Shield the task so stopping the wait does not |
184 | | - # cancel it mid-flight, and close any socket it yields later. |
185 | | - task = asyncio.ensure_future(result) |
186 | | - try: |
187 | | - sock = await asyncio.wait_for(asyncio.shield(task), remaining) |
188 | | - except asyncio.CancelledError: |
189 | | - task.add_done_callback(_close_late_socket) |
190 | | - raise |
191 | | - except asyncio.TimeoutError: |
192 | | - task.add_done_callback(_close_late_socket) |
193 | | - _raise_connection_failure( |
194 | | - address, |
195 | | - socket.timeout("timed out"), |
196 | | - timeout_details=_get_timeout_details(opts), |
197 | | - ) |
198 | | - if not isinstance(sock, socket.socket) or isinstance(sock, ssl.SSLSocket): |
199 | | - _close_rejected_kms_socket(sock) |
200 | | - raise ConfigurationError( |
201 | | - "kms_connect_callback must return a connected, unwrapped " |
202 | | - f"socket.socket, not {type(sock)}." |
203 | | - ) |
204 | | - # wrap_socket refuses a non-blocking socket, so normalize the mode here. |
205 | | - try: |
206 | | - sock.getpeername() |
207 | | - except OSError: |
208 | | - _close_rejected_kms_socket(sock) |
209 | | - raise ConfigurationError( |
210 | | - "kms_connect_callback must return an already connected socket." |
211 | | - ) from None |
212 | | - if sock.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) != socket.SOCK_STREAM: |
213 | | - _close_rejected_kms_socket(sock) |
214 | | - raise ConfigurationError( |
215 | | - "kms_connect_callback must return a stream socket, not a datagram one." |
216 | | - ) |
217 | | - # The callback may have consumed much of the CSOT budget, and wrapping |
218 | | - # resets the socket timeout, so recompute the remaining time here and for |
219 | | - # the KMS request that follows. |
220 | | - sock.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)) |
221 | | - try: |
222 | | - conn = await _async_wrap_socket_tls(sock, address, opts) |
223 | | - except asyncio.CancelledError: |
224 | | - # The executor may still be wrapping the socket; close it so a TLS |
225 | | - # proxy's relay threads wind down instead of leaking. |
226 | | - sock.close() |
227 | | - raise |
228 | | - except Exception as exc: |
229 | | - _raise_connection_failure(address, exc, timeout_details=_get_timeout_details(opts)) |
230 | | - conn.settimeout(max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)) |
231 | | - return conn |
232 | | - |
233 | | - |
234 | 119 | class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc] |
235 | 120 | def __init__( |
236 | 121 | self, |
|
0 commit comments