codecraftai-protocol-ssh 1.3.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.
- ccai/protocols/ssh/__init__.py +3 -0
- ccai/protocols/ssh/py.typed +0 -0
- ccai/protocols/ssh/sshagent.py +375 -0
- ccai/protocols/ssh/sshauth.py +118 -0
- ccai/protocols/ssh/sshbase.py +780 -0
- ccai/protocols/ssh/sshclient.py +170 -0
- ccai/protocols/ssh/sshconnection.py +1285 -0
- ccai/protocols/ssh/sshconst.py +49 -0
- ccai/protocols/ssh/sshconstants.py +185 -0
- ccai/protocols/ssh/sshhelpers.py +695 -0
- ccai/protocols/ssh/sshkeys.py +197 -0
- ccai/protocols/ssh/sshpacket.py +138 -0
- ccai/protocols/ssh/sshsession.py +433 -0
- ccai/protocols/ssh/sshsftp.py +297 -0
- ccai/protocols/ssh/sshtransport.py +756 -0
- ccai/protocols/ssh/sshtunnel.py +464 -0
- codecraftai_protocol_ssh-1.3.0.dist-info/METADATA +98 -0
- codecraftai_protocol_ssh-1.3.0.dist-info/RECORD +20 -0
- codecraftai_protocol_ssh-1.3.0.dist-info/WHEEL +4 -0
- codecraftai_protocol_ssh-1.3.0.dist-info/licenses/LICENSE.md +204 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: sshtunnel
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Module containing the :class:`SshTunnel` class.
|
|
5
|
+
|
|
6
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__author__ = "Myron Walker"
|
|
11
|
+
__copyright__ = "Copyright 2025, Code Craft AI LLC"
|
|
12
|
+
__credits__ = []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from types import TracebackType
|
|
16
|
+
import socket
|
|
17
|
+
import select
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
from ccai.protocols.ssh.sshclient import SshClient
|
|
21
|
+
from ccai.protocols.ssh.sshconnection import SshConnection, SshChannel
|
|
22
|
+
|
|
23
|
+
from ccai.xmodules.xlogging.enhancedloggers import getEnhancedLogger
|
|
24
|
+
|
|
25
|
+
logger = getEnhancedLogger()
|
|
26
|
+
|
|
27
|
+
# How often, in seconds, each direction of a forwarded connection wakes up to check whether the tunnel is
|
|
28
|
+
# shutting down or the other direction has finished.
|
|
29
|
+
PUMP_POLL_INTERVAL = 1.0
|
|
30
|
+
|
|
31
|
+
# The number of bytes read from the local socket or the channel per forwarding step.
|
|
32
|
+
PUMP_CHUNK_SIZE = 32768
|
|
33
|
+
|
|
34
|
+
# How long, in seconds, a finished connection waits for its channel to socket forwarding thread to exit.
|
|
35
|
+
PUMP_JOIN_TIMEOUT = 5.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SshTunnel:
|
|
39
|
+
"""
|
|
40
|
+
The :class:`SshTunnel` represents a single SSH port forwarding tunnel for connecting to a remote service
|
|
41
|
+
through an SSH connection. Each SshTunnel instance manages exactly one port forward.
|
|
42
|
+
|
|
43
|
+
Note: SshTunnel instances should only be created through SshAgent.open_tunnel() method.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, ssh_client: SshClient, local_port: int, remote_host: str, remote_port: int, _internal_create: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
Initialize an :class:`SshTunnel` instance with an existing SSH client and tunnel parameters.
|
|
49
|
+
|
|
50
|
+
Note: This constructor should not be called directly. Use SshAgent.open_tunnel() instead.
|
|
51
|
+
|
|
52
|
+
:param ssh_client: An established SshClient connection.
|
|
53
|
+
:param local_port: Local port to bind for forwarding. If 0, an available port will be chosen.
|
|
54
|
+
:param remote_host: Remote host to forward to.
|
|
55
|
+
:param remote_port: Remote port to forward to.
|
|
56
|
+
:param _internal_create: Internal flag to prevent direct instantiation.
|
|
57
|
+
"""
|
|
58
|
+
if not _internal_create:
|
|
59
|
+
raise RuntimeError("SshTunnel instances should only be created through SshAgent.open_tunnel() method")
|
|
60
|
+
|
|
61
|
+
self._ssh_client = ssh_client
|
|
62
|
+
self._local_port = local_port
|
|
63
|
+
self._remote_host = remote_host
|
|
64
|
+
self._remote_port = remote_port
|
|
65
|
+
self._actual_local_port = None
|
|
66
|
+
self._local_socket = None
|
|
67
|
+
self._forward_thread = None
|
|
68
|
+
self._active = False
|
|
69
|
+
self._owns_ssh_client = True # This tunnel owns the SSH client and should close it
|
|
70
|
+
self._connection_threads = [] # Track active connection threads
|
|
71
|
+
self._connection_lock = threading.Lock() # Protect connection thread list
|
|
72
|
+
|
|
73
|
+
# Start the tunnel immediately
|
|
74
|
+
self._start_tunnel()
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def _create_internal(cls, ssh_client: SshClient, local_port: int, remote_host: str, remote_port: int) -> 'SshTunnel':
|
|
79
|
+
"""
|
|
80
|
+
Internal method for creating SshTunnel instances. Only used by SshAgent.
|
|
81
|
+
|
|
82
|
+
:param ssh_client: An established SshClient connection.
|
|
83
|
+
:param local_port: Local port to bind for forwarding. If 0, an available port will be chosen.
|
|
84
|
+
:param remote_host: Remote host to forward to.
|
|
85
|
+
:param remote_port: Remote port to forward to.
|
|
86
|
+
|
|
87
|
+
:returns: A new SshTunnel instance.
|
|
88
|
+
"""
|
|
89
|
+
return cls(ssh_client, local_port, remote_host, remote_port, _internal_create=True)
|
|
90
|
+
|
|
91
|
+
def __enter__(self):
|
|
92
|
+
"""
|
|
93
|
+
Method that implements the __enter__ semantics used for 'with' statements.
|
|
94
|
+
"""
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __exit__(self, ex_type: type, ex_val: Exception, ex_tb: TracebackType) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Method that implements the __exit__ semantics used for 'with' statements.
|
|
100
|
+
Shuts down the tunnel and cleans up all threads and resources.
|
|
101
|
+
|
|
102
|
+
:returns: Returns false indicating that exceptions are not handled.
|
|
103
|
+
"""
|
|
104
|
+
self.shutdown()
|
|
105
|
+
handled = False
|
|
106
|
+
return handled
|
|
107
|
+
|
|
108
|
+
def close(self):
|
|
109
|
+
"""
|
|
110
|
+
Closes all port forwards and cleans up resources. Also closes the SSH client.
|
|
111
|
+
This is an alias for shutdown() for backward compatibility.
|
|
112
|
+
"""
|
|
113
|
+
self.shutdown()
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
def shutdown(self):
|
|
117
|
+
"""
|
|
118
|
+
Shuts down the tunnel by stopping the port forward and cleaning up all threads and resources.
|
|
119
|
+
Also closes the SSH client since this tunnel owns it.
|
|
120
|
+
"""
|
|
121
|
+
logger.info(f"Shutting down SSH tunnel 127.0.0.1:{self._actual_local_port} -> {self._remote_host}:{self._remote_port}")
|
|
122
|
+
|
|
123
|
+
# Mark as inactive
|
|
124
|
+
self._active = False
|
|
125
|
+
|
|
126
|
+
# Close the listening socket to stop accepting new connections
|
|
127
|
+
if self._local_socket:
|
|
128
|
+
try:
|
|
129
|
+
self._local_socket.close()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.debug(f"Error closing socket: {e}")
|
|
132
|
+
finally:
|
|
133
|
+
self._local_socket = None
|
|
134
|
+
|
|
135
|
+
# Wait for the forwarding thread to complete
|
|
136
|
+
if self._forward_thread and self._forward_thread.is_alive():
|
|
137
|
+
logger.debug("Waiting for forwarding thread to complete...")
|
|
138
|
+
self._forward_thread.join(timeout=10.0)
|
|
139
|
+
if self._forward_thread.is_alive():
|
|
140
|
+
logger.warning("Forwarding thread did not complete within timeout")
|
|
141
|
+
|
|
142
|
+
# Wait for connection handler threads to complete
|
|
143
|
+
with self._connection_lock:
|
|
144
|
+
active_connections = [t for t in self._connection_threads if t.is_alive()]
|
|
145
|
+
|
|
146
|
+
if active_connections:
|
|
147
|
+
logger.debug(f"Waiting for {len(active_connections)} connection handler threads to complete...")
|
|
148
|
+
for thread in active_connections:
|
|
149
|
+
thread.join(timeout=5.0)
|
|
150
|
+
if thread.is_alive():
|
|
151
|
+
logger.warning(f"Connection handler thread did not complete within timeout")
|
|
152
|
+
|
|
153
|
+
# Close the SSH client since this tunnel owns it
|
|
154
|
+
if self._owns_ssh_client and self._ssh_client:
|
|
155
|
+
try:
|
|
156
|
+
self._ssh_client.close()
|
|
157
|
+
logger.debug("SSH client closed")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.warning(f"Error closing SSH client: {e}")
|
|
160
|
+
finally:
|
|
161
|
+
self._ssh_client = None
|
|
162
|
+
|
|
163
|
+
logger.info("SSH tunnel shutdown complete")
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
def _start_tunnel(self):
|
|
167
|
+
"""
|
|
168
|
+
Private method that starts the port forwarding tunnel.
|
|
169
|
+
"""
|
|
170
|
+
# Create a local socket to listen on
|
|
171
|
+
self._local_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
172
|
+
self._local_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
173
|
+
|
|
174
|
+
# If local_port is 0, let the OS choose an available port
|
|
175
|
+
self._local_socket.bind(('127.0.0.1', self._local_port))
|
|
176
|
+
self._actual_local_port = self._local_socket.getsockname()[1]
|
|
177
|
+
self._local_socket.listen(5)
|
|
178
|
+
# Closing a listening socket does not wake a thread blocked in accept() on every platform, so accept with
|
|
179
|
+
# a timeout and let the forwarding loop re-check for shutdown and a dead transport. Accepted connections
|
|
180
|
+
# are still created in blocking mode.
|
|
181
|
+
self._local_socket.settimeout(PUMP_POLL_INTERVAL)
|
|
182
|
+
|
|
183
|
+
# Mark as active
|
|
184
|
+
self._active = True
|
|
185
|
+
|
|
186
|
+
# Start the forwarding loop in a separate daemon thread
|
|
187
|
+
self._forward_thread = threading.Thread(
|
|
188
|
+
target=self._forward_loop,
|
|
189
|
+
daemon=True
|
|
190
|
+
)
|
|
191
|
+
self._forward_thread.start()
|
|
192
|
+
|
|
193
|
+
logger.info(f"SSH tunnel port forward started: 127.0.0.1:{self._actual_local_port} -> {self._remote_host}:{self._remote_port}")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
def _forward_loop(self):
|
|
197
|
+
"""
|
|
198
|
+
Private method that handles the port forwarding loop by accepting connections and forwarding
|
|
199
|
+
traffic over the SSH transport.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
while self._active:
|
|
203
|
+
try:
|
|
204
|
+
transport = self._ssh_client.get_transport()
|
|
205
|
+
if transport is None or not transport.is_active():
|
|
206
|
+
logger.error("SSH transport is no longer active, stopping tunnel")
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
connection = self._ssh_client.connection
|
|
210
|
+
|
|
211
|
+
# Accept incoming connections
|
|
212
|
+
conn, addr = self._local_socket.accept()
|
|
213
|
+
logger.debug(f"Accepted connection from {addr} for tunnel to {self._remote_host}:{self._remote_port}")
|
|
214
|
+
|
|
215
|
+
# Clean up finished connection threads before creating new ones
|
|
216
|
+
self._cleanup_finished_threads()
|
|
217
|
+
|
|
218
|
+
# Create and track connection handler thread
|
|
219
|
+
conn_thread = threading.Thread(
|
|
220
|
+
target=self._handle_connection,
|
|
221
|
+
args=(conn, connection),
|
|
222
|
+
daemon=True
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
with self._connection_lock:
|
|
226
|
+
self._connection_threads.append(conn_thread)
|
|
227
|
+
|
|
228
|
+
conn_thread.start()
|
|
229
|
+
|
|
230
|
+
except socket.timeout:
|
|
231
|
+
# No connection within the poll interval; loop to re-check for shutdown.
|
|
232
|
+
continue
|
|
233
|
+
except socket.error as e:
|
|
234
|
+
# Socket errors during accept() are expected when shutting down
|
|
235
|
+
if self._active:
|
|
236
|
+
logger.error(f"Socket error in port forward: {e}")
|
|
237
|
+
else:
|
|
238
|
+
logger.debug(f"Socket error during shutdown (expected): {e}")
|
|
239
|
+
break
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"Unexpected error in port forward: {e}")
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.error(f"Port forward thread error: {e}")
|
|
246
|
+
finally:
|
|
247
|
+
try:
|
|
248
|
+
if self._local_socket:
|
|
249
|
+
self._local_socket.close()
|
|
250
|
+
except Exception:
|
|
251
|
+
# Best-effort cleanup at forwarding thread exit; the listener is being discarded and there is
|
|
252
|
+
# no caller to report a close failure to.
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
def _handle_connection(self, conn: socket.socket, connection: SshConnection):
|
|
256
|
+
"""
|
|
257
|
+
Handle a single connection through the tunnel.
|
|
258
|
+
|
|
259
|
+
:param conn: The client connection socket.
|
|
260
|
+
:param connection: The SSH connection to use for forwarding.
|
|
261
|
+
"""
|
|
262
|
+
chan = None
|
|
263
|
+
try:
|
|
264
|
+
# RFC 4254 section 7.2: the originator is the local client whose connection is being forwarded.
|
|
265
|
+
chan = connection.open_channel(
|
|
266
|
+
"direct-tcpip",
|
|
267
|
+
dest_addr=(self._remote_host, self._remote_port),
|
|
268
|
+
src_addr=conn.getpeername()
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
self._pump_channel(conn, chan)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
logger.debug(f"Connection handler error: {e}")
|
|
274
|
+
finally:
|
|
275
|
+
try:
|
|
276
|
+
conn.close()
|
|
277
|
+
except Exception:
|
|
278
|
+
# Best-effort cleanup; a failure to close the local socket must not prevent the channel from being closed.
|
|
279
|
+
pass
|
|
280
|
+
if chan is not None:
|
|
281
|
+
try:
|
|
282
|
+
chan.close()
|
|
283
|
+
except Exception:
|
|
284
|
+
# Best-effort cleanup; the forwarded connection is finished and the channel is being discarded.
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _pump_channel(self, conn: socket.socket, chan: SshChannel):
|
|
288
|
+
"""
|
|
289
|
+
Forward data between a local socket connection and an SSH channel until one side closes.
|
|
290
|
+
|
|
291
|
+
An :class:`SshChannel` has no file descriptor, so it cannot be passed to ``select``. The socket to
|
|
292
|
+
channel direction runs on this thread and waits on the socket with ``select``; the channel to socket
|
|
293
|
+
direction runs on a companion thread that blocks in :meth:`SshChannel.recv`, which is fed by the
|
|
294
|
+
transport thread. Both directions check for tunnel shutdown every ``PUMP_POLL_INTERVAL`` seconds and
|
|
295
|
+
stop together as soon as either side closes. When the peer only sends EOF (half-close, RFC 4254 section
|
|
296
|
+
5.3) the local client gets end-of-stream and can keep sending until it closes.
|
|
297
|
+
|
|
298
|
+
:param conn: The local client connection socket.
|
|
299
|
+
:param chan: The open ``direct-tcpip`` channel for the connection.
|
|
300
|
+
"""
|
|
301
|
+
pump_done = threading.Event()
|
|
302
|
+
|
|
303
|
+
channel_reader = threading.Thread(
|
|
304
|
+
target=self._pump_channel_to_socket,
|
|
305
|
+
args=(conn, chan, pump_done),
|
|
306
|
+
daemon=True
|
|
307
|
+
)
|
|
308
|
+
channel_reader.start()
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
self._pump_socket_to_channel(conn, chan, pump_done)
|
|
312
|
+
finally:
|
|
313
|
+
pump_done.set()
|
|
314
|
+
channel_reader.join(timeout=PUMP_JOIN_TIMEOUT)
|
|
315
|
+
if channel_reader.is_alive():
|
|
316
|
+
logger.warning("Channel to socket forwarding thread did not complete within timeout")
|
|
317
|
+
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
def _pump_socket_to_channel(self, conn: socket.socket, chan: SshChannel, pump_done: threading.Event):
|
|
321
|
+
"""
|
|
322
|
+
Forward data from the local socket to the SSH channel until the local client closes its side, the
|
|
323
|
+
other direction finishes, or the tunnel shuts down.
|
|
324
|
+
|
|
325
|
+
:param conn: The local client connection socket.
|
|
326
|
+
:param chan: The channel to forward to.
|
|
327
|
+
:param pump_done: Set when either direction has finished.
|
|
328
|
+
"""
|
|
329
|
+
while self._active is True and pump_done.is_set() is False and chan.closed is False:
|
|
330
|
+
read_ready, _, _ = select.select([conn], [], [], PUMP_POLL_INTERVAL)
|
|
331
|
+
if conn not in read_ready:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
data = conn.recv(PUMP_CHUNK_SIZE)
|
|
335
|
+
if len(data) == 0:
|
|
336
|
+
# The local client closed its side of the connection.
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
chan.send(data)
|
|
340
|
+
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
def _pump_channel_to_socket(self, conn: socket.socket, chan: SshChannel, pump_done: threading.Event):
|
|
344
|
+
"""
|
|
345
|
+
Forward data from the SSH channel to the local socket until the peer closes the channel, the other
|
|
346
|
+
direction finishes, or the tunnel shuts down. Runs on its own thread.
|
|
347
|
+
|
|
348
|
+
:param conn: The local client connection socket.
|
|
349
|
+
:param chan: The channel to forward from.
|
|
350
|
+
:param pump_done: Set when either direction has finished; set by this method when it finishes, unless
|
|
351
|
+
the peer only half-closed the channel.
|
|
352
|
+
"""
|
|
353
|
+
finish_pump = True
|
|
354
|
+
try:
|
|
355
|
+
while self._active is True and pump_done.is_set() is False:
|
|
356
|
+
data = chan.recv(PUMP_CHUNK_SIZE, timeout=PUMP_POLL_INTERVAL)
|
|
357
|
+
if len(data) > 0:
|
|
358
|
+
conn.sendall(data)
|
|
359
|
+
elif chan.closed is True or chan.eof_received is True:
|
|
360
|
+
# Everything the peer sent has been forwarded; let the local client see end-of-stream now
|
|
361
|
+
# instead of when the socket direction next polls.
|
|
362
|
+
try:
|
|
363
|
+
conn.shutdown(socket.SHUT_WR)
|
|
364
|
+
except OSError as xcpt:
|
|
365
|
+
logger.debug(f"Error shutting down the local socket at the end of the channel data: {xcpt}")
|
|
366
|
+
|
|
367
|
+
if chan.closed is False:
|
|
368
|
+
# Half-close: the peer sends no more data but still receives, so the socket direction
|
|
369
|
+
# keeps forwarding until the local client or the peer closes.
|
|
370
|
+
finish_pump = False
|
|
371
|
+
break
|
|
372
|
+
# Otherwise the receive timed out; loop to re-check for shutdown.
|
|
373
|
+
except OSError as xcpt:
|
|
374
|
+
# The local client went away while data was being written to it.
|
|
375
|
+
logger.debug(f"Local socket error while forwarding channel data: {xcpt}")
|
|
376
|
+
finally:
|
|
377
|
+
if finish_pump is True:
|
|
378
|
+
pump_done.set()
|
|
379
|
+
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
def _cleanup_finished_threads(self):
|
|
383
|
+
"""
|
|
384
|
+
Remove finished threads from the connection thread list.
|
|
385
|
+
"""
|
|
386
|
+
with self._connection_lock:
|
|
387
|
+
self._connection_threads = [t for t in self._connection_threads if t.is_alive()]
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def local_port(self) -> int:
|
|
391
|
+
"""
|
|
392
|
+
Returns the actual local port being used for the tunnel.
|
|
393
|
+
|
|
394
|
+
:returns: The local port number.
|
|
395
|
+
|
|
396
|
+
:raises: RuntimeError if the tunnel is not properly initialized.
|
|
397
|
+
"""
|
|
398
|
+
if self._actual_local_port is None:
|
|
399
|
+
raise RuntimeError("Tunnel is not properly initialized - no local port assigned")
|
|
400
|
+
return self._actual_local_port
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def remote_host(self) -> str:
|
|
404
|
+
"""
|
|
405
|
+
Returns the remote host being forwarded to.
|
|
406
|
+
|
|
407
|
+
:returns: The remote host.
|
|
408
|
+
"""
|
|
409
|
+
return self._remote_host
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def remote_port(self) -> int:
|
|
413
|
+
"""
|
|
414
|
+
Returns the remote port being forwarded to.
|
|
415
|
+
|
|
416
|
+
:returns: The remote port number.
|
|
417
|
+
"""
|
|
418
|
+
return self._remote_port
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def is_active(self) -> bool:
|
|
422
|
+
"""
|
|
423
|
+
Returns whether the tunnel is currently active.
|
|
424
|
+
|
|
425
|
+
:returns: True if active, False otherwise.
|
|
426
|
+
"""
|
|
427
|
+
return self._active
|
|
428
|
+
|
|
429
|
+
def get_connection_string(self, database: str = None, username: str = None, password: str = None, db_type: str = 'postgresql') -> str:
|
|
430
|
+
"""
|
|
431
|
+
Helper method to generate database connection strings for use with SQLAlchemy or other ORMs.
|
|
432
|
+
|
|
433
|
+
:param database: Optional database name.
|
|
434
|
+
:param username: Optional username for the connection string.
|
|
435
|
+
:param password: Optional password for the connection string.
|
|
436
|
+
:param db_type: Database type (postgresql, mysql, etc.).
|
|
437
|
+
|
|
438
|
+
:returns: A connection string template for the tunneled connection.
|
|
439
|
+
|
|
440
|
+
:raises: RuntimeError if the tunnel is not properly initialized.
|
|
441
|
+
"""
|
|
442
|
+
local_port = self.local_port # This will raise RuntimeError if not initialized
|
|
443
|
+
base_url = f"127.0.0.1:{local_port}"
|
|
444
|
+
|
|
445
|
+
conn_str: str
|
|
446
|
+
|
|
447
|
+
# Build authentication part
|
|
448
|
+
auth_part = ""
|
|
449
|
+
if username:
|
|
450
|
+
if password:
|
|
451
|
+
auth_part = f"{username}:{password}@"
|
|
452
|
+
else:
|
|
453
|
+
auth_part = f"{username}@"
|
|
454
|
+
|
|
455
|
+
# Build connection string
|
|
456
|
+
if database:
|
|
457
|
+
conn_str = f"{db_type}://{auth_part}{base_url}/{database}"
|
|
458
|
+
elif auth_part or db_type != 'postgresql':
|
|
459
|
+
# Include protocol if we have auth or non-default db_type
|
|
460
|
+
conn_str = f"{db_type}://{auth_part}{base_url}"
|
|
461
|
+
else:
|
|
462
|
+
conn_str = base_url
|
|
463
|
+
|
|
464
|
+
return conn_str
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codecraftai-protocol-ssh
|
|
3
|
+
Version: 1.3.0
|
|
4
|
+
Summary: Code Craft AI - SSH Protocol
|
|
5
|
+
License-Expression: LicenseRef-CodeCraftAI-Source-Available
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Keywords: python
|
|
8
|
+
Author: Myron Walker
|
|
9
|
+
Author-email: myron.walker@gmail.com
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: POSIX
|
|
13
|
+
Requires-Dist: codecraftai-credentials (<2.0.0)
|
|
14
|
+
Requires-Dist: codecraftai-foundations (<2.0.0)
|
|
15
|
+
Requires-Dist: codecraftai-interop (<2.0.0)
|
|
16
|
+
Requires-Dist: codecraftai-xmodules (<2.0.0)
|
|
17
|
+
Requires-Dist: cryptography (>=45.0.5,<51.0.0)
|
|
18
|
+
Project-URL: Issues, https://github.com/code-craft-ai-company/codecraftai-protocol-ssh/issues
|
|
19
|
+
Project-URL: Repository, https://github.com/code-craft-ai-company/codecraftai-protocol-ssh
|
|
20
|
+
Description-Content-Type: text/x-rst
|
|
21
|
+
|
|
22
|
+
==============================
|
|
23
|
+
Code Craft AI Protocol SSH
|
|
24
|
+
==============================
|
|
25
|
+
|
|
26
|
+
The ``codecraftai-protocol-ssh`` package is the dedicated SSH-protocol implementation for the ccai suite. It provides the building blocks (transport, session, packet handling, SFTP, tunnels, key and authentication handling, and a client/agent facade) used by any higher-level ccai package that needs to manage remote systems over SSH. It is consumed by ``codecraftai-interop``, ``codecraftai-landscaping`` (SSH-based coordinators), and the sysman packages that reach hosts over SSH.
|
|
27
|
+
|
|
28
|
+
Highlights
|
|
29
|
+
==========
|
|
30
|
+
|
|
31
|
+
- Connection lifecycle classes: ``SshBase``, ``SshClient``, ``SshConnection``, ``SshSession``, ``SshTransport``, and ``SshAgent``.
|
|
32
|
+
- ``SshAspects`` for packaging behaviour parameters (retry, timeout, action pattern) on SSH calls in the aspects style.
|
|
33
|
+
- Key and authentication helpers in ``sshkeys.py`` and ``sshauth.py``.
|
|
34
|
+
- SFTP and tunnel support in ``sshsftp.py`` and ``sshtunnel.py``.
|
|
35
|
+
- RFC-aligned implementations of the SSH transport and user authentication layers.
|
|
36
|
+
|
|
37
|
+
Repository Structure
|
|
38
|
+
====================
|
|
39
|
+
|
|
40
|
+
- ``development/`` - Runtime environment scripts.
|
|
41
|
+
- ``repository-setup/`` - Repository homing and setup scripts.
|
|
42
|
+
- ``source/packages/`` - Package source code.
|
|
43
|
+
- ``source/tests/`` - ``unittest``-based tests.
|
|
44
|
+
- ``source/testroots/testbase/`` - TestBase test roots.
|
|
45
|
+
- ``specs/`` - Requirements, design, and reference RFC documents.
|
|
46
|
+
- ``userguide/`` - Standards, conventions, and documentation.
|
|
47
|
+
|
|
48
|
+
Environment Setup
|
|
49
|
+
=================
|
|
50
|
+
|
|
51
|
+
When working in a Codex container:
|
|
52
|
+
|
|
53
|
+
.. code-block:: bash
|
|
54
|
+
|
|
55
|
+
./development/setup-agent-container
|
|
56
|
+
|
|
57
|
+
For local development:
|
|
58
|
+
|
|
59
|
+
.. code-block:: bash
|
|
60
|
+
|
|
61
|
+
./repository-setup/rehome-repository
|
|
62
|
+
./development/setup-envrionment
|
|
63
|
+
|
|
64
|
+
Running Tests
|
|
65
|
+
=============
|
|
66
|
+
|
|
67
|
+
Activate the virtual environment:
|
|
68
|
+
|
|
69
|
+
.. code-block:: bash
|
|
70
|
+
|
|
71
|
+
source .venv/bin/activate
|
|
72
|
+
|
|
73
|
+
Run ``unittest`` tests:
|
|
74
|
+
|
|
75
|
+
.. code-block:: bash
|
|
76
|
+
|
|
77
|
+
python -m unittest discover -s source/tests -p "test_*.py"
|
|
78
|
+
|
|
79
|
+
Run TestBase tests (when test roots are populated):
|
|
80
|
+
|
|
81
|
+
.. code-block:: bash
|
|
82
|
+
|
|
83
|
+
testbase testing run --root source/testroots/testbase --includes=ccai --output=./.output
|
|
84
|
+
|
|
85
|
+
Standards and Conventions
|
|
86
|
+
=========================
|
|
87
|
+
|
|
88
|
+
- Naming conventions: ``userguide/10-02-naming-conventions.rst``
|
|
89
|
+
- Code organization: ``userguide/10-01-code-organization.rst``
|
|
90
|
+
- Coding standards: ``userguide/10-00-coding-standards.rst``
|
|
91
|
+
|
|
92
|
+
Additional Resources
|
|
93
|
+
====================
|
|
94
|
+
|
|
95
|
+
- Design documentation: ``specs/design.md``
|
|
96
|
+
- Requirements: ``specs/requirements.md``
|
|
97
|
+
- SSH protocol RFCs: ``specs/rfc4251.txt``, ``specs/rfc4252.txt``, ``specs/rfc4254.txt``
|
|
98
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
ccai/protocols/ssh/__init__.py,sha256=lu-IgDXxPNOP1XbgPcTHUDZRLydfLM_g2SVOZkVbVEk,36
|
|
2
|
+
ccai/protocols/ssh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
ccai/protocols/ssh/sshagent.py,sha256=b0ibg4zcEB1v_1MfsoLx0YIxoaNialYPGOYfZA7MzKA,16161
|
|
4
|
+
ccai/protocols/ssh/sshauth.py,sha256=v91KY-nzS68mpzaISpmUN83kQJPHdt4ZjIxbMhClky4,4002
|
|
5
|
+
ccai/protocols/ssh/sshbase.py,sha256=srnHnSXbT2WuW51eQQowQj1hvrEh9hMZ8h23xLkBP7E,33054
|
|
6
|
+
ccai/protocols/ssh/sshclient.py,sha256=-RRJzVF7dq176GS3m6B1qeHrVa7THliaNTeKZtEjv_4,6073
|
|
7
|
+
ccai/protocols/ssh/sshconnection.py,sha256=VqlnzvobgiHNCWrRlPA96rjRrpHRsL6sih5K4Bw7H4U,61138
|
|
8
|
+
ccai/protocols/ssh/sshconst.py,sha256=YP-C14z6ZJVfY6K-1MEshsp-GXh6r-W1HoE2hWYWk30,1686
|
|
9
|
+
ccai/protocols/ssh/sshconstants.py,sha256=x69F3VmGNxa_x0epms9OwNjK31dPeJh7mFDaUeO3FeQ,3904
|
|
10
|
+
ccai/protocols/ssh/sshhelpers.py,sha256=FZKZ0OmgsGZv4rOtLsICp7Kbk1na7NkTtm9sEr6rl6g,28592
|
|
11
|
+
ccai/protocols/ssh/sshkeys.py,sha256=Voe-cgVcEauXr8HRUCYI_xfdlFx3Q8xn3YM1v4Szu8A,6036
|
|
12
|
+
ccai/protocols/ssh/sshpacket.py,sha256=vKrIboQ5a5F3EtPoQfHSjoSzgd4fL1cSQ4aaqzabdpI,4231
|
|
13
|
+
ccai/protocols/ssh/sshsession.py,sha256=6fsVPv6FUHJptyMbEtCOGbsmdfpGVWXA9Ao7o8mP5so,20280
|
|
14
|
+
ccai/protocols/ssh/sshsftp.py,sha256=AivLsw8iQTMklsBhW7MSJDdN21fAkwYKvW2g4pHA_n0,10463
|
|
15
|
+
ccai/protocols/ssh/sshtransport.py,sha256=Zycje7iO2Z21NZOyn8wa7KcN_2M7QYHIZmV3St7T9-s,31314
|
|
16
|
+
ccai/protocols/ssh/sshtunnel.py,sha256=40xId1h9yftoFRC-ot_6gud2MJALzZte-pn6G9rxf0E,19278
|
|
17
|
+
codecraftai_protocol_ssh-1.3.0.dist-info/METADATA,sha256=rtsxPI7Op5-T0KbznoF_0z7EReHr4apUFVWFRY252sg,3453
|
|
18
|
+
codecraftai_protocol_ssh-1.3.0.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
|
|
19
|
+
codecraftai_protocol_ssh-1.3.0.dist-info/licenses/LICENSE.md,sha256=5tyUZ2kpPxNXbULzrdiT14-ZV-jCBeBFL0xyO-zt1mg,17907
|
|
20
|
+
codecraftai_protocol_ssh-1.3.0.dist-info/RECORD,,
|