cloud-sql-python-connector 1.22.0__py3-none-any.whl → 1.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,235 @@
1
+ """
2
+ Copyright 2026 Google LLC
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ https://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+ import socket
21
+ import ssl
22
+ from typing import Any
23
+
24
+ MAX_PG_STARTUP_PACKET_LEN = 10000
25
+ PG_PROTOCOL_VERSION_30 = 196608 # 0x00030000
26
+ PG_SSL_REQUEST_CODE = 80877103 # 0x04d2162f
27
+ PG_GSS_ENC_REQUEST_CODE = 80877104 # 0x04d21630
28
+
29
+
30
+ class PostgresConnection:
31
+ """Wraps a socket to inspect initial client-side PostgreSQL v3 StartupMessage bytes.
32
+
33
+ Extracts the (user, database) principal pair for Auto-IAM refresh probes.
34
+ Handles chunked streaming across multiple write/send operations.
35
+ Once the startup message is detected or determined not to be a PostgreSQL v3
36
+ startup packet, startup_message_sent is set to True, the internal buffer is
37
+ cleared, and all subsequent writes pass through directly with zero overhead.
38
+
39
+ Like standard Python socket and stream wrappers, callers own synchronization
40
+ for concurrent writes; no internal lock is acquired on the write path.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ sock: socket.socket | ssl.SSLSocket,
46
+ on_startup: Callable[[str, str], None],
47
+ ) -> None:
48
+ self._sock = sock
49
+ self._on_startup = on_startup
50
+ self._buf = bytearray()
51
+ self.startup_message_sent = False
52
+
53
+ def _inspect(self, data: bytes | bytearray | memoryview) -> None:
54
+ if self.startup_message_sent or not data:
55
+ return
56
+ self._buf.extend(data)
57
+ user, database, complete = parse_postgres_startup_packet(self._buf)
58
+ if complete:
59
+ self.startup_message_sent = True
60
+ self._buf.clear()
61
+ if user:
62
+ self._on_startup(user, database)
63
+ elif len(self._buf) > MAX_PG_STARTUP_PACKET_LEN + 16:
64
+ self.startup_message_sent = True
65
+ self._buf.clear()
66
+
67
+ def send(
68
+ self,
69
+ data: bytes | bytearray | memoryview,
70
+ *args: Any,
71
+ **kwargs: Any,
72
+ ) -> int:
73
+ sent = self._sock.send(data, *args, **kwargs)
74
+ if not self.startup_message_sent:
75
+ chunk = data[:sent] if isinstance(sent, int) else data
76
+ if chunk:
77
+ self._inspect(chunk)
78
+ return sent
79
+
80
+ def sendall(
81
+ self,
82
+ data: bytes | bytearray | memoryview,
83
+ *args: Any,
84
+ **kwargs: Any,
85
+ ) -> None:
86
+ self._sock.sendall(data, *args, **kwargs)
87
+ if not self.startup_message_sent and data:
88
+ self._inspect(data)
89
+
90
+ def write(self, data: bytes | bytearray | memoryview) -> int:
91
+ written = (
92
+ self._sock.write(data)
93
+ if hasattr(self._sock, "write")
94
+ else self._sock.send(data)
95
+ )
96
+ if not self.startup_message_sent:
97
+ chunk = data[:written] if isinstance(written, int) else data
98
+ if chunk:
99
+ self._inspect(chunk)
100
+ return written
101
+
102
+ @property
103
+ def _io_refs(self) -> int:
104
+ return getattr(self._sock, "_io_refs", 0)
105
+
106
+ @_io_refs.setter
107
+ def _io_refs(self, value: int) -> None:
108
+ self._sock._io_refs = value # type: ignore[union-attr]
109
+
110
+ def makefile(
111
+ self,
112
+ mode: str = "r",
113
+ buffering: int | None = None,
114
+ *,
115
+ encoding: str | None = None,
116
+ errors: str | None = None,
117
+ newline: str | None = None,
118
+ ) -> Any:
119
+ """Returns an I/O stream connected to this wrapped socket so makefile writes are inspected."""
120
+ return socket.socket.makefile( # type: ignore[call-overload]
121
+ self,
122
+ mode=mode,
123
+ buffering=buffering,
124
+ encoding=encoding,
125
+ errors=errors,
126
+ newline=newline,
127
+ )
128
+
129
+ def close(self) -> None:
130
+ self._sock.close()
131
+
132
+ def fileno(self) -> int:
133
+ return self._sock.fileno()
134
+
135
+ def __enter__(self) -> Any:
136
+ return self
137
+
138
+ def __exit__(self, *args: object) -> Any:
139
+ if hasattr(self._sock, "__exit__"):
140
+ return self._sock.__exit__(*args)
141
+ return None
142
+
143
+ def __getattr__(self, name: str) -> Any:
144
+ return getattr(self._sock, name)
145
+
146
+
147
+ def parse_postgres_startup_packet(
148
+ buf: bytes | bytearray | memoryview,
149
+ ) -> tuple[str, str, bool]:
150
+ """Parses a buffer to extract the PostgreSQL v3 StartupMessage (user, database) pair.
151
+
152
+ Returns:
153
+ (user, database, complete):
154
+ complete is True if the packet was fully parsed (or rejected),
155
+ False if more bytes are needed (partial packet).
156
+ """
157
+ data = bytes(buf)
158
+ # Step 1: Skip prepended SSLRequest (80877103) or GSSENCRequest (80877104) (can occur in succession).
159
+ while len(data) >= 8:
160
+ pkt_len = int.from_bytes(data[0:4], byteorder="big")
161
+ code = int.from_bytes(data[4:8], byteorder="big")
162
+ if pkt_len == 8 and (
163
+ code == PG_SSL_REQUEST_CODE or code == PG_GSS_ENC_REQUEST_CODE
164
+ ):
165
+ data = data[8:]
166
+ else:
167
+ break
168
+
169
+ # Step 2: Wait for at least 8 bytes (4-byte packet length + 4-byte protocol code).
170
+ if len(data) < 8:
171
+ return "", "", False
172
+
173
+ pkt_len = int.from_bytes(data[0:4], byteorder="big")
174
+ code = int.from_bytes(data[4:8], byteorder="big")
175
+
176
+ # Step 3: Validate protocol version (v3.0) and packet length bounds.
177
+ if (
178
+ code != PG_PROTOCOL_VERSION_30
179
+ or pkt_len < 8
180
+ or pkt_len > MAX_PG_STARTUP_PACKET_LEN
181
+ ):
182
+ return "", "", True
183
+
184
+ # Step 4: Wait until the complete packet body has arrived.
185
+ if len(data) < pkt_len:
186
+ return "", "", False
187
+
188
+ # Step 5: Decode null-terminated key-value pairs (user, database) from the payload.
189
+ payload = data[8:pkt_len]
190
+ user = ""
191
+ database = ""
192
+ while payload and payload[0] != 0:
193
+ try:
194
+ k_end = payload.index(0)
195
+ except ValueError:
196
+ break
197
+ key = payload[:k_end].decode("utf-8", errors="replace")
198
+ payload = payload[k_end + 1 :]
199
+ try:
200
+ v_end = payload.index(0)
201
+ except ValueError:
202
+ break
203
+ val = payload[:v_end].decode("utf-8", errors="replace")
204
+ payload = payload[v_end + 1 :]
205
+
206
+ if key == "user":
207
+ user = val
208
+ elif key == "database":
209
+ database = val
210
+
211
+ if user and not database:
212
+ database = user
213
+
214
+ # Step 6: Successfully extracted credentials.
215
+ return user, database, True
216
+
217
+
218
+ def build_postgres_startup_packet(user: str, database: str) -> bytes:
219
+ """Builds a PostgreSQL v3.0 StartupMessage for (user, database)."""
220
+ if not database:
221
+ database = user
222
+ body = (
223
+ b"user\x00"
224
+ + user.encode("utf-8")
225
+ + b"\x00database\x00"
226
+ + database.encode("utf-8")
227
+ + b"\x00\x00"
228
+ )
229
+ total_len = 8 + len(body)
230
+ return total_len.to_bytes(4, byteorder="big") + b"\x00\x03\x00\x00" + body
231
+
232
+
233
+ def build_terminate_packet() -> bytes:
234
+ """Builds a PostgreSQL Terminate packet ('X' followed by int32 length 4)."""
235
+ return b"X\x00\x00\x00\x04"
@@ -12,20 +12,128 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import contextlib
15
16
  import logging
