关闭Twisted conch SSH连接的正确方法是什么?

时间:2012-12-17 19:39:59

标签: python twisted twisted.conch

关闭Twisted conch SSH连接的正确方法是什么?有明确的方法吗?

我看到的所有Twisted海螺示例关闭了SSH通道,然后停止了反应堆。反应堆关闭似乎处理关闭连接。但是,我正在将wxreactor与wxPython一起使用,我不想停止反应堆,但是当我完成它时我想关闭ssh连接。

在查看t.c.s.connection后,看起来似乎是serviceStopped()方法。它关闭所有打开的通道并在完成时运行_cleanupGlobalDeferreds(),但后来我开始获得如下例外的异常:

Unhandled Error
Traceback (most recent call last):
  File "C:\Users\me\venv\lib\site-packages\twisted\internet\tcp.py", line 203, in doRead
    return self._dataReceived(data)
  File "C:\Users\me\venv\lib\site-packages\twisted\internet\tcp.py", line 209, in _dataReceived
    rval = self.protocol.dataReceived(data)
  File "C:\Users\me\venv\lib\site-packages\twisted\conch\ssh\transport.py", line 438, in dataReceived
    self.dispatchMessage(messageNum, packet[1:])
  File "C:\Users\me\venv\lib\site-packages\twisted\conch\ssh\transport.py", line 460, in dispatchMessage
    messageNum, payload)
--- <exception caught here> ---
  File "C:\Users\me\venv\lib\site-packages\twisted\python\log.py", line 84, in callWithLogger
    return callWithContext({"system": lp}, func, *args, **kw)
  File "C:\Users\me\venv\lib\site-packages\twisted\python\log.py", line 69, in callWithContext
    return context.call({ILogContext: newCtx}, func, *args, **kw)
  File "C:\Users\me\venv\lib\site-packages\twisted\python\context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "C:\Users\me\venv\lib\site-packages\twisted\python\context.py", line 81, in callWithContext
    return func(*args,**kw)
  File "C:\Users\me\venv\lib\site-packages\twisted\conch\ssh\service.py", line 44, in packetReceived
    return f(packet)
  File "C:\Users\me\venv\lib\site-packages\twisted\conch\ssh\connection.py", line 228, in ssh_CHANNEL_DATA
    channel = self.channels[localChannel]
exceptions.KeyError: 0

看起来我在关闭频道后仍然从服务器获取数据。 #twisted中的某个人似乎认为我不应该自己调用serviceStopped(),因为它应该由Twisted的不同部分自动调用。

我在Twisted源代码中做了一些讨论,发现serviceStopped应该由t.c.s.t.SSHClientTransport.connectionLost()调用。

我正在跟踪我的SFTP客户端对象并通过其transport属性访问SSH连接。以下是您可以在本地运行以演示此问题的示例。可以获取原始here

from os.path import basename
import sys

from twisted.conch.client.connect import connect
from twisted.conch.client.options import ConchOptions
from twisted.internet.defer import Deferred
from twisted.conch.ssh import channel, userauth
from twisted.conch.ssh.common import NS
from twisted.conch.ssh.connection import SSHConnection
from twisted.conch.ssh.filetransfer import FXF_WRITE, FXF_CREAT, \
    FXF_TRUNC, FileTransferClient
from twisted.internet import reactor, defer
from twisted.python.log import startLogging

ACTIVE_CLIENTS = {}
USERNAME = 'user'           # change me!
PASSWORD = 'password'       # change me!
HOST = ('hostname', 22)     # change me!
TEST_FILE_PATH = __file__
TEST_FILE_NAME = basename(__file__)


def openSFTP(user, host):
    conn = SFTPConnection()
    options = ConchOptions()
    options['host'], options['port'] = host
    conn._sftp = Deferred()
    auth = SimpleUserAuth(user, conn)
    connect(options['host'], options['port'], options, verifyHostKey, auth)
    return conn._sftp


def verifyHostKey(ui, hostname, ip, key):
    return defer.succeed(True)


class SimpleUserAuth(userauth.SSHUserAuthClient):
    def getPassword(self):
        return defer.succeed(PASSWORD)


class SFTPConnection(SSHConnection):
    def serviceStarted(self):
        self.openChannel(SFTPChannel())


class SFTPChannel(channel.SSHChannel):
    name = 'session'

    def channelOpen(self, ignoredData):
        d = self.conn.sendRequest(self, 'subsystem', NS('sftp'),
                                  wantReply=True)
        d.addCallback(self._cbFTP)
        d.addErrback(self.printErr)

    def _cbFTP(self, ignore):
        client = FileTransferClient()
        client.makeConnection(self)
        self.dataReceived = client.dataReceived
        ACTIVE_CLIENTS.update({self.conn.transport.transport.addr: client})
        self.conn._sftp.callback(None)

    def printErr(self, msg):
        print msg
        return msg


