【问题标题】:What is the correct way to close a Twisted conch SSH connection?关闭 Twisted conch SSH 连接的正确方法是什么?
【发布时间】:2012-12-04 22:48:34
【问题描述】:

关闭 Twisted conch SSH 连接的正确方法是什么?有没有明确的方法来做到这一点?

我见过的所有 Twisted conch 示例都会关闭 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 客户端对象并通过它们的传输属性访问 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 当 twisted 期望它是别的东西时?

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

【问题讨论】:

    标签: python twisted twisted.conch


    【解决方案1】:

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

    但是,它们通常也没有人们想象的那么有用,因为除了实现conch 命令行工具之外,它们并不太注意做任何“其他”的事情。

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

    【讨论】:

      【解决方案2】:

      我遇到了同样的问题。我确信 sendDisconnect() 不调用父实现是一个错误。在SSHClientTransport 上调用loseConnection() 并没有关闭我的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))
      

      【讨论】:

        最近更新 更多