1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
2
# See LICENSE for details.
6
"""This module contains the implementation of the ssh-connection service, which
7
allows access to the shell and port-forwarding.
9
This module is unstable.
11
Maintainer: U{Paul Swartz<mailto:z3p@twistedmatrix.com>}
16
from twisted.internet import protocol, reactor, defer
17
from twisted.python import log
18
from twisted.conch import error
19
import service, common, session, forwarding
21
class SSHConnection(service.SSHService):
22
name = 'ssh-connection'
25
self.localChannelID = 0 # this is the current # to use for channel ID
26
self.localToRemoteChannel = {} # local channel ID -> remote channel ID
27
self.channels = {} # local channel ID -> subclass of SSHChannel
28
self.channelsToRemoteChannel = {} # subclass of SSHChannel ->
30
self.deferreds = {} # local channel -> list of deferreds for pending
31
# requests or 'global' -> list of deferreds for
33
self.transport = None # gets set later
35
def serviceStarted(self):
36
if hasattr(self.transport, 'avatar'):
37
self.transport.avatar.conn = self
39
def serviceStopped(self):
40
map(self.channelClosed, self.channels.values())
43
def ssh_GLOBAL_REQUEST(self, packet):
44
requestType, rest = common.getNS(packet)
45
wantReply, rest = ord(rest[0]), rest[1:]
46
reply = MSG_REQUEST_FAILURE
48
ret = self.gotGlobalRequest(requestType, rest)
50
reply = MSG_REQUEST_SUCCESS
51
if type(ret) in (types.TupleType, types.ListType):
54
reply = MSG_REQUEST_FAILURE
56
self.transport.sendPacket(reply, data)
58
def ssh_REQUEST_SUCCESS(self, packet):
61
self.deferreds['global'].pop(0).callback(data)
63
def ssh_REQUEST_FAILURE(self, packet):
65
self.deferreds['global'].pop(0).errback(
66
error.ConchError('global request failed', packet))
68
def ssh_CHANNEL_OPEN(self, packet):
69
channelType, rest = common.getNS(packet)
70
senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[: 12])
73
channel = self.getChannel(channelType, windowSize, maxPacket, packet)
74
localChannel = self.localChannelID
75
self.localChannelID+=1
76
channel.id = localChannel
77
self.channels[localChannel] = channel
78
self.channelsToRemoteChannel[channel] = senderChannel
79
self.localToRemoteChannel[localChannel] = senderChannel
80
self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION,
81
struct.pack('>4L', senderChannel, localChannel,
82
channel.localWindowSize,
83
channel.localMaxPacket)+channel.specificData)
84
log.callWithLogger(channel, channel.channelOpen, '')
86
log.msg('channel open failed')
88
if isinstance(e, error.ConchError):
89
reason, textualInfo = e.args[0], e.data
91
reason = OPEN_CONNECT_FAILED
92
textualInfo = "unknown failure"
93
self.transport.sendPacket(MSG_CHANNEL_OPEN_FAILURE,
94
struct.pack('>2L', senderChannel, reason)+ \
95
common.NS(textualInfo)+common.NS(''))
97
def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
98
localChannel, remoteChannel, windowSize, maxPacket = struct.unpack('>4L', packet[: 16])
99
specificData = packet[16:]
100
channel = self.channels[localChannel]
102
self.localToRemoteChannel[localChannel] = remoteChannel
103
self.channelsToRemoteChannel[channel] = remoteChannel
104
channel.remoteWindowLeft = windowSize
105
channel.remoteMaxPacket = maxPacket
106
log.callWithLogger(channel, channel.channelOpen, specificData)
108
def ssh_CHANNEL_OPEN_FAILURE(self, packet):
109
localChannel, reasonCode = struct.unpack('>2L', packet[: 8])
110
reasonDesc = common.getNS(packet[8:])[0]
111
channel = self.channels[localChannel]
112
del self.channels[localChannel]
114
reason = error.ConchError(reasonDesc, reasonCode)
115
log.callWithLogger(channel, channel.openFailed, reason)
117
def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
118
localChannel, bytesToAdd = struct.unpack('>2L', packet[: 8])
119
channel = self.channels[localChannel]
120
log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd)
122
def ssh_CHANNEL_DATA(self, packet):
123
localChannel, dataLength = struct.unpack('>2L', packet[: 8])
124
channel = self.channels[localChannel]
125
# XXX should this move to dataReceived to put client in charge?
126
if dataLength > channel.localWindowLeft or \
127
dataLength > channel.localMaxPacket: # more data than we want
128
log.callWithLogger(channel, lambda s=self,c=channel:
129
log.msg('too much data') and s.sendClose(c))
131
#packet = packet[:channel.localWindowLeft+4]
132
data = common.getNS(packet[4:])[0]
133
channel.localWindowLeft-=dataLength
134
if channel.localWindowLeft < channel.localWindowSize/2:
135
self.adjustWindow(channel, channel.localWindowSize - \
136
channel.localWindowLeft)
137
#log.msg('local window left: %s/%s' % (channel.localWindowLeft,
138
# channel.localWindowSize))
139
log.callWithLogger(channel, channel.dataReceived, data)
141
def ssh_CHANNEL_EXTENDED_DATA(self, packet):
142
localChannel, typeCode, dataLength = struct.unpack('>3L', packet[: 12])
143
channel = self.channels[localChannel]
144
if dataLength > channel.localWindowLeft or \
145
dataLength > channel.localMaxPacket:
146
log.callWithLogger(channel, lambda s=self,c=channel:
147
log.msg('too much extdata') and s.sendClose(c))
149
data = common.getNS(packet[8:])[0]
150
channel.localWindowLeft -= dataLength
151
if channel.localWindowLeft < channel.localWindowSize/2:
152
self.adjustWindow(channel, channel.localWindowSize - \
153
channel.localWindowLeft)
154
log.callWithLogger(channel, channel.extReceived, typeCode, data)
156
def ssh_CHANNEL_EOF(self, packet):
157
localChannel = struct.unpack('>L', packet[: 4])[0]
158
channel = self.channels[localChannel]
159
log.callWithLogger(channel, channel.eofReceived)
161
def ssh_CHANNEL_CLOSE(self, packet):
162
localChannel = struct.unpack('>L', packet[: 4])[0]
163
channel = self.channels[localChannel]
164
if channel.remoteClosed:
166
log.callWithLogger(channel, channel.closeReceived)
167
channel.remoteClosed = 1
168
if channel.localClosed and channel.remoteClosed:
169
self.channelClosed(channel)
171
def ssh_CHANNEL_REQUEST(self, packet):
172
localChannel = struct.unpack('>L', packet[: 4])[0]
173
requestType, rest = common.getNS(packet[4:])
174
wantReply = ord(rest[0])
175
channel = self.channels[localChannel]
176
d = log.callWithLogger(channel, channel.requestReceived, requestType, rest[1:])
178
if isinstance(d, defer.Deferred):
179
d.addCallback(self._cbChannelRequest, localChannel)
180
d.addErrback(self._ebChannelRequest, localChannel)
182
self._cbChannelRequest(None, localChannel)
184
self._ebChannelRequest(None, localChannel)
186
def _cbChannelRequest(self, result, localChannel):
187
self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L',
188
self.localToRemoteChannel[localChannel]))
190
def _ebChannelRequest(self, result, localChannel):
191
self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L',
192
self.localToRemoteChannel[localChannel]))
194
def ssh_CHANNEL_SUCCESS(self, packet):
195
localChannel = struct.unpack('>L', packet[: 4])[0]
196
if self.deferreds.get(localChannel):
197
d = self.deferreds[localChannel].pop(0)
198
log.callWithLogger(self.channels[localChannel],
199
d.callback, packet[4:])
201
def ssh_CHANNEL_FAILURE(self, packet):
202
localChannel = struct.unpack('>L', packet[: 4])[0]
203
if self.deferreds.get(localChannel):
204
d = self.deferreds[localChannel].pop(0)
205
log.callWithLogger(self.channels[localChannel],
207
error.ConchError('channel request failed'))
209
# methods for users of the connection to call
211
def sendGlobalRequest(self, request, data, wantReply = 0):
213
Send a global request for this connection. Current this is only used
214
for remote->local TCP forwarding.
216
@type request: C{str}
218
@type wantReply: C{bool}
219
@rtype C{Deferred}/C{None}
221
self.transport.sendPacket(MSG_GLOBAL_REQUEST,
223
+ (wantReply and '\xff' or '\x00')
227
self.deferreds.setdefault('global', []).append(d)
230
def openChannel(self, channel, extra = ''):
232
Open a new channel on this connection.
234
@type channel: subclass of C{SSHChannel}
237
log.msg('opening channel %s with %s %s'%(self.localChannelID,
238
channel.localWindowSize, channel.localMaxPacket))
239
self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name)
240
+struct.pack('>3L', self.localChannelID,
241
channel.localWindowSize, channel.localMaxPacket)
243
channel.id = self.localChannelID
244
self.channels[self.localChannelID] = channel
245
self.localChannelID+=1
247
def sendRequest(self, channel, requestType, data, wantReply = 0):
249
Send a request to a channel.
251
@type channel: subclass of C{SSHChannel}
252
@type requestType: C{str}
254
@type wantReply: C{bool}
255
@rtype C{Deferred}/C{None}
257
if channel.localClosed:
259
log.msg('sending request %s' % requestType)
260
self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L',
261
self.channelsToRemoteChannel[channel])
262
+ common.NS(requestType)+chr(wantReply)
266
self.deferreds.setdefault(channel.id, []).append(d)
269
def adjustWindow(self, channel, bytesToAdd):
271
Tell the other side that we will receive more data. This should not
272
normally need to be called as it is managed automatically.
274
@type channel: subclass of L{SSHChannel}
275
@type bytesToAdd: C{int}
277
if channel.localClosed:
278
return # we're already closed
279
self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L',
280
self.channelsToRemoteChannel[channel],
282
log.msg('adding %i to %i in channel %i' % (bytesToAdd, channel.localWindowLeft, channel.id))
283
channel.localWindowLeft+=bytesToAdd
285
def sendData(self, channel, data):
287
Send data to a channel. This should not normally be used: instead use
288
channel.write(data) as it manages the window automatically.
290
@type channel: subclass of L{SSHChannel}
293
if channel.localClosed:
294
return # we're already closed
295
self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L',
296
self.channelsToRemoteChannel[channel])+ \
299
def sendExtendedData(self, channel, dataType, data):
301
Send extended data to a channel. This should not normally be used:
302
instead use channel.writeExtendedData(data, dataType) as it manages
303
the window automatically.
305
@type channel: subclass of L{SSHChannel}
306
@type dataType: C{int}
309
if channel.localClosed:
310
return # we're already closed
311
self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L',
312
self.channelsToRemoteChannel[channel],dataType) \
315
def sendEOF(self, channel):
317
Send an EOF (End of File) for a channel.
319
@type channel: subclass of L{SSHChannel}
321
if channel.localClosed:
322
return # we're already closed
323
log.msg('sending eof')
324
self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L',
325
self.channelsToRemoteChannel[channel]))
327
def sendClose(self, channel):
331
@type channel: subclass of L{SSHChannel}
333
if channel.localClosed:
334
return # we're already closed
335
log.msg('sending close %i' % channel.id)
336
self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L',
337
self.channelsToRemoteChannel[channel]))
338
channel.localClosed = 1
339
if channel.localClosed and channel.remoteClosed:
340
self.channelClosed(channel)
342
# methods to override
343
def getChannel(self, channelType, windowSize, maxPacket, data):
345
The other side requested a channel of some sort.
346
channelType is the type of channel being requested,
347
windowSize is the initial size of the remote window,
348
maxPacket is the largest packet we should send,
349
data is any other packet data (often nothing).
351
We return a subclass of L{SSHChannel}.
353
By default, this dispatches to a method 'channel_channelType' with any
354
non-alphanumerics in the channelType replace with _'s. If it cannot
355
find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
356
The method is called with arguments of windowSize, maxPacket, data.
358
@type channelType: C{str}
359
@type windowSize: C{int}
360
@type maxPacket: C{int}
362
@rtype: subclass of L{SSHChannel}/C{tuple}
364
log.msg('got channel %s request' % channelType)
365
if hasattr(self.transport, "avatar"): # this is a server!
366
chan = self.transport.avatar.lookupChannel(channelType,
373
channelType = channelType.translate(TRANSLATE_TABLE)
374
f = getattr(self, 'channel_%s' % channelType, None)
376
return OPEN_UNKNOWN_CHANNEL_TYPE, "don't know that channel"
377
return f(windowSize, maxPacket, data)
379
def gotGlobalRequest(self, requestType, data):
381
We got a global request. pretty much, this is just used by the client
382
to request that we forward a port from the server to the client.
384
- 1: request accepted
385
- 1, <data>: request accepted with request specific data
388
By default, this dispatches to a method 'global_requestType' with
389
-'s in requestType replaced with _'s. The found method is passed data.
390
If this method cannot be found, this method returns 0. Otherwise, it
391
returns the return value of that method.
393
@type requestType: C{str}
395
@rtype: C{int}/C{tuple}
397
log.msg('got global %s request' % requestType)
398
if hasattr(self.transport, 'avatar'): # this is a server!
399
return self.transport.avatar.gotGlobalRequest(requestType, data)
401
requestType = requestType.replace('-','_')
402
f = getattr(self, 'global_%s' % requestType, None)
407
def channelClosed(self, channel):
409
Called when a channel is closed.
410
It clears the local state related to the channel, and calls
412
MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
413
If you don't, things will break mysteriously.
415
channel.localClosed = channel.remoteClosed = 1
416
del self.localToRemoteChannel[channel.id]
417
del self.channels[channel.id]
418
del self.channelsToRemoteChannel[channel]
419
self.deferreds[channel.id] = []
420
log.callWithLogger(channel, channel.closed)
422
MSG_GLOBAL_REQUEST = 80
423
MSG_REQUEST_SUCCESS = 81
424
MSG_REQUEST_FAILURE = 82
425
MSG_CHANNEL_OPEN = 90
426
MSG_CHANNEL_OPEN_CONFIRMATION = 91
427
MSG_CHANNEL_OPEN_FAILURE = 92
428
MSG_CHANNEL_WINDOW_ADJUST = 93
429
MSG_CHANNEL_DATA = 94
430
MSG_CHANNEL_EXTENDED_DATA = 95
432
MSG_CHANNEL_CLOSE = 97
433
MSG_CHANNEL_REQUEST = 98
434
MSG_CHANNEL_SUCCESS = 99
435
MSG_CHANNEL_FAILURE = 100
437
OPEN_ADMINISTRATIVELY_PROHIBITED = 1
438
OPEN_CONNECT_FAILED = 2
439
OPEN_UNKNOWN_CHANNEL_TYPE = 3
440
OPEN_RESOURCE_SHORTAGE = 4
442
EXTENDED_DATA_STDERR = 1
446
for v in dir(connection):
448
messages[getattr(connection, v)] = v # doesn't handle doubles
451
alphanums = string.letters + string.digits
452
TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_' for i in range(256)])
453
SSHConnection.protocolMessages = messages