Python + Twisted + FtpClient + SOCKS

Posted

技术标签:

【中文标题】Python + Twisted + FtpClient + SOCKS【英文标题】: 【发布时间】:2012-12-26 12:49:35 【问题描述】:

我刚开始使用 Twisted。我想连接到 FTP 服务器并执行一些基本操作(如果可能,使用线程)。我正在使用这个example。

哪个做得很好。问题是如何在代码中添加 SOCKS4/5 代理使用?有人可以提供一个工作示例吗?我也试过这个link。

但是,

    # Copyright (c) Twisted Matrix Laboratories.
    # See LICENSE for details.


    """
    An example of using the FTP client
    """

    # Twisted imports
    from twisted.protocols.ftp import FTPClient, FTPFileListProtocol
    from twisted.internet.protocol import Protocol, ClientCreator
    from twisted.python import usage
    from twisted.internet import reactor, endpoints

    # Socks support test
    from socksclient import SOCKSv4ClientProtocol, SOCKSWrapper
    from twisted.web import client

    # Standard library imports
    import string
    import sys
    try:
        from cStringIO import StringIO
    except ImportError:
        from StringIO import StringIO


    class BufferingProtocol(Protocol):
        """Simple utility class that holds all data written to it in a buffer."""
        def __init__(self):
            self.buffer = StringIO()

        def dataReceived(self, data):
            self.buffer.write(data)

    # Define some callbacks

    def success(response):
        print 'Success!  Got response:'
        print '---'
        if response is None:
            print None
        else:
            print string.join(response, '\n')
        print '---'


    def fail(error):
        print 'Failed.  Error was:'
        print error

    def showFiles(result, fileListProtocol):
        print 'Processed file listing:'
        for file in fileListProtocol.files:
            print '    %s: %d bytes, %s' \
                  % (file['filename'], file['size'], file['date'])
        print 'Total: %d files' % (len(fileListProtocol.files))

    def showBuffer(result, bufferProtocol):
        print 'Got data:'
        print bufferProtocol.buffer.getvalue()


    class Options(usage.Options):
        optParameters = [['host', 'h', 'example.com'],
                         ['port', 'p', 21],
                         ['username', 'u', 'webmaster'],
                         ['password', None, 'justapass'],
                         ['passive', None, 0],
                         ['debug', 'd', 1],
                        ]

    # Socks support                    
    def wrappercb(proxy):
        print "connected to proxy", proxy
        pass

    def run():
        def sockswrapper(proxy, url):
            dest = client._parse(url) # scheme, host, port, path
            endpoint = endpoints.TCP4ClientEndpoint(reactor, dest[1], dest[2])
            return SOCKSWrapper(reactor, proxy[1], proxy[2], endpoint)

        # Get config
        config = Options()
        config.parseOptions()
        config.opts['port'] = int(config.opts['port'])
        config.opts['passive'] = int(config.opts['passive'])
        config.opts['debug'] = int(config.opts['debug'])

        # Create the client
        FTPClient.debug = config.opts['debug']
        creator = ClientCreator(reactor, FTPClient, config.opts['username'],
                                config.opts['password'], passive=config.opts['passive'])
        #creator.connectTCP(config.opts['host'], config.opts['port']).addCallback(connectionMade).addErrback(connectionFailed)

        # Socks support
        proxy = (None, '1.1.1.1', 1111, True, None, None)
        sw = sockswrapper(proxy, "ftp://example.com")
        d = sw.connect(creator)
        d.addCallback(wrappercb)

        reactor.run()

    def connectionFailed(f):
        print "Connection Failed:", f
        reactor.stop()

    def connectionMade(ftpClient):
        # Get the current working directory
        ftpClient.pwd().addCallbacks(success, fail)

        # Get a detailed listing of the current directory
        fileList = FTPFileListProtocol()
        d = ftpClient.list('.', fileList)
        d.addCallbacks(showFiles, fail, callbackArgs=(fileList,))

        # Change to the parent directory
        ftpClient.cdup().addCallbacks(success, fail)

        # Create a buffer
        proto = BufferingProtocol()

        # Get short listing of current directory, and quit when done
        d = ftpClient.nlst('.', proto)
        d.addCallbacks(showBuffer, fail, callbackArgs=(proto,))
        d.addCallback(lambda result: reactor.stop())


    # this only runs if the module was *not* imported
    if __name__ == '__main__':
        run()

我知道代码是错误的。我需要解决方案。

【问题讨论】:

FTP 是一个复杂的(阅读:不好的)协议,在继续之前需要澄清一些要点。第一:您需要支持主动转账还是 PASV 转账就足够了?第二:一定要用twisted吗? Python 附带一个 ftplib,可以扩展它以支持 socks。第三:你的 socks 代理是在 VIP 后面还是直接连接到 Internet 的单个主机? 1.任何类型的转移都可以。 2.它需要具有不同 SOCKS 的多线程。欢迎对如何使用 ftplib 和不同的袜子进行连接的任何想法。 3.只是一个普通的代理。 【参考方案1】:

好的,这里有一个解决方案(gist),它使用了python内置的ftplib,以及开源的SocksiPy module。

它不使用twisted,也不显式使用线程,但是使用threading.Threadthreading.Queue in python's standard threading module 中的threading.Queue 很容易在线程之间进行通信

基本上,我们需要继承 ftplib.FTP 以支持替换我们自己的 create_connection 方法并添加代理配置语义。

“主”逻辑只是配置一个通过 localhost socks 代理连接的 FTP 客户端,例如由ssh -D localhost:1080 socksproxy.example.com 创建的代理,并将 GNU autoconf 的源快照下载到本地磁盘。