@defer.inlineCallbacks
def main():
    d = openSFTP(USERNAME, HOST)
    _ = yield d

    client = ACTIVE_CLIENTS[HOST]
    d = client.openFile(TEST_FILE_NAME, FXF_WRITE | FXF_CREAT | FXF_TRUNC, {})
    df = yield d

    sf = open(TEST_FILE_PATH, 'rb')
    d = df.writeChunk(0, sf.read())
    _ = yield d

    sf.close()
    d = df.close()
    _ = yield d

    ACTIVE_CLIENTS[HOST].transport.loseConnection()
    # loseConnection() call above causes the following log messages:
    # [SSHChannel session (0) on SSHService ssh-connection on SSHClientTransport,client] sending close 0
    # [SSHChannel session (0) on SSHService ssh-connection on SSHClientTransport,client] unhandled request for exit-status
    # [SSHChannel session (0) on SSHService ssh-connection on SSHClientTransport,client] remote close
    # [SSHChannel session (0) on SSHService ssh-connection on SSHClientTransport,client] closed
    # I can see the channel closed on the server side:
    # sshd[4485]: debug1: session_exit_message: session 0 channel 0 pid 4486
    # sshd[4485]: debug1: session_exit_message: release channel 0
    # sshd[4485]: debug1: session_by_channel: session 0 channel 0

    ACTIVE_CLIENTS[HOST].transport.conn.transport.loseConnection()
    # loseConnection() call above does not close the SSH connection.

    reactor.callLater(5, reactor.stop)
    # Stopping the reactor closes the SSH connection and logs the following messages:
    # [SSHClientTransport,client] connection lost
    # [SSHClientTransport,client] Stopping factory <twisted.conch.client.direct.SSHClientFactory instance at 0x02E5AF30>
    # [-] Main loop terminated.
    # On the server side:
    # sshd[4485]: Closing connection to xxx.xxx.xxx.xxx


if __name__ == '__main__':
    startLogging(sys.stdout)
    reactor.callWhenRunning(main)
    reactor.run()

要关闭SSH连接,我正在呼叫ACTIVE_CLIENTS[HOST].transport.conn.transport(t.c.c.d.SSHClientTransport instance).loseConnection(),呼叫t.c.c.d.SSHClientTransport.sendDisconnect()。这是sendDisconnect()方法:

def sendDisconnect(self, code, reason):
    if self.factory.d is None:
        return
    d, self.factory.d = self.factory.d, None
    transport.SSHClientTransport.sendDisconnect(self, code, reason)
    d.errback(error.ConchError(reason, code))
调用此方法时,

self.factory.d似乎始终为None,因此它返回时不调用t.c.s.t.SSHClientTransport.sendDisconnect()。我认为它最初是t.c.c.d.connect中的延迟集,但在某些时候它被设置为None。

我怀疑SSHClientTransport.loseConnection()是关闭SSH连接的正确方法,但是为什么self.factory.d设置为None时扭曲期望它是其他东西?

如果lostConnection()不是关闭SSH连接的正确方法,有人会指出我正确的方向吗?

2 个答案:

答案 0 :(得分:4)

听起来您正在使用twisted.conch.client.direct.SSHClientFactorytwisted.conch.client.direct.SSHClientTransport。这些类最直接用于实现conch命令行工具。这意味着它们对于构建SSH客户端非常有用,因为这正是conch的原因。

然而,它们在某种程度上也不如人们想象的那么有用,因为他们并没有太注意做任何“其他”而不是实现conch命令行工具。

更普遍适用的SSH客户端传输类是twisted.conch.ssh.transport.SSHClientTransport。此类没有任何额外的逻辑来实现conch命令行工具的某些特定行为。它只有SSH客户端逻辑。例如,它在self.factory.d内没有无法解释的sendDisconnect检查 - 其sendDisconnect实现只发送一个断开数据包,然后关闭连接。

答案 1 :(得分:1)

我遇到了同样的问题。我确信它是sendDisconnect()不会调用父实现的错误。在loseConnection()上调用SSHClientTransport并不会为我关闭TCP连接,我可以使用lsof -p PID查看。要解决此问题,我使用自己的connect()方法来注入我自己的SSHClientTransport实现。使用以下代码解决了该问题:

class SSHClientTransport(direct.SSHClientTransport):
    '''
    Orignal sendDisconnect() is bugged.
    '''

    def sendDisconnect(self, code, reason):
        d, self.factory.d = self.factory.d, None
        # call the sendDisconnect() on the base SSHTransport,
        # not the imediate parent class
        transport.SSHClientTransport.sendDisconnect(self, code, reason)
        if d:
            d.errback(error.ConchError(reason, code))