16
17
  import os
18
+ import platform
17
19
  import selectors
18
20
  import socket
19
21
  import ssl
20
22
  import tempfile
21
23
  import threading
22
- from typing import Any, TYPE_CHECKING
24
+ from types import TracebackType
25
+ from typing import Any, Self, TYPE_CHECKING
26
+
27
+ from google.cloud.sql.connector.sqldata_client import SqlDataSocket
23
28
 
24
29
  if TYPE_CHECKING:
25
30
  import psycopg
26
31
 
27
32
  logger = logging.getLogger(name=__name__)
28
33
 
34
+ _BUFFER_SIZE = 8192
35
+ _DEFAULT_PGSQL_PORT = 5432
36
+ _LOOPBACK_HOST = "127.0.0.1"
37
+ _UNIX_SOCKET_NAME = ".s.PGSQL.5432"
38
+
39
+
40
+ def _close_socket(sock: socket.socket, *, shutdown: bool = True) -> None:
41
+ """Safely shut down and close a socket, ignoring OSErrors."""
42
+ if shutdown:
43
+ try:
44
+ sock.shutdown(socket.SHUT_RDWR)
45
+ except OSError:
46
+ pass
47
+ try:
48
+ sock.close()
49
+ except OSError:
50
+ pass
51
+
52
+
53
+ def _forward_bytes(
54
+ src: socket.socket,
55
+ dst: socket.socket,
56
+ src_name: str,
57
+ dst_name: str,
58
+ *,
59
+ nonblocking: bool = False,
60
+ ) -> bool:
61
+ """Read a chunk of bytes from src and send it to dst.
62
+
63
+ Returns:
64
+ bool: True if bytes were forwarded, False on EOF or OSError.
65
+ """
66
+ try:
67
+ if nonblocking:
68
+ src.setblocking(False)
69
+ try:
70
+ data = src.recv(_BUFFER_SIZE)
71
+ finally:
72
+ if nonblocking:
73
+ with contextlib.suppress(OSError):
74
+ src.setblocking(True)
75
+ except OSError as e:
76
+ if nonblocking and isinstance(e, (ssl.SSLWantReadError, BlockingIOError)):
77
+ return True
78
+ logger.debug(f"Failed to receive from {src_name} socket: {e}")
79
+ return False
80
+ if not data:
81
+ logger.debug(f"EOF reached on {src_name} socket")
82
+ return False
83
+ try:
84
+ dst.sendall(data)
85
+ except OSError as e:
86
+ logger.debug(f"Failed to send to {dst_name} socket: {e}")
87
+ return False
88
+ return True
89
+
90
+
91
+ def _forward_pending(local: socket.socket, remote: "ssl.SSLSocket") -> bool:
92
+ """Read any pending decrypted data from SSL buffer and forward it to local.
93
+
94
+ Returns:
95
+ bool: True if EOF was reached or error occurred (should exit).
96
+ """
97
+ if not hasattr(remote, "pending"):
98
+ return False
99
+ pending_bytes = remote.pending()
100
+ if not isinstance(pending_bytes, int):
101
+ return False
102
+
103
+ while pending_bytes > 0:
104
+ if not _forward_bytes(remote, local, "remote", "local"):
105
+ return True
106
+ try:
107
+ pending_bytes = remote.pending()
108
+ except OSError:
109
+ break
110
+ if not isinstance(pending_bytes, int):
111
+ break
112
+ return False
113
+
114
+
115
+ def _forward_local_to_remote(local: socket.socket, remote: SqlDataSocket) -> None:
116
+ """Forward bytes from local socket to remote SqlDataSocket until EOF or error."""
117
+ try:
118
+ while _forward_bytes(local, remote, "local", "remote"):
119
+ pass
120
+ finally:
121
+ _close_socket(remote, shutdown=False)
122
+
123
+
124
+ def _proxy_sqldata(local: socket.socket, remote: SqlDataSocket) -> None:
125
+ """Bidirectional thread-based proxy for queue-backed SqlDataSocket."""
126
+ remote.settimeout(None)
127
+ threading.Thread(
128
+ target=_forward_local_to_remote, args=(local, remote), daemon=True
129
+ ).start()
130
+ try:
131
+ while _forward_bytes(remote, local, "remote", "local"):
132
+ pass
133
+ finally:
134
+ _close_socket(local)
135
+ _close_socket(remote, shutdown=False)
136
+
29
137
 
