Connecting to 'Explicit FTP over TLS' in P

2019-01-25 23:56发布

问题:

I cannot figure out how to see the file contents of an FTP site using ftplib.

I can connect to the FTP site using WinSCP just fine, and see the 6 files in the root directory.

In Python 3.4, I am using the following code:

        from ftplib import FTP_TLS
        ftps = FTP_TLS(timeout=100)           
        ftps.connect(ipAddress, 21)
        ftps.auth()
        ftps.prot_p()
        ftps.login('username', 'password')

Which produces:

        Out[72]: '230 User logged in.'

I can then run this:

        ftps.pwd()

...and I see that I am in the root directory:

        Out[73]: '/'

Everything seems to be gravy. BUT, when I try to see what is in the directory, using ftps.dir() or ftps.retrlines('NLST'), or anything else I have tried, I get a timeout:

TimeoutError: [WinError 10060] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond

I have googled around, and found several people saying it could be a 'passive vs active' connection setting problem. However, I asked the administrator of the FTP if it is passive or active, and he acted incredulous, just saying, "It's not SFTP. It is explicit FTP over TLS!!"

I am pretty ignorant of any of this. I am just trying to use ftplib (or any other package!) to connect to his FTP and download some files.

What am I missing??

EDIT: I just figured out WinSCP is using passive mode, so that seems to be what works. As I understand it, ftplib is passive by default, but I went ahead and set the flag to true anyways. This did nothing, and I am still having the same issue.

I also checked my firewall (grabbing at straws), and it is completely turned off, so there should be no issues there.

I also tried sending explicit commands with the '.sendcmd()' method, which seems to be able to send commands, but will not grab any responses (or it cannot see the responses).

Is there some reason why I am able to see the directory I am in, but not any other data? I am obviously connected and talking to the server, and it would seem like the directory being available and viewable to me would imply data can go back and forth. Is the data possibly coming back in some way ftplib doesn't recognize? Any thoughts on where should I begin with troubleshooting this?

Finally, again, it seems like everything is setup correctly on their end, as I can see everything fine in WinSCP. What settings should I check to compare to ftplib?

EDIT 2:

As requested, I set the debug level (thanks for this tip), and below is the output:

ftps = FTP_TLS(timeout=15)
ftps.set_debuglevel(2)                   
ftps.connect(ipAddress, 21)
ftps.set_pasv(True)            
ftps.auth()            
ftps.prot_p()
ftps.login('username', 'password')
ftps.retrlines('NLST')
*get* '220 Microsoft FTP Service\n'
*resp* '220 Microsoft FTP Service'
*cmd* 'AUTH TLS'
*put* 'AUTH TLS\r\n'
*get* '234 AUTH command ok. Expecting TLS Negotiation.\n'
*resp* '234 AUTH command ok. Expecting TLS Negotiation.'
*cmd* 'PBSZ 0'
*put* 'PBSZ 0\r\n'
*get* '200 PBSZ command successful.\n'
*resp* '200 PBSZ command successful.'
*cmd* 'PROT P'
*put* 'PROT P\r\n'
*get* '200 PROT command successful.\n'
*resp* '200 PROT command successful.'
*cmd* 'USER username'
*put* 'USER username\r\n'
*get* '331 Password required for username.\n'
*resp* '331 Password required for username.'
*cmd* 'PASS ****************'
*put* 'PASS ****************\r\n'
*get* '230 User logged in.\n'
*resp* '230 User logged in.'
*cmd* 'TYPE A'
*put* 'TYPE A\r\n'
*get* '200 Type set to A.\n'
*resp* '200 Type set to A.'
*cmd* 'PASV'
*put* 'PASV\r\n'
*get* '227 Entering Passive Mode (10,19,1,137,195,128).\n'
*resp* '227 Entering Passive Mode (10,19,1,137,195,128).'
Traceback (most recent call last):

  File "<ipython-input-13-a79eb3c23dc5>", line 8, in <module>
    ftps.retrlines('NLST')

  File "C:\Anaconda3\lib\ftplib.py", line 467, in retrlines
    with self.transfercmd(cmd) as conn, 
  File "C:\Anaconda3\lib\ftplib.py", line 398, in transfercmd
    return self.ntransfercmd(cmd, rest)[0]

  File "C:\Anaconda3\lib\ftplib.py", line 793, in ntransfercmd
    conn, size = FTP.ntransfercmd(self, cmd, rest)

  File "C:\Anaconda3\lib\ftplib.py", line 360, in ntransfercmd
    source_address=self.source_address)

  File "C:\Anaconda3\lib\socket.py", line 516, in create_connection
    raise err

  File "C:\Anaconda3\lib\socket.py", line 507, in create_connection
    sock.connect(sa)

timeout: timed out

EDIT 3:

I tried to just set the passive flag to false, but when I ask for files, I get the following:

ftps.retrlines('NLST')
*cmd* 'TYPE A'
*put* 'TYPE A\r\n'
*get* '200 Type set to A.\n'
*resp* '200 Type set to A.'
*cmd* 'PORT 10,1,10,100,223,39'
*put* 'PORT 10,1,10,100,223,39\r\n'
*get* '501 Server cannot accept argument.\n'
*resp* '501 Server cannot accept argument.'

EDIT 4:

I enabled logging on WinSCP, and it looks like the helpful Steffen Ullrich was right! This was in the log for WinSCP:

< 2017-05-19 08:44:27.880 227 Entering Passive Mode (10,19,1,137,195,139).
< 2017-05-19 08:44:27.880 Server sent passive reply with unroutable address 10.19.1.137, using host address instead.

So, how do I get ftplib to do the same thing?

EDIT 5:

Found THIS - which is just rewriting the source to make it look to the host. Anyone else got a simpler, less intrusive/possibly-destructive way to do this?

As a second option, since the admin seems less than up to snuff, is there something explicit I can tell him to go do to the host to make this work better (i.e. - have passive return the correct IP)?

回答1:

*get* '227 Entering Passive Mode (10,19,1,137,195,128).\n'

The problem is that the server is returning the wrong IP address inside the response to the PASV command. This is typical for servers in some internal network behind some firewall. In this case 10.19.1.137 is returned, which is an IP address usable in local networks only.

This is a broken setup of the FTP server. But unfortunately such broken setup is common so many clients work around it by ignoring the given IP address in the response and using instead the IP address of the control connection. ftplib has no support for such workaround. But one can monkey patch it to provide such support:

from ftplib import FTP_TLS

# replace original makepasv function with one which always returns
# the peerhost of the control connections as peerhost for the data
# connection
_old_makepasv = FTP_TLS.makepasv
def _new_makepasv(self):
    host,port = _old_makepasv(self)
    host = self.sock.getpeername()[0]
    return host,port
FTP_TLS.makepasv = _new_makepasv

ftp = FTP_TLS(ipAddress)
ftp.login(...)
ftp.nlst()

This is successfully tested with Python 2.7.12 and Python 3.5.2