扭曲的贝壳文件传输

12 投票
2 回答
7545 浏览
提问于 2025-04-16 13:00

我正在尝试用 Python 和 twisted conch 实现一个非常简单的文件传输客户端。这个客户端的功能就是把几个文件以编程的方式传输到一个远程的 ssh/sftp 服务器上。这个功能需要提供用户名、密码、文件列表和目标服务器的目录,然后只需要完成身份验证和文件复制,最好能在不同的操作系统上都能用。

我看了一些关于 twisted 的入门资料,成功做出了一个 SSH 客户端,可以在远程服务器上执行 cat 命令。但我在扩展这个功能,让它能移动文件时遇到了很大的困难。我查看了 cftp.py 和文件传输的测试代码,但对于 twisted 还是感到一头雾水。

有没有人能给我一些建议或者参考资料,帮我找到正确的方向?我目前构建的 SSH 客户端是基于 这个链接 的。

2 个回答

0

SSH客户端并不是一个和其他操作系统服务完全独立的东西。你真的想要支持 .ssh 文件夹、密钥链等等吗?也许更快、更稳妥的方法是给scp(在Linux和OSX上)和pscp(在Windows上)做一个封装。这样做看起来更像是“Linux的做法”,就是把现有的小工具组合成一个复杂的东西。

36

使用Twisted Conch进行SFTP文件传输分为几个不同的阶段(如果你仔细看,会发现它们是不同的)。基本上,首先你需要建立一个连接,并在这个连接上打开一个通道,同时运行一个sftp子系统。呼,终于搞定了。然后,你就可以使用连接到这个通道的FileTransferClient实例的方法,执行你想要的SFTP操作。

建立SSH连接的基本步骤可以通过来自twisted.conch.client包的API来处理。这里有一个函数,它把twisted.conch.client.default.connect的些许复杂性封装成一个稍微简单的接口:

from twisted.internet.defer import Deferred
from twisted.conch.scripts.cftp import ClientOptions
from twisted.conch.client.connect import connect
from twisted.conch.client.default import SSHUserAuthClient, verifyHostKey

def sftp(user, host, port):
    options = ClientOptions()
    options['host'] = host
    options['port'] = port
    conn = SFTPConnection()
    conn._sftp = Deferred()  
    auth = SSHUserAuthClient(user, options, conn)
    connect(host, port, options, verifyHostKey, auth)
    return conn._sftp

这个函数需要一个用户名、主机名(或IP地址)和端口号,并使用与给定用户名相关的账户,建立到该地址的经过认证的SSH连接。

实际上,它做的事情比这稍微多一点,因为SFTP的设置有点混合在这里。不过暂时忽略SFTPConnection和那个_sftp的Deferred。

ClientOptions基本上就是一个花哨的字典,connect需要查看它连接的内容,以便验证主机密钥。

SSHUserAuthClient是定义如何进行身份验证的对象。这个类知道如何尝试一些常见的方法,比如查看~/.ssh和与本地SSH代理通信。如果你想改变身份验证的方式,这就是你需要调整的对象。你可以继承SSHUserAuthClient,重写它的getPasswordgetPublicKeygetPrivateKey和/或signData方法,或者你可以写一个完全不同的类,包含你想要的其他身份验证逻辑。查看实现代码,看看SSH协议实现调用了哪些方法来完成身份验证。

所以这个函数会设置一个SSH连接并进行身份验证。完成后,SFTPConnection实例就会派上用场。注意SSHUserAuthClient是如何将SFTPConnection实例作为参数传递的。一旦身份验证成功,它就将连接的控制权交给那个实例。特别是,该实例会调用serviceStarted。下面是SFTPConnection类的完整实现:

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

非常简单:它只做一件事,就是打开一个新通道。它传入的SFTPSession实例可以与这个新通道进行交互。以下是我定义SFTPSession的方式:

class SFTPSession(SSHChannel):
    name = 'session'

    def channelOpen(self, whatever):
        d = self.conn.sendRequest(
            self, 'subsystem', NS('sftp'), wantReply=True)
        d.addCallbacks(self._cbSFTP)


    def _cbSFTP(self, result):
        client = FileTransferClient()
        client.makeConnection(self)
        self.dataReceived = client.dataReceived
        self.conn._sftp.callback(client)