30
138
  def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None:
31
139
  """Single-threaded selectors-based proxy to avoid SSLSocket thread-safety issues."""
@@ -33,104 +141,145 @@ def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None:
33
141
  sel.register(local, selectors.EVENT_READ, data="local")
34
142
  sel.register(remote, selectors.EVENT_READ, data="remote")
35
143
 
36
- def forward_pending() -> bool:
37
- """Read any pending decrypted data from SSL buffer and forward it.
38
- Returns True if EOF was reached or error occurred (should exit).
39
- """
40
- if not hasattr(remote, "pending"):
41
- return False
42
- pending_bytes = remote.pending()
43
- if not isinstance(pending_bytes, int):
44
- return False
45
-
46
- while pending_bytes > 0:
47
- try:
48
- data = remote.recv(8192)
49
- except OSError as e:
50
- logger.debug("psycopg proxy: remote recv pending error: %s", e)
51
- return True
52
- if not data:
53
- logger.debug("psycopg proxy: remote pending EOF")
54
- return True
55
- try:
56
- local.sendall(data)
57
- except OSError as e:
58
- logger.debug("psycopg proxy: local send pending error: %s", e)
59
- return True
60
- try:
61
- pending_bytes = remote.pending()
62
- except OSError:
63
- break
64
- if not isinstance(pending_bytes, int):
65
- break
66
- return False
67
-
68
144
  try:
