1 # -*- test-case-name: twisted.conch.test.test_connection -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # 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 Maintainer: Paul Swartz
14 from twisted.conch.ssh import service, common
15 from twisted.conch import error
16 from twisted.internet import defer
17 from twisted.python import log
19 class SSHConnection(service.SSHService):
21 An implementation of the 'ssh-connection' service. It is used to
22 multiplex multiple channels over the single SSH connection.
24 @ivar localChannelID: the next number to use as a local channel ID.
25 @type localChannelID: C{int}
26 @ivar channels: a C{dict} mapping a local channel ID to C{SSHChannel}
28 @type channels: C{dict}
29 @ivar localToRemoteChannel: a C{dict} mapping a local channel ID to a
31 @type localToRemoteChannel: C{dict}
32 @ivar channelsToRemoteChannel: a C{dict} mapping a C{SSHChannel} subclass
34 @type channelsToRemoteChannel: C{dict}
35 @ivar deferreds: a C{dict} mapping a local channel ID to a C{list} of
36 C{Deferreds} for outstanding channel requests. Also, the 'global'
37 key stores the C{list} of pending global request C{Deferred}s.
39 name = 'ssh-connection'
42 self.localChannelID = 0 # this is the current # to use for channel ID
43 self.localToRemoteChannel = {} # local channel ID -> remote channel ID
44 self.channels = {} # local channel ID -> subclass of SSHChannel
45 self.channelsToRemoteChannel = {} # subclass of SSHChannel ->
47 self.deferreds = {"global": []} # local channel -> list of deferreds
48 # for pending requests or 'global' -> list of
49 # deferreds for global requests
50 self.transport = None # gets set later
53 def serviceStarted(self):
54 if hasattr(self.transport, 'avatar'):
55 self.transport.avatar.conn = self
58 def serviceStopped(self):
60 Called when the connection is stopped.
62 map(self.channelClosed, self.channels.values())
63 self._cleanupGlobalDeferreds()
66 def _cleanupGlobalDeferreds(self):
68 All pending requests that have returned a deferred must be errbacked
69 when this service is stopped, otherwise they might be left uncalled and
72 for d in self.deferreds["global"]:
73 d.errback(error.ConchError("Connection stopped."))
74 del self.deferreds["global"][:]
78 def ssh_GLOBAL_REQUEST(self, packet):
80 The other side has made a global request. Payload::
83 <request specific data>
85 This dispatches to self.gotGlobalRequest.
87 requestType, rest = common.getNS(packet)
88 wantReply, rest = ord(rest[0]), rest[1:]
89 ret = self.gotGlobalRequest(requestType, rest)
91 reply = MSG_REQUEST_FAILURE
94 reply = MSG_REQUEST_SUCCESS
95 if isinstance(ret, (tuple, list)):
97 self.transport.sendPacket(reply, data)
99 def ssh_REQUEST_SUCCESS(self, packet):
101 Our global request succeeded. Get the appropriate Deferred and call
102 it back with the packet we received.
105 self.deferreds['global'].pop(0).callback(packet)
107 def ssh_REQUEST_FAILURE(self, packet):
109 Our global request failed. Get the appropriate Deferred and errback
110 it with the packet we received.
113 self.deferreds['global'].pop(0).errback(
114 error.ConchError('global request failed', packet))
116 def ssh_CHANNEL_OPEN(self, packet):
118 The other side wants to get a channel. Payload::
120 uint32 remote channel number
121 uint32 remote window size
122 uint32 remote maximum packet size
123 <channel specific data>
125 We get a channel from self.getChannel(), give it a local channel number
126 and notify the other side. Then notify the channel by calling its
129 channelType, rest = common.getNS(packet)
130 senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[:12])
133 channel = self.getChannel(channelType, windowSize, maxPacket,
135 localChannel = self.localChannelID
136 self.localChannelID += 1
137 channel.id = localChannel
138 self.channels[localChannel] = channel
139 self.channelsToRemoteChannel[channel] = senderChannel
140 self.localToRemoteChannel[localChannel] = senderChannel
141 self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION,
142 struct.pack('>4L', senderChannel, localChannel,
143 channel.localWindowSize,
144 channel.localMaxPacket)+channel.specificData)
145 log.callWithLogger(channel, channel.channelOpen, packet)
147 log.msg('channel open failed')
149 if isinstance(e, error.ConchError):
150 textualInfo, reason = e.args
151 if isinstance(textualInfo, (int, long)):
152 # See #3657 and #3071
153 textualInfo, reason = reason, textualInfo
155 reason = OPEN_CONNECT_FAILED
156 textualInfo = "unknown failure"
157 self.transport.sendPacket(
158 MSG_CHANNEL_OPEN_FAILURE,
159 struct.pack('>2L', senderChannel, reason) +
160 common.NS(textualInfo) + common.NS(''))
162 def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
164 The other side accepted our MSG_CHANNEL_OPEN request. Payload::
165 uint32 local channel number
166 uint32 remote channel number
167 uint32 remote window size
168 uint32 remote maximum packet size
169 <channel specific data>
171 Find the channel using the local channel number and notify its
174 (localChannel, remoteChannel, windowSize,
175 maxPacket) = struct.unpack('>4L', packet[: 16])
176 specificData = packet[16:]
177 channel = self.channels[localChannel]
179 self.localToRemoteChannel[localChannel] = remoteChannel
180 self.channelsToRemoteChannel[channel] = remoteChannel
181 channel.remoteWindowLeft = windowSize
182 channel.remoteMaxPacket = maxPacket
183 log.callWithLogger(channel, channel.channelOpen, specificData)
185 def ssh_CHANNEL_OPEN_FAILURE(self, packet):
187 The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
188 uint32 local channel number
190 string reason description
192 Find the channel using the local channel number and notify it by
193 calling its openFailed() method.
195 localChannel, reasonCode = struct.unpack('>2L', packet[:8])
196 reasonDesc = common.getNS(packet[8:])[0]
197 channel = self.channels[localChannel]
198 del self.channels[localChannel]
200 reason = error.ConchError(reasonDesc, reasonCode)
201 log.callWithLogger(channel, channel.openFailed, reason)
203 def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
205 The other side is adding bytes to its window. Payload::
206 uint32 local channel number
209 Call the channel's addWindowBytes() method to add new bytes to the
212 localChannel, bytesToAdd = struct.unpack('>2L', packet[:8])
213 channel = self.channels[localChannel]
214 log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd)
216 def ssh_CHANNEL_DATA(self, packet):
218 The other side is sending us data. Payload::
219 uint32 local channel number
222 Check to make sure the other side hasn't sent too much data (more
223 than what's in the window, or more than the maximum packet size). If
224 they have, close the channel. Otherwise, decrease the available
225 window and pass the data to the channel's dataReceived().
227 localChannel, dataLength = struct.unpack('>2L', packet[:8])
228 channel = self.channels[localChannel]
229 # XXX should this move to dataReceived to put client in charge?
230 if (dataLength > channel.localWindowLeft or
231 dataLength > channel.localMaxPacket): # more data than we want
232 log.callWithLogger(channel, log.msg, 'too much data')
233 self.sendClose(channel)
235 #packet = packet[:channel.localWindowLeft+4]
236 data = common.getNS(packet[4:])[0]
237 channel.localWindowLeft -= dataLength
238 if channel.localWindowLeft < channel.localWindowSize / 2:
239 self.adjustWindow(channel, channel.localWindowSize - \
240 channel.localWindowLeft)
241 #log.msg('local window left: %s/%s' % (channel.localWindowLeft,
242 # channel.localWindowSize))
243 log.callWithLogger(channel, channel.dataReceived, data)
245 def ssh_CHANNEL_EXTENDED_DATA(self, packet):
247 The other side is sending us exteneded data. Payload::
248 uint32 local channel number
252 Check to make sure the other side hasn't sent too much data (more
253 than what's in the window, or or than the maximum packet size). If
254 they have, close the channel. Otherwise, decrease the available
255 window and pass the data and type code to the channel's
258 localChannel, typeCode, dataLength = struct.unpack('>3L', packet[:12])
259 channel = self.channels[localChannel]
260 if (dataLength > channel.localWindowLeft or
261 dataLength > channel.localMaxPacket):
262 log.callWithLogger(channel, log.msg, 'too much extdata')
263 self.sendClose(channel)
265 data = common.getNS(packet[8:])[0]
266 channel.localWindowLeft -= dataLength
267 if channel.localWindowLeft < channel.localWindowSize / 2:
268 self.adjustWindow(channel, channel.localWindowSize -
269 channel.localWindowLeft)
270 log.callWithLogger(channel, channel.extReceived, typeCode, data)
272 def ssh_CHANNEL_EOF(self, packet):
274 The other side is not sending any more data. Payload::
275 uint32 local channel number
277 Notify the channel by calling its eofReceived() method.
279 localChannel = struct.unpack('>L', packet[:4])[0]
280 channel = self.channels[localChannel]
281 log.callWithLogger(channel, channel.eofReceived)
283 def ssh_CHANNEL_CLOSE(self, packet):
285 The other side is closing its end; it does not want to receive any
287 uint32 local channel number
289 Notify the channnel by calling its closeReceived() method. If
290 the channel has also sent a close message, call self.channelClosed().
292 localChannel = struct.unpack('>L', packet[:4])[0]
293 channel = self.channels[localChannel]
294 log.callWithLogger(channel, channel.closeReceived)
295 channel.remoteClosed = True
296 if channel.localClosed and channel.remoteClosed:
297 self.channelClosed(channel)
299 def ssh_CHANNEL_REQUEST(self, packet):
301 The other side is sending a request to a channel. Payload::
302 uint32 local channel number
305 <request specific data>
307 Pass the message to the channel's requestReceived method. If the
308 other side wants a reply, add callbacks which will send the
311 localChannel = struct.unpack('>L', packet[: 4])[0]
312 requestType, rest = common.getNS(packet[4:])
313 wantReply = ord(rest[0])
314 channel = self.channels[localChannel]
315 d = defer.maybeDeferred(log.callWithLogger, channel,
316 channel.requestReceived, requestType, rest[1:])
318 d.addCallback(self._cbChannelRequest, localChannel)
319 d.addErrback(self._ebChannelRequest, localChannel)
322 def _cbChannelRequest(self, result, localChannel):
324 Called back if the other side wanted a reply to a channel request. If
325 the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
326 a C{error.ConchError}
328 @param result: the value returned from the channel's requestReceived()
329 method. If it's False, the request failed.
330 @type result: C{bool}
331 @param localChannel: the local channel ID of the channel to which the
333 @type localChannel: C{int}
334 @raises ConchError: if the result is False.
337 raise error.ConchError('failed request')
338 self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L',
339 self.localToRemoteChannel[localChannel]))
341 def _ebChannelRequest(self, result, localChannel):
343 Called if the other wisde wanted a reply to the channel requeset and
344 the channel request failed.
346 @param result: a Failure, but it's not used.
347 @param localChannel: the local channel ID of the channel to which the
349 @type localChannel: C{int}
351 self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L',
352 self.localToRemoteChannel[localChannel]))
354 def ssh_CHANNEL_SUCCESS(self, packet):
356 Our channel request to the other other side succeeded. Payload::
357 uint32 local channel number
359 Get the C{Deferred} out of self.deferreds and call it back.
361 localChannel = struct.unpack('>L', packet[:4])[0]
362 if self.deferreds.get(localChannel):
363 d = self.deferreds[localChannel].pop(0)
364 log.callWithLogger(self.channels[localChannel],
367 def ssh_CHANNEL_FAILURE(self, packet):
369 Our channel request to the other side failed. Payload::
370 uint32 local channel number
372 Get the C{Deferred} out of self.deferreds and errback it with a
375 localChannel = struct.unpack('>L', packet[:4])[0]
376 if self.deferreds.get(localChannel):
377 d = self.deferreds[localChannel].pop(0)
378 log.callWithLogger(self.channels[localChannel],
380 error.ConchError('channel request failed'))
382 # methods for users of the connection to call
384 def sendGlobalRequest(self, request, data, wantReply=0):
386 Send a global request for this connection. Current this is only used
387 for remote->local TCP forwarding.
389 @type request: C{str}
391 @type wantReply: C{bool}
392 @rtype C{Deferred}/C{None}
394 self.transport.sendPacket(MSG_GLOBAL_REQUEST,
396 + (wantReply and '\xff' or '\x00')
400 self.deferreds['global'].append(d)
403 def openChannel(self, channel, extra=''):
405 Open a new channel on this connection.
407 @type channel: subclass of C{SSHChannel}
410 log.msg('opening channel %s with %s %s'%(self.localChannelID,
411 channel.localWindowSize, channel.localMaxPacket))
412 self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name)
413 + struct.pack('>3L', self.localChannelID,
414 channel.localWindowSize, channel.localMaxPacket)
416 channel.id = self.localChannelID
417 self.channels[self.localChannelID] = channel
418 self.localChannelID += 1
420 def sendRequest(self, channel, requestType, data, wantReply=0):
422 Send a request to a channel.
424 @type channel: subclass of C{SSHChannel}
425 @type requestType: C{str}
427 @type wantReply: C{bool}
428 @rtype C{Deferred}/C{None}
430 if channel.localClosed:
432 log.msg('sending request %s' % requestType)
433 self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L',
434 self.channelsToRemoteChannel[channel])
435 + common.NS(requestType)+chr(wantReply)
439 self.deferreds.setdefault(channel.id, []).append(d)
442 def adjustWindow(self, channel, bytesToAdd):
444 Tell the other side that we will receive more data. This should not
445 normally need to be called as it is managed automatically.
447 @type channel: subclass of L{SSHChannel}
448 @type bytesToAdd: C{int}
450 if channel.localClosed:
451 return # we're already closed
452 self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L',
453 self.channelsToRemoteChannel[channel],
455 log.msg('adding %i to %i in channel %i' % (bytesToAdd,
456 channel.localWindowLeft, channel.id))
457 channel.localWindowLeft += bytesToAdd
459 def sendData(self, channel, data):
461 Send data to a channel. This should not normally be used: instead use
462 channel.write(data) as it manages the window automatically.
464 @type channel: subclass of L{SSHChannel}
467 if channel.localClosed:
468 return # we're already closed
469 self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L',
470 self.channelsToRemoteChannel[channel]) +
473 def sendExtendedData(self, channel, dataType, data):
475 Send extended data to a channel. This should not normally be used:
476 instead use channel.writeExtendedData(data, dataType) as it manages
477 the window automatically.
479 @type channel: subclass of L{SSHChannel}
480 @type dataType: C{int}
483 if channel.localClosed:
484 return # we're already closed
485 self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L',
486 self.channelsToRemoteChannel[channel],dataType) \
489 def sendEOF(self, channel):
491 Send an EOF (End of File) for a channel.
493 @type channel: subclass of L{SSHChannel}
495 if channel.localClosed:
496 return # we're already closed
497 log.msg('sending eof')
498 self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L',
499 self.channelsToRemoteChannel[channel]))
501 def sendClose(self, channel):
505 @type channel: subclass of L{SSHChannel}
507 if channel.localClosed:
508 return # we're already closed
509 log.msg('sending close %i' % channel.id)
510 self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L',
511 self.channelsToRemoteChannel[channel]))
512 channel.localClosed = True
513 if channel.localClosed and channel.remoteClosed:
514 self.channelClosed(channel)
516 # methods to override
517 def getChannel(self, channelType, windowSize, maxPacket, data):
519 The other side requested a channel of some sort.
520 channelType is the type of channel being requested,
521 windowSize is the initial size of the remote window,
522 maxPacket is the largest packet we should send,
523 data is any other packet data (often nothing).
525 We return a subclass of L{SSHChannel}.
527 By default, this dispatches to a method 'channel_channelType' with any
528 non-alphanumerics in the channelType replace with _'s. If it cannot
529 find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
530 The method is called with arguments of windowSize, maxPacket, data.
532 @type channelType: C{str}
533 @type windowSize: C{int}
534 @type maxPacket: C{int}
536 @rtype: subclass of L{SSHChannel}/C{tuple}
538 log.msg('got channel %s request' % channelType)
539 if hasattr(self.transport, "avatar"): # this is a server!
540 chan = self.transport.avatar.lookupChannel(channelType,
545 channelType = channelType.translate(TRANSLATE_TABLE)
546 f = getattr(self, 'channel_%s' % channelType, None)
548 chan = f(windowSize, maxPacket, data)
552 raise error.ConchError('unknown channel',
553 OPEN_UNKNOWN_CHANNEL_TYPE)
558 def gotGlobalRequest(self, requestType, data):
560 We got a global request. pretty much, this is just used by the client
561 to request that we forward a port from the server to the client.
563 - 1: request accepted
564 - 1, <data>: request accepted with request specific data
567 By default, this dispatches to a method 'global_requestType' with
568 -'s in requestType replaced with _'s. The found method is passed data.
569 If this method cannot be found, this method returns 0. Otherwise, it
570 returns the return value of that method.
572 @type requestType: C{str}
574 @rtype: C{int}/C{tuple}
576 log.msg('got global %s request' % requestType)
577 if hasattr(self.transport, 'avatar'): # this is a server!
578 return self.transport.avatar.gotGlobalRequest(requestType, data)
580 requestType = requestType.replace('-','_')
581 f = getattr(self, 'global_%s' % requestType, None)
586 def channelClosed(self, channel):
588 Called when a channel is closed.
589 It clears the local state related to the channel, and calls
591 MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
592 If you don't, things will break mysteriously.
594 @type channel: L{SSHChannel}
596 if channel in self.channelsToRemoteChannel: # actually open
597 channel.localClosed = channel.remoteClosed = True
598 del self.localToRemoteChannel[channel.id]
599 del self.channels[channel.id]
600 del self.channelsToRemoteChannel[channel]
601 for d in self.deferreds.setdefault(channel.id, []):
602 d.errback(error.ConchError("Channel closed."))
603 del self.deferreds[channel.id][:]
604 log.callWithLogger(channel, channel.closed)
606 MSG_GLOBAL_REQUEST = 80
607 MSG_REQUEST_SUCCESS = 81
608 MSG_REQUEST_FAILURE = 82
609 MSG_CHANNEL_OPEN = 90
610 MSG_CHANNEL_OPEN_CONFIRMATION = 91
611 MSG_CHANNEL_OPEN_FAILURE = 92
612 MSG_CHANNEL_WINDOW_ADJUST = 93
613 MSG_CHANNEL_DATA = 94
614 MSG_CHANNEL_EXTENDED_DATA = 95
616 MSG_CHANNEL_CLOSE = 97
617 MSG_CHANNEL_REQUEST = 98
618 MSG_CHANNEL_SUCCESS = 99
619 MSG_CHANNEL_FAILURE = 100
621 OPEN_ADMINISTRATIVELY_PROHIBITED = 1
622 OPEN_CONNECT_FAILED = 2
623 OPEN_UNKNOWN_CHANNEL_TYPE = 3
624 OPEN_RESOURCE_SHORTAGE = 4
626 EXTENDED_DATA_STDERR = 1
629 for name, value in locals().copy().items():
630 if name[:4] == 'MSG_':
631 messages[value] = name # doesn't handle doubles
634 alphanums = string.letters + string.digits
635 TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_'
636 for i in range(256)])
637 SSHConnection.protocolMessages = messages