SFTPConnection一样,这个类有一个方法会在连接准备好时被调用。在这种情况下,它是在通道成功打开时调用的,方法名是channelOpen

最后,启动SFTP子系统的要求已经到位。所以channelOpen会通过通道发送一个请求来启动那个子系统。它请求一个回复,以便知道这个请求是否成功(或失败)。它还为获得的Deferred添加了一个回调,以便将FileTransferClient连接到自己。

FileTransferClient实例实际上会格式化和解析在这个连接通道上移动的字节。换句话说,它就是SFTP协议的实现。它运行在SSH协议之上,而这个示例中创建的其他对象负责处理SSH协议。但就它而言,它在dataReceived方法中接收字节,解析这些字节并将数据分发给回调,同时提供接受结构化Python对象的方法,将这些对象格式化为正确的字节,并写入其传输。

不过,这些内容对使用它并不直接重要。然而,在给出如何使用它执行SFTP操作的示例之前,让我们先讲讲_sftp属性。这是我粗略的方法,用来让这个新连接的FileTransferClient实例对其他代码可用,而这些代码实际上知道该怎么做。将SFTP设置代码与实际使用SFTP连接的代码分开,使得前者更容易重用,同时更改后者。

所以我在sftp中设置的Deferred会在_cbSFTP中触发,连接上FileTransferClient。调用sftp的代码得到了这个Deferred,这样代码就可以像这样做:

def transfer(client):
    d = client.makeDirectory('foobarbaz', {})
    def cbDir(ignored):
        print 'Made directory'
    d.addCallback(cbDir)   
    return d


def main():
    ...
    d = sftp(user, host, port)
    d.addCallback(transfer)

所以首先sftp设置了整个连接,一直到将本地的FileTransferClient实例连接到一个有某个SSH服务器的SFTP子系统的字节流上,然后transfer使用这个实例来创建一个目录,调用FileTransferClient的某个方法来执行一些SFTP操作。

下面是一个完整的代码示例,你应该能够运行它,并在某个SFTP服务器上看到一个目录被创建:

from sys import stdout

from twisted.python.log import startLogging, err

from twisted.internet import reactor
from twisted.internet.defer import Deferred

from twisted.conch.ssh.common import NS
from twisted.conch.scripts.cftp import ClientOptions
from twisted.conch.ssh.filetransfer import FileTransferClient
from twisted.conch.client.connect import connect
from twisted.conch.client.default import SSHUserAuthClient, verifyHostKey
from twisted.conch.ssh.connection import SSHConnection
from twisted.conch.ssh.channel import SSHChannel


class SFTPSession(SSHChannel):
    name = 'session'

    def channelOpen(self, whatever):
        d = self.conn.sendRequest(
            self, 'subsystem', NS('sftp'), wantReply=True)
        d.addCallbacks(self._cbSFTP)


    def _cbSFTP(self, result):
        client = FileTransferClient()
        client.makeConnection(self)
        self.dataReceived = client.dataReceived
        self.conn._sftp.callback(client)



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


def sftp(user, host, port):
    options = ClientOptions()
    options['host'] = host
    options['port'] = port
    conn = SFTPConnection()
    conn._sftp = Deferred()
    auth = SSHUserAuthClient(user, options, conn)
    connect(host, port, options, verifyHostKey, auth)
    return conn._sftp


def transfer(client):
    d = client.makeDirectory('foobarbaz', {})
    def cbDir(ignored):
        print 'Made directory'
    d.addCallback(cbDir)
    return d


def main():
    startLogging(stdout)

    user = 'exarkun'
    host = 'localhost'
    port = 22
    d = sftp(user, host, port)
    d.addCallback(transfer)
    d.addErrback(err, "Problem with SFTP transfer")
    d.addCallback(lambda ignored: reactor.stop())
    reactor.run()


if __name__ == '__main__':
    main()

makeDirectory是一个相对简单的操作。makeDirectory方法返回一个Deferred,当目录创建成功(或出现错误)时会触发。传输文件稍微复杂一些,因为你需要提供要发送的数据,或者定义如果你是下载而不是上传时,接收到的数据将如何解释。

不过,如果你查看FileTransferClient的方法的文档字符串,你应该能看到如何使用它的其他功能——对于实际的文件传输,openFile是主要关注的内容。它会给你一个Deferred,当它与一个ISFTPFile提供者连接时触发。这个对象有读取和写入文件内容的方法。

撰写回答