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,1285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SSH Connection Layer
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from queue import Empty, Queue
|
|
11
|
+
from typing import Optional, Union
|
|
12
|
+
|
|
13
|
+
from ccai.errors.exceptions import SemanticError
|
|
14
|
+
|
|
15
|
+
from ccai.protocols.ssh import sshconstants
|
|
16
|
+
from ccai.protocols.ssh.sshconst import DEFAULT_SSH_TIMEOUT
|
|
17
|
+
from ccai.protocols.ssh.sshpacket import SshPacket
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# RFC 4254 section 5.2: implementations must handle channel windows of up to 2^32 - 1 bytes and a window
|
|
22
|
+
# must never be increased above that value.
|
|
23
|
+
MAX_CHANNEL_WINDOW_SIZE = 0xFFFFFFFF
|
|
24
|
+
|
|
25
|
+
# How often, in seconds, a sender that is blocked on an exhausted remote window, or an opener that is waiting for
|
|
26
|
+
# the peer to confirm a channel, wakes up to check that the transport underneath the channel is still alive.
|
|
27
|
+
# The message it is waiting for, or a channel close, wakes it immediately.
|
|
28
|
+
WINDOW_WAIT_POLL_INTERVAL = 1.0
|
|
29
|
+
|
|
30
|
+
# Channel types whose SSH_MSG_CHANNEL_OPEN carries the connect-to and originator addresses (RFC 4254 section 7).
|
|
31
|
+
TCPIP_CHANNEL_KINDS = ("direct-tcpip", "forwarded-tcpip")
|
|
32
|
+
|
|
33
|
+
# Expert hints added to the error when the peer refuses a channel request with SSH_MSG_CHANNEL_FAILURE.
|
|
34
|
+
CHANNEL_REQUEST_FAILURE_HINTS = {
|
|
35
|
+
"pty-req": "The server may not allow pseudo-terminals for this user (sshd_config 'PermitTTY', or 'no-pty' in "
|
|
36
|
+
"authorized_keys).",
|
|
37
|
+
"shell": "The server may not allow interactive shells for this user (for example 'ForceCommand' or a "
|
|
38
|
+
"restricted shell).",
|
|
39
|
+
"exec": "The server may restrict the commands this user can run (for example 'ForceCommand' in sshd_config or "
|
|
40
|
+
"'command=' in authorized_keys).",
|
|
41
|
+
"subsystem": "The server may not have this subsystem configured (for example 'Subsystem sftp ...' in "
|
|
42
|
+
"sshd_config).",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class SshChannel(threading.Thread):
|
|
46
|
+
"""
|
|
47
|
+
Represents a logical channel within the SSH connection.
|
|
48
|
+
Implements a file-like interface/socket-like interface.
|
|
49
|
+
"""
|
|
50
|
+
def __init__(self, connection, local_id: int):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.connection = connection
|
|
53
|
+
self.local_id = local_id
|
|
54
|
+
self.remote_id = None
|
|
55
|
+
self.max_packet_size = 32768
|
|
56
|
+
# The maximum packet size the peer will accept, from SSH_MSG_CHANNEL_OPEN_CONFIRMATION.
|
|
57
|
+
# RFC 4254 section 5.1: data sent to the peer must not exceed this size.
|
|
58
|
+
self.remote_max_packet_size = None
|
|
59
|
+
# The initial receive window advertised to the peer in SSH_MSG_CHANNEL_OPEN (RFC 4254 section 5.2).
|
|
60
|
+
self.window_size = 1024 * 1024
|
|
61
|
+
# The number of bytes the peer may still send before it must wait for an SSH_MSG_CHANNEL_WINDOW_ADJUST
|
|
62
|
+
# from us. It only goes negative if the peer overruns the window, which is logged.
|
|
63
|
+
self.local_window = self.window_size
|
|
64
|
+
# The number of bytes the peer is still willing to receive: the initial window from
|
|
65
|
+
# SSH_MSG_CHANNEL_OPEN_CONFIRMATION plus every SSH_MSG_CHANNEL_WINDOW_ADJUST, minus the data sent since.
|
|
66
|
+
# 'None' until the peer has announced a window, in which case sends are not window limited.
|
|
67
|
+
self.remote_window = None
|
|
68
|
+
# The default number of seconds 'send' waits for the peer to open an exhausted window before it
|
|
69
|
+
# raises a TimeoutError. 'None' waits for as long as the channel and the transport stay open.
|
|
70
|
+
self.send_timeout = DEFAULT_SSH_TIMEOUT
|
|
71
|
+
self.active = False
|
|
72
|
+
self.closed = False
|
|
73
|
+
# Received data chunks for recv(), and stderr chunks (SSH_MSG_CHANNEL_EXTENDED_DATA of type
|
|
74
|
+
# SSH_EXTENDED_DATA_STDERR) for recv_stderr(). An empty chunk is the end-of-stream marker that wakes a
|
|
75
|
+
# blocked reader when the peer sends EOF or the channel closes.
|
|
76
|
+
self.in_buffer = Queue()
|
|
77
|
+
self.in_stderr_buffer = Queue()
|
|
78
|
+
# The bytes of a received chunk that did not fit in the caller's 'size'; the next read returns them first.
|
|
79
|
+
# Only touched while holding the stream's reader lock, which also keeps concurrent readers in order.
|
|
80
|
+
self._in_pending = bytearray()
|
|
81
|
+
self._in_stderr_pending = bytearray()
|
|
82
|
+
self._recv_lock = threading.Lock()
|
|
83
|
+
self._recv_stderr_lock = threading.Lock()
|
|
84
|
+
# True once the peer sent SSH_MSG_CHANNEL_EOF: it sends no more data, but may still receive (RFC 4254
|
|
85
|
+
# section 5.3). True once we sent SSH_MSG_CHANNEL_EOF with shutdown_write(): we send no more data.
|
|
86
|
+
self.eof_received = False
|
|
87
|
+
self.eof_sent = False
|
|
88
|
+
# Our want-reply channel requests that are waiting for SSH_MSG_CHANNEL_SUCCESS / FAILURE, oldest first.
|
|
89
|
+
# The peer answers requests in order (RFC 4254 section 5.4); '_request_lock' keeps the queue in the order
|
|
90
|
+
# the requests are put on the wire.
|
|
91
|
+
self._pending_requests = deque()
|
|
92
|
+
self._request_lock = threading.Lock()
|
|
93
|
+
# The command's exit status from the peer's "exit-status" request, or -1 once the peer reported that the
|
|
94
|
+
# command was killed by a signal ("exit-signal", RFC 4254 section 6.10).
|
|
95
|
+
self.exit_status = None
|
|
96
|
+
# The details of an "exit-signal" request: the signal name without the "SIG" prefix, whether a core was
|
|
97
|
+
# dumped, and the peer's error message.
|
|
98
|
+
self.exit_signal = None
|
|
99
|
+
self.exit_signal_core_dumped = None
|
|
100
|
+
self.exit_signal_message = None
|
|
101
|
+
# RFC 4254 section 5.3: each side sends SSH_MSG_CHANNEL_CLOSE exactly once, whichever side closes first.
|
|
102
|
+
self._close_sent = False
|
|
103
|
+
self._close_received = False
|
|
104
|
+
# The (reason code, description) from SSH_MSG_CHANNEL_OPEN_FAILURE, if the peer refused the channel.
|
|
105
|
+
self._open_failure = None
|
|
106
|
+
# The number of received bytes the application has consumed since the last WINDOW_ADJUST we sent.
|
|
107
|
+
self._local_window_consumed = 0
|
|
108
|
+
# Guards the window accounting and the 'closed' flag, and wakes senders that are blocked on an
|
|
109
|
+
# exhausted remote window when a WINDOW_ADJUST arrives or the channel closes.
|
|
110
|
+
self._window_condition = threading.Condition(threading.Lock())
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def open(self):
|
|
114
|
+
# Already opened by Connection?
|
|
115
|
+
self.active = True
|
|
116
|
+
|
|
117
|
+
def send(self, data: Union[bytes, str], *, timeout: Optional[float] = None) -> int:
|
|
118
|
+
"""
|
|
119
|
+
Sends data to the peer on this channel, honoring the RFC 4254 section 5.2 flow control.
|
|
120
|
+
|
|
121
|
+
The data is split into SSH_MSG_CHANNEL_DATA packets that are no larger than the smaller of the local
|
|
122
|
+
and the peer's maximum packet size, and never larger than what remains of the peer's window. When the
|
|
123
|
+
peer's window is exhausted the call blocks until the peer sends SSH_MSG_CHANNEL_WINDOW_ADJUST, the
|
|
124
|
+
channel closes, or the timeout expires.
|
|
125
|
+
|
|
126
|
+
:param data: The data to send. A ``str`` is encoded as UTF-8 so the window accounting is in bytes.
|
|
127
|
+
:param timeout: The maximum number of seconds to wait each time the peer's window is exhausted. When
|
|
128
|
+
``None`` the channel's ``send_timeout`` is used.
|
|
129
|
+
|
|
130
|
+
:returns: The number of bytes sent, which is always the full length of the data.
|
|
131
|
+
:raises: ConnectionError, TimeoutError
|
|
132
|
+
"""
|
|
133
|
+
if not self.active or self.closed:
|
|
134
|
+
raise ConnectionError("Channel closed")
|
|
135
|
+
|
|
136
|
+
if self.eof_sent is True:
|
|
137
|
+
err_msg_lines = [
|
|
138
|
+
"Cannot send on SSH channel local_id={} remote_id={} after shutdown_write().".format(
|
|
139
|
+
self.local_id, self.remote_id),
|
|
140
|
+
"shutdown_write() sent SSH_MSG_CHANNEL_EOF, after which no more data may be sent (RFC 4254 "
|
|
141
|
+
"section 5.3).",
|
|
142
|
+
]
|
|
143
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
144
|
+
raise ConnectionError(err_msg)
|
|
145
|
+
|
|
146
|
+
if isinstance(data, str):
|
|
147
|
+
data = data.encode('utf-8')
|
|
148
|
+
|
|
149
|
+
wait_timeout = timeout
|
|
150
|
+
if wait_timeout is None:
|
|
151
|
+
wait_timeout = self.send_timeout
|
|
152
|
+
|
|
153
|
+
# Split into chunks no larger than the smaller of the local and the peer's
|
|
154
|
+
# maximum packet size (RFC 4254 section 5.1).
|
|
155
|
+
chunk_size = self.max_packet_size
|
|
156
|
+
if self.remote_max_packet_size is not None and self.remote_max_packet_size < chunk_size:
|
|
157
|
+
chunk_size = self.remote_max_packet_size
|
|
158
|
+
|
|
159
|
+
total_length = len(data)
|
|
160
|
+
sent_length = 0
|
|
161
|
+
while sent_length < total_length:
|
|
162
|
+
wanted_length = total_length - sent_length
|
|
163
|
+
if wanted_length > chunk_size:
|
|
164
|
+
wanted_length = chunk_size
|
|
165
|
+
|
|
166
|
+
# Reserve the window for this packet under the lock, but put the packet on the wire outside of the
|
|
167
|
+
# lock so the transport thread can always deliver a WINDOW_ADJUST or data while we are writing.
|
|
168
|
+
packet_length = self._reserve_remote_window(wanted_length, wait_timeout, sent_length, total_length)
|
|
169
|
+
|
|
170
|
+
chunk = data[sent_length:sent_length + packet_length]
|
|
171
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_DATA)
|
|
172
|
+
pkt.add_uint32(self.remote_id)
|
|
173
|
+
pkt.add_string(chunk)
|
|
174
|
+
self.connection.transport.send_packet(pkt)
|
|
175
|
+
|
|
176
|
+
sent_length += packet_length
|
|
177
|
+
|
|
178
|
+
return sent_length
|
|
179
|
+
|
|
180
|
+
def _reserve_remote_window(self, wanted_length: int, timeout: Optional[float], sent_length: int,
|
|
181
|
+
total_length: int) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Waits until the peer's window has room and then takes up to ``wanted_length`` bytes out of it.
|
|
184
|
+
|
|
185
|
+
:param wanted_length: The number of bytes the caller would like to send in the next packet.
|
|
186
|
+
:param timeout: The maximum number of seconds to wait for an exhausted window, ``None`` to wait for as
|
|
187
|
+
long as the channel and the transport stay open.
|
|
188
|
+
:param sent_length: The number of bytes of the current send that are already on the wire (for errors).
|
|
189
|
+
:param total_length: The total number of bytes in the current send (for errors).
|
|
190
|
+
|
|
191
|
+
:returns: The number of bytes reserved, between 1 and ``wanted_length``.
|
|
192
|
+
:raises: ConnectionError, TimeoutError
|
|
193
|
+
"""
|
|
194
|
+
reserved_length = wanted_length
|
|
195
|
+
|
|
196
|
+
with self._window_condition:
|
|
197
|
+
wait_start = time.monotonic()
|
|
198
|
+
|
|
199
|
+
while True:
|
|
200
|
+
if self.closed is True:
|
|
201
|
+
err_msg = self._format_window_wait_error(
|
|
202
|
+
"The SSH channel closed while sending data.", timeout, wait_start, sent_length, total_length)
|
|
203
|
+
raise ConnectionError(err_msg)
|
|
204
|
+
|
|
205
|
+
if self.remote_window is None:
|
|
206
|
+
# The peer has not announced a window (the channel was activated without an
|
|
207
|
+
# SSH_MSG_CHANNEL_OPEN_CONFIRMATION), so there is no window to enforce.
|
|
208
|
+
return reserved_length
|
|
209
|
+
|
|
210
|
+
if self.remote_window > 0:
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
transport_active = self.connection.transport.active
|
|
214
|
+
if transport_active is False:
|
|
215
|
+
err_msg = self._format_window_wait_error(
|
|
216
|
+
"The SSH transport closed while waiting for the peer to open its channel window.",
|
|
217
|
+
timeout, wait_start, sent_length, total_length)
|
|
218
|
+
raise ConnectionError(err_msg)
|
|
219
|
+
|
|
220
|
+
wait_time = WINDOW_WAIT_POLL_INTERVAL
|
|
221
|
+
if timeout is not None:
|
|
222
|
+
waited = time.monotonic() - wait_start
|
|
223
|
+
remaining = timeout - waited
|
|
224
|
+
if remaining <= 0:
|
|
225
|
+
err_msg = self._format_window_wait_error(
|
|
226
|
+
"Timed out waiting for the SSH peer to open its channel window "
|
|
227
|
+
"(SSH_MSG_CHANNEL_WINDOW_ADJUST).", timeout, wait_start, sent_length, total_length)
|
|
228
|
+
raise TimeoutError(err_msg)
|
|
229
|
+
if remaining < wait_time:
|
|
230
|
+
wait_time = remaining
|
|
231
|
+
|
|
232
|
+
self._window_condition.wait(timeout=wait_time)
|
|
233
|
+
|
|
234
|
+
if self.remote_window < reserved_length:
|
|
235
|
+
reserved_length = self.remote_window
|
|
236
|
+
self.remote_window -= reserved_length
|
|
237
|
+
|
|
238
|
+
return reserved_length
|
|
239
|
+
|
|
240
|
+
def _format_window_wait_error(self, headline: str, timeout: Optional[float], wait_start: float,
|
|
241
|
+
sent_length: int, total_length: int) -> str:
|
|
242
|
+
"""
|
|
243
|
+
Builds the error message for a send that could not complete because of the channel's flow control.
|
|
244
|
+
|
|
245
|
+
:param headline: The first line of the message, describing what went wrong.
|
|
246
|
+
:param timeout: The timeout that was in effect for the wait.
|
|
247
|
+
:param wait_start: The ``time.monotonic()`` value when the wait started.
|
|
248
|
+
:param sent_length: The number of bytes of the send that are already on the wire.
|
|
249
|
+
:param total_length: The total number of bytes in the send.
|
|
250
|
+
|
|
251
|
+
:returns: The multi-line error message.
|
|
252
|
+
"""
|
|
253
|
+
waited = time.monotonic() - wait_start
|
|
254
|
+
|
|
255
|
+
err_msg_lines = [
|
|
256
|
+
headline,
|
|
257
|
+
"CHANNEL: local_id={} remote_id={}".format(self.local_id, self.remote_id),
|
|
258
|
+
"PROGRESS: sent {} of {} bytes".format(sent_length, total_length),
|
|
259
|
+
"REMOTE WINDOW: {} bytes (peer max packet size={})".format(self.remote_window,
|
|
260
|
+
self.remote_max_packet_size),
|
|
261
|
+
"TIMEOUT: {} seconds (waited {:.3f} seconds)".format(timeout, waited),
|
|
262
|
+
"",
|
|
263
|
+
"RFC 4254 section 5.2: once the peer's window is exhausted no more data may be sent until the peer",
|
|
264
|
+
"sends SSH_MSG_CHANNEL_WINDOW_ADJUST. The remote side is not consuming the data sent to it; check that",
|
|
265
|
+
"the remote process is reading its input, or pass a larger 'timeout' / set the channel's 'send_timeout'.",
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
269
|
+
return err_msg
|
|
270
|
+
|
|
271
|
+
def recv(self, size: int = 1024, timeout: Optional[float] = None) -> bytes:
|
|
272
|
+
"""
|
|
273
|
+
Reads up to ``size`` bytes of data from the channel. The bytes of a received packet that do not fit are
|
|
274
|
+
kept and returned first by the next call, so callers can read exact amounts (for example SFTP framing).
|
|
275
|
+
|
|
276
|
+
:param size: The maximum number of bytes to return.
|
|
277
|
+
:param timeout: The maximum number of seconds to wait for data, ``None`` to wait until data arrives or
|
|
278
|
+
the stream ends.
|
|
279
|
+
|
|
280
|
+
:returns: Between 1 and ``size`` bytes, or ``b""`` if the timeout expired or the stream ended (the peer
|
|
281
|
+
sent EOF or the channel closed; check ``eof_received`` / ``closed`` to tell them apart).
|
|
282
|
+
:raises: SemanticError if ``size`` is less than 1.
|
|
283
|
+
"""
|
|
284
|
+
data = self._read_stream(self.in_buffer, self._in_pending, self._recv_lock, size, timeout)
|
|
285
|
+
return data
|
|
286
|
+
|
|
287
|
+
def recv_stderr(self, size: int = 1024, timeout: Optional[float] = None) -> bytes:
|
|
288
|
+
"""
|
|
289
|
+
Reads up to ``size`` bytes of the peer's stderr (SSH_MSG_CHANNEL_EXTENDED_DATA of type
|
|
290
|
+
SSH_EXTENDED_DATA_STDERR). Stderr shares the channel window with ordinary data (RFC 4254 section 5.2),
|
|
291
|
+
so as in paramiko a caller that lets stderr pile up unread eventually stalls the peer.
|
|
292
|
+
|
|
293
|
+
:param size: The maximum number of bytes to return.
|
|
294
|
+
:param timeout: The maximum number of seconds to wait for data, ``None`` to wait until data arrives or
|
|
295
|
+
the stream ends.
|
|
296
|
+
|
|
297
|
+
:returns: Between 1 and ``size`` bytes, or ``b""`` if the timeout expired or the stream ended.
|
|
298
|
+
:raises: SemanticError if ``size`` is less than 1.
|
|
299
|
+
"""
|
|
300
|
+
data = self._read_stream(self.in_stderr_buffer, self._in_stderr_pending, self._recv_stderr_lock, size,
|
|
301
|
+
timeout)
|
|
302
|
+
return data
|
|
303
|
+
|
|
304
|
+
def _read_stream(self, stream_buffer: Queue, pending: bytearray, reader_lock: threading.Lock, size: int,
|
|
305
|
+
timeout: Optional[float]) -> bytes:
|
|
306
|
+
"""
|
|
307
|
+
Reads up to ``size`` bytes from one of the channel's receive streams and gives the consumed bytes back
|
|
308
|
+
to the peer's window.
|
|
309
|
+
|
|
310
|
+
:param stream_buffer: The queue of received chunks.
|
|
311
|
+
:param pending: The leftover bytes of the last chunk taken from the queue.
|
|
312
|
+
:param reader_lock: The lock that serializes readers of this stream.
|
|
313
|
+
:param size: The maximum number of bytes to return.
|
|
314
|
+
:param timeout: The maximum number of seconds to wait, ``None`` to wait for data or the end of stream.
|
|
315
|
+
|
|
316
|
+
:returns: Between 1 and ``size`` bytes, or ``b""`` on timeout or at the end of the stream.
|
|
317
|
+
:raises: SemanticError if ``size`` is less than 1.
|
|
318
|
+
"""
|
|
319
|
+
if size < 1:
|
|
320
|
+
err_msg = "The 'size' passed to recv() / recv_stderr() must be at least 1, not {}.".format(size)
|
|
321
|
+
raise SemanticError(err_msg)
|
|
322
|
+
|
|
323
|
+
wait_start = time.monotonic()
|
|
324
|
+
if timeout is None:
|
|
325
|
+
acquired = reader_lock.acquire()
|
|
326
|
+
else:
|
|
327
|
+
acquired = reader_lock.acquire(timeout=timeout)
|
|
328
|
+
|
|
329
|
+
data = b""
|
|
330
|
+
if acquired is True:
|
|
331
|
+
try:
|
|
332
|
+
remaining = None
|
|
333
|
+
if timeout is not None:
|
|
334
|
+
remaining = timeout - (time.monotonic() - wait_start)
|
|
335
|
+
if remaining < 0:
|
|
336
|
+
remaining = 0
|
|
337
|
+
|
|
338
|
+
data = self._read_stream_locked(stream_buffer, pending, size, remaining)
|
|
339
|
+
finally:
|
|
340
|
+
reader_lock.release()
|
|
341
|
+
|
|
342
|
+
data_length = len(data)
|
|
343
|
+
if data_length > 0:
|
|
344
|
+
# The application has consumed this data, so the peer may be given room to send more.
|
|
345
|
+
self._consume_local_window(data_length)
|
|
346
|
+
|
|
347
|
+
return data
|
|
348
|
+
|
|
349
|
+
def _read_stream_locked(self, stream_buffer: Queue, pending: bytearray, size: int,
|
|
350
|
+
timeout: Optional[float]) -> bytes:
|
|
351
|
+
"""
|
|
352
|
+
The body of :meth:`_read_stream`, called while holding the stream's reader lock.
|
|
353
|
+
|
|
354
|
+
:returns: Between 1 and ``size`` bytes, or ``b""`` on timeout or at the end of the stream.
|
|
355
|
+
"""
|
|
356
|
+
if len(pending) == 0:
|
|
357
|
+
receive_finished = self.closed is True or self.eof_received is True
|
|
358
|
+
if receive_finished is True and stream_buffer.empty():
|
|
359
|
+
return b""
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
chunk = stream_buffer.get(timeout=timeout)
|
|
363
|
+
except Empty:
|
|
364
|
+
return b""
|
|
365
|
+
|
|
366
|
+
if len(chunk) == 0:
|
|
367
|
+
# The end-of-stream marker queued by handle_eof() / handle_close().
|
|
368
|
+
return b""
|
|
369
|
+
|
|
370
|
+
pending.extend(chunk)
|
|
371
|
+
|
|
372
|
+
data = bytes(pending[:size])
|
|
373
|
+
del pending[:size]
|
|
374
|
+
return data
|
|
375
|
+
|
|
376
|
+
def _signal_end_of_stream(self):
|
|
377
|
+
"""
|
|
378
|
+
Wakes readers that are blocked on either receive stream so they see the end of the stream.
|
|
379
|
+
"""
|
|
380
|
+
self.in_buffer.put(b"")
|
|
381
|
+
self.in_stderr_buffer.put(b"")
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
def close(self):
|
|
385
|
+
"""
|
|
386
|
+
Closes the channel. SSH_MSG_CHANNEL_CLOSE is sent to the peer exactly once (RFC 4254 section 5.3): not
|
|
387
|
+
at all if it was already sent in reply to the peer's close, and deferred until the peer confirms the
|
|
388
|
+
channel if the channel was abandoned before its SSH_MSG_CHANNEL_OPEN_CONFIRMATION arrived.
|
|
389
|
+
"""
|
|
390
|
+
with self._window_condition:
|
|
391
|
+
self.closed = True
|
|
392
|
+
# Wake any sender that is blocked on an exhausted remote window so it can fail fast.
|
|
393
|
+
self._window_condition.notify_all()
|
|
394
|
+
remote_id = self._claim_close_locked()
|
|
395
|
+
|
|
396
|
+
if remote_id is not None:
|
|
397
|
+
self._send_close(remote_id)
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
def _claim_close_locked(self) -> Optional[int]:
|
|
401
|
+
"""
|
|
402
|
+
Decides, while holding the channel lock, whether this caller is the one that sends our
|
|
403
|
+
SSH_MSG_CHANNEL_CLOSE. It is sent only once, and only once the peer's channel number is known.
|
|
404
|
+
|
|
405
|
+
:returns: The peer's channel number if the caller must send the close, otherwise None.
|
|
406
|
+
"""
|
|
407
|
+
remote_id = None
|
|
408
|
+
if self._close_sent is False and self.remote_id is not None:
|
|
409
|
+
self._close_sent = True
|
|
410
|
+
remote_id = self.remote_id
|
|
411
|
+
return remote_id
|
|
412
|
+
|
|
413
|
+
def _send_close(self, remote_id: int):
|
|
414
|
+
"""
|
|
415
|
+
Sends SSH_MSG_CHANNEL_CLOSE to the peer.
|
|
416
|
+
|
|
417
|
+
:param remote_id: The peer's channel number.
|
|
418
|
+
"""
|
|
419
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_CLOSE)
|
|
420
|
+
pkt.add_uint32(remote_id)
|
|
421
|
+
self.connection.transport.send_packet(pkt)
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
def request_shell(self, *, timeout: Optional[float] = None):
|
|
425
|
+
"""
|
|
426
|
+
Asks the peer to start a shell on this channel (RFC 4254 section 6.5) and waits for its answer.
|
|
427
|
+
|
|
428
|
+
:param timeout: The maximum number of seconds to wait for the answer, ``None`` for the connection's
|
|
429
|
+
``channel_timeout``.
|
|
430
|
+
|
|
431
|
+
:raises: ConnectionError if the peer refuses (the channel is then closed, as in paramiko),
|
|
432
|
+
TimeoutError if it does not answer in time.
|
|
433
|
+
"""
|
|
434
|
+
self._send_request("shell", want_reply=True, timeout=timeout)
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
def request_exec(self, command: Union[str, bytes], *, timeout: Optional[float] = None):
|
|
438
|
+
"""
|
|
439
|
+
Asks the peer to run a command on this channel (RFC 4254 section 6.5) and waits for its answer: the
|
|
440
|
+
command is sent as an SSH ``string``, a uint32 length followed by the bytes.
|
|
441
|
+
|
|
442
|
+
:param command: The command line. A ``str`` is encoded as UTF-8, ``bytes`` are sent as they are.
|
|
443
|
+
:param timeout: The maximum number of seconds to wait for the answer, ``None`` for the connection's
|
|
444
|
+
``channel_timeout``.
|
|
445
|
+
|
|
446
|
+
:raises: ConnectionError if the peer refuses (the channel is then closed, as in paramiko),
|
|
447
|
+
TimeoutError if it does not answer in time.
|
|
448
|
+
"""
|
|
449
|
+
data = SshPacket()
|
|
450
|
+
data.add_string(command)
|
|
451
|
+
|
|
452
|
+
self._send_request("exec", want_reply=True, data=data.payload, timeout=timeout)
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
def request_subsystem(self, subsystem: Union[str, bytes], *, timeout: Optional[float] = None):
|
|
456
|
+
"""
|
|
457
|
+
Asks the peer to start a subsystem, for example ``sftp``, on this channel (RFC 4254 section 6.5) and waits
|
|
458
|
+
for its answer: the subsystem name is sent as an SSH ``string``, a uint32 length followed by the bytes.
|
|
459
|
+
|
|
460
|
+
:param subsystem: The subsystem name. A ``str`` is encoded as UTF-8, ``bytes`` are sent as they are.
|
|
461
|
+
:param timeout: The maximum number of seconds to wait for the answer, ``None`` for the connection's
|
|
462
|
+
``channel_timeout``.
|
|
463
|
+
|
|
464
|
+
:raises: ConnectionError if the peer refuses (the channel is then closed, as in paramiko),
|
|
465
|
+
TimeoutError if it does not answer in time.
|
|
466
|
+
"""
|
|
467
|
+
data = SshPacket()
|
|
468
|
+
data.add_string(subsystem)
|
|
469
|
+
|
|
470
|
+
self._send_request("subsystem", want_reply=True, data=data.payload, timeout=timeout)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
def request_pty(self, term: str = 'vt100', width: int = 80, height: int = 24,
|
|
474
|
+
pixel_width: int = 0, pixel_height: int = 0, modes: dict = None, *,
|
|
475
|
+
timeout: Optional[float] = None):
|
|
476
|
+
"""
|
|
477
|
+
Asks the peer for a pseudo-terminal on this channel (RFC 4254 section 6.2) and waits for its answer.
|
|
478
|
+
|
|
479
|
+
:param timeout: The maximum number of seconds to wait for the answer, ``None`` for the connection's
|
|
480
|
+
``channel_timeout``.
|
|
481
|
+
|
|
482
|
+
:raises: ConnectionError if the peer refuses (the channel is then closed, as in paramiko),
|
|
483
|
+
TimeoutError if it does not answer in time.
|
|
484
|
+
"""
|
|
485
|
+
if modes is None:
|
|
486
|
+
modes = {}
|
|
487
|
+
|
|
488
|
+
data = SshPacket()
|
|
489
|
+
data.add_string(term)
|
|
490
|
+
data.add_uint32(width)
|
|
491
|
+
data.add_uint32(height)
|
|
492
|
+
data.add_uint32(pixel_width)
|
|
493
|
+
data.add_uint32(pixel_height)
|
|
494
|
+
data.add_string(self._encode_terminal_modes(modes))
|
|
495
|
+
|
|
496
|
+
self._send_request("pty-req", want_reply=True, data=data.payload, timeout=timeout)
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
def resize_pty(self, width: int, height: int, pixel_width: int = 0, pixel_height: int = 0):
|
|
500
|
+
"""
|
|
501
|
+
Send a window-change request.
|
|
502
|
+
"""
|
|
503
|
+
data = SshPacket()
|
|
504
|
+
data.add_uint32(width)
|
|
505
|
+
data.add_uint32(height)
|
|
506
|
+
data.add_uint32(pixel_width)
|
|
507
|
+
data.add_uint32(pixel_height)
|
|
508
|
+
|
|
509
|
+
self._send_request("window-change", want_reply=False, data=data.payload)
|
|
510
|
+
|
|
511
|
+
def _encode_terminal_modes(self, modes: dict) -> bytes:
|
|
512
|
+
"""
|
|
513
|
+
Encode terminal modes dictionary into bytes.
|
|
514
|
+
"""
|
|
515
|
+
pkt = SshPacket()
|
|
516
|
+
for opcode, argument in modes.items():
|
|
517
|
+
pkt.add_byte(opcode)
|
|
518
|
+
pkt.add_uint32(argument)
|
|
519
|
+
pkt.add_byte(sshconstants.TTY_OP_END)
|
|
520
|
+
return pkt.payload
|
|
521
|
+
|
|
522
|
+
def _send_request(self, request_type: str, want_reply: bool, data: Optional[Union[bytes, bytearray]] = None,
|
|
523
|
+
*, timeout: Optional[float] = None):
|
|
524
|
+
"""
|
|
525
|
+
Sends SSH_MSG_CHANNEL_REQUEST (RFC 4254 section 5.4). When ``want_reply`` is set it waits for the
|
|
526
|
+
peer's SSH_MSG_CHANNEL_SUCCESS / FAILURE; the peer answers requests in the order they were sent.
|
|
527
|
+
|
|
528
|
+
:param request_type: The request type, for example ``exec``.
|
|
529
|
+
:param want_reply: Whether the peer should answer with SSH_MSG_CHANNEL_SUCCESS / FAILURE.
|
|
530
|
+
:param data: The type-specific data, already encoded in SSH wire format (strings carry their uint32
|
|
531
|
+
length prefix). It is appended to the message as is.
|
|
532
|
+
:param timeout: The maximum number of seconds to wait for the answer, ``None`` for the connection's
|
|
533
|
+
``channel_timeout``.
|
|
534
|
+
|
|
535
|
+
:raises: SemanticError if ``data`` is not already encoded bytes, ConnectionError if the peer refuses the
|
|
536
|
+
request (the channel is then closed, as in paramiko) or the channel or transport closes,
|
|
537
|
+
TimeoutError if the peer does not answer in time.
|
|
538
|
+
"""
|
|
539
|
+
if data is not None and not isinstance(data, (bytes, bytearray)):
|
|
540
|
+
err_msg_lines = [
|
|
541
|
+
"The type-specific data of a '{}' channel request must be SSH-encoded bytes, not {}.".format(
|
|
542
|
+
request_type, type(data).__name__),
|
|
543
|
+
"Encode it with SshPacket (for example add_string) so strings carry their uint32 length prefix.",
|
|
544
|
+
]
|
|
545
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
546
|
+
raise SemanticError(err_msg)
|
|
547
|
+
|
|
548
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_REQUEST)
|
|
549
|
+
pkt.add_uint32(self.remote_id)
|
|
550
|
+
pkt.add_string(request_type)
|
|
551
|
+
pkt.add_boolean(want_reply)
|
|
552
|
+
if data is not None:
|
|
553
|
+
pkt.add_bytes(data)
|
|
554
|
+
|
|
555
|
+
if want_reply is False:
|
|
556
|
+
self.connection.transport.send_packet(pkt)
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
wait_timeout = timeout
|
|
560
|
+
if wait_timeout is None:
|
|
561
|
+
wait_timeout = self.connection.channel_timeout
|
|
562
|
+
|
|
563
|
+
# Queue the request before it goes on the wire: the transport thread may deliver the answer before this
|
|
564
|
+
# thread starts waiting for it.
|
|
565
|
+
pending_request = {"request_type": request_type, "succeeded": None, "abandoned": False}
|
|
566
|
+
with self._request_lock:
|
|
567
|
+
with self._window_condition:
|
|
568
|
+
self._pending_requests.append(pending_request)
|
|
569
|
+
try:
|
|
570
|
+
self.connection.transport.send_packet(pkt)
|
|
571
|
+
except Exception:
|
|
572
|
+
# Nothing was sent, so no answer will come; keep the queue aligned with the peer's answers.
|
|
573
|
+
with self._window_condition:
|
|
574
|
+
self._pending_requests.remove(pending_request)
|
|
575
|
+
raise
|
|
576
|
+
|
|
577
|
+
self._wait_for_request_reply(pending_request, wait_timeout)
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
def _wait_for_request_reply(self, pending_request: dict, timeout: Optional[float]):
|
|
581
|
+
"""
|
|
582
|
+
Waits for the peer's SSH_MSG_CHANNEL_SUCCESS / FAILURE to a request queued by :meth:`_send_request`.
|
|
583
|
+
|
|
584
|
+
:param pending_request: The queued request.
|
|
585
|
+
:param timeout: The maximum number of seconds to wait, ``None`` for as long as the channel is open.
|
|
586
|
+
|
|
587
|
+
:raises: ConnectionError if the peer refuses the request (the channel is then closed, as in paramiko)
|
|
588
|
+
or the channel or transport closes, TimeoutError if the peer does not answer in time.
|
|
589
|
+
"""
|
|
590
|
+
request_type = pending_request["request_type"]
|
|
591
|
+
|
|
592
|
+
with self._window_condition:
|
|
593
|
+
wait_start = time.monotonic()
|
|
594
|
+
|
|
595
|
+
while pending_request["succeeded"] is None:
|
|
596
|
+
if self.closed is True:
|
|
597
|
+
err_msg = self._format_request_error(
|
|
598
|
+
"The SSH channel closed before the peer answered the '{}' request.".format(request_type),
|
|
599
|
+
request_type, timeout, wait_start)
|
|
600
|
+
raise ConnectionError(err_msg)
|
|
601
|
+
|
|
602
|
+
transport_active = self.connection.transport.active
|
|
603
|
+
if transport_active is False:
|
|
604
|
+
err_msg = self._format_request_error(
|
|
605
|
+
"The SSH transport closed before the peer answered the '{}' request.".format(request_type),
|
|
606
|
+
request_type, timeout, wait_start)
|
|
607
|
+
raise ConnectionError(err_msg)
|
|
608
|
+
|
|
609
|
+
wait_time = WINDOW_WAIT_POLL_INTERVAL
|
|
610
|
+
if timeout is not None:
|
|
611
|
+
waited = time.monotonic() - wait_start
|
|
612
|
+
remaining = timeout - waited
|
|
613
|
+
if remaining <= 0:
|
|
614
|
+
# The request stays queued so a late answer is still matched to it and not to the next one.
|
|
615
|
+
pending_request["abandoned"] = True
|
|
616
|
+
err_msg = self._format_request_error(
|
|
617
|
+
"Timed out waiting for the SSH peer to answer the '{}' request "
|
|
618
|
+
"(SSH_MSG_CHANNEL_SUCCESS / SSH_MSG_CHANNEL_FAILURE).".format(request_type),
|
|
619
|
+
request_type, timeout, wait_start)
|
|
620
|
+
raise TimeoutError(err_msg)
|
|
621
|
+
if remaining < wait_time:
|
|
622
|
+
wait_time = remaining
|
|
623
|
+
|
|
624
|
+
self._window_condition.wait(timeout=wait_time)
|
|
625
|
+
|
|
626
|
+
succeeded = pending_request["succeeded"]
|
|
627
|
+
refused_msg = None
|
|
628
|
+
if succeeded is False:
|
|
629
|
+
refused_msg = self._format_request_error(
|
|
630
|
+
"The SSH peer refused the '{}' request (SSH_MSG_CHANNEL_FAILURE); the channel has been "
|
|
631
|
+
"closed.".format(request_type), request_type, timeout, wait_start)
|
|
632
|
+
|
|
633
|
+
if refused_msg is not None:
|
|
634
|
+
# paramiko closes the channel when one of these requests fails; the channel cannot be used as intended.
|
|
635
|
+
self.close()
|
|
636
|
+
raise ConnectionError(refused_msg)
|
|
637
|
+
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
def _format_request_error(self, headline: str, request_type: str, timeout: Optional[float],
|
|
641
|
+
wait_start: float) -> str:
|
|
642
|
+
"""
|
|
643
|
+
Builds the error message for a channel request that did not succeed.
|
|
644
|
+
|
|
645
|
+
:param headline: The first line of the message, describing what went wrong.
|
|
646
|
+
:param request_type: The request type.
|
|
647
|
+
:param timeout: The timeout that was in effect for the wait.
|
|
648
|
+
:param wait_start: The ``time.monotonic()`` value when the wait started.
|
|
649
|
+
|
|
650
|
+
:returns: The multi-line error message.
|
|
651
|
+
"""
|
|
652
|
+
waited = time.monotonic() - wait_start
|
|
653
|
+
|
|
654
|
+
err_msg_lines = [
|
|
655
|
+
headline,
|
|
656
|
+
"CHANNEL: local_id={} remote_id={}".format(self.local_id, self.remote_id),
|
|
657
|
+
"REQUEST: {}".format(request_type),
|
|
658
|
+
"TIMEOUT: {} seconds (waited {:.3f} seconds)".format(timeout, waited),
|
|
659
|
+
]
|
|
660
|
+
hint = CHANNEL_REQUEST_FAILURE_HINTS.get(request_type)
|
|
661
|
+
if hint is not None:
|
|
662
|
+
err_msg_lines.append("")
|
|
663
|
+
err_msg_lines.append(hint)
|
|
664
|
+
|
|
665
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
666
|
+
return err_msg
|
|
667
|
+
|
|
668
|
+
def handle_request_reply(self, succeeded: bool):
|
|
669
|
+
"""
|
|
670
|
+
Handles SSH_MSG_CHANNEL_SUCCESS / SSH_MSG_CHANNEL_FAILURE: the answer to our oldest want-reply request
|
|
671
|
+
(RFC 4254 section 5.4 answers requests in order).
|
|
672
|
+
|
|
673
|
+
:param succeeded: True for SSH_MSG_CHANNEL_SUCCESS, False for SSH_MSG_CHANNEL_FAILURE.
|
|
674
|
+
"""
|
|
675
|
+
pending_request = None
|
|
676
|
+
with self._window_condition:
|
|
677
|
+
if len(self._pending_requests) > 0:
|
|
678
|
+
pending_request = self._pending_requests.popleft()
|
|
679
|
+
pending_request["succeeded"] = succeeded
|
|
680
|
+
self._window_condition.notify_all()
|
|
681
|
+
|
|
682
|
+
if pending_request is None:
|
|
683
|
+
reply_name = "SSH_MSG_CHANNEL_FAILURE"
|
|
684
|
+
if succeeded is True:
|
|
685
|
+
reply_name = "SSH_MSG_CHANNEL_SUCCESS"
|
|
686
|
+
logger.warning("Ignoring an %s on channel local_id=%s: no request is waiting for an answer.",
|
|
687
|
+
reply_name, self.local_id)
|
|
688
|
+
elif pending_request["abandoned"] is True and succeeded is False:
|
|
689
|
+
logger.warning("The SSH peer refused the '%s' request on channel local_id=%s after the caller stopped "
|
|
690
|
+
"waiting for the answer.", pending_request["request_type"], self.local_id)
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
def handle_request(self, request_type: str, want_reply: bool, packet: SshPacket):
|
|
694
|
+
"""
|
|
695
|
+
Handles an SSH_MSG_CHANNEL_REQUEST from the peer (RFC 4254 section 5.4). The command's "exit-status"
|
|
696
|
+
and "exit-signal" (section 6.10) are recorded. Any other request is not supported by this client and,
|
|
697
|
+
if the peer wants a reply (for example OpenSSH's ``keepalive@openssh.com``), is answered with
|
|
698
|
+
SSH_MSG_CHANNEL_FAILURE as the RFC requires.
|
|
699
|
+
|
|
700
|
+
:param request_type: The request type.
|
|
701
|
+
:param want_reply: Whether the peer expects SSH_MSG_CHANNEL_SUCCESS / FAILURE.
|
|
702
|
+
:param packet: The message, positioned at the type-specific data.
|
|
703
|
+
"""
|
|
704
|
+
handled = False
|
|
705
|
+
|
|
706
|
+
if request_type == "exit-status":
|
|
707
|
+
exit_status = packet.get_uint32()
|
|
708
|
+
with self._window_condition:
|
|
709
|
+
self.exit_status = exit_status
|
|
710
|
+
handled = True
|
|
711
|
+
|
|
712
|
+
elif request_type == "exit-signal":
|
|
713
|
+
signal_name = packet.get_string().decode('utf-8', errors='replace')
|
|
714
|
+
core_dumped = packet.get_boolean()
|
|
715
|
+
error_message = packet.get_string().decode('utf-8', errors='replace')
|
|
716
|
+
logger.warning("The command on channel local_id=%s was killed by signal %s (core dumped=%s): %s",
|
|
717
|
+
self.local_id, signal_name, core_dumped, error_message)
|
|
718
|
+
with self._window_condition:
|
|
719
|
+
self.exit_signal = signal_name
|
|
720
|
+
self.exit_signal_core_dumped = core_dumped
|
|
721
|
+
self.exit_signal_message = error_message
|
|
722
|
+
# No exit status is returned for a signalled command; -1 is the value recv_exit_status() uses
|
|
723
|
+
# for a missing status (as in paramiko).
|
|
724
|
+
self.exit_status = -1
|
|
725
|
+
handled = True
|
|
726
|
+
|
|
727
|
+
else:
|
|
728
|
+
logger.debug("Unsupported channel request '%s' (want_reply=%s) on channel local_id=%s.",
|
|
729
|
+
request_type, want_reply, self.local_id)
|
|
730
|
+
|
|
731
|
+
if want_reply is True:
|
|
732
|
+
reply_id = sshconstants.SSH_MSG_CHANNEL_SUCCESS
|
|
733
|
+
if handled is False:
|
|
734
|
+
reply_id = sshconstants.SSH_MSG_CHANNEL_FAILURE
|
|
735
|
+
|
|
736
|
+
with self._window_condition:
|
|
737
|
+
# Nothing more may be sent on the channel once our SSH_MSG_CHANNEL_CLOSE is out.
|
|
738
|
+
remote_id = None
|
|
739
|
+
if self._close_sent is False:
|
|
740
|
+
remote_id = self.remote_id
|
|
741
|
+
|
|
742
|
+
if remote_id is not None:
|
|
743
|
+
reply = SshPacket(reply_id)
|
|
744
|
+
reply.add_uint32(remote_id)
|
|
745
|
+
self.connection.transport.send_packet(reply)
|
|
746
|
+
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
def handle_open_confirmation(self, remote_id: int, window_size: int, max_packet_size: int):
|
|
750
|
+
"""
|
|
751
|
+
Handles SSH_MSG_CHANNEL_OPEN_CONFIRMATION: records the peer's channel number, its maximum packet size
|
|
752
|
+
and the initial window it is willing to receive (RFC 4254 sections 5.1 and 5.2), then opens the channel.
|
|
753
|
+
|
|
754
|
+
:param remote_id: The peer's channel number.
|
|
755
|
+
:param window_size: The peer's initial window size in bytes.
|
|
756
|
+
:param max_packet_size: The largest data packet the peer will accept.
|
|
757
|
+
"""
|
|
758
|
+
close_remote_id = None
|
|
759
|
+
|
|
760
|
+
with self._window_condition:
|
|
761
|
+
self.remote_id = remote_id
|
|
762
|
+
self.remote_max_packet_size = max_packet_size
|
|
763
|
+
self.remote_window = window_size
|
|
764
|
+
|
|
765
|
+
if self.closed is True:
|
|
766
|
+
# The channel was abandoned (its open timed out, or it was closed) before the peer confirmed it.
|
|
767
|
+
# The peer now has an open channel, so close it rather than leaving it orphaned.
|
|
768
|
+
close_remote_id = self._claim_close_locked()
|
|
769
|
+
else:
|
|
770
|
+
self.active = True
|
|
771
|
+
|
|
772
|
+
self._window_condition.notify_all()
|
|
773
|
+
|
|
774
|
+
if close_remote_id is not None:
|
|
775
|
+
logger.warning("Closing channel local_id=%s remote_id=%s because the peer confirmed it after it was "
|
|
776
|
+
"abandoned.", self.local_id, remote_id)
|
|
777
|
+
self._send_close(close_remote_id)
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
def handle_open_failure(self, reason_code: int, description: str):
|
|
781
|
+
"""
|
|
782
|
+
Handles SSH_MSG_CHANNEL_OPEN_FAILURE: records why the peer refused the channel and wakes the opener.
|
|
783
|
+
|
|
784
|
+
:param reason_code: The RFC 4254 section 5.1 reason code.
|
|
785
|
+
:param description: The peer's description of the failure.
|
|
786
|
+
"""
|
|
787
|
+
with self._window_condition:
|
|
788
|
+
self._open_failure = (reason_code, description)
|
|
789
|
+
self.closed = True
|
|
790
|
+
self._window_condition.notify_all()
|
|
791
|
+
|
|
792
|
+
self._signal_end_of_stream()
|
|
793
|
+
return
|
|
794
|
+
|
|
795
|
+
def wait_for_open(self, timeout: Optional[float], kind: str = "session"):
|
|
796
|
+
"""
|
|
797
|
+
Waits for the peer to confirm the channel with SSH_MSG_CHANNEL_OPEN_CONFIRMATION.
|
|
798
|
+
|
|
799
|
+
:param timeout: The maximum number of seconds to wait, ``None`` to wait for as long as the transport is
|
|
800
|
+
open.
|
|
801
|
+
:param kind: The channel type that was requested (for errors).
|
|
802
|
+
|
|
803
|
+
:raises: ConnectionError if the peer refuses the channel or the channel or transport closes,
|
|
804
|
+
TimeoutError if the peer does not answer in time. A channel that times out is abandoned: it is
|
|
805
|
+
closed as soon as the peer confirms it.
|
|
806
|
+
"""
|
|
807
|
+
with self._window_condition:
|
|
808
|
+
wait_start = time.monotonic()
|
|
809
|
+
|
|
810
|
+
while self.active is not True:
|
|
811
|
+
if self._open_failure is not None:
|
|
812
|
+
reason_code, description = self._open_failure
|
|
813
|
+
reason_name = sshconstants.SSH_OPEN_FAILURE_REASON_NAMES.get(reason_code, "UNKNOWN")
|
|
814
|
+
err_msg = self._format_open_wait_error(
|
|
815
|
+
"The SSH peer refused to open a '{}' channel.".format(kind), kind, timeout, wait_start,
|
|
816
|
+
["REASON: {} ({})".format(reason_name, reason_code),
|
|
817
|
+
"DESCRIPTION: {}".format(description)])
|
|
818
|
+
raise ConnectionError(err_msg)
|
|
819
|
+
|
|
820
|
+
if self.closed is True:
|
|
821
|
+
err_msg = self._format_open_wait_error(
|
|
822
|
+
"The SSH channel closed before the peer confirmed it.", kind, timeout, wait_start, [])
|
|
823
|
+
raise ConnectionError(err_msg)
|
|
824
|
+
|
|
825
|
+
transport_active = self.connection.transport.active
|
|
826
|
+
if transport_active is False:
|
|
827
|
+
err_msg = self._format_open_wait_error(
|
|
828
|
+
"The SSH transport closed while waiting for the peer to confirm the channel.",
|
|
829
|
+
kind, timeout, wait_start, [])
|
|
830
|
+
raise ConnectionError(err_msg)
|
|
831
|
+
|
|
832
|
+
wait_time = WINDOW_WAIT_POLL_INTERVAL
|
|
833
|
+
if timeout is not None:
|
|
834
|
+
waited = time.monotonic() - wait_start
|
|
835
|
+
remaining = timeout - waited
|
|
836
|
+
if remaining <= 0:
|
|
837
|
+
# Abandon the channel so a late confirmation is answered with SSH_MSG_CHANNEL_CLOSE.
|
|
838
|
+
self.closed = True
|
|
839
|
+
err_msg = self._format_open_wait_error(
|
|
840
|
+
"Timed out waiting for the SSH peer to confirm the channel "
|
|
841
|
+
"(SSH_MSG_CHANNEL_OPEN_CONFIRMATION).", kind, timeout, wait_start, [])
|
|
842
|
+
raise TimeoutError(err_msg)
|
|
843
|
+
if remaining < wait_time:
|
|
844
|
+
wait_time = remaining
|
|
845
|
+
|
|
846
|
+
self._window_condition.wait(timeout=wait_time)
|
|
847
|
+
|
|
848
|
+
return
|
|
849
|
+
|
|
850
|
+
def _format_open_wait_error(self, headline: str, kind: str, timeout: Optional[float], wait_start: float,
|
|
851
|
+
detail_lines: list) -> str:
|
|
852
|
+
"""
|
|
853
|
+
Builds the error message for a channel that could not be opened.
|
|
854
|
+
|
|
855
|
+
:param headline: The first line of the message, describing what went wrong.
|
|
856
|
+
:param kind: The channel type that was requested.
|
|
857
|
+
:param timeout: The timeout that was in effect for the wait.
|
|
858
|
+
:param wait_start: The ``time.monotonic()`` value when the wait started.
|
|
859
|
+
:param detail_lines: Additional lines describing the failure.
|
|
860
|
+
|
|
861
|
+
:returns: The multi-line error message.
|
|
862
|
+
"""
|
|
863
|
+
waited = time.monotonic() - wait_start
|
|
864
|
+
|
|
865
|
+
err_msg_lines = [
|
|
866
|
+
headline,
|
|
867
|
+
"CHANNEL: kind={} local_id={}".format(kind, self.local_id),
|
|
868
|
+
]
|
|
869
|
+
err_msg_lines.extend(detail_lines)
|
|
870
|
+
err_msg_lines.append("TIMEOUT: {} seconds (waited {:.3f} seconds)".format(timeout, waited))
|
|
871
|
+
|
|
872
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
873
|
+
return err_msg
|
|
874
|
+
|
|
875
|
+
def handle_window_adjust(self, bytes_to_add: int):
|
|
876
|
+
"""
|
|
877
|
+
Handles SSH_MSG_CHANNEL_WINDOW_ADJUST: adds ``bytes_to_add`` to the peer's window and wakes any sender
|
|
878
|
+
that is blocked on an exhausted window. RFC 4254 section 5.2 says the window must never be increased
|
|
879
|
+
above 2^32 - 1 bytes; a peer that tries is logged and the window is clamped to that maximum.
|
|
880
|
+
|
|
881
|
+
:param bytes_to_add: The number of bytes the peer added to its window.
|
|
882
|
+
"""
|
|
883
|
+
with self._window_condition:
|
|
884
|
+
if self.remote_window is None:
|
|
885
|
+
logger.warning("Ignoring SSH_MSG_CHANNEL_WINDOW_ADJUST of %d bytes for channel local_id=%s "
|
|
886
|
+
"because the peer has not confirmed the channel open yet.",
|
|
887
|
+
bytes_to_add, self.local_id)
|
|
888
|
+
else:
|
|
889
|
+
new_window = self.remote_window + bytes_to_add
|
|
890
|
+
if new_window > MAX_CHANNEL_WINDOW_SIZE:
|
|
891
|
+
logger.warning("SSH peer adjusted the window of channel local_id=%s remote_id=%s by %d bytes "
|
|
892
|
+
"to %d, above the RFC 4254 maximum of %d bytes; clamping it to the maximum.",
|
|
893
|
+
self.local_id, self.remote_id, bytes_to_add, new_window,
|
|
894
|
+
MAX_CHANNEL_WINDOW_SIZE)
|
|
895
|
+
new_window = MAX_CHANNEL_WINDOW_SIZE
|
|
896
|
+
|
|
897
|
+
self.remote_window = new_window
|
|
898
|
+
self._window_condition.notify_all()
|
|
899
|
+
|
|
900
|
+
return
|
|
901
|
+
|
|
902
|
+
def handle_data(self, data: bytes):
|
|
903
|
+
"""
|
|
904
|
+
Handles SSH_MSG_CHANNEL_DATA: charges the data to the window we granted the peer and queues it for
|
|
905
|
+
:meth:`recv`. The window is given back to the peer as :meth:`recv` consumes the data.
|
|
906
|
+
|
|
907
|
+
:param data: The data the peer sent.
|
|
908
|
+
"""
|
|
909
|
+
data_length = len(data)
|
|
910
|
+
self._charge_local_window(data_length)
|
|
911
|
+
|
|
912
|
+
# An empty packet carries nothing, and queueing it would look like the end-of-stream marker.
|
|
913
|
+
if data_length > 0:
|
|
914
|
+
if self.eof_received is True:
|
|
915
|
+
logger.warning("SSH peer sent %d bytes on channel local_id=%s after SSH_MSG_CHANNEL_EOF "
|
|
916
|
+
"(RFC 4254 section 5.3); keeping them.", data_length, self.local_id)
|
|
917
|
+
self.in_buffer.put(data)
|
|
918
|
+
return
|
|
919
|
+
|
|
920
|
+
def handle_extended_data(self, data_type_code: int, data: bytes):
|
|
921
|
+
"""
|
|
922
|
+
Handles SSH_MSG_CHANNEL_EXTENDED_DATA. Stderr (SSH_EXTENDED_DATA_STDERR) is queued for
|
|
923
|
+
:meth:`recv_stderr` and, like ordinary data, given back to the peer's window as it is read (RFC 4254
|
|
924
|
+
section 5.2: extended data consumes the same window). Other types are not defined by the RFC and are
|
|
925
|
+
discarded; they are counted as consumed at once so they cannot close the window.
|
|
926
|
+
|
|
927
|
+
:param data_type_code: The extended data type.
|
|
928
|
+
:param data: The data the peer sent.
|
|
929
|
+
"""
|
|
930
|
+
data_length = len(data)
|
|
931
|
+
self._charge_local_window(data_length)
|
|
932
|
+
|
|
933
|
+
if data_length > 0:
|
|
934
|
+
if data_type_code == sshconstants.SSH_EXTENDED_DATA_STDERR:
|
|
935
|
+
self.in_stderr_buffer.put(data)
|
|
936
|
+
else:
|
|
937
|
+
logger.debug("Discarding %d bytes of extended data of unsupported type %d on channel local_id=%s.",
|
|
938
|
+
data_length, data_type_code, self.local_id)
|
|
939
|
+
self._consume_local_window(data_length)
|
|
940
|
+
return
|
|
941
|
+
|
|
942
|
+
def handle_eof(self):
|
|
943
|
+
"""
|
|
944
|
+
Handles SSH_MSG_CHANNEL_EOF: the peer sends no more data (RFC 4254 section 5.3). Once the data already
|
|
945
|
+
received has been read, :meth:`recv` and :meth:`recv_stderr` return ``b""``. The channel stays open and
|
|
946
|
+
data can still be sent to the peer (half-close).
|
|
947
|
+
"""
|
|
948
|
+
with self._window_condition:
|
|
949
|
+
self.eof_received = True
|
|
950
|
+
|
|
951
|
+
self._signal_end_of_stream()
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
def handle_close(self):
|
|
955
|
+
"""
|
|
956
|
+
Handles SSH_MSG_CHANNEL_CLOSE from the peer. RFC 4254 section 5.3 requires us to send back
|
|
957
|
+
SSH_MSG_CHANNEL_CLOSE unless we already have, so it is answered here; a later :meth:`close` does not
|
|
958
|
+
send it again. Data that was already received stays readable with :meth:`recv`.
|
|
959
|
+
"""
|
|
960
|
+
with self._window_condition:
|
|
961
|
+
self._close_received = True
|
|
962
|
+
self.closed = True
|
|
963
|
+
# Wake any sender that is blocked on an exhausted remote window so it can fail fast.
|
|
964
|
+
self._window_condition.notify_all()
|
|
965
|
+
remote_id = self._claim_close_locked()
|
|
966
|
+
|
|
967
|
+
self._signal_end_of_stream()
|
|
968
|
+
|
|
969
|
+
if remote_id is not None:
|
|
970
|
+
self._send_close(remote_id)
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
def _charge_local_window(self, data_length: int):
|
|
974
|
+
"""
|
|
975
|
+
Takes received data out of the window we granted the peer.
|
|
976
|
+
|
|
977
|
+
:param data_length: The number of bytes received.
|
|
978
|
+
"""
|
|
979
|
+
with self._window_condition:
|
|
980
|
+
self.local_window -= data_length
|
|
981
|
+
local_window = self.local_window
|
|
982
|
+
|
|
983
|
+
if local_window < 0:
|
|
984
|
+
# RFC 4254 section 5.2 allows the extra data to be ignored, but dropping it would silently lose output,
|
|
985
|
+
# so keep it and record the protocol violation.
|
|
986
|
+
logger.warning("SSH peer overran the receive window of channel local_id=%s remote_id=%s by %d bytes.",
|
|
987
|
+
self.local_id, self.remote_id, -local_window)
|
|
988
|
+
return
|
|
989
|
+
|
|
990
|
+
def _consume_local_window(self, data_length: int):
|
|
991
|
+
"""
|
|
992
|
+
Records that the application consumed received data and, once at least half of the initial window
|
|
993
|
+
has been consumed, gives it back to the peer with SSH_MSG_CHANNEL_WINDOW_ADJUST (RFC 4254 section 5.2).
|
|
994
|
+
|
|
995
|
+
:param data_length: The number of bytes consumed.
|
|
996
|
+
"""
|
|
997
|
+
adjust_length = 0
|
|
998
|
+
remote_id = None
|
|
999
|
+
|
|
1000
|
+
with self._window_condition:
|
|
1001
|
+
self._local_window_consumed += data_length
|
|
1002
|
+
|
|
1003
|
+
adjust_threshold = self.window_size // 2
|
|
1004
|
+
if self.closed is False and self.remote_id is not None and self._local_window_consumed > 0:
|
|
1005
|
+
if self._local_window_consumed >= adjust_threshold:
|
|
1006
|
+
adjust_length = self._local_window_consumed
|
|
1007
|
+
# Never grow the window above the RFC 4254 maximum.
|
|
1008
|
+
window_headroom = MAX_CHANNEL_WINDOW_SIZE - self.local_window
|
|
1009
|
+
if adjust_length > window_headroom:
|
|
1010
|
+
adjust_length = window_headroom
|
|
1011
|
+
|
|
1012
|
+
self._local_window_consumed -= adjust_length
|
|
1013
|
+
self.local_window += adjust_length
|
|
1014
|
+
remote_id = self.remote_id
|
|
1015
|
+
|
|
1016
|
+
if adjust_length > 0:
|
|
1017
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_WINDOW_ADJUST)
|
|
1018
|
+
pkt.add_uint32(remote_id)
|
|
1019
|
+
pkt.add_uint32(adjust_length)
|
|
1020
|
+
self.connection.transport.send_packet(pkt)
|
|
1021
|
+
return
|
|
1022
|
+
|
|
1023
|
+
def recv_ready(self) -> bool:
|
|
1024
|
+
"""
|
|
1025
|
+
Returns True if :meth:`recv` has something to return without waiting: data, or the end of the stream.
|
|
1026
|
+
|
|
1027
|
+
:returns: True if a call to :meth:`recv` will not block.
|
|
1028
|
+
"""
|
|
1029
|
+
ready = len(self._in_pending) > 0 or self.in_buffer.empty() is False
|
|
1030
|
+
return ready
|
|
1031
|
+
|
|
1032
|
+
def recv_stderr_ready(self) -> bool:
|
|
1033
|
+
"""
|
|
1034
|
+
Returns True if :meth:`recv_stderr` has something to return without waiting: data, or the end of the
|
|
1035
|
+
stream.
|
|
1036
|
+
|
|
1037
|
+
:returns: True if a call to :meth:`recv_stderr` will not block.
|
|
1038
|
+
"""
|
|
1039
|
+
ready = len(self._in_stderr_pending) > 0 or self.in_stderr_buffer.empty() is False
|
|
1040
|
+
return ready
|
|
1041
|
+
|
|
1042
|
+
def exit_status_ready(self) -> bool:
|
|
1043
|
+
"""
|
|
1044
|
+
Returns True once the peer has reported the command's exit status or signal, or the channel has closed.
|
|
1045
|
+
Like ``paramiko.Channel.exit_status_ready`` a closed channel is ready, because the RFC only recommends
|
|
1046
|
+
that the peer reports an exit status, and a peer that closes without one would otherwise leave callers
|
|
1047
|
+
waiting for a status that never comes.
|
|
1048
|
+
|
|
1049
|
+
:returns: True if :meth:`recv_exit_status` has its final value.
|
|
1050
|
+
"""
|
|
1051
|
+
ready = self.exit_status is not None or self.closed is True
|
|
1052
|
+
return ready
|
|
1053
|
+
|
|
1054
|
+
def recv_exit_status(self):
|
|
1055
|
+
return self.exit_status if self.exit_status is not None else -1
|
|
1056
|
+
|
|
1057
|
+
def send_ready(self) -> bool:
|
|
1058
|
+
"""
|
|
1059
|
+
Returns True if :meth:`send` can put at least one byte on the wire without waiting for the peer to open
|
|
1060
|
+
its window. Like ``paramiko.Channel.send_ready`` a channel that is not open reports ready, because
|
|
1061
|
+
:meth:`send` then fails immediately instead of blocking.
|
|
1062
|
+
|
|
1063
|
+
:returns: False only while the channel is open and the peer's window is exhausted.
|
|
1064
|
+
"""
|
|
1065
|
+
ready = True
|
|
1066
|
+
with self._window_condition:
|
|
1067
|
+
if self.active is True and self.closed is False and self.remote_window is not None:
|
|
1068
|
+
if self.remote_window <= 0:
|
|
1069
|
+
ready = False
|
|
1070
|
+
return ready
|
|
1071
|
+
|
|
1072
|
+
def shutdown_write(self):
|
|
1073
|
+
"""
|
|
1074
|
+
Sends SSH_MSG_CHANNEL_EOF: tells the peer that no more data will be sent (RFC 4254 section 5.3).
|
|
1075
|
+
Receiving is not affected. EOF is sent once and never after our SSH_MSG_CHANNEL_CLOSE; :meth:`send`
|
|
1076
|
+
fails afterwards.
|
|
1077
|
+
"""
|
|
1078
|
+
remote_id = None
|
|
1079
|
+
with self._window_condition:
|
|
1080
|
+
if self.eof_sent is False and self._close_sent is False and self.remote_id is not None:
|
|
1081
|
+
self.eof_sent = True
|
|
1082
|
+
remote_id = self.remote_id
|
|
1083
|
+
|
|
1084
|
+
if remote_id is not None:
|
|
1085
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_EOF)
|
|
1086
|
+
pkt.add_uint32(remote_id)
|
|
1087
|
+
self.connection.transport.send_packet(pkt)
|
|
1088
|
+
return
|
|
1089
|
+
|
|
1090
|
+
class SshConnection:
|
|
1091
|
+
"""
|
|
1092
|
+
Handles multiplexing of channels.
|
|
1093
|
+
"""
|
|
1094
|
+
def __init__(self, transport):
|
|
1095
|
+
self.transport = transport
|
|
1096
|
+
self.channels = {} # local_id -> SshChannel
|
|
1097
|
+
self.next_channel_id = 0
|
|
1098
|
+
self.lock = threading.Lock()
|
|
1099
|
+
# The default number of seconds 'open_channel' waits for the peer to confirm a channel (the meaning of
|
|
1100
|
+
# paramiko's 'channel_timeout'). 'None' waits for as long as the transport is open.
|
|
1101
|
+
self.channel_timeout = DEFAULT_SSH_TIMEOUT
|
|
1102
|
+
|
|
1103
|
+
# Register as handler (Assuming Transport supports it, or we manually pump)
|
|
1104
|
+
# For now, we assume Transport calls dispatch_message
|
|
1105
|
+
|
|
1106
|
+
def dispatch_message(self, packet: SshPacket):
|
|
1107
|
+
bytes_copy = packet.payload[:]
|
|
1108
|
+
msg_id = bytes_copy[0]
|
|
1109
|
+
|
|
1110
|
+
# Helper to read from packet while preserving message_id byte if not consumed
|
|
1111
|
+
# Actually my Packet interface consumes.
|
|
1112
|
+
packet._position = 0
|
|
1113
|
+
packet.get_byte() # Skip msg ID
|
|
1114
|
+
|
|
1115
|
+
# Channel handlers are called outside of the connection lock: they take the channel's own lock and may
|
|
1116
|
+
# send packets, which must not stall other threads that are opening channels.
|
|
1117
|
+
if msg_id == sshconstants.SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
|
|
1118
|
+
local_id = packet.get_uint32()
|
|
1119
|
+
remote_id = packet.get_uint32()
|
|
1120
|
+
window_size = packet.get_uint32()
|
|
1121
|
+
max_packet = packet.get_uint32()
|
|
1122
|
+
|
|
1123
|
+
chan = self._find_channel(local_id)
|
|
1124
|
+
if chan is not None:
|
|
1125
|
+
chan.handle_open_confirmation(remote_id, window_size, max_packet)
|
|
1126
|
+
|
|
1127
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_OPEN_FAILURE:
|
|
1128
|
+
local_id = packet.get_uint32()
|
|
1129
|
+
reason_code = packet.get_uint32()
|
|
1130
|
+
description = packet.get_string().decode('utf-8', errors='replace')
|
|
1131
|
+
|
|
1132
|
+
chan = self._find_channel(local_id)
|
|
1133
|
+
if chan is not None:
|
|
1134
|
+
chan.handle_open_failure(reason_code, description)
|
|
1135
|
+
# The peer never allocated its side of the channel, so our channel number is free again.
|
|
1136
|
+
with self.lock:
|
|
1137
|
+
self.channels.pop(local_id, None)
|
|
1138
|
+
|
|
1139
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_WINDOW_ADJUST:
|
|
1140
|
+
local_id = packet.get_uint32()
|
|
1141
|
+
bytes_to_add = packet.get_uint32()
|
|
1142
|
+
|
|
1143
|
+
chan = self._find_channel(local_id)
|
|
1144
|
+
if chan is not None:
|
|
1145
|
+
chan.handle_window_adjust(bytes_to_add)
|
|
1146
|
+
|
|
1147
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_DATA:
|
|
1148
|
+
local_id = packet.get_uint32()
|
|
1149
|
+
data = packet.get_string()
|
|
1150
|
+
|
|
1151
|
+
chan = self._find_channel(local_id)
|
|
1152
|
+
if chan is not None:
|
|
1153
|
+
chan.handle_data(data)
|
|
1154
|
+
|
|
1155
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_EXTENDED_DATA:
|
|
1156
|
+
local_id = packet.get_uint32()
|
|
1157
|
+
data_type_code = packet.get_uint32()
|
|
1158
|
+
data = packet.get_string()
|
|
1159
|
+
|
|
1160
|
+
chan = self._find_channel(local_id)
|
|
1161
|
+
if chan is not None:
|
|
1162
|
+
chan.handle_extended_data(data_type_code, data)
|
|
1163
|
+
|
|
1164
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_EOF:
|
|
1165
|
+
local_id = packet.get_uint32()
|
|
1166
|
+
|
|
1167
|
+
chan = self._find_channel(local_id)
|
|
1168
|
+
if chan is not None:
|
|
1169
|
+
chan.handle_eof()
|
|
1170
|
+
|
|
1171
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_SUCCESS or msg_id == sshconstants.SSH_MSG_CHANNEL_FAILURE:
|
|
1172
|
+
local_id = packet.get_uint32()
|
|
1173
|
+
succeeded = msg_id == sshconstants.SSH_MSG_CHANNEL_SUCCESS
|
|
1174
|
+
|
|
1175
|
+
chan = self._find_channel(local_id)
|
|
1176
|
+
if chan is not None:
|
|
1177
|
+
chan.handle_request_reply(succeeded)
|
|
1178
|
+
|
|
1179
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_REQUEST:
|
|
1180
|
+
local_id = packet.get_uint32()
|
|
1181
|
+
request_type = packet.get_string().decode('ascii', errors='replace')
|
|
1182
|
+
want_reply = packet.get_boolean()
|
|
1183
|
+
|
|
1184
|
+
chan = self._find_channel(local_id)
|
|
1185
|
+
if chan is not None:
|
|
1186
|
+
chan.handle_request(request_type, want_reply, packet)
|
|
1187
|
+
|
|
1188
|
+
elif msg_id == sshconstants.SSH_MSG_GLOBAL_REQUEST:
|
|
1189
|
+
request_name = packet.get_string().decode('ascii', errors='replace')
|
|
1190
|
+
want_reply = packet.get_boolean()
|
|
1191
|
+
|
|
1192
|
+
# RFC 4254 section 4: a request the recipient does not support is answered with
|
|
1193
|
+
# SSH_MSG_REQUEST_FAILURE when the sender wants a reply. This client supports no global requests.
|
|
1194
|
+
logger.debug("Unsupported global request '%s' (want_reply=%s).", request_name, want_reply)
|
|
1195
|
+
if want_reply is True:
|
|
1196
|
+
reply = SshPacket(sshconstants.SSH_MSG_REQUEST_FAILURE)
|
|
1197
|
+
self.transport.send_packet(reply)
|
|
1198
|
+
|
|
1199
|
+
elif msg_id == sshconstants.SSH_MSG_CHANNEL_CLOSE:
|
|
1200
|
+
local_id = packet.get_uint32()
|
|
1201
|
+
|
|
1202
|
+
chan = self._find_channel(local_id)
|
|
1203
|
+
if chan is not None:
|
|
1204
|
+
chan.handle_close()
|
|
1205
|
+
# Handle others...
|
|
1206
|
+
return
|
|
1207
|
+
|
|
1208
|
+
def _find_channel(self, local_id: int) -> Optional[SshChannel]:
|
|
1209
|
+
"""
|
|
1210
|
+
Looks up a channel by its local channel number.
|
|
1211
|
+
|
|
1212
|
+
:param local_id: The local channel number the peer addressed.
|
|
1213
|
+
|
|
1214
|
+
:returns: The channel, or None (logged) if no channel has that number.
|
|
1215
|
+
"""
|
|
1216
|
+
with self.lock:
|
|
1217
|
+
chan = self.channels.get(local_id)
|
|
1218
|
+
|
|
1219
|
+
if chan is None:
|
|
1220
|
+
logger.debug("Ignoring a channel message for unknown local channel %s.", local_id)
|
|
1221
|
+
return chan
|
|
1222
|
+
|
|
1223
|
+
def open_channel(self, kind: str = "session", dest_addr: Optional[tuple] = None, src_addr: Optional[tuple] = None,
|
|
1224
|
+
*, timeout: Optional[float] = None) -> SshChannel:
|
|
1225
|
+
"""
|
|
1226
|
+
Opens a channel and waits for the peer to confirm it, so the returned channel already knows the peer's
|
|
1227
|
+
channel number, window and maximum packet size and requests can be sent on it immediately.
|
|
1228
|
+
|
|
1229
|
+
:param kind: The channel type, for example ``session`` or ``direct-tcpip``.
|
|
1230
|
+
:param dest_addr: For ``direct-tcpip`` / ``forwarded-tcpip``: the ``(host, port)`` the peer connects to.
|
|
1231
|
+
:param src_addr: For ``direct-tcpip`` / ``forwarded-tcpip``: the ``(address, port)`` of the originator.
|
|
1232
|
+
:param timeout: The maximum number of seconds to wait for the peer's confirmation. When ``None`` the
|
|
1233
|
+
connection's ``channel_timeout`` is used.
|
|
1234
|
+
|
|
1235
|
+
:returns: The open channel.
|
|
1236
|
+
:raises: ConnectionError, TimeoutError, SemanticError
|
|
1237
|
+
"""
|
|
1238
|
+
tcpip_kind = kind in TCPIP_CHANNEL_KINDS
|
|
1239
|
+
if tcpip_kind is True:
|
|
1240
|
+
if dest_addr is None or src_addr is None:
|
|
1241
|
+
err_msg = "A '{}' channel requires both 'dest_addr' and 'src_addr' (RFC 4254 section 7).".format(kind)
|
|
1242
|
+
raise SemanticError(err_msg)
|
|
1243
|
+
elif dest_addr is not None or src_addr is not None:
|
|
1244
|
+
err_msg = "'dest_addr' and 'src_addr' only apply to {} channels, not to '{}' channels.".format(
|
|
1245
|
+
TCPIP_CHANNEL_KINDS, kind)
|
|
1246
|
+
raise SemanticError(err_msg)
|
|
1247
|
+
|
|
1248
|
+
if threading.current_thread() is self.transport:
|
|
1249
|
+
err_msg_lines = [
|
|
1250
|
+
"open_channel() was called on the SSH transport's reader thread.",
|
|
1251
|
+
"It waits for SSH_MSG_CHANNEL_OPEN_CONFIRMATION, which only that thread can deliver, so it would hang",
|
|
1252
|
+
"until it timed out. Open channels from another thread, not from a message handler.",
|
|
1253
|
+
]
|
|
1254
|
+
err_msg = os.linesep.join(err_msg_lines)
|
|
1255
|
+
raise SemanticError(err_msg)
|
|
1256
|
+
|
|
1257
|
+
wait_timeout = timeout
|
|
1258
|
+
if wait_timeout is None:
|
|
1259
|
+
wait_timeout = self.channel_timeout
|
|
1260
|
+
|
|
1261
|
+
with self.lock:
|
|
1262
|
+
local_id = self.next_channel_id
|
|
1263
|
+
self.next_channel_id += 1
|
|
1264
|
+
chan = SshChannel(self, local_id)
|
|
1265
|
+
self.channels[local_id] = chan
|
|
1266
|
+
|
|
1267
|
+
pkt = SshPacket(sshconstants.SSH_MSG_CHANNEL_OPEN)
|
|
1268
|
+
pkt.add_string(kind)
|
|
1269
|
+
pkt.add_uint32(local_id)
|
|
1270
|
+
pkt.add_uint32(chan.window_size)
|
|
1271
|
+
pkt.add_uint32(chan.max_packet_size)
|
|
1272
|
+
|
|
1273
|
+
if tcpip_kind is True:
|
|
1274
|
+
dest_host, dest_port = dest_addr
|
|
1275
|
+
src_host, src_port = src_addr
|
|
1276
|
+
pkt.add_string(dest_host)
|
|
1277
|
+
pkt.add_uint32(dest_port)
|
|
1278
|
+
pkt.add_string(src_host)
|
|
1279
|
+
pkt.add_uint32(src_port)
|
|
1280
|
+
|
|
1281
|
+
self.transport.send_packet(pkt)
|
|
1282
|
+
|
|
1283
|
+
# The confirmation is delivered by the transport thread; it may already have arrived.
|
|
1284
|
+
chan.wait_for_open(wait_timeout, kind=kind)
|
|
1285
|
+
return chan
|