Today's project involves automatically uploading electrical metering data to an FTPS server (explicit FTP over TLS, otherwise knowns as ESFTP). Shouldn't be a problem, since Python supports FTPS out of the box. Only it doesn't work. Here's the code:
import ftplib
ftp = ftplib.FTP_TLS('host', 'user', 'password')
ftp.set_debuglevel(1)
ftp.cwd('directory')
with open('filename', 'rb') as f:
ftp.storbinary('STOR filename', f)
ftp.quit()
After a successful initial connection, the data transfer connection fails:
*cmd* 'PASV'
*resp* '227 Entering Passive Mode (10,200,0,100,255,150).'
*cmd* 'CWD collect/CSV'
*resp* '250 CWD command successful'
*cmd* 'TYPE I'
*resp* '200 Type set to I'
*cmd* 'PASV'
*resp* '227 Entering Passive Mode (10,200,0,100,255,123).'
Traceback (most recent call last):
File "metering/__main__.py", line 143, in main
ftp.storbinary('STOR {}'.format(filename), stream)
File ".../lib/python3.5/ftplib.py", line 503, in storbinary
with self.transfercmd(cmd, rest) as conn:
File ".../lib/python3.5/ftplib.py", line 398, in transfercmd
return self.ntransfercmd(cmd, rest)[0]
File ".../lib/python3.5/ftplib.py", line 793, in ntransfercmd
conn, size = FTP.ntransfercmd(self, cmd, rest)
File ".../lib/python3.5/ftplib.py", line 360, in ntransfercmd
source_address=self.source_address)
File ".../lib/python3.5/socket.py", line 711, in create_connection
raise err
File ".../lib/python3.5/socket.py", line 702, in create_connection
sock.connect(sa)
OSError: [Errno 113] No route to host
The most confusing aspect was that the transfer worked perfectly well via FileZilla or Gnome "Connect to Server".
I eventually noticed a message in the FileZilla logs, Server sent passive reply with unroutable address. Using server address instead
. It turns out that the FTPS server was mis-configured and was replying to the PASV
command with an internal IP address that was not accessible from the public internet. It seems that this is a common enough configuration issue that some FTP clients detect the problem and use the existing server address instead. Python's FTP client doesn't do this though.
The solution was to sub-class the FTP_TLS
class and force it to ignore the response:
class FTP_TLS_IgnoreHost(ftplib.FTP_TLS):
def makepasv(self):
_, port = super().makepasv()
return self.host, port
ftp = FTP_TLS_IgnoreHost('host', 'user', 'password')
The makepasv
method parses the remote server's response to PASV
and returns a host and a port. We're extending this method to throw away the returned host and use the one we already have from the original connection. The underscore is just a convention to indicate that we don't care about the first item in the tuple, the host (it's special in some languages like Prolog, but not in Python). Note of course that this approach doesn't attempt to detect unroutable addresses like FileZilla, it just assumes.
There are no doubt third-party packages that provide this functionality and more, but for such a small extension, it wasn't worth the hassle.