69
145
  while True:
70
146
  # First check if there is any pending data in SSL buffer
71
- if forward_pending():
147
+ if _forward_pending(local, remote):
72
148
  break
73
149
 
74
150
  events = sel.select()
75
151
 
76
- for key, mask in events:
77
- if key.data == "local":
78
- try:
79
- data = local.recv(8192)
80
- except OSError as e:
81
- logger.debug("psycopg proxy: local recv error: %s", e)
82
- return
83
- if not data:
84
- logger.debug("psycopg proxy: local EOF")
85
- return
86
- try:
87
- remote.sendall(data)
88
- except OSError as e:
89
- logger.debug("psycopg proxy: remote send error: %s", e)
90
- return
91
- elif key.data == "remote":
92
- try:
93
- data = remote.recv(8192)
94
- except OSError as e:
95
- logger.debug("psycopg proxy: remote recv error: %s", e)
96
- return
97
- if not data:
98
- logger.debug("psycopg proxy: remote EOF")
99
- return
100
- try:
101
- local.sendall(data)
102
- except OSError as e:
103
- logger.debug("psycopg proxy: local send error: %s", e)
104
- return
152
+ for key, _ in events:
153
+ if key.data == "local" and not _forward_bytes(
154
+ local, remote, "local", "remote"
155
+ ):
156
+ return
157
+ if key.data == "remote" and not _forward_bytes(
158
+ remote, local, "remote", "local", nonblocking=True
159
+ ):
160
+ return
105
161
  except OSError as e:
106
- logger.debug("psycopg proxy: OSError in loop: %s", e)
162
+ logger.debug(f"Selector loop error in proxy: {e}")
107
163
  finally:
108
164
  sel.close()
