Python FTP 隐式 TLS 连接问题

37
我需要连接到一个FTPS服务器,我可以使用lftp成功连接。但是当我尝试使用Python ftplib.FTP_TLS连接时,它会超时,堆栈跟踪显示它正在等待服务器发送欢迎消息或类似消息。有人知道问题在哪里以及如何克服吗?我想知道是否需要在服务器端执行某些操作,但是为什么lftp客户端能正常工作呢?非常感谢任何帮助。
这是堆栈跟踪:
    ftp = ftplib.FTP_TLS()  
    ftp.connect(cfg.HOST, cfg.PORT, timeout=60)
  File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 135, in connect  
    self.welcome = self.getresp()  
  File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 210, in getresp  
    resp = self.getmultiline()  
  File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 196, in getmultiline  
    line = self.getline()  
  File "C:\Users\username\Softwares\Python27\lib\ftplib.py", line 183, in getline  
    line = self.file.readline()  
  File "C:\Users\username\Softwares\Python27\lib\socket.py", line 447, in readline  
    data = self._sock.recv(self._rbufsize)  
socket.timeout: timed out  

使用lftp成功登录相同的ftps服务器:

$ lftp
lftp :~> open ftps://ip_address:990
lftp ip_address:~> set ftps:initial-prot P
lftp ip_address:~> login ftps_user_id  ftps_user_passwd
lftp sftp_user_id@ip_address:~> ls
ls: Fatal error: SSL_connect: self signed certificate
lftp ftps_user_id@ip_address:~> set ssl:verif-certificate off
lftp ftps_user_id@ip_address:~> ls
lftp ftps_user_id@ip_address:/>

顺便提一下,我正在使用Python 2.7.3。我在Google上进行了相当多的搜索,但没有找到任何有用的东西。

我仍然遇到这个问题,如果有人能帮忙就非常感激。仔细观察FTP.connect(),连接到服务器不是问题,但从服务器获取确认(或欢迎消息)是一个问题。lftp没有这个问题,FileZilla也没有任何问题,如这里的日志所示 -

Status: Connecting to xx.xx.xx.xxx:990...  
Status: Connection established, initializing TLS...  
Status: Verifying certificate...  
Status: TLS/SSL connection established, waiting for welcome message...  
Response:   220-      Vous allez vous connecter sur un serveur prive  
Response:   220-     Seules les personnes habilitees y sont autorisees  
Response:   220 Les contrevenants s'exposent aux poursuites prevues par la loi.  
Command:    USER xxxxxxxxxxxxx  
Response:   331 Password required for xxxxxxxxxxxxx.  
Command:    PASS **********  
Response:   230 Login OK. Proceed.  
Command:    PBSZ 0  
Response:   200 PBSZ Command OK. Protection buffer size set to 0.  
Command:    PROT P  
Response:   200 PROT Command OK. Using Private data connection  
Status: Connected  
Status: Retrieving directory listing...  
Command:    PWD  
Response:   257 "/" is current folder.  
Command:    TYPE I  
Response:   200 Type set to I.  
Command:    PASV  
Response:   227 Entering Passive Mode (81,93,20,199,4,206).  
Command:    MLSD  
Response:   150 Opening BINARY mode data connection for MLSD /.  
Response:   226 Transfer complete. 0 bytes transferred. 0 bps.  
Status: Directory listing successful  

我认为如果在ftp.connect调用之前展示更多的设置代码会有所帮助。 - David K. Hess
添加了设置代码...实际上并没有太多内容。因为我调用的是没有参数的构造函数,所以我必须分两步来完成。由于FTPS端口号为990,这不是默认端口号,所以我必须这样做。希望这可以帮助到您。 - Rg Glpj
你是否检查了 ftp.connect 的参数是否正确?例如,cfg.HOST 是一个适当的字符串,cfg.PORT 是一个适当的整数。此外,您可以通过设置ftp.debugging = True来开启一些有限的调试功能。 - David K. Hess
谢谢回复。是的,HOST是一个正确的字符串(实际上是一个IP地址),而PORT是整数990。打开调试模式不会抛出比上面显示的超时堆栈跟踪更多的信息。 - Rg Glpj
只是想说谢谢。我们将来会使用这些代码,这为我们节省了很多时间! - SilentSteel
遇到了完全相同的问题,@George Leslie-Waksman提供了完美的解决方案。 - Joshua Burns
7个回答

