在使用Python列出ftp目录时,遇到OSError错误

6

使用Python 3.6.3版本。

我正在通过FTP_TLS连接到FTP,使用ftplib库。

ftps = ftplib.FTP_TLS('example.com', timeout=5)

# TLS is more secure than SSL
ftps.ssl_version = ssl.PROTOCOL_TLS

# login before securing control channel
ftps.login('username@example.com', 'password')

# switch to secure data connection
ftps.prot_p()

# Explicitly set Passive mode
ftps.set_pasv(True)

这一切都可以运作。我可以发送ftps.mkd('mydir')(创建目录)和ftps.cwd('mydir')(更改工作目录),两者都可以正常工作。
但是,如果我发送任何一个以下命令(就我所知,它们基本上是同义词):
        ftps.dir()
        ftps.nlst()
        ftps.retrlines('LIST')
        ftps.retrlines('MLSD')

然后我收到一个异常(下面也包括所有的ftplib调试信息;通常与FileZilla显示的内容相匹配):

*cmd* 'AUTH TLS'
*put* 'AUTH TLS\r\n'
*get* '234 AUTH TLS OK.\n'
*resp* '234 AUTH TLS OK.'
*cmd* 'USER username@example.com'
*put* 'USER username@example.com\r\n'
*get* '331 User username@example.com OK. Password required\n'
*resp* '331 User username@example.com OK. Password required'
*cmd* 'PASS ******************************'
*put* 'PASS ******************************\r\n'
*get* '230 OK. Current restricted directory is /\n'
*resp* '230 OK. Current restricted directory is /'
*cmd* 'PBSZ 0'
*put* 'PBSZ 0\r\n'
*get* '200 PBSZ=0\n'
*resp* '200 PBSZ=0'
*cmd* 'PROT P'
*put* 'PROT P\r\n'
*get* '200 Data protection level set to "private"\n'
*resp* '200 Data protection level set to "private"'
*cmd* 'MKD mydir'
*put* 'MKD mydir\r\n'
*get* '257 "mydir" : The directory was successfully created\n'
*resp* '257 "mydir" : The directory was successfully created'
*cmd* 'CWD mydir'
*put* 'CWD mydir\r\n'
*get* '250 OK. Current directory is /mydir\n'
*resp* '250 OK. Current directory is /mydir'
*cmd* 'TYPE A'
*put* 'TYPE A\r\n'
*get* '200 TYPE is now ASCII\n'
*resp* '200 TYPE is now ASCII'
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (8,8,8,8,8,8)\n'
*resp* '227 Entering Passive Mode (8,8,8,8,8,8)'
*cmd* 'MLSD'
*put* 'MLSD\r\n'
*get* '150 Accepted data connection\n'
*resp* '150 Accepted data connection'
Traceback (most recent call last):
  File "c:\my_script.py", line 384, in run_ftps
    ftps.retrlines('MLSD')
  File "c:\libs\Python36\lib\ftplib.py", line 485, in retrlines
    conn.unwrap()
  File "C:\libs\Python36\lib\ssl.py", line 1051, in unwrap
    s = self._sslobj.unwrap()
  File "C:\libs\Python36\lib\ssl.py", line 698, in unwrap
    return self._sslobj.shutdown()
OSError: [Errno 0] Error

通过FileZilla,相同的FTP命令(LIST)可以正常工作。

通过谷歌搜索,我找到的最相关的信息是这个网址:https://bugs.python.org/msg253161 - 但我不确定它是否相关或相关。

简短版本: "OSError:[Errno 0] Error"实际上是什么意思,以及如何列出目录内容?

编辑:问题似乎只会在FTP_TLS上发生。 通过普通的FTP连接可以正常工作,但我需要FTP_TLS。

3个回答

15

问题可能是FTP服务器要求新数据通道中的TLS会话与控制通道相同。这在Python 3.7中尚未得到解决。按照此处找到的解决方案,将ftplib.FTP_TLS作为子类,并由我进行了小的修复:

import ftplib
from ssl import SSLSocket


class ReusedSslSocket(SSLSocket):
    def unwrap(self):
        pass


class MyFTP_TLS(ftplib.FTP_TLS):
    """Explicit FTPS, with shared TLS session"""
    def ntransfercmd(self, cmd, rest=None):
        conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest)
        if self._prot_p:
            conn = self.context.wrap_socket(conn,
                                            server_hostname=self.host,
                                            session=self.sock.session)  # reuses TLS session            
            conn.__class__ = ReusedSslSocket  # we should not close reused ssl socket when file transfers finish
        return conn, size

像这样使用:

ftps = MyFTP_TLS('example.com', timeout=5)

2
    s = self._sslobj.unwrap()
  File "E:\Software\_libs\Python36\lib\ssl.py", line 698, in unwrap
    return self._sslobj.shutdown()
OSError: [Errno 0] Error

这个错误是由于服务器在没有进行适当的TLS关闭的情况下关闭了TCP连接造成的。"OSError: [Errno 0]"表示在底层TCP套接字中实际上没有错误,即服务器正常关闭了TCP套接字。只是,在此之前,服务器没有执行所需的TLS级别关闭。
对我来说,这看起来像是服务器实现中的一个bug,但也可能是导致这种情况的配置选项。而且,Python ftplib似乎比其他客户端更不容忍这种不正确的协议实现。但是,您可以通过搜索"Server did not properly shut down TLS connection"来找到其他客户端的类似报告。
修复方法可能是将ftplib.py中retrlinesretrbinary中的conn.unwrap()放入try...except语句中并忽略该错误。

1
我今天遇到了这个问题,想要对这个解决方案进行补充。
我需要让makepasv返回FTP服务器的主机名,而不是防火墙后面的内部IP地址。这个passv修复加上TLS会话修复解决了我的问题。
class ReusedSslSocket(SSLSocket):
    def unwrap(self):
        pass
class FTP_TLS_IgnoreHost(FTP_TLS):
    def makepasv(self):
        _, port = super().makepasv()
        return self.host, port
    """Explicit FTPS, with shared TLS session"""
    def ntransfercmd(self, cmd, rest=None):
        conn, size = FTP.ntransfercmd(self, cmd, rest)
        if self._prot_p:
            conn = self.context.wrap_socket(conn,
                      server_hostname=self.host,
                      session=self.sock.session)  # reuses TLS session            
            conn.__class__ = ReusedSslSocket  # we should not close reused ssl socket when file transfers finish
        return conn, size

ftp = FTP_TLS_IgnoreHost(host, timeout=5)
ftp.ssl_version = ssl.PROTOCOL_TLS
ftp.auth()
ftp.login(user,passwd)
ftp.prot_p()
ftp.set_pasv(True)
pwd = ftp.pwd()
print(pwd)
print(ftp.nlst())
ftp.close()

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