109
- for s in (local, remote):
165
+ _close_socket(local)
166
+ _close_socket(remote)
167
+
168
+
169
+ class _PsycopgProxy:
170
+ """Context manager that sets up a local listener socket and background proxy.
171
+
172
+ Encapsulates the platform-specific listener creation (Unix domain socket on
173
+ POSIX, TCP loopback on Windows), background connection acceptance, and
174
+ deterministic cleanup of temporary filesystem resources.
175
+ """
176
+
177
+ def __init__(
178
+ self, ip_address: str, remote_sock: "ssl.SSLSocket | SqlDataSocket"
179
+ ) -> None:
180
+ self._ip_address = ip_address
181
+ self._remote_sock = remote_sock
182
+ self._tmpdir: str | None = None
183
+ self._socket_path: str | None = None
184
+ self._local_sock: socket.socket
185
+ self.host: str = ""
186
+ self.port: int = _DEFAULT_PGSQL_PORT
187
+
188
+ def __enter__(self) -> Self:
189
+ self._local_sock = self._create_local_socket()
190
+ threading.Thread(target=self._accept_and_proxy, daemon=True).start()
191
+ return self
192
+
193
+ def __exit__(
194
+ self,
195
+ exc_type: type[BaseException] | None,
196
+ exc_val: BaseException | None,
197
+ exc_tb: TracebackType | None,
198
+ ) -> None:
199
+ if exc_type is not None:
200
+ # psycopg never connected (or failed mid-handshake); close the server
201
+ # socket so the proxy thread unblocks and exits cleanly.
202
+ _close_socket(self._local_sock, shutdown=False)
203
+ _close_socket(self._remote_sock, shutdown=False)
204
+ self._cleanup_socket_files()
205
+
206
+ def _create_local_socket(self) -> socket.socket:
207
+ """Determine the platform and create a bound local listener socket."""
208
+ if platform.system() == "Windows":
209
+ # On Windows, libpq does not support Unix domain sockets.
210
+ # Use a local TCP loopback socket on an ephemeral port.
211
+ local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
212
+ local_sock.bind((_LOOPBACK_HOST, 0))
213
+ local_sock.listen(1)
214
+ self.host = _LOOPBACK_HOST
215
+ self.port = local_sock.getsockname()[1]
216
+ logger.debug(
217
+ f"['{self._ip_address}']: Created TCP loopback listener "
218
+ f"on {self.host}:{self.port}"
219
+ )
220
+ return local_sock
221
+
222
+ self._tmpdir = tempfile.mkdtemp()
223
+ self._socket_path = os.path.join(self._tmpdir, _UNIX_SOCKET_NAME)
224
+ logger.debug(
225
+ f"['{self._ip_address}']: Created Unix socket at {self._socket_path}"
226
+ )
227
+ local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
228
+ local_sock.bind(self._socket_path)
229
+ local_sock.listen(1)
230
+ self.host = self._tmpdir
231
+ self.port = _DEFAULT_PGSQL_PORT
232
+ return local_sock
233
+
234
+ def _accept_and_proxy(self) -> None:
235
+ """Accept one connection then proxy bytes until the connection closes."""
236
+ local_conn: socket.socket | None = None
237
+ try:
238
+ local_conn, _ = self._local_sock.accept()
239
+ self._local_sock.close()
240
+ logger.debug(
241
+ f"['{self._ip_address}']: Accepted local connection, starting proxy"
242
+ )
243
+ if isinstance(self._remote_sock, SqlDataSocket):
244
+ _proxy_sqldata(local_conn, self._remote_sock)
245
+ else:
246
+ _proxy(local_conn, self._remote_sock)
247
+ except Exception as e: # noqa: BLE001
248
+ logger.debug(f"['{self._ip_address}']: Error in accept/proxy thread: {e}")
249
+ if local_conn is not None:
250
+ _close_socket(local_conn)
251
+ _close_socket(self._remote_sock)
252
+
253
+ def _cleanup_socket_files(self) -> None:
254
+ """Remove temporary Unix socket file and directory if created."""
255
+ if self._socket_path:
110
256
  try:
111
- s.shutdown(socket.SHUT_RDWR)
257
+ os.remove(self._socket_path)
112
258
  except OSError:
113
259
  pass
260
+ if self._tmpdir:
114
261
  try:
115
- s.close()
262
+ os.rmdir(self._tmpdir)
116
263
  except OSError:
117
264
  pass
118
265
 
119
266
 
120
267
  def connect(
121
- ip_address: str, remote_sock: "ssl.SSLSocket", **kwargs: Any
268
+ ip_address: str,
269
+ remote_sock: "ssl.SSLSocket | SqlDataSocket",
270
+ **kwargs: Any,
122
271
  ) -> "psycopg.Connection":