57

在扩展目前已提出的解决方案时,问题在于隐式FTPS连接需要套接字自动进行ssl包装,而在我们有机会调用login()之前。很多人所提出的子类都是在connect方法的上下文中完成这个过程,我们可以通过修改self.sock的get/set属性来自动包装套接字,从而更普遍地解决这个问题:

import ftplib
import ssl

class ImplicitFTP_TLS(ftplib.FTP_TLS):
    """FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sock = None

    @property
    def sock(self):
        """Return the socket."""
        return self._sock

    @sock.setter
    def sock(self, value):
        """When modifying the socket, ensure that it is ssl wrapped."""
        if value is not None and not isinstance(value, ssl.SSLSocket):
            value = self.context.wrap_socket(value)
        self._sock = value

使用方式基本与标准的FTP_TLS类相同:

ftp_client = ImplicitFTP_TLS()
ftp_client.connect(host='ftp.example.com', port=990)
ftp_client.login(user='USERNAME', passwd='PASSWORD')
ftp_client.prot_p()

6
DudE,你真是个救命恩人。我一直在尝试使用TLS/SSL隐式连接使ftplib正常工作,但却一直碰壁。这个方法非常顺利。 - Joshua Burns
4
我在使用这段代码时遇到了错误:ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1131),请问有什么建议吗? - Arya
非常感谢您的发布,这正是我一直在寻找的。真的非常感激。 - VISHAL AGGARWAL

23

我已经花了半天时间解决了同一个问题。

针对隐式FTP TLS/SSL(默认端口为990),我们的客户端程序必须在创建套接字之后立即建立TLS/SSL连接。但是Python的FTP_TLS类没有从FTP类重新加载connect函数。我们需要进行修复:

class tyFTP(ftplib.FTP_TLS):
  def __init__(self,
               host='',
               user='',
               passwd='',
               acct='',
               keyfile=None,
               certfile=None,
               timeout=60):

    ftplib.FTP_TLS.__init__(self,
                            host=host,
                            user=user,
                            passwd=passwd,
                            acct=acct,
                            keyfile=keyfile,
                            certfile=certfile,
                            timeout=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
    try:
        self.sock = socket.create_connection((self.host, self.port), self.timeout)
        self.af = self.sock.family
        # add this line!!!
        self.sock = ssl.wrap_socket(self.sock,
                                    self.keyfile,
                                    self.certfile,
                                    ssl_version=ssl.PROTOCOL_TLSv1)
        # add end
        self.file = self.sock.makefile('rb')
        self.welcome = self.getresp()
    except Exception as e:
        print(e)
    return self.welcome

这个派生类重新加载 connect 函数并在套接字周围构建一个 TLS 的封装器。在成功连接和登录到FTP服务器后,您需要调用:FTP_TLS.prot_p(),然后才能执行任何 FTP 命令!

希望这可以帮助 ^_^


这段代码适用于SFTP吗?我在应用这段代码时遇到了超时错误。 - Stephen Tetreault
init 中是否可以初始化到自定义端口的连接?由于它连接到21号端口,上述方法似乎不起作用。参考文档:“在进行身份验证之前,像往常一样连接到21号端口隐式地保护FTP控制连接。”- http://docs.python.org/2/library/ftplib.html - Ernest
5
这段代码对我似乎不起作用。可能是FTPS服务器端协议配置问题。但根据此线程:http://openssl.6102.n7.nabble.com/error-1408F10B-SSL-routines-SSL3-GET-RECORD-wrong-version-number-td22477.html,添加到该行的 ssl_version=ssl.PROTOCOL_TLSv1 部分会导致我出现 "SSLError: [Errno 1] _ssl.c:504: error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number" 的错误。 - Devy
1
我在使用getline时遇到了TypeError: elif line[-1:] in CRLF:,通过更新self.file = self.sock.makefile('rb')self.file = self.sock.makefile('r', encoding=self.encoding)来解决问题。 - D Read

12

这是一个稍微更加“工业化”的实现。

让我们注意到在之前的示例中,init 中缺少了名为 'context' 的属性。

下面的代码在 Python 2.7 和 Python 3 中都完美运行。

代码

import ftplib, socket, ssl
FTPTLS_OBJ = ftplib.FTP_TLS

# Class to manage implicit FTP over TLS connections, with passive transfer mode
# - Important note:
#   If you connect to a VSFTPD server, check that the vsftpd.conf file contains
#   the property require_ssl_reuse=NO
class FTPTLS(FTPTLS_OBJ):

    host = "127.0.0.1"
    port = 990
    user = "anonymous"
    timeout = 60

    logLevel = 0

    # Init both this and super
    def __init__(self, host=None, user=None, passwd=None, acct=None, keyfile=None, certfile=None, context=None, timeout=60):        
        FTPTLS_OBJ.__init__(self, host, user, passwd, acct, keyfile, certfile, context, timeout)

    # Custom function: Open a new FTPS session (both connection & login)
    def openSession(self, host="127.0.0.1", port=990, user="anonymous", password=None, timeout=60):
        self.user = user
        # connect()
        ret = self.connect(host, port, timeout)
        # prot_p(): Set up secure data connection.
        try:
            ret = self.prot_p()
            if (self.logLevel > 1): self._log("INFO - FTPS prot_p() done: " + ret)
        except Exception as e:
            if (self.logLevel > 0): self._log("ERROR - FTPS prot_p() failed - " + str(e))
            raise e
        # login()
        try:
            ret = self.login(user=user, passwd=password)
            if (self.logLevel > 1): self._log("INFO - FTPS login() done: " + ret)
        except Exception as e:
            if (self.logLevel > 0): self._log("ERROR - FTPS login() failed - " + str(e))
            raise e
        if (self.logLevel > 1): self._log("INFO - FTPS session successfully opened")

    # Override function
    def connect(self, host="127.0.0.1", port=990, timeout=60):
        self.host = host
        self.port = port
        self.timeout = timeout
        try:
            self.sock = socket.create_connection((self.host, self.port), self.timeout)
            self.af = self.sock.family
            self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
            self.file = self.sock.makefile('r')
            self.welcome = self.getresp()
            if (self.logLevel > 1): self._log("INFO - FTPS connect() done: " + self.welcome)
        except Exception as e:
            if (self.logLevel > 0): self._log("ERROR - FTPS connect() failed - " + str(e))
            raise e
        return self.welcome

    # Override function
    def makepasv(self):
        host, port = FTPTLS_OBJ.makepasv(self)
        # Change the host back to the original IP that was used for the connection
        host = socket.gethostbyname(self.host)
        return host, port

    # Custom function: Close the session
    def closeSession(self):
        try:
            self.close()
            if (self.logLevel > 1): self._log("INFO - FTPS close() done")
        except Exception as e:
            if (self.logLevel > 0): self._log("ERROR - FTPS close() failed - " + str(e))
            raise e
        if (self.logLevel > 1): self._log("INFO - FTPS session successfully closed")

    # Private method for logs
    def _log(self, msg):
        # Be free here on how to implement your own way to redirect logs (e.g: to a console, to a file, etc.)
        print(msg)

用法示例

host = "www.myserver.com"
port = 990
user = "myUserId"
password = "myPassword"

myFtps = FTPTLS()
myFtps.logLevel = 2
myFtps.openSession(host, port, user, password)
print(myFtps.retrlines("LIST"))
myFtps.closeSession()

输出示例

INFO - FTPS connect() done: 220 (vsFTPd 3.0.2)
INFO - FTPS prot_p() done: 200 PROT now Private.
INFO - FTPS login() done: 230 Login successful.
INFO - FTPS session successfully opened
-rw-------    1 ftp      ftp         86735 Mar 22 16:55 MyModel.yaml
-rw-------    1 ftp      ftp          9298 Mar 22 16:55 MyData.csv
226 Directory send OK.
INFO - FTPS close() done
INFO - FTPS session successfully closed

9

继承NERV的回答 - 这对我帮助很大,以下是我如何解决在端口990上需要身份验证的隐式TLS连接问题。

文件名: ImplicitTLS.py

from ftplib import FTP_TLS
import socket
import ssl

class tyFTP(FTP_TLS):
    def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, timeout=60):
        FTP_TLS.__init__(self, host, user, passwd, acct, keyfile, certfile, timeout)
    def connect(self, host='', port=0, timeout=-999):
        if host != '':
            self.host = host
        if port > 0:
            self.port = port
        if timeout != -999:
            self.timeout = timeout

        try: 
            self.sock = socket.create_connection((self.host, self.port), self.timeout)
            self.af = self.sock.family
            self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ssl_version=ssl.PROTOCOL_TLSv1)
            self.file = self.sock.makefile('rb')
            self.welcome = self.getresp()
        except Exception as e:
            print e
        return self.welcome

然后从我的主应用程序中我执行了以下操作:
from ImplicityTLS import tyFTP
server = tyFTP()
server.connect(host="xxxxx", port=990)
server.login(user="yyyy", passwd="fffff")
server.prot_p()

就是这样,我能够下载文件等等。感谢NERV提供的原始答案。


谢谢 @Brad Decker, 但我的程序还存在一个问题。使用这个解决方法后,ftp.connect()ftp.login()ftp.prot_p()甚至ftp.cwd()方法都能够正常工作,但是当我尝试调用ftp.retrlines('LIST')时却出现了错误。File "D:\Python\Python27\lib\ssl.py", line 808, in do_handshake self._sslobj.do_handshake() ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:590)。你有什么建议吗?谢谢。 - Yohn
这对我有效,但仅当您不将主机传递到__init__中时。如果您这样做,这会触发在主FTP类中尝试连接的操作,如果您使用端口990,则会失败。我通过添加一个端口参数并在调用FTP_TLS.__init__之前设置self.port来解决了这个问题。 - Kevin Shea

6

NERV和Brad Decker的答案和示例真的很有帮助。向他们致敬。他们为我节省了几个小时。

不幸的是,最初它对我不起作用。

在我的情况下,只要我从ssl.wrap_socket方法中删除ssl_version参数,连接就可以正常工作。此外,要向服务器发送任何命令,我必须重写FTP_TLS类中的ntransfercmd方法,并在那里删除ssl_version参数。

这是对我有效的代码:

from ftplib import FTP_TLS, FTP
import socket
import ssl

class IMPLICIT_FTP_TLS(FTP_TLS):
    def __init__(self, host='', user='', passwd='', acct='', keyfile=None,
        certfile=None, timeout=60):
        FTP_TLS.__init__(self, host, user, passwd, acct, keyfile, certfile, 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
        try:
            self.sock = socket.create_connection((self.host, self.port), self.timeout)
            self.af = self.sock.family
            self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
            self.file = self.sock.makefile('rb')
            self.welcome = self.getresp()
        except Exception as e:
            print (e)
        return self.welcome

    def ntransfercmd(self, cmd, rest=None):
        conn, size = FTP.ntransfercmd(self, cmd, rest)
        if self._prot_p:
            conn = ssl.wrap_socket(conn, self.keyfile, self.certfile)
        return conn, size

还有必要的样例:

>>> ftps = IMPLICIT_FTP_TLS()
>>> ftps.connect(host='your.ftp.host', port=990)
>>> ftps.login(user="your_user", passwd="your_passwd")
>>> ftps.prot_p()
>>> ftps.retrlines('LIST')

抱歉打扰了一个旧的帖子,但这是我能找到的最接近我的情况。我已经尝试过这个方法,但是出现了以下错误(我使用的是Python 3.3)。[WinError 10060] 由于连接方在一段时间后没有正确答复或已经拒绝连接,因此连接尝试失败,或者已经建立的连接失败,因为连接的主机没有响应。 - ryansin
1
也就是...在执行ftps.login(user="", passwd="")时出现了以下错误:File "C:\Python33\lib\ftplib.py", line 703, in login self.auth() File "C:\Python33\lib\ftplib.py", line 711, in auth resp = self.voidcmd('AUTH TLS') File "C:\Python33\lib\ftplib.py", line 268, in voidcmd self.putcmd(cmd) File "C:\Python33\lib\ftplib.py", line 195, in putcmd self.putline(line) File "C:\Python33\lib\ftplib.py", line 190, in putline self.sock.sendall(line.encode(self.encoding)) AttributeError: 'NoneType' object has no attribute 'sendall' - ryansin
谢谢,你的解决方案对我很有效。就像一个魔术一样。 ;) - sanfilippopablo

3

我曾经也遇到过使用隐式TLS的FTP服务器出现相同问题(也是在被动模式下)。 我遇到了多个错误:

450 TLS session of data connection has not resumed or the session does not match the control connection

针对提出的解决方案,记录一下,基于@George Leslie-Waksman的方案如下:

class ImplicitFTP_TLS(ftplib.FTP_TLS):
    """
    FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS.
    Prefer explicit TLS whenever possible.
    """

    def __init__(self, *args, **kwargs):
        """Initialise self."""
        super().__init__(*args, **kwargs)
        self._sock = None

    @property
    def sock(self):
        """Return the socket."""
        return self._sock

    @sock.setter
    def sock(self, value):
        """When modifying the socket, ensure that it is SSL wrapped."""
        if value is not None and not isinstance(value, ssl.SSLSocket):
            value = self.context.wrap_socket(value)
        self._sock = value

    def ntransfercmd(self, cmd, rest=None):
        """Override the ntransfercmd method"""
        conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
        conn = self.sock.context.wrap_socket(
            conn, server_hostname=self.host, session=self.sock.session
        )
        return conn, size

新颖之处在于 ntransfercmd 的覆盖。
然后它的使用方式类似于 ftplib.FTP_TLS(先前帖子中有大量示例)。
覆盖 connect 而不是使用 sock 属性/设置器也可以起作用。

我不太明白你做了什么,但非常感谢,它起作用了,你救了我。 - Lucas

2
我知道这个帖子已经很老了,而且ftp不像以前那么流行,但无论如何,如果有人需要,我想做出额外的贡献。我遇到了一个类似的情况,尝试使用IMPLICIT(端口990)ftps在被动模式下连接到ftp服务器。在这种情况下,服务器在协商初始连接之后,通常会提供一个新的主机IP地址和端口,可能与用于建立初始连接的地址不同,数据传输实际上应该在这些地址上发生。没有大问题,包括python在内的ftps客户端都可以处理这个问题,只是这个特定的服务器提供了一个非可路由的(可能是防火墙内部的)IP地址。我注意到FileZilla可以轻松连接,但是python ftplib不能。然后我遇到了这个帖子:

如何在ftplib中用服务器地址替换非路由IP地址

它给了我提示。使用Grzegorz Wierzowiecki的方法,我扩展了那个帖子中提到的方法,并得出了这个解决方案,解决了我的问题。
import ftplib, os, sys
import socket
import ssl
FTPS_OBJ = ftplib.FTP_TLS



def conn_i_ftps(FTP_Site, Login_Name, Login_Password):
    print "Starting IMPLICIT ftp_tls..."
    ftps = tyFTP()
    print ftps.connect(host=FTP_Site, port=990, timeout=120)
    ftps.prot_p()
    ftps.login(user=Login_Name, passwd=Login_Password)
    print "Logged In"
    ftps.retrlines('LIST')
    # return ftps


class tyFTP(FTPS_OBJ):
    def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, timeout=60):
        FTPS_OBJ.__init__(self, host, user, passwd, acct, keyfile, certfile, timeout)

    def connect(self, host='', port=0, timeout=-999):
        if host != '':
            self.host = host
        if port > 0:
            self.port = port
        if timeout != -999:
            self.timeout = timeout

        try:
            self.sock = socket.create_connection((self.host, self.port), self.timeout)
            self.af = self.sock.family
            self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
            self.file = self.sock.makefile('rb')
            self.welcome = self.getresp()
        except Exception as e:
            print e
        return self.welcome

    def makepasv(self):
        print port #<---Show passively assigned port
        print host #<---Show the non-routable, passively assigned IP
        host, port = FTPS_OBJ.makepasv(self)
        host = socket.gethostbyname(self.host) #<---- This changes the host back to the original IP that was used for the connection
        print 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
        print host #<----Showing the original IP
        return host, port

然后代码就像这样被调用。
FTP_Site       =  "ftp.someserver.com"
Login_Name     =  "some_name"
Login_Password =  "some_passwd"

conn_i_ftps(FTP_Site, Login_Name, Login_Password)

我想可以用一个IF语句来判断非路由地址,并将主机改回的行包装起来,就像这样:
if host.split(".")[0] in (10, 192, 172):
     host = socket.gethostbyname(self.host)
     .
     .
     .

嗨@andrew-3D,我遇到了同样的问题。我尝试了你的解决方案,但是在你的代码行ftps.retrlines('LIST')中出现了错误-> AttributeError: 'int' object has no attribute 'wrap_socket'。有什么建议吗? - David López
你的帖子今天救了我,@andrew-3D,非常感谢! - direvus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接