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.
@@ -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