ccssh 1.0.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.
ccssh/chansock.py ADDED
@@ -0,0 +1,227 @@
1
+ """
2
+ A socket-like object tunneled over one SSH direct-tcpip channel.
3
+
4
+ Used for -J (ProxyJump): the outer SSH handshake runs over a plain TCP
5
+ socket, the inner one over this. Implements the small surface SyncTransport
6
+ needs: sendall / recv / settimeout / close.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Protocol
12
+
13
+ from ccssh import channel as CH
14
+ from ccssh import wire as W
15
+ from ccssh.transport import CCSSHTimeoutError
16
+
17
+
18
+ class StreamSocket(Protocol):
19
+ """
20
+ The socket surface SyncTransport needs. Real sockets and ChannelSocket
21
+ both satisfy it structurally.
22
+ """
23
+
24
+ def sendall(self, data: bytes) -> None: ...
25
+ def recv(self, n: int) -> bytes: ...
26
+ def settimeout(self, timeout: float | None) -> None: ...
27
+ def close(self) -> None: ...
28
+
29
+
30
+ class ChannelSocket(StreamSocket):
31
+ """
32
+ One direct-tcpip channel, socket-shaped. Single-threaded by design:
33
+ the owning connection must not run concurrent channel operations
34
+ (same rule as exec/sftp/shell).
35
+ """
36
+
37
+ def __init__(self, transport, srv: int, window: int, maxpacket: int, timeout: float | None = 10.0, mux=None, chan: int | None = None):
38
+ self._t = transport
39
+ self._srv = srv
40
+ self._window = window
41
+ # Honor the server's maxpacket but never exceed ours: a rogue
42
+ # server announcing megabytes would make enc_data raise mid-send.
43
+ self._maxpacket = max(1024, min(maxpacket, CH.MAXPKT))
44
+ self._timeout = timeout
45
+ self._mux = mux
46
+ self._chan = chan
47
+ self._rbuf = b""
48
+ self._consumed = 0
49
+ self._closed = False
50
+ self._eof = False
51
+ self._skips = 0
52
+
53
+ def _deadline(self) -> float | None:
54
+ if self._timeout is None:
55
+ return None # blocking mode, like a plain socket
56
+ return time.monotonic() + self._timeout
57
+
58
+ def _expired(self, deadline: float | None) -> bool:
59
+ return deadline is not None and time.monotonic() > deadline
60
+
61
+ def _check_chan(self, rr: W.Reader) -> bool:
62
+ """
63
+ True if this packet is addressed to our channel.
64
+
65
+ With a mux the queue is already per-channel; without one the jump
66
+ connection carries only this channel. Either way, a foreign id is
67
+ never ours - skip it instead of crediting/buffering it.
68
+ """
69
+ try:
70
+ rcpt = rr.u32()
71
+ except Exception:
72
+ return False
73
+ if self._chan is not None and rcpt != self._chan and rcpt != self._srv:
74
+ return False
75
+ return True
76
+
77
+ def _recv_one(self, timeout: float | None = None):
78
+ """One inbound packet: mux queue when shared, else direct read."""
79
+ if self._mux is not None and self._chan is not None:
80
+ return self._mux.recv_chan(self._chan, timeout)
81
+ return self._t.recv_packet(keepalive=False)
82
+
83
+ def settimeout(self, timeout: float | None) -> None:
84
+ # None means blocking mode, like a plain socket (never 3600s).
85
+ self._timeout = timeout
86
+
87
+ def sendall(self, data: bytes) -> None:
88
+ if self._closed:
89
+ raise OSError("channel closed")
90
+ off = 0
91
+ while off < len(data):
92
+ chunk = data[off:off + self._maxpacket]
93
+ self._top_up(len(chunk))
94
+ self._t.send_packet(CH.enc_data(self._srv, chunk))
95
+ off += len(chunk)
96
+
97
+ def _got_data(self, chunk: bytes) -> None:
98
+ """
99
+ Buffer inbound channel data and re-credit the server window.
100
+ """
101
+ if len(self._rbuf) + len(chunk) > CH.MAX_BUFFER:
102
+ self._closed = True
103
+ raise OSError("channel receive buffer exceeded")
104
+ self._rbuf += chunk
105
+ self._consumed += len(chunk)
106
+ if self._consumed >= CH.WINDOW_THRESHOLD:
107
+ try:
108
+ self._t.send_packet(CH.enc_window_adjust(self._srv, self._consumed))
109
+ except Exception:
110
+ return # keep the credit: retry on the next chunk
111
+ self._consumed = 0
112
+
113
+ def _note_service(self) -> None:
114
+ """Count non-channel service packets inside a window/recv wait."""
115
+ self._skips += 1
116
+ if self._skips > 64:
117
+ raise OSError("too many service messages")
118
+
119
+ def _top_up(self, need: int) -> None:
120
+ """
121
+ Wait for channel window, answering service traffic meanwhile.
122
+ The jump connection carries only this channel, so any DATA that
123
+ is not ours cannot arrive; anything else is service noise.
124
+ """
125
+ deadline = self._deadline()
126
+ while self._window < need:
127
+ if self._expired(deadline):
128
+ raise OSError("channel send window exhausted")
129
+ try:
130
+ p = self._recv_one(timeout=None if deadline is None else max(0.01, deadline - time.monotonic()))
131
+ except CCSSHTimeoutError as e:
132
+ raise OSError("channel send window exhausted") from e
133
+ m = p[0]
134
+ rr = W.Reader(p)
135
+ rr.byte()
136
+ if m == CH.MSG_WINDOW_ADJUST:
137
+ if not self._check_chan(rr):
138
+ self._note_service()
139
+ continue
140
+ try:
141
+ self._window = CH.check_window(self._window, rr.u32())
142
+ except ValueError as e:
143
+ raise OSError(f"bad window adjust: {e}") from e
144
+ self._skips = 0
145
+ elif m == CH.MSG_CHANNEL_DATA:
146
+ if not self._check_chan(rr):
147
+ self._note_service()
148
+ continue
149
+ self._got_data(rr.string())
150
+ self._skips = 0
151
+ elif m == CH.MSG_CHANNEL_CLOSE:
152
+ self._closed = True
153
+ raise OSError("channel closed by peer")
154
+ elif m == CH.MSG_CHANNEL_EOF:
155
+ self._eof = True
156
+ continue
157
+ elif m == 20:
158
+ raise OSError("peer started rekey with an open channel")
159
+ else:
160
+ self._note_service()
161
+
162
+ def recv(self, n: int) -> bytes:
163
+ if n <= 0:
164
+ return b""
165
+ if (self._closed or self._eof) and not self._rbuf:
166
+ return b""
167
+ deadline = self._deadline()
168
+ while len(self._rbuf) < n:
169
+ if self._expired(deadline):
170
+ if self._rbuf:
171
+ break
172
+ raise CCSSHTimeoutError("channel read timeout")
173
+ try:
174
+ p = self._recv_one(timeout=None if deadline is None else max(0.01, deadline - time.monotonic()))
175
+ except CCSSHTimeoutError:
176
+ if self._rbuf:
177
+ break
178
+ raise
179
+ m = p[0]
180
+ rr = W.Reader(p)
181
+ rr.byte()
182
+ if m == CH.MSG_CHANNEL_DATA:
183
+ if not self._check_chan(rr):
184
+ self._note_service()
185
+ continue
186
+ self._got_data(rr.string())
187
+ self._skips = 0
188
+ elif m == CH.MSG_WINDOW_ADJUST:
189
+ if not self._check_chan(rr):
190
+ self._note_service()
191
+ continue
192
+ try:
193
+ self._window = CH.check_window(self._window, rr.u32())
194
+ except ValueError as e:
195
+ raise OSError(f"bad window adjust: {e}") from e
196
+ self._skips = 0
197
+ elif m == CH.MSG_CHANNEL_EOF:
198
+ rr.u32()
199
+ self._eof = True
200
+ break
201
+ elif m == CH.MSG_CHANNEL_CLOSE:
202
+ rr.u32()
203
+ self._closed = True
204
+ break
205
+ elif m == 20:
206
+ raise OSError("peer started rekey with an open channel")
207
+ else:
208
+ self._note_service()
209
+ out, self._rbuf = self._rbuf[:n], self._rbuf[n:]
210
+ return out
211
+
212
+ def close(self) -> None:
213
+ if not self._closed:
214
+ self._closed = True
215
+ try:
216
+ self._t.send_packet(CH.enc_eof(self._srv))
217
+ except Exception:
218
+ pass
219
+ try:
220
+ self._t.send_packet(CH.enc_close(self._srv))
221
+ except Exception:
222
+ pass
223
+ if self._mux is not None and self._chan is not None:
224
+ try:
225
+ self._mux.unregister(self._chan)
226
+ except Exception:
227
+ pass