FileZilla Server从1.0版本开始,FTPS默认会强制采用TLS_RESUME。采用Python的FTP_TLS模块的话,需要自己继承一个子类,并进行修改实现TLS_RESUME, 并skip掉SSLSocket的unwrap。
#If you met the problem of 450 TLS session of data connection has not resumed or the session does not match the control connection
#I met this problem on filezilla server, it is because python FTP_TLS can't support resume session
#there is a workaround to solve this problem
#reference: https://stackoverflow.com/questions/46633536/getting-a-oserror-when-trying-to-list-ftp-directories-in-python
class ReusedSslSocket(SSLSocket):
def unwrap(self):
pass
#MyFTP_TLS is derieved to support TLS_RESUME(filezilla server)
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
如果是TLS_RESUME模式,就可以用自己的子类来处理。
ftp_host='192.168.1.100'
ftp_port=990
# ftps = ftplib.FTP_TLS(host=ftp_host,timeout=15)
ftps = MyFTP_TLS(host=ftp_host,timeout=15)
ftps.auth()
ftps.port=ftp_port
print(ftps.login('sample','xxxxxxx'))
ftps.prot_p()
ftps.set_pasv(1)
print(ftps.welcome)
print(ftps.cwd("/test"))
print(ftps.retrlines('LIST'))
但是当用storbinary上传文件的时候,就会收到一个425错误信息。425信息看上去,就是当你从命令session拿服务器回应消息时,服务器端发现数据连接异常。
ftplib.error_temp: 425 Error while transfering data: ECONNABORTED - Connection aborted
同时,还发现一个有趣的现象,服务器端有文件上传,但是大小是0. 尝试客户端用time.sleep(3)等待3秒,即便还是拿到425信息,但是文件上传却是正常的。
搜索一些论坛的讨论,讨论了产生这个问题的原因,但是并没有找到一个直接的Python的解决方法。基本意见认为直接忽略425。
In this PR, the SSL Stream gets closed first. Also, if the method is available, the SSL Stream gets shutdown and will not be disposed in a hard way, which can lead to an error on FileZilla servers. The socket will also send a shutdown signal, so that the other endpoint knows, that the connection will be closed.
结合上面文件字节是0的现象,同时从服务器端同时看到错误信息如下,server端会认为传送数据的TLS连接没有被适当的关闭。
基于这些,我做了一个假设,FileZilla Server采用一个并不寻常的方式。
1)服务器端不会同List一样做同步响应,收到传送的文件字节流后,不会直接响应客户端的(从等待后文件能正常写入推测)。
2) 传送文件结束后,要求客户端自己管理session关闭,而不能不关闭。
基于这个推测,继承改写了storbinary函数。主要是把conn的基类在overwrite到正常的SSLSocket,从而调用unwrap的时候能正常触发shutdown。
def storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None):
"""Store a file in binary mode. A new port is created for you.
Args:
cmd: A STOR command.
fp: A file-like object with a read(num_bytes) method.
blocksize: The maximum data size to read from fp and send over
the connection at once. [default: 8192]
callback: An optional single parameter callable that is called on
each block of data after it is sent. [default: None]
rest: Passed to transfercmd(). [default: None]
Returns:
The response code.
"""
self.voidcmd('TYPE I')
with self.transfercmd(cmd, rest) as conn:
while 1:
buf = fp.read(blocksize)
if not buf:
break
conn.sendall(buf)
if callback:
callback(buf)
# shutdown ssl layer
if ftplib._SSLSocket is not None and isinstance(conn, ftplib._SSLSocket):
conn.__class__=SSLSocket
conn.unwrap()
return self.voidresp()
这个时候,发现果然起了作用,但是客户端会挂住,直到15秒的timeout后触发一个异常(socket.timeout: The read operation timed out)。
此时,文件被正常上传,同时服务器端向客户端发出226的成功指令。如下图,226是在15秒后,就是客户端timeout异常触发的时候,但是服务器端认为连接此时被客户端正常地关闭了。
根据这个情况,我的假设有效,可以进一步推测客户端挂住的原因很大可能是Filezilla的服务器端也是采用同样的策略,完全不会理会客户端关闭的请求,不做任何回应,在收到客户端的shutdown通知后,就直接单方面处理关闭数据连接。由此,想到了一个折中的方法,客户端捕获并忽略timeout的异常。
代码修改:
# shutdown ssl layer
if ftplib._SSLSocket is not None and isinstance(conn, ftplib._SSLSocket):
conn.__class__=SSLSocket
conn.setblocking(False)
try:
conn.unwrap()
except Exception as re:
print("met error '{0}', but it is harmless".format(re))
此时,客户端采取不需要blocking的模式,捕获的timeout异常认为正常。
经过这个修改后,个人觉得是一个不错的结果,至少比忽略425要好,服务器端也不会有异常信息。



