plato-sdk-v2 2.7.1__py3-none-any.whl → 2.7.2__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.
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
12
12
 
13
13
  from plato._generated.api.v1.simulator import get_db_config
14
14
  from plato._generated.models import DbConfigResponse
15
+ from plato.v2.utils.gateway_tunnel import GatewayTunnel, find_free_port
15
16
  from plato.v2.utils.models import (
16
17
  ApiCleanupResult,
17
18
  DatabaseCleanupResult,
@@ -19,12 +20,10 @@ from plato.v2.utils.models import (
19
20
  EnvironmentInfo,
20
21
  SessionCleanupResult,
21
22
  )
22
- from plato.v2.utils.proxy_tunnel import ProxyTunnel, find_free_port, make_db_url
23
+ from plato.v2.utils.proxy_tunnel import make_db_url
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
26
- TEMP_PASSWORD = "newpass"
27
-
28
27
 
29
28
  class DatabaseCleaner:
30
29
  """Handles database audit_log cleanup operations."""
@@ -145,14 +144,13 @@ class DatabaseCleaner:
145
144
  config: DbConfigResponse,
146
145
  local_port: int,
147
146
  ) -> DatabaseCleanupResult:
148
- """Connect to a single DB via tunnel and truncate audit_log tables."""
147
+ """Connect to a single DB via gateway tunnel and truncate audit_log tables."""
149
148
  db_port = config.db_port
150
149
 