import ftplib
import socket
import socks  # socksipy (https://github.com/mikedougherty/SocksiPy)


class FTP(ftplib.FTP):
    def __init__(self, host='', user='', passwd='', acct='',
                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                 proxyconfig=None):
        """Like ftplib.FTP constructor, but with an added `proxyconfig` kwarg

        `proxyconfig` should be a dictionary that may contain the following
        keys:

        proxytype - The type of the proxy to be used. Three types
                are supported: PROXY_TYPE_SOCKS4 (including socks4a),
                PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
        addr -      The address of the server (IP or DNS).
        port -      The port of the server. Defaults to 1080 for SOCKS
                servers and 8080 for HTTP proxy servers.
        rdns -      Should DNS queries be preformed on the remote side
                (rather than the local side). The default is True.
                Note: This has no effect with SOCKS4 servers.
        username -  Username to authenticate with to the server.
                The default is no authentication.
        password -  Password to authenticate with to the server.
                Only relevant when username is also provided.
        """
        self.proxyconfig = proxyconfig or 
        ftplib.FTP.__init__(self, host, user, passwd, acct, timeout)

    def connect(self, host='', port=0, timeout=-999):
        '''Connect to host.  Arguments are:
         - host: hostname to connect to (string, default previous host)
         - port: port to connect to (integer, default previous port)
        '''
        if host != '':
            self.host = host
        if port > 0:
            self.port = port
        if timeout != -999:
            self.timeout = timeout
        self.sock = self.create_connection(self.host, self.port)
        self.af = self.sock.family
        self.file = self.sock.makefile('rb')
        self.welcome = self.getresp()
        return self.welcome

    def create_connection(self, host=None, port=None):
        host, port = host or self.host, port or self.port
        if self.proxyconfig:

            phost, pport = self.proxyconfig['addr'], self.proxyconfig['port']
            err = None
            for res in socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM):
                af, socktype, proto, canonname, sa = res
                sock = None
                try:
                    sock = socks.socksocket(af, socktype, proto)
                    sock.setproxy(**self.proxyconfig)

                    if self.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
                        sock.settimeout(self.timeout)
                    sock.connect((host, port))
                    return sock

                except socket.error as _:
                    err = _
                    if sock is not None:
                        sock.close()

            if err is not None:
                raise err
            else:
                raise socket.error("getaddrinfo returns an empty list")
        else:
            sock = socket.create_connection((host, port), self.timeout)
        return sock

    def ntransfercmd(self, cmd, rest=None):
        size = None
        if self.passiveserver:
            host, port = self.makepasv()
            conn = self.create_connection(host, port)
            try:
                if rest is not None:
                    self.sendcmd("REST %s" % rest)
                resp = self.sendcmd(cmd)
                # Some servers apparently send a 200 reply to
                # a LIST or STOR command, before the 150 reply
                # (and way before the 226 reply). This seems to
                # be in violation of the protocol (which only allows
                # 1xx or error messages for LIST), so we just discard
                # this response.
                if resp[0] == '2':
                    resp = self.getresp()
                if resp[0] != '1':
                    raise ftplib.error_reply, resp
            except:
                conn.close()
                raise
        else:
            raise Exception("Active transfers not supported")
        if resp[:3] == '150':
            # this is conditional in case we received a 125
            size = ftplib.parse150(resp)
        return conn, size


if __name__ == '__main__':
    ftp = FTP(host='ftp.gnu.org', user='anonymous', passwd='guest',
              proxyconfig=dict(proxytype=socks.PROXY_TYPE_SOCKS5, rdns=False,
                               addr='localhost', port=1080))
    with open('autoconf-2.69.tar.xz', mode='w') as f:
        ftp.retrbinary("RETR /gnu/autoconf/autoconf-2.69.tar.xz", f.write)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         

详细说明我为什么问我最初的一些问题:

1) 您需要支持主动转账还是 PASV 转账就足够了?

通过 socks 代理进行主动传输要困难得多,因为它们需要使用 PORT 命令。使用 PORT 命令,您的 ftp 客户端告诉 FTP 服务器连接到特定端口(例如,在您的 PC 上)上的 you 以发送数据。这可能不适用于防火墙或 NAT/路由器后面的用户。如果您的 SOCKS 代理服务器没有在防火墙后面,或者具有公共 IP,则可以支持主动传输,但这很复杂:它需要您的 SOCKS 服务器(ssh -D 确实支持)和客户端库(socksipy 不支持)支持远程端口绑定。它还需要在应用程序中使用适当的挂钩(如果passiveserver = False,我的示例会引发异常)来执行远程 BIND 而不是本地 BIND。

2) 一定要用twisted吗?

Twisted 很棒,但我不是最擅长的,而且我还没有找到真正出色的 SOCKS 客户端实现。理想情况下,会有一个库允许您定义和/或将代理链接在一起,返回一个实现IReactorTCP 接口的对象,但我还没有找到类似的东西。

3) 您的 socks 代理是在 VIP 后面还是直接连接到 Internet 的单个主机?

这很重要,因为 PASV 传输安全的工作方式。在 PASV 传输中,客户端要求服务器提供连接端口以开始数据传输。当服务器接受该端口上的连接时,它应该验证客户端与请求传输的连接来自相同的源 IP。如果您的 SOCKS 服务器位于 VIP 后面,则为 PASV 传输建立的连接的出站 IP 不太可能与主要通信连接的出站 IP 匹配。

【讨论】:

以上是关于Python + Twisted + FtpClient + SOCKS的主要内容,如果未能解决你的问题,请参考以下文章

python之twisted模块安装

Python Twisted介绍

Twisted on Python3的真实现状是什么?

python twisted 模块安装问题

python3下的twisted

Python Twisted