123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- # -*- test-case-name: twisted.conch.test.test_connection -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- This module contains the implementation of the ssh-connection service, which
- allows access to the shell and port-forwarding.
- Maintainer: Paul Swartz
- """
- from __future__ import division, absolute_import
- import string
- import struct
- import twisted.internet.error
- from twisted.conch.ssh import service, common
- from twisted.conch import error
- from twisted.internet import defer
- from twisted.python import log
- from twisted.python.compat import (
- nativeString, networkString, long, _bytesChr as chr)
- class SSHConnection(service.SSHService):
- """
- An implementation of the 'ssh-connection' service. It is used to
- multiplex multiple channels over the single SSH connection.
- @ivar localChannelID: the next number to use as a local channel ID.
- @type localChannelID: L{int}
- @ivar channels: a L{dict} mapping a local channel ID to C{SSHChannel}
- subclasses.
- @type channels: L{dict}
- @ivar localToRemoteChannel: a L{dict} mapping a local channel ID to a
- remote channel ID.
- @type localToRemoteChannel: L{dict}
- @ivar channelsToRemoteChannel: a L{dict} mapping a C{SSHChannel} subclass
- to remote channel ID.
- @type channelsToRemoteChannel: L{dict}
- @ivar deferreds: a L{dict} mapping a local channel ID to a C{list} of
- C{Deferreds} for outstanding channel requests. Also, the 'global'
- key stores the C{list} of pending global request C{Deferred}s.
- """
- name = b'ssh-connection'
- def __init__(self):
- self.localChannelID = 0 # this is the current # to use for channel ID
- self.localToRemoteChannel = {} # local channel ID -> remote channel ID
- self.channels = {} # local channel ID -> subclass of SSHChannel
- self.channelsToRemoteChannel = {} # subclass of SSHChannel ->
- # remote channel ID
- self.deferreds = {"global": []} # local channel -> list of deferreds
- # for pending requests or 'global' -> list of
- # deferreds for global requests
- self.transport = None # gets set later
- def serviceStarted(self):
- if hasattr(self.transport, 'avatar'):
- self.transport.avatar.conn = self
- def serviceStopped(self):
- """
- Called when the connection is stopped.
- """
- # Close any fully open channels
- for channel in list(self.channelsToRemoteChannel.keys()):
- self.channelClosed(channel)
- # Indicate failure to any channels that were in the process of
- # opening but not yet open.
- while self.channels:
- (_, channel) = self.channels.popitem()
- log.callWithLogger(channel, channel.openFailed,
- twisted.internet.error.ConnectionLost())
- # Errback any unfinished global requests.
- self._cleanupGlobalDeferreds()
- def _cleanupGlobalDeferreds(self):
- """
- All pending requests that have returned a deferred must be errbacked
- when this service is stopped, otherwise they might be left uncalled and
- uncallable.
- """
- for d in self.deferreds["global"]:
- d.errback(error.ConchError("Connection stopped."))
- del self.deferreds["global"][:]
- # packet methods
- def ssh_GLOBAL_REQUEST(self, packet):
- """
- The other side has made a global request. Payload::
- string request type
- bool want reply
- <request specific data>
- This dispatches to self.gotGlobalRequest.
- """
- requestType, rest = common.getNS(packet)
- wantReply, rest = ord(rest[0:1]), rest[1:]
- ret = self.gotGlobalRequest(requestType, rest)
- if wantReply:
- reply = MSG_REQUEST_FAILURE
- data = b''
- if ret:
- reply = MSG_REQUEST_SUCCESS
- if isinstance(ret, (tuple, list)):
- data = ret[1]
- self.transport.sendPacket(reply, data)
- def ssh_REQUEST_SUCCESS(self, packet):
- """
- Our global request succeeded. Get the appropriate Deferred and call
- it back with the packet we received.
- """
- log.msg('RS')
- self.deferreds['global'].pop(0).callback(packet)
- def ssh_REQUEST_FAILURE(self, packet):
- """
- Our global request failed. Get the appropriate Deferred and errback
- it with the packet we received.
- """
- log.msg('RF')
- self.deferreds['global'].pop(0).errback(
- error.ConchError('global request failed', packet))
- def ssh_CHANNEL_OPEN(self, packet):
- """
- The other side wants to get a channel. Payload::
- string channel name
- uint32 remote channel number
- uint32 remote window size
- uint32 remote maximum packet size
- <channel specific data>
- We get a channel from self.getChannel(), give it a local channel number
- and notify the other side. Then notify the channel by calling its
- channelOpen method.
- """
- channelType, rest = common.getNS(packet)
- senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[:12])
- packet = rest[12:]
- try:
- channel = self.getChannel(channelType, windowSize, maxPacket,
- packet)
- localChannel = self.localChannelID
- self.localChannelID += 1
- channel.id = localChannel
- self.channels[localChannel] = channel
- self.channelsToRemoteChannel[channel] = senderChannel
- self.localToRemoteChannel[localChannel] = senderChannel
- self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION,
- struct.pack('>4L', senderChannel, localChannel,
- channel.localWindowSize,
- channel.localMaxPacket)+channel.specificData)
- log.callWithLogger(channel, channel.channelOpen, packet)
- except Exception as e:
- log.err(e, 'channel open failed')
- if isinstance(e, error.ConchError):
- textualInfo, reason = e.args
- if isinstance(textualInfo, (int, long)):
- # See #3657 and #3071
- textualInfo, reason = reason, textualInfo
- else:
- reason = OPEN_CONNECT_FAILED
- textualInfo = "unknown failure"
- self.transport.sendPacket(
- MSG_CHANNEL_OPEN_FAILURE,
- struct.pack('>2L', senderChannel, reason) +
- common.NS(networkString(textualInfo)) + common.NS(b''))
- def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet):
- """
- The other side accepted our MSG_CHANNEL_OPEN request. Payload::
- uint32 local channel number
- uint32 remote channel number
- uint32 remote window size
- uint32 remote maximum packet size
- <channel specific data>
- Find the channel using the local channel number and notify its
- channelOpen method.
- """
- (localChannel, remoteChannel, windowSize,
- maxPacket) = struct.unpack('>4L', packet[: 16])
- specificData = packet[16:]
- channel = self.channels[localChannel]
- channel.conn = self
- self.localToRemoteChannel[localChannel] = remoteChannel
- self.channelsToRemoteChannel[channel] = remoteChannel
- channel.remoteWindowLeft = windowSize
- channel.remoteMaxPacket = maxPacket
- log.callWithLogger(channel, channel.channelOpen, specificData)
- def ssh_CHANNEL_OPEN_FAILURE(self, packet):
- """
- The other side did not accept our MSG_CHANNEL_OPEN request. Payload::
- uint32 local channel number
- uint32 reason code
- string reason description
- Find the channel using the local channel number and notify it by
- calling its openFailed() method.
- """
- localChannel, reasonCode = struct.unpack('>2L', packet[:8])
- reasonDesc = common.getNS(packet[8:])[0]
- channel = self.channels[localChannel]
- del self.channels[localChannel]
- channel.conn = self
- reason = error.ConchError(reasonDesc, reasonCode)
- log.callWithLogger(channel, channel.openFailed, reason)
- def ssh_CHANNEL_WINDOW_ADJUST(self, packet):
- """
- The other side is adding bytes to its window. Payload::
- uint32 local channel number
- uint32 bytes to add
- Call the channel's addWindowBytes() method to add new bytes to the
- remote window.
- """
- localChannel, bytesToAdd = struct.unpack('>2L', packet[:8])
- channel = self.channels[localChannel]
- log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd)
- def ssh_CHANNEL_DATA(self, packet):
- """
- The other side is sending us data. Payload::
- uint32 local channel number
- string data
- Check to make sure the other side hasn't sent too much data (more
- than what's in the window, or more than the maximum packet size). If
- they have, close the channel. Otherwise, decrease the available
- window and pass the data to the channel's dataReceived().
- """
- localChannel, dataLength = struct.unpack('>2L', packet[:8])
- channel = self.channels[localChannel]
- # XXX should this move to dataReceived to put client in charge?
- if (dataLength > channel.localWindowLeft or
- dataLength > channel.localMaxPacket): # more data than we want
- log.callWithLogger(channel, log.msg, 'too much data')
- self.sendClose(channel)
- return
- #packet = packet[:channel.localWindowLeft+4]
- data = common.getNS(packet[4:])[0]
- channel.localWindowLeft -= dataLength
- if channel.localWindowLeft < channel.localWindowSize // 2:
- self.adjustWindow(channel, channel.localWindowSize - \
- channel.localWindowLeft)
- #log.msg('local window left: %s/%s' % (channel.localWindowLeft,
- # channel.localWindowSize))
- log.callWithLogger(channel, channel.dataReceived, data)
- def ssh_CHANNEL_EXTENDED_DATA(self, packet):
- """
- The other side is sending us exteneded data. Payload::
- uint32 local channel number
- uint32 type code
- string data
- Check to make sure the other side hasn't sent too much data (more
- than what's in the window, or than the maximum packet size). If
- they have, close the channel. Otherwise, decrease the available
- window and pass the data and type code to the channel's
- extReceived().
- """
- localChannel, typeCode, dataLength = struct.unpack('>3L', packet[:12])
- channel = self.channels[localChannel]
- if (dataLength > channel.localWindowLeft or
- dataLength > channel.localMaxPacket):
- log.callWithLogger(channel, log.msg, 'too much extdata')
- self.sendClose(channel)
- return
- data = common.getNS(packet[8:])[0]
- channel.localWindowLeft -= dataLength
- if channel.localWindowLeft < channel.localWindowSize // 2:
- self.adjustWindow(channel, channel.localWindowSize -
- channel.localWindowLeft)
- log.callWithLogger(channel, channel.extReceived, typeCode, data)
- def ssh_CHANNEL_EOF(self, packet):
- """
- The other side is not sending any more data. Payload::
- uint32 local channel number
- Notify the channel by calling its eofReceived() method.
- """
- localChannel = struct.unpack('>L', packet[:4])[0]
- channel = self.channels[localChannel]
- log.callWithLogger(channel, channel.eofReceived)
- def ssh_CHANNEL_CLOSE(self, packet):
- """
- The other side is closing its end; it does not want to receive any
- more data. Payload::
- uint32 local channel number
- Notify the channnel by calling its closeReceived() method. If
- the channel has also sent a close message, call self.channelClosed().
- """
- localChannel = struct.unpack('>L', packet[:4])[0]
- channel = self.channels[localChannel]
- log.callWithLogger(channel, channel.closeReceived)
- channel.remoteClosed = True
- if channel.localClosed and channel.remoteClosed:
- self.channelClosed(channel)
- def ssh_CHANNEL_REQUEST(self, packet):
- """
- The other side is sending a request to a channel. Payload::
- uint32 local channel number
- string request name
- bool want reply
- <request specific data>
- Pass the message to the channel's requestReceived method. If the
- other side wants a reply, add callbacks which will send the
- reply.
- """
- localChannel = struct.unpack('>L', packet[:4])[0]
- requestType, rest = common.getNS(packet[4:])
- wantReply = ord(rest[0:1])
- channel = self.channels[localChannel]
- d = defer.maybeDeferred(log.callWithLogger, channel,
- channel.requestReceived, requestType, rest[1:])
- if wantReply:
- d.addCallback(self._cbChannelRequest, localChannel)
- d.addErrback(self._ebChannelRequest, localChannel)
- return d
- def _cbChannelRequest(self, result, localChannel):
- """
- Called back if the other side wanted a reply to a channel request. If
- the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise
- a C{error.ConchError}
- @param result: the value returned from the channel's requestReceived()
- method. If it's False, the request failed.
- @type result: L{bool}
- @param localChannel: the local channel ID of the channel to which the
- request was made.
- @type localChannel: L{int}
- @raises ConchError: if the result is False.
- """
- if not result:
- raise error.ConchError('failed request')
- self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L',
- self.localToRemoteChannel[localChannel]))
- def _ebChannelRequest(self, result, localChannel):
- """
- Called if the other wisde wanted a reply to the channel requeset and
- the channel request failed.
- @param result: a Failure, but it's not used.
- @param localChannel: the local channel ID of the channel to which the
- request was made.
- @type localChannel: L{int}
- """
- self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L',
- self.localToRemoteChannel[localChannel]))
- def ssh_CHANNEL_SUCCESS(self, packet):
- """
- Our channel request to the other side succeeded. Payload::
- uint32 local channel number
- Get the C{Deferred} out of self.deferreds and call it back.
- """
- localChannel = struct.unpack('>L', packet[:4])[0]
- if self.deferreds.get(localChannel):
- d = self.deferreds[localChannel].pop(0)
- log.callWithLogger(self.channels[localChannel],
- d.callback, '')
- def ssh_CHANNEL_FAILURE(self, packet):
- """
- Our channel request to the other side failed. Payload::
- uint32 local channel number
- Get the C{Deferred} out of self.deferreds and errback it with a
- C{error.ConchError}.
- """
- localChannel = struct.unpack('>L', packet[:4])[0]
- if self.deferreds.get(localChannel):
- d = self.deferreds[localChannel].pop(0)
- log.callWithLogger(self.channels[localChannel],
- d.errback,
- error.ConchError('channel request failed'))
- # methods for users of the connection to call
- def sendGlobalRequest(self, request, data, wantReply=0):
- """
- Send a global request for this connection. Current this is only used
- for remote->local TCP forwarding.
- @type request: L{bytes}
- @type data: L{bytes}
- @type wantReply: L{bool}
- @rtype C{Deferred}/L{None}
- """
- self.transport.sendPacket(MSG_GLOBAL_REQUEST,
- common.NS(request)
- + (wantReply and b'\xff' or b'\x00')
- + data)
- if wantReply:
- d = defer.Deferred()
- self.deferreds['global'].append(d)
- return d
- def openChannel(self, channel, extra=b''):
- """
- Open a new channel on this connection.
- @type channel: subclass of C{SSHChannel}
- @type extra: L{bytes}
- """
- log.msg('opening channel %s with %s %s'%(self.localChannelID,
- channel.localWindowSize, channel.localMaxPacket))
- self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name)
- + struct.pack('>3L', self.localChannelID,
- channel.localWindowSize, channel.localMaxPacket)
- + extra)
- channel.id = self.localChannelID
- self.channels[self.localChannelID] = channel
- self.localChannelID += 1
- def sendRequest(self, channel, requestType, data, wantReply=0):
- """
- Send a request to a channel.
- @type channel: subclass of C{SSHChannel}
- @type requestType: L{bytes}
- @type data: L{bytes}
- @type wantReply: L{bool}
- @rtype C{Deferred}/L{None}
- """
- if channel.localClosed:
- return
- log.msg('sending request %r' % (requestType))
- self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L',
- self.channelsToRemoteChannel[channel])
- + common.NS(requestType)+chr(wantReply)
- + data)
- if wantReply:
- d = defer.Deferred()
- self.deferreds.setdefault(channel.id, []).append(d)
- return d
- def adjustWindow(self, channel, bytesToAdd):
- """
- Tell the other side that we will receive more data. This should not
- normally need to be called as it is managed automatically.
- @type channel: subclass of L{SSHChannel}
- @type bytesToAdd: L{int}
- """
- if channel.localClosed:
- return # we're already closed
- self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L',
- self.channelsToRemoteChannel[channel],
- bytesToAdd))
- log.msg('adding %i to %i in channel %i' % (bytesToAdd,
- channel.localWindowLeft, channel.id))
- channel.localWindowLeft += bytesToAdd
- def sendData(self, channel, data):
- """
- Send data to a channel. This should not normally be used: instead use
- channel.write(data) as it manages the window automatically.
- @type channel: subclass of L{SSHChannel}
- @type data: L{bytes}
- """
- if channel.localClosed:
- return # we're already closed
- self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L',
- self.channelsToRemoteChannel[channel]) +
- common.NS(data))
- def sendExtendedData(self, channel, dataType, data):
- """
- Send extended data to a channel. This should not normally be used:
- instead use channel.writeExtendedData(data, dataType) as it manages
- the window automatically.
- @type channel: subclass of L{SSHChannel}
- @type dataType: L{int}
- @type data: L{bytes}
- """
- if channel.localClosed:
- return # we're already closed
- self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L',
- self.channelsToRemoteChannel[channel],dataType) \
- + common.NS(data))
- def sendEOF(self, channel):
- """
- Send an EOF (End of File) for a channel.
- @type channel: subclass of L{SSHChannel}
- """
- if channel.localClosed:
- return # we're already closed
- log.msg('sending eof')
- self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L',
- self.channelsToRemoteChannel[channel]))
- def sendClose(self, channel):
- """
- Close a channel.
- @type channel: subclass of L{SSHChannel}
- """
- if channel.localClosed:
- return # we're already closed
- log.msg('sending close %i' % channel.id)
- self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L',
- self.channelsToRemoteChannel[channel]))
- channel.localClosed = True
- if channel.localClosed and channel.remoteClosed:
- self.channelClosed(channel)
- # methods to override
- def getChannel(self, channelType, windowSize, maxPacket, data):
- """
- The other side requested a channel of some sort.
- channelType is the type of channel being requested,
- windowSize is the initial size of the remote window,
- maxPacket is the largest packet we should send,
- data is any other packet data (often nothing).
- We return a subclass of L{SSHChannel}.
- By default, this dispatches to a method 'channel_channelType' with any
- non-alphanumerics in the channelType replace with _'s. If it cannot
- find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error.
- The method is called with arguments of windowSize, maxPacket, data.
- @type channelType: L{bytes}
- @type windowSize: L{int}
- @type maxPacket: L{int}
- @type data: L{bytes}
- @rtype: subclass of L{SSHChannel}/L{tuple}
- """
- log.msg('got channel %r request' % (channelType))
- if hasattr(self.transport, "avatar"): # this is a server!
- chan = self.transport.avatar.lookupChannel(channelType,
- windowSize,
- maxPacket,
- data)
- else:
- channelType = channelType.translate(TRANSLATE_TABLE)
- attr = 'channel_%s' % nativeString(channelType)
- f = getattr(self, attr, None)
- if f is not None:
- chan = f(windowSize, maxPacket, data)
- else:
- chan = None
- if chan is None:
- raise error.ConchError('unknown channel',
- OPEN_UNKNOWN_CHANNEL_TYPE)
- else:
- chan.conn = self
- return chan
- def gotGlobalRequest(self, requestType, data):
- """
- We got a global request. pretty much, this is just used by the client
- to request that we forward a port from the server to the client.
- Returns either:
- - 1: request accepted
- - 1, <data>: request accepted with request specific data
- - 0: request denied
- By default, this dispatches to a method 'global_requestType' with
- -'s in requestType replaced with _'s. The found method is passed data.
- If this method cannot be found, this method returns 0. Otherwise, it
- returns the return value of that method.
- @type requestType: L{bytes}
- @type data: L{bytes}
- @rtype: L{int}/L{tuple}
- """
- log.msg('got global %s request' % requestType)
- if hasattr(self.transport, 'avatar'): # this is a server!
- return self.transport.avatar.gotGlobalRequest(requestType, data)
- requestType = nativeString(requestType.replace(b'-',b'_'))
- f = getattr(self, 'global_%s' % requestType, None)
- if not f:
- return 0
- return f(data)
- def channelClosed(self, channel):
- """
- Called when a channel is closed.
- It clears the local state related to the channel, and calls
- channel.closed().
- MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}.
- If you don't, things will break mysteriously.
- @type channel: L{SSHChannel}
- """
- if channel in self.channelsToRemoteChannel: # actually open
- channel.localClosed = channel.remoteClosed = True
- del self.localToRemoteChannel[channel.id]
- del self.channels[channel.id]
- del self.channelsToRemoteChannel[channel]
- for d in self.deferreds.pop(channel.id, []):
- d.errback(error.ConchError("Channel closed."))
- log.callWithLogger(channel, channel.closed)
- MSG_GLOBAL_REQUEST = 80
- MSG_REQUEST_SUCCESS = 81
- MSG_REQUEST_FAILURE = 82
- MSG_CHANNEL_OPEN = 90
- MSG_CHANNEL_OPEN_CONFIRMATION = 91
- MSG_CHANNEL_OPEN_FAILURE = 92
- MSG_CHANNEL_WINDOW_ADJUST = 93
- MSG_CHANNEL_DATA = 94
- MSG_CHANNEL_EXTENDED_DATA = 95
- MSG_CHANNEL_EOF = 96
- MSG_CHANNEL_CLOSE = 97
- MSG_CHANNEL_REQUEST = 98
- MSG_CHANNEL_SUCCESS = 99
- MSG_CHANNEL_FAILURE = 100
- OPEN_ADMINISTRATIVELY_PROHIBITED = 1
- OPEN_CONNECT_FAILED = 2
- OPEN_UNKNOWN_CHANNEL_TYPE = 3
- OPEN_RESOURCE_SHORTAGE = 4
- EXTENDED_DATA_STDERR = 1
- messages = {}
- for name, value in locals().copy().items():
- if name[:4] == 'MSG_':
- messages[value] = name # Doesn't handle doubles
- alphanums = networkString(string.ascii_letters + string.digits)
- TRANSLATE_TABLE = b''.join([chr(i) in alphanums and chr(i) or b'_'
- for i in range(256)])
- SSHConnection.protocolMessages = messages
|