151
- tunnel = ProxyTunnel(
152
- env_id=job_id,
153
- db_port=db_port,
154
- temp_password=TEMP_PASSWORD,
155
- host_port=local_port,
150
+ tunnel = GatewayTunnel(
151
+ job_id=job_id,
152
+ remote_port=db_port,
153
+ local_port=local_port,
156
154
  )
157
155
 
158
156
  try:
@@ -0,0 +1,221 @@
1
+ """TLS + SNI gateway tunnel for database connections.
2
+
3
+ Routes traffic through gateway.plato.so:443 using SNI-based routing,
4
+ replacing the deprecated HTTP CONNECT proxy approach.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import socket
13
+ import ssl
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Default gateway configuration
18
+ DEFAULT_GATEWAY_HOST = "gateway.plato.so"
19
+ DEFAULT_GATEWAY_PORT = 443
20
+
21
+
22
+ def get_gateway_config() -> tuple[str, int]:
23
+ """Get gateway host and port from environment or defaults.
24
+
25
+ Returns:
26
+ Tuple of (host, port) for the gateway.
27
+ """
28
+ host = os.environ.get("PLATO_GATEWAY_HOST", DEFAULT_GATEWAY_HOST)
29
+ port = int(os.environ.get("PLATO_GATEWAY_PORT", str(DEFAULT_GATEWAY_PORT)))
30
+ return host, port
31
+
32
+
33
+ def find_free_port(start_port: int = 55432) -> int:
34
+ """Find the first available TCP port starting from start_port."""
35
+ port = start_port
36
+ while port < 65535:
37
+ try:
38
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
39
+ s.bind(("127.0.0.1", port))
40
+ return port
41
+ except OSError:
42
+ port += 1
43
+ raise RuntimeError(f"No free port found starting from {start_port}")
44
+
45
+
46
+ class GatewayTunnel:
47
+ """Async TLS + SNI gateway tunnel for database connections.
48
+
49
+ Routes local connections through gateway.plato.so using SNI-based routing.
50
+ This replaces the deprecated HTTP CONNECT proxy tunnel.
51
+
52
+ Usage:
53
+ tunnel = GatewayTunnel(job_id="abc123", remote_port=5432, local_port=55432)
54
+ await tunnel.start()
55
+ # Connect to localhost:55432 to reach the VM's port 5432
56
+ await tunnel.stop()
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ job_id: str,
62
+ remote_port: int,
63
+ local_port: int,
64
+ gateway_host: str | None = None,
65
+ gateway_port: int | None = None,
66
+ verify_ssl: bool = True,
67
+ ):
68
+ """Initialize the gateway tunnel.
69
+
70
+ Args:
71
+ job_id: The job/environment ID to connect to.
72
+ remote_port: Port on the VM to forward to.
73
+ local_port: Local port to listen on.
74
+ gateway_host: Gateway hostname (default: from env or gateway.plato.so).
75
+ gateway_port: Gateway port (default: from env or 443).
76
+ verify_ssl: Whether to verify SSL certificates.
77
+ """
78
+ self.job_id = job_id
79
+ self.remote_port = remote_port
80
+ self.local_port = local_port
81
+ self.verify_ssl = verify_ssl
82
+
83
+ # Get gateway config
84
+ default_host, default_port = get_gateway_config()
85
+ self.gateway_host = gateway_host or default_host
86
+ self.gateway_port = gateway_port or default_port
87
+
88
+ # SNI for routing: {job_id}--{port}.gateway.plato.so
89
+ self.sni = f"{job_id}--{remote_port}.{self.gateway_host}"
90
+
91
+ self._server: asyncio.AbstractServer | None = None
92
+ self._client_tasks: set[asyncio.Task] = set()
93
+
94
+ async def _open_gateway_connection(
95
+ self,
96
+ timeout: float = 30.0,
97
+ ) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
98
+ """Open a TLS connection to the gateway with SNI routing.
99
+
100
+ Returns:
101
+ Tuple of (reader, writer) for the gateway connection.
102
+ """
103
+ # Create SSL context
104
+ ssl_ctx = ssl.create_default_context()
105
+ if not self.verify_ssl:
106
+ ssl_ctx.check_hostname = False
107
+ ssl_ctx.verify_mode = ssl.CERT_NONE
108
+
109
+ # Connect with TLS, using SNI for routing
110
+ reader, writer = await asyncio.wait_for(
111
+ asyncio.open_connection(
112
+ self.gateway_host,
113
+ self.gateway_port,
114
+ ssl=ssl_ctx,
115
+ server_hostname=self.sni, # SNI determines which VM/port to route to
116
+ ),
117
+ timeout=timeout,
118
+ )
119
+
120
+ # Enable TCP keepalive
121
+ sock = writer.get_extra_info("socket")
122
+ if isinstance(sock, socket.socket):
123
+ try:
124
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
125
+ # macOS/BSD keepalive idle time
126
+ TCP_KEEPALIVE = getattr(socket, "TCP_KEEPALIVE", 0x10)
127
+ sock.setsockopt(socket.IPPROTO_TCP, TCP_KEEPALIVE, 30)
128
+ except OSError:
129
+ pass # Best effort
130
+
131
+ return reader, writer
132
+
133
+ async def _pipe(
134
+ self,
135
+ src: asyncio.StreamReader,
136
+ dst: asyncio.StreamWriter,
137
+ ) -> None:
138
+ """Forward data from src to dst until EOF."""
139
+ try:
140
+ while True:
141
+ data = await src.read(65536)
142
+ if not data:
143
+ break
144
+ dst.write(data)
145
+ await dst.drain()
146
+ except (ConnectionResetError, BrokenPipeError, OSError):
147
+ pass
148
+ finally:
149
+ try:
150
+ dst.close()
151
+ await dst.wait_closed()
152
+ except Exception:
153
+ pass
154
+
155
+ async def _handle_client(
156
+ self,
157
+ client_reader: asyncio.StreamReader,
158
+ client_writer: asyncio.StreamWriter,
159
+ ) -> None:
160
+ """Handle a single client connection by forwarding through gateway."""
161
+ task = asyncio.current_task()
162
+ if task:
163
+ self._client_tasks.add(task)
164
+
165
+ try:
166
+ # Connect to gateway via TLS with SNI
167
+ gateway_reader, gateway_writer = await self._open_gateway_connection()
168
+
169
+ # Bidirectional forwarding
170
+ await asyncio.gather(
171
+ self._pipe(client_reader, gateway_writer),
172
+ self._pipe(gateway_reader, client_writer),
173
+ )
174
+ except Exception as e:
175
+ logger.warning(f"Gateway tunnel error: {e}")
176
+ try:
177
+ client_writer.close()
178
+ await client_writer.wait_closed()
179
+ except Exception:
180
+ pass
181
+ finally:
182
+ if task:
183
+ self._client_tasks.discard(task)
184
+
185
+ async def start(self) -> None:
186
+ """Start the gateway tunnel server."""
187
+ logger.info(
188
+ f"Starting gateway tunnel: localhost:{self.local_port} -> "
189
+ f"{self.job_id}:{self.remote_port} via {self.gateway_host}"
190
+ )
191
+
192
+ self._server = await asyncio.start_server(
193
+ self._handle_client,
194
+ host="127.0.0.1",
195
+ port=self.local_port,
196
+ )
197
+
198
+ # Small delay to ensure binding is settled
199
+ await asyncio.sleep(0.1)
200
+
201
+ if not self._server.sockets:
202
+ raise RuntimeError("Gateway tunnel failed to start: no listening sockets")
203
+
204
+ logger.info(f"Gateway tunnel established on port {self.local_port}")
205
+
206
+ async def stop(self) -> None:
207
+ """Stop the gateway tunnel server."""
208
+ if self._server is not None:
209
+ logger.info("Stopping gateway tunnel")
210
+
211
+ # Stop accepting new connections
212
+ self._server.close()
213
+ await self._server.wait_closed()
214
+ self._server = None
215
+
216
+ # Cancel active client tasks
217
+ for t in list(self._client_tasks):
218
+ t.cancel()
219
+ self._client_tasks.clear()
220
+
221
+ logger.info("Gateway tunnel stopped")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plato-sdk-v2
3
- Version: 2.7.1
3
+ Version: 2.7.2
4
4
  Summary: Python SDK for the Plato API