123
272
  """Create a psycopg DBAPI connection object.
124
273
 
125
274
  Because psycopg does not accept a pre-connected socket, this function
126
- creates a temporary Unix domain socket, tells psycopg to connect there,
127
- and runs a background proxy that forwards bytes between that socket and
128
- the already-established Cloud SQL TLS connection.
275
+ creates a local listener (Unix domain socket on Unix, TCP loopback on Windows),
276
+ tells psycopg to connect there, and runs a background proxy that forwards bytes
277
+ between that socket and the already-established Cloud SQL connection.
129
278
 
130
279
  Args:
131
280
  ip_address (str): IP address of the Cloud SQL instance.
132
- remote_sock (ssl.SSLSocket): SSL/TLS secure socket stream connected to the
133
- Cloud SQL proxy server.
281
+ remote_sock (ssl.SSLSocket | SqlDataSocket): Secure socket stream
282
+ connected to the Cloud SQL instance.
134
283
 
135
284
  Returns:
136
285
  psycopg.Connection: A psycopg Connection object for the Cloud SQL instance.
@@ -142,94 +291,33 @@ def connect(
142
291
  'Unable to import module "psycopg." Please install and try again.'
143
292
  )
144
293
 
145
- if not hasattr(socket, "AF_UNIX"):
146
- raise NotImplementedError(
147
- "Unix domain sockets (AF_UNIX) are not supported on this platform"
148
- )
149
-
150
- tmpdir = tempfile.mkdtemp()
151
- socket_path = os.path.join(tmpdir, ".s.PGSQL.5432")
152
- logger.debug("psycopg: created Unix socket at %s", socket_path)
153
-
154
- local_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
155
- local_sock.bind(socket_path)
156
- local_sock.listen(1)
157
-
158
- def _accept_and_proxy() -> None:
159
- """Accept one connection then proxy bytes until the connection closes."""
160
- unix_conn = None
161
- try:
162
- unix_conn, _ = local_sock.accept()
163
- local_sock.close()
164
- logger.debug("psycopg proxy: accepted connection, starting proxy")
165
- _proxy(unix_conn, remote_sock)
166
- except Exception as e: # noqa: BLE001
167
- logger.debug("psycopg proxy: error in accept/proxy thread: %s", e)
168
- # Ensure cleanup on any exception
169
- if unix_conn:
170
- try:
171
- unix_conn.shutdown(socket.SHUT_RDWR)
172
- except OSError:
173
- pass
174
- try:
175
- unix_conn.close()
176
- except OSError:
177
- pass
178
- try:
179
- remote_sock.shutdown(socket.SHUT_RDWR)
180
- except OSError:
181
- pass
182
- try:
183
- remote_sock.close()
184
- except OSError:
185
- pass
186
-
187
- threading.Thread(target=_accept_and_proxy, daemon=True).start()
188
-
189
294
  user = kwargs.pop("user")
190
295
  db = kwargs.pop("db")
191
296
  passwd = kwargs.pop("password", None)
192
297
  # SSL is already handled by the underlying SSLSocket; disable it on the
193
- # Unix socket so psycopg does not attempt a second TLS handshake.
298
+ # local socket so psycopg does not attempt a second TLS handshake.
194
299
  kwargs.pop("sslmode", None)
195
300
  timeout = kwargs.pop("timeout", None)
196
301
  if timeout is not None:
197
302
  kwargs["connect_timeout"] = int(timeout)
198
303
 
199
- logger.debug("psycopg: connecting as user=%s dbname=%s", user, db)
200
- try:
201
- conn = psycopg.connect(
202
- user=user,
203
- dbname=db,
204
- password=passwd,
205
- host=tmpdir,
206
- port=5432,
207
- sslmode="disable",
208
- **kwargs,
304
+ with _PsycopgProxy(ip_address, remote_sock) as proxy:
305
+ logger.debug(
306
+ f"['{ip_address}']: Connecting as user={user} dbname={db} "
307
+ f"to {proxy.host}:{proxy.port}"
209
308
  )
210
- logger.debug("psycopg: connection established")
211
- return conn
212
- except Exception as e:
213
- logger.debug("psycopg: connection failed: %s", e)
214
- # psycopg never connected (or failed mid-handshake); close the server
215
- # socket so the proxy thread unblocks and exits cleanly.
216
309
  try:
217
- local_sock.close()
218
- except OSError:
219
- pass
220
- try:
221
- remote_sock.close()
222
- except OSError:
223
- pass
224
- raise
225
- finally:
226
- # The socket file and its parent directory are only needed during the
227
- # initial connect() call; remove them now regardless of outcome.
228
- try:
229
- os.remove(socket_path)
230
- except OSError:
231
- pass
232
- try:
233
- os.rmdir(tmpdir)
234
- except OSError:
235
- pass
310
+ conn = psycopg.connect(
311
+ user=user,
312
+ dbname=db,
313
+ password=passwd,
314
+ host=proxy.host,
315
+ port=proxy.port,
316
+ sslmode="disable",
317
+ **kwargs,
318
+ )
319
+ logger.debug(f"['{ip_address}']: Connection established")
320
+ return conn
321
+ except Exception as e:
322
+ logger.debug(f"['{ip_address}']: Connection failed: {e}")
323
+ raise