|
14 | 14 |
|
15 | 15 | import logging |
16 | 16 | import os |
| 17 | +import selectors |
17 | 18 | import socket |
18 | 19 | import ssl |
19 | 20 | import tempfile |
|
25 | 26 |
|
26 | 27 | logger = logging.getLogger(name=__name__) |
27 | 28 |
|
28 | | -_CHUNK_SIZE = 8 * 1024 # bytes per recv() call inside the proxy forwarding loop |
29 | | - |
30 | 29 |
|
31 | 30 | def _proxy(local: socket.socket, remote: "ssl.SSLSocket") -> None: |
32 | | - """Bidirectionally proxy bytes between a local Unix socket and a remote |
33 | | - SSL socket. |
34 | | -
|
35 | | - Spawns one daemon thread for the remote→local direction and runs the |
36 | | - local→remote direction in the calling thread. Blocks until the calling |
37 | | - thread's direction reaches EOF or a socket error, at which point both |
38 | | - sockets are closed so the other thread also unblocks and exits. |
| 31 | + """Single-threaded selectors-based proxy to avoid SSLSocket thread-safety issues.""" |
| 32 | + sel = selectors.DefaultSelector() |
| 33 | + sel.register(local, selectors.EVENT_READ, data="local") |
| 34 | + sel.register(remote, selectors.EVENT_READ, data="remote") |
| 35 | + |
| 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 | + while remote.pending() > 0: |
| 41 | + try: |
| 42 | + data = remote.recv(8192) |
| 43 | + except OSError as e: |
| 44 | + logger.debug("psycopg proxy: remote recv pending error: %s", e) |
| 45 | + return True |
| 46 | + if not data: |
| 47 | + logger.debug("psycopg proxy: remote pending EOF") |
| 48 | + return True |
| 49 | + try: |
| 50 | + local.sendall(data) |
| 51 | + except OSError as e: |
| 52 | + logger.debug("psycopg proxy: local send pending error: %s", e) |
| 53 | + return True |
| 54 | + return False |
39 | 55 |
|
40 | | - Args: |
41 | | - local: The Unix domain socket connected to the database driver. |
42 | | - remote: The SSL socket connected to the Cloud SQL proxy server. |
43 | | - """ |
44 | | - def forward(src: Any, dst: Any) -> None: |
45 | | - buf = bytearray(_CHUNK_SIZE) |
46 | | - view = memoryview(buf) |
47 | | - try: |
48 | | - while True: |
49 | | - n = src.recv_into(view) |
50 | | - if n == 0: |
51 | | - logger.debug("psycopg proxy: EOF on %s, closing both sockets", src) |
52 | | - break |
53 | | - dst.sendall(view[:n]) |
54 | | - except (OSError, ssl.SSLError) as e: |
55 | | - logger.debug("psycopg proxy: socket error on %s: %s", src, e) |
56 | | - finally: |
57 | | - # Close both ends so the sibling thread also unblocks. |
58 | | - for s in (local, remote): |
59 | | - try: |
60 | | - s.shutdown(socket.SHUT_RDWR) |
61 | | - except OSError: |
62 | | - pass |
63 | | - try: |
64 | | - s.close() |
65 | | - except OSError: |
66 | | - pass |
67 | | - |
68 | | - threading.Thread(target=forward, args=(remote, local), daemon=True).start() |
69 | | - forward(local, remote) # run in calling thread rather than spawning a third |
| 56 | + try: |
| 57 | + while True: |
| 58 | + # First check if there is any pending data in SSL buffer |
| 59 | + if forward_pending(): |
| 60 | + break |
| 61 | + |
| 62 | + events = sel.select(timeout=30) |
| 63 | + if not events: |
| 64 | + logger.debug("psycopg proxy: inactivity timeout (30s)") |
| 65 | + break |
| 66 | + |
| 67 | + for key, mask in events: |
| 68 | + if key.data == "local": |
| 69 | + try: |
| 70 | + data = local.recv(8192) |
| 71 | + except OSError as e: |
| 72 | + logger.debug("psycopg proxy: local recv error: %s", e) |
| 73 | + return |
| 74 | + if not data: |
| 75 | + logger.debug("psycopg proxy: local EOF") |
| 76 | + return |
| 77 | + try: |
| 78 | + remote.sendall(data) |
| 79 | + except OSError as e: |
| 80 | + logger.debug("psycopg proxy: remote send error: %s", e) |
| 81 | + return |
| 82 | + elif key.data == "remote": |
| 83 | + try: |
| 84 | + data = remote.recv(8192) |
| 85 | + except OSError as e: |
| 86 | + logger.debug("psycopg proxy: remote recv error: %s", e) |
| 87 | + return |
| 88 | + if not data: |
| 89 | + logger.debug("psycopg proxy: remote EOF") |
| 90 | + return |
| 91 | + try: |
| 92 | + local.sendall(data) |
| 93 | + except OSError as e: |
| 94 | + logger.debug("psycopg proxy: local send error: %s", e) |
| 95 | + return |
| 96 | + except OSError as e: |
| 97 | + logger.debug("psycopg proxy: OSError in loop: %s", e) |
| 98 | + finally: |
| 99 | + sel.close() |
| 100 | + for s in (local, remote): |
| 101 | + try: |
| 102 | + s.shutdown(socket.SHUT_RDWR) |
| 103 | + except OSError: |
| 104 | + pass |
| 105 | + try: |
| 106 | + s.close() |
| 107 | + except OSError: |
| 108 | + pass |
70 | 109 |
|
71 | 110 |
|
72 | 111 | def connect( |
|
0 commit comments