| 1 | # -*- test-case-name: twisted.conch.test.test_channel -*- |
|---|
| 2 | # Copyright (c) Twisted Matrix Laboratories. |
|---|
| 3 | # See LICENSE for details. |
|---|
| 4 | |
|---|
| 5 | # |
|---|
| 6 | """ |
|---|
| 7 | The parent class for all the SSH Channels. Currently implemented channels |
|---|
| 8 | are session. direct-tcp, and forwarded-tcp. |
|---|
| 9 | |
|---|
| 10 | Maintainer: Paul Swartz |
|---|
| 11 | """ |
|---|
| 12 | |
|---|
| 13 | from twisted.python import log |
|---|
| 14 | from twisted.internet import interfaces |
|---|
| 15 | from zope.interface import implements |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | class SSHChannel(log.Logger): |
|---|
| 19 | """ |
|---|
| 20 | A class that represents a multiplexed channel over an SSH connection. |
|---|
| 21 | The channel has a local window which is the maximum amount of data it will |
|---|
| 22 | receive, and a remote which is the maximum amount of data the remote side |
|---|
| 23 | will accept. There is also a maximum packet size for any individual data |
|---|
| 24 | packet going each way. |
|---|
| 25 | |
|---|
| 26 | @ivar name: the name of the channel. |
|---|
| 27 | @type name: C{str} |
|---|
| 28 | @ivar localWindowSize: the maximum size of the local window in bytes. |
|---|
| 29 | @type localWindowSize: C{int} |
|---|
| 30 | @ivar localWindowLeft: how many bytes are left in the local window. |
|---|
| 31 | @type localWindowLeft: C{int} |
|---|
| 32 | @ivar localMaxPacket: the maximum size of packet we will accept in bytes. |
|---|
| 33 | @type localMaxPacket: C{int} |
|---|
| 34 | @ivar remoteWindowLeft: how many bytes are left in the remote window. |
|---|
| 35 | @type remoteWindowLeft: C{int} |
|---|
| 36 | @ivar remoteMaxPacket: the maximum size of a packet the remote side will |
|---|
| 37 | accept in bytes. |
|---|
| 38 | @type remoteMaxPacket: C{int} |
|---|
| 39 | @ivar conn: the connection this channel is multiplexed through. |
|---|
| 40 | @type conn: L{SSHConnection} |
|---|
| 41 | @ivar data: any data to send to the other size when the channel is |
|---|
| 42 | requested. |
|---|
| 43 | @type data: C{str} |
|---|
| 44 | @ivar avatar: an avatar for the logged-in user (if a server channel) |
|---|
| 45 | @ivar localClosed: True if we aren't accepting more data. |
|---|
| 46 | @type localClosed: C{bool} |
|---|
| 47 | @ivar remoteClosed: True if the other size isn't accepting more data. |
|---|
| 48 | @type remoteClosed: C{bool} |
|---|
| 49 | """ |
|---|
| 50 | |
|---|
| 51 | implements(interfaces.ITransport) |
|---|
| 52 | |
|---|
| 53 | name = None # only needed for client channels |
|---|
| 54 | |
|---|
| 55 | def __init__(self, localWindow = 0, localMaxPacket = 0, |
|---|
| 56 | remoteWindow = 0, remoteMaxPacket = 0, |
|---|
| 57 | conn = None, data=None, avatar = None): |
|---|
| 58 | self.localWindowSize = localWindow or 131072 |
|---|
| 59 | self.localWindowLeft = self.localWindowSize |
|---|
| 60 | self.localMaxPacket = localMaxPacket or 32768 |
|---|
| 61 | self.remoteWindowLeft = remoteWindow |
|---|
| 62 | self.remoteMaxPacket = remoteMaxPacket |
|---|
| 63 | self.areWriting = 1 |
|---|
| 64 | self.conn = conn |
|---|
| 65 | self.data = data |
|---|
| 66 | self.avatar = avatar |
|---|
| 67 | self.specificData = '' |
|---|
| 68 | self.buf = '' |
|---|
| 69 | self.extBuf = [] |
|---|
| 70 | self.closing = 0 |
|---|
| 71 | self.localClosed = 0 |
|---|
| 72 | self.remoteClosed = 0 |
|---|
| 73 | self.id = None # gets set later by SSHConnection |
|---|
| 74 | |
|---|
| 75 | def __str__(self): |
|---|
| 76 | return '<SSHChannel %s (lw %i rw %i)>' % (self.name, |
|---|
| 77 | self.localWindowLeft, self.remoteWindowLeft) |
|---|
| 78 | |
|---|
| 79 | def logPrefix(self): |
|---|
| 80 | id = (self.id is not None and str(self.id)) or "unknown" |
|---|
| 81 | return "SSHChannel %s (%s) on %s" % (self.name, id, |
|---|
| 82 | self.conn.logPrefix()) |
|---|
| 83 | |
|---|
| 84 | def channelOpen(self, specificData): |
|---|
| 85 | """ |
|---|
| 86 | Called when the channel is opened. specificData is any data that the |
|---|
| 87 | other side sent us when opening the channel. |
|---|
| 88 | |
|---|
| 89 | @type specificData: C{str} |
|---|
| 90 | """ |
|---|
| 91 | log.msg('channel open') |
|---|
| 92 | |
|---|
| 93 | def openFailed(self, reason): |
|---|
| 94 | """ |
|---|
| 95 | Called when the the open failed for some reason. |
|---|
| 96 | reason.desc is a string descrption, reason.code the the SSH error code. |
|---|
| 97 | |
|---|
| 98 | @type reason: L{error.ConchError} |
|---|
| 99 | """ |
|---|
| 100 | log.msg('other side refused open\nreason: %s'% reason) |
|---|
| 101 | |
|---|
| 102 | def addWindowBytes(self, bytes): |
|---|
| 103 | """ |
|---|
| 104 | Called when bytes are added to the remote window. By default it clears |
|---|
| 105 | the data buffers. |
|---|
| 106 | |
|---|
| 107 | @type bytes: C{int} |
|---|
| 108 | """ |
|---|
| 109 | self.remoteWindowLeft = self.remoteWindowLeft+bytes |
|---|
| 110 | if not self.areWriting and not self.closing: |
|---|
| 111 | self.areWriting = True |
|---|
| 112 | self.startWriting() |
|---|
| 113 | if self.buf: |
|---|
| 114 | b = self.buf |
|---|
| 115 | self.buf = '' |
|---|
| 116 | self.write(b) |
|---|
| 117 | if self.extBuf: |
|---|
| 118 | b = self.extBuf |
|---|
| 119 | self.extBuf = [] |
|---|
| 120 | for (type, data) in b: |
|---|
| 121 | self.writeExtended(type, data) |
|---|
| 122 | |
|---|
| 123 | def requestReceived(self, requestType, data): |
|---|
| 124 | """ |
|---|
| 125 | Called when a request is sent to this channel. By default it delegates |
|---|
| 126 | to self.request_<requestType>. |
|---|
| 127 | If this function returns true, the request succeeded, otherwise it |
|---|
| 128 | failed. |
|---|
| 129 | |
|---|
| 130 | @type requestType: C{str} |
|---|
| 131 | @type data: C{str} |
|---|
| 132 | @rtype: C{bool} |
|---|
| 133 | """ |
|---|
| 134 | foo = requestType.replace('-', '_') |
|---|
| 135 | f = getattr(self, 'request_%s'%foo, None) |
|---|
| 136 | if f: |
|---|
| 137 | return f(data) |
|---|
| 138 | log.msg('unhandled request for %s'%requestType) |
|---|
| 139 | return 0 |
|---|
| 140 | |
|---|
| 141 | def dataReceived(self, data): |
|---|
| 142 | """ |
|---|
| 143 | Called when we receive data. |
|---|
| 144 | |
|---|
| 145 | @type data: C{str} |
|---|
| 146 | """ |
|---|
| 147 | log.msg('got data %s'%repr(data)) |
|---|
| 148 | |
|---|
| 149 | def extReceived(self, dataType, data): |
|---|
| 150 | """ |
|---|
| 151 | Called when we receive extended data (usually standard error). |
|---|
| 152 | |
|---|
| 153 | @type dataType: C{int} |
|---|
| 154 | @type data: C{str} |
|---|
| 155 | """ |
|---|
| 156 | log.msg('got extended data %s %s'%(dataType, repr(data))) |
|---|
| 157 | |
|---|
| 158 | def eofReceived(self): |
|---|
| 159 | """ |
|---|
| 160 | Called when the other side will send no more data. |
|---|
| 161 | """ |
|---|
| 162 | log.msg('remote eof') |
|---|
| 163 | |
|---|
| 164 | def closeReceived(self): |
|---|
| 165 | """ |
|---|
| 166 | Called when the other side has closed the channel. |
|---|
| 167 | """ |
|---|
| 168 | log.msg('remote close') |
|---|
| 169 | self.loseConnection() |
|---|
| 170 | |
|---|
| 171 | def closed(self): |
|---|
| 172 | """ |
|---|
| 173 | Called when the channel is closed. This means that both our side and |
|---|
| 174 | the remote side have closed the channel. |
|---|
| 175 | """ |
|---|
| 176 | log.msg('closed') |
|---|
| 177 | |
|---|
| 178 | # transport stuff |
|---|
| 179 | def write(self, data): |
|---|
| 180 | """ |
|---|
| 181 | Write some data to the channel. If there is not enough remote window |
|---|
| 182 | available, buffer until it is. Otherwise, split the data into |
|---|
| 183 | packets of length remoteMaxPacket and send them. |
|---|
| 184 | |
|---|
| 185 | @type data: C{str} |
|---|
| 186 | """ |
|---|
| 187 | if self.buf: |
|---|
| 188 | self.buf += data |
|---|
| 189 | return |
|---|
| 190 | top = len(data) |
|---|
| 191 | if top > self.remoteWindowLeft: |
|---|
| 192 | data, self.buf = (data[:self.remoteWindowLeft], |
|---|
| 193 | data[self.remoteWindowLeft:]) |
|---|
| 194 | self.areWriting = 0 |
|---|
| 195 | self.stopWriting() |
|---|
| 196 | top = self.remoteWindowLeft |
|---|
| 197 | rmp = self.remoteMaxPacket |
|---|
| 198 | write = self.conn.sendData |
|---|
| 199 | r = range(0, top, rmp) |
|---|
| 200 | for offset in r: |
|---|
| 201 | write(self, data[offset: offset+rmp]) |
|---|
| 202 | self.remoteWindowLeft -= top |
|---|
| 203 | if self.closing and not self.buf: |
|---|
| 204 | self.loseConnection() # try again |
|---|
| 205 | |
|---|
| 206 | def writeExtended(self, dataType, data): |
|---|
| 207 | """ |
|---|
| 208 | Send extended data to this channel. If there is not enough remote |
|---|
| 209 | window available, buffer until there is. Otherwise, split the data |
|---|
| 210 | into packets of length remoteMaxPacket and send them. |
|---|
| 211 | |
|---|
| 212 | @type dataType: C{int} |
|---|
| 213 | @type data: C{str} |
|---|
| 214 | """ |
|---|
| 215 | if self.extBuf: |
|---|
| 216 | if self.extBuf[-1][0] == dataType: |
|---|
| 217 | self.extBuf[-1][1] += data |
|---|
| 218 | else: |
|---|
| 219 | self.extBuf.append([dataType, data]) |
|---|
| 220 | return |
|---|
| 221 | if len(data) > self.remoteWindowLeft: |
|---|
| 222 | data, self.extBuf = (data[:self.remoteWindowLeft], |
|---|
| 223 | [[dataType, data[self.remoteWindowLeft:]]]) |
|---|
| 224 | self.areWriting = 0 |
|---|
| 225 | self.stopWriting() |
|---|
| 226 | while len(data) > self.remoteMaxPacket: |
|---|
| 227 | self.conn.sendExtendedData(self, dataType, |
|---|
| 228 | data[:self.remoteMaxPacket]) |
|---|
| 229 | data = data[self.remoteMaxPacket:] |
|---|
| 230 | self.remoteWindowLeft -= self.remoteMaxPacket |
|---|
| 231 | if data: |
|---|
| 232 | self.conn.sendExtendedData(self, dataType, data) |
|---|
| 233 | self.remoteWindowLeft -= len(data) |
|---|
| 234 | if self.closing: |
|---|
| 235 | self.loseConnection() # try again |
|---|
| 236 | |
|---|
| 237 | def writeSequence(self, data): |
|---|
| 238 | """ |
|---|
| 239 | Part of the Transport interface. Write a list of strings to the |
|---|
| 240 | channel. |
|---|
| 241 | |
|---|
| 242 | @type data: C{list} of C{str} |
|---|
| 243 | """ |
|---|
| 244 | self.write(''.join(data)) |
|---|
| 245 | |
|---|
| 246 | def loseConnection(self): |
|---|
| 247 | """ |
|---|
| 248 | Close the channel if there is no buferred data. Otherwise, note the |
|---|
| 249 | request and return. |
|---|
| 250 | """ |
|---|
| 251 | self.closing = 1 |
|---|
| 252 | if not self.buf and not self.extBuf: |
|---|
| 253 | self.conn.sendClose(self) |
|---|
| 254 | |
|---|
| 255 | def getPeer(self): |
|---|
| 256 | """ |
|---|
| 257 | Return a tuple describing the other side of the connection. |
|---|
| 258 | |
|---|
| 259 | @rtype: C{tuple} |
|---|
| 260 | """ |
|---|
| 261 | return('SSH', )+self.conn.transport.getPeer() |
|---|
| 262 | |
|---|
| 263 | def getHost(self): |
|---|
| 264 | """ |
|---|
| 265 | Return a tuple describing our side of the connection. |
|---|
| 266 | |
|---|
| 267 | @rtype: C{tuple} |
|---|
| 268 | """ |
|---|
| 269 | return('SSH', )+self.conn.transport.getHost() |
|---|
| 270 | |
|---|
| 271 | def stopWriting(self): |
|---|
| 272 | """ |
|---|
| 273 | Called when the remote buffer is full, as a hint to stop writing. |
|---|
| 274 | This can be ignored, but it can be helpful. |
|---|
| 275 | """ |
|---|
| 276 | |
|---|
| 277 | def startWriting(self): |
|---|
| 278 | """ |
|---|
| 279 | Called when the remote buffer has more room, as a hint to continue |
|---|
| 280 | writing. |
|---|
| 281 | """ |
|---|