5
5
  Author-email: Plato <support@plato.so>
6
6
  License-Expression: MIT
@@ -494,7 +494,8 @@ plato/v2/sync/environment.py,sha256=WnDzbyEHpwCSEP8XnfNSjIYS7rt7lYR4HGJjzprZmTQ,
494
494
  plato/v2/sync/flow_executor.py,sha256=N41-WCWIJVcCR2UmPUEiK7roNacYoeONkRXpR7lUgT8,13941
495
495
  plato/v2/sync/session.py,sha256=okXqF-CjMmA82WRy2zPXaGidbovgjAENSqiuvE4_jKE,30420
496
496
  plato/v2/utils/__init__.py,sha256=XLeFFsjXkm9g2raMmo7Wt4QN4hhCrNZDJKnpffJ4LtM,38
497
- plato/v2/utils/db_cleanup.py,sha256=lnI5lsMHNHpG85Y99MaE4Rzc3618piuzhvH-uXO1zIc,8702
497
+ plato/v2/utils/db_cleanup.py,sha256=JMzAAJz0ZnoUXtd8F4jpQmBpJpos2__RkgN_cuEearg,8692
498
+ plato/v2/utils/gateway_tunnel.py,sha256=eWgwf4VV8-jx6iCuHFgCISsAOVmNOOjCB56EuZLsnOA,7171
498
499
  plato/v2/utils/models.py,sha256=PwehSSnIRG-tM3tWL1PzZEH77ZHhIAZ9R0UPs6YknbM,1441
499
500
  plato/v2/utils/proxy_tunnel.py,sha256=8ZTd0jCGSfIHMvSv1fgEyacuISWnGPHLPbDglWroTzY,10463
500
501
  plato/worlds/README.md,sha256=XFOkEA3cNNcrWkk-Cxnsl-zn-y0kvUENKQRSqFKpdqw,5479
@@ -503,7 +504,7 @@ plato/worlds/base.py,sha256=-RR71bSxEFI5yydtrtq-AAbuw98CIjvmrbztqzB9oIc,31041
503
504
  plato/worlds/build_hook.py,sha256=KSoW0kqa5b7NyZ7MYOw2qsZ_2FkWuz0M3Ru7AKOP7Qw,3486
504
505
  plato/worlds/config.py,sha256=O1lUXzxp-Z_M7izslT8naXgE6XujjzwYFFrDDzUOueI,12736
505
506
  plato/worlds/runner.py,sha256=r9B2BxBae8_dM7y5cJf9xhThp_I1Qvf_tlPq2rs8qC8,4013
506
- plato_sdk_v2-2.7.1.dist-info/METADATA,sha256=W64dXq4E_YTbyTp5SBJJBm3sxSryOHQSB6oXU8x5_mI,8652
507
- plato_sdk_v2-2.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
508
- plato_sdk_v2-2.7.1.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
509
- plato_sdk_v2-2.7.1.dist-info/RECORD,,
507
+ plato_sdk_v2-2.7.2.dist-info/METADATA,sha256=fv3apoCuAT5uplSgW9dbuMmduW2z7NqhVSOVErRIb5k,8652
508
+ plato_sdk_v2-2.7.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
509
+ plato_sdk_v2-2.7.2.dist-info/entry_points.txt,sha256=upGMbJCx6YWUTKrPoYvYUYfFCqYr75nHDwhA-45m6p8,136
510
+ plato_sdk_v2-2.7.2.dist-info/RECORD,,