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,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSH Client Implementation
|
|
3
|
+
"""
|
|
4
|
+
import socket
|
|
5
|
+
import logging
|
|
6
|
+
from ccai.protocols.ssh.sshtransport import SshTransport
|
|
7
|
+
from ccai.protocols.ssh.sshconnection import SshConnection
|
|
8
|
+
from ccai.protocols.ssh.sshsftp import SshSFTPClient
|
|
9
|
+
from ccai.protocols.ssh.sshauth import SshAuthAgent
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AutoAddPolicy:
|
|
15
|
+
"""
|
|
16
|
+
Host-key policy that trusts and records any host key not already known.
|
|
17
|
+
Convenient for first-use / test environments; not for untrusted networks.
|
|
18
|
+
"""
|
|
19
|
+
auto_add = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RejectPolicy:
|
|
23
|
+
"""
|
|
24
|
+
Host-key policy that rejects any host key that is not already known.
|
|
25
|
+
"""
|
|
26
|
+
auto_add = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SshClient:
|
|
30
|
+
"""
|
|
31
|
+
High-level SSH Client.
|
|
32
|
+
Mimics paramiko.SSHClient interaction where possible.
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.transport = None
|
|
36
|
+
self.connection = None
|
|
37
|
+
self._policy = None # Missing host key policy
|
|
38
|
+
self._host_keys = {} # hostname -> set of trusted host-key blobs
|
|
39
|
+
|
|
40
|
+
def set_missing_host_key_policy(self, policy):
|
|
41
|
+
self._policy = policy
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def load_system_host_keys(self, filename=None):
|
|
45
|
+
# Loading persisted known_hosts is not yet implemented; trust is
|
|
46
|
+
# governed by the configured missing-host-key policy and any keys
|
|
47
|
+
# added via add_host_key.
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
def add_host_key(self, hostname, key_blob):
|
|
51
|
+
"""
|
|
52
|
+
Record a trusted host-key blob for a host.
|
|
53
|
+
"""
|
|
54
|
+
known = self._host_keys.get(hostname)
|
|
55
|
+
if known is None:
|
|
56
|
+
known = set()
|
|
57
|
+
self._host_keys[hostname] = known
|
|
58
|
+
known.add(bytes(key_blob))
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def _verify_host_key(self, hostname, key_type, key_blob):
|
|
62
|
+
"""
|
|
63
|
+
Host-key trust verifier handed to the transport. Returns True only if
|
|
64
|
+
the key is already trusted for the host, or the policy auto-adds it.
|
|
65
|
+
"""
|
|
66
|
+
blob = bytes(key_blob)
|
|
67
|
+
known = self._host_keys.get(hostname)
|
|
68
|
+
if known is not None and blob in known:
|
|
69
|
+
rtnval = True
|
|
70
|
+
return rtnval
|
|
71
|
+
|
|
72
|
+
auto_add = getattr(self._policy, "auto_add", False)
|
|
73
|
+
if auto_add is True:
|
|
74
|
+
self.add_host_key(hostname, blob)
|
|
75
|
+
logger.warning("Auto-added untrusted host key for %s (%s)", hostname, key_type)
|
|
76
|
+
rtnval = True
|
|
77
|
+
return rtnval
|
|
78
|
+
|
|
79
|
+
logger.error("Rejecting unknown host key for %s (%s)", hostname, key_type)
|
|
80
|
+
rtnval = False
|
|
81
|
+
return rtnval
|
|
82
|
+
|
|
83
|
+
def connect(self, hostname, port=22, username=None, password=None, pkey=None,
|
|
84
|
+
key_filename=None, timeout=None, allow_agent=True, look_for_keys=True,
|
|
85
|
+
compress=False, sock=None, gss_auth=False, gss_kex=False,
|
|
86
|
+
gss_deleg_creds=False, gss_host=None, banner_timeout=None,
|
|
87
|
+
auth_timeout=None, channel_timeout=None, disabled_algorithms=None):
|
|
88
|
+
|
|
89
|
+
# 1. Create Socket if not provided
|
|
90
|
+
if sock is None:
|
|
91
|
+
sock = socket.create_connection((hostname, port), timeout=timeout)
|
|
92
|
+
|
|
93
|
+
# 2. Start Transport and wire host-key trust before the handshake runs.
|
|
94
|
+
self.transport = SshTransport(sock)
|
|
95
|
+
self.transport.hostname = hostname
|
|
96
|
+
self.transport.host_key_verifier = self._verify_host_key
|
|
97
|
+
self.transport.connect() # sends version + starts KEX
|
|
98
|
+
|
|
99
|
+
# 3. Wait for key exchange to complete (both directions encrypted) before
|
|
100
|
+
# sending any authentication traffic.
|
|
101
|
+
kex_timeout = auth_timeout if auth_timeout is not None else 30
|
|
102
|
+
completed = self.transport.kex_complete.wait(timeout=kex_timeout)
|
|
103
|
+
if self.transport.kex_error is not None:
|
|
104
|
+
kex_error = self.transport.kex_error
|
|
105
|
+
self.close()
|
|
106
|
+
raise kex_error
|
|
107
|
+
if completed is not True:
|
|
108
|
+
self.close()
|
|
109
|
+
raise TimeoutError("SSH key exchange did not complete")
|
|
110
|
+
if self.transport.active is not True:
|
|
111
|
+
self.close()
|
|
112
|
+
raise ConnectionError("SSH transport closed during key exchange")
|
|
113
|
+
|
|
114
|
+
# 4. Authenticate. Register the auth agent so the transport delivers
|
|
115
|
+
# SERVICE_ACCEPT / USERAUTH_* responses to it.
|
|
116
|
+
auth_agent = SshAuthAgent(self.transport)
|
|
117
|
+
self.transport.register_handler(auth_agent.dispatch_message)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if password:
|
|
121
|
+
auth_agent.authenticate_password(username, password)
|
|
122
|
+
else:
|
|
123
|
+
# 'none' probes for the list of allowed methods.
|
|
124
|
+
auth_agent.authenticate_none(username)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.close()
|
|
127
|
+
raise e
|
|
128
|
+
|
|
129
|
+
# 5. Start the connection layer and route subsequent packets to it.
|
|
130
|
+
self.connection = SshConnection(self.transport)
|
|
131
|
+
if channel_timeout is not None:
|
|
132
|
+
# As in paramiko, 'channel_timeout' bounds how long a channel open waits for the peer to confirm it.
|
|
133
|
+
self.connection.channel_timeout = channel_timeout
|
|
134
|
+
self.transport.register_handler(self.connection.dispatch_message)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
def get_transport(self):
|
|
138
|
+
return self.transport
|
|
139
|
+
|
|
140
|
+
def open_sftp(self):
|
|
141
|
+
"""
|
|
142
|
+
Open SFTP Client.
|
|
143
|
+
"""
|
|
144
|
+
chan = self.connection.open_channel("session")
|
|
145
|
+
rtnval = SshSFTPClient(chan)
|
|
146
|
+
return rtnval
|
|
147
|
+
|
|
148
|
+
def exec_command(self, command, bufsize=-1, timeout=None, get_pty=False, environment=None):
|
|
149
|
+
"""
|
|
150
|
+
Execute a command.
|
|
151
|
+
Returns (stdin, stdout, stderr).
|
|
152
|
+
"""
|
|
153
|
+
chan = self.connection.open_channel("session")
|
|
154
|
+
if get_pty:
|
|
155
|
+
chan.request_pty()
|
|
156
|
+
chan.request_exec(command)
|
|
157
|
+
|
|
158
|
+
rtnval = (None, chan, None)
|
|
159
|
+
return rtnval
|
|
160
|
+
|
|
161
|
+
def invoke_shell(self, term='vt100', width=80, height=24, width_pixels=0, height_pixels=0, environment=None):
|
|
162
|
+
chan = self.connection.open_channel("session")
|
|
163
|
+
chan.request_pty(term=term, width=width, height=height)
|
|
164
|
+
chan.request_shell()
|
|
165
|
+
return chan
|
|
166
|
+
|
|
167
|
+
def close(self):
|
|
168
|
+
if self.transport:
|
|
169
|
+
self.transport.close()
|
|
170
|
+
return
|