diff options
Diffstat (limited to 'Lib/ftplib.py')
-rw-r--r-- | Lib/ftplib.py | 314 |
1 files changed, 259 insertions, 55 deletions
diff --git a/Lib/ftplib.py b/Lib/ftplib.py index ea91c1707c..7c398879bc 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -33,17 +33,12 @@ python ftplib.py -d localhost -l -p -l # Modified by Jack to work on the mac. # Modified by Siebren to support docstrings and PASV. # Modified by Phil Schwartz to add storbinary and storlines callbacks. +# Modified by Giampaolo Rodola' to add TLS support. # import os import sys - -# Import SOCKS module if it exists, else standard socket module socket -try: - import SOCKS; socket = SOCKS; del SOCKS # import SOCKS as socket - from socket import getfqdn; socket.getfqdn = getfqdn; del getfqdn -except ImportError: - import socket +import socket from socket import _GLOBAL_DEFAULT_TIMEOUT __all__ = ["FTP","Netrc"] @@ -119,6 +114,20 @@ class FTP: if user: self.login(user, passwd, acct) + def __enter__(self): + return self + + # Context management protocol: try to quit() if active + def __exit__(self, *args): + if self.sock is not None: + try: + self.quit() + except (socket.error, EOFError): + pass + finally: + if self.sock is not None: + self.close() + def connect(self, host='', port=0, timeout=-999): '''Connect to host. Arguments are: - host: hostname to connect to (string, default previous host) @@ -162,7 +171,7 @@ class FTP: def sanitize(self, s): if s[:5] == 'pass ' or s[:5] == 'PASS ': i = len(s) - while i > 5 and s[i-1] in '\r\n': + while i > 5 and s[i-1] in {'\r', '\n'}: i = i-1 s = s[:5] + '*'*(i-5) + s[i:] return repr(s) @@ -212,7 +221,7 @@ class FTP: if self.debugging: print('*resp*', self.sanitize(resp)) self.lastresp = resp[:3] c = resp[:1] - if c in ('1', '2', '3'): + if c in {'1', '2', '3'}: return resp if c == '4': raise error_temp(resp) @@ -236,7 +245,7 @@ class FTP: if self.debugging > 1: print('*put urgent*', self.sanitize(line)) self.sock.sendall(line, MSG_OOB) resp = self.getmultiline() - if resp[:3] not in ('426', '225', '226'): + if resp[:3] not in {'426', '225', '226'}: raise error_proto(resp) def sendcmd(self, cmd): @@ -352,6 +361,7 @@ class FTP: conn, sockaddr = sock.accept() if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT: conn.settimeout(self.timeout) + sock.close() if resp[:3] == '150': # this is conditional in case we received a 125 size = parse150(resp) @@ -366,7 +376,7 @@ class FTP: if not user: user = 'anonymous' if not passwd: passwd = '' if not acct: acct = '' - if user == 'anonymous' and passwd in ('', '-'): + if user == 'anonymous' and passwd in {'', '-'}: # If there is no anonymous ftp password specified # then we'll just use anonymous@ # We don't send any other thing because: @@ -397,13 +407,12 @@ class FTP: The response code. """ self.voidcmd('TYPE I') - conn = self.transfercmd(cmd, rest) - while 1: - data = conn.recv(blocksize) - if not data: - break - callback(data) - conn.close() + with self.transfercmd(cmd, rest) as conn: + while 1: + data = conn.recv(blocksize) + if not data: + break + callback(data) return self.voidresp() def retrlines(self, cmd, callback = None): @@ -420,23 +429,21 @@ class FTP: """ if callback is None: callback = print_line resp = self.sendcmd('TYPE A') - conn = self.transfercmd(cmd) - fp = conn.makefile('r', encoding=self.encoding) - while 1: - line = fp.readline() - if self.debugging > 2: print('*retr*', repr(line)) - if not line: - break - if line[-2:] == CRLF: - line = line[:-2] - elif line[-1:] == '\n': - line = line[:-1] - callback(line) - fp.close() - conn.close() + with self.transfercmd(cmd) as conn, \ + conn.makefile('r', encoding=self.encoding) as fp: + while 1: + line = fp.readline() + if self.debugging > 2: print('*retr*', repr(line)) + if not line: + break + if line[-2:] == CRLF: + line = line[:-2] + elif line[-1:] == '\n': + line = line[:-1] + callback(line) return self.voidresp() - def storbinary(self, cmd, fp, blocksize=8192, callback=None): + 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: @@ -446,18 +453,18 @@ class FTP: the connection at once. [default: 8192] callback: An optional single parameter callable that is called on 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') - conn = self.transfercmd(cmd) - while 1: - buf = fp.read(blocksize) - if not buf: break - conn.sendall(buf) - if callback: callback(buf) - conn.close() + with self.transfercmd(cmd, rest) as conn: + while 1: + buf = fp.read(blocksize) + if not buf: break + conn.sendall(buf) + if callback: callback(buf) return self.voidresp() def storlines(self, cmd, fp, callback=None): @@ -473,16 +480,15 @@ class FTP: The response code. """ self.voidcmd('TYPE A') - conn = self.transfercmd(cmd) - while 1: - buf = fp.readline() - if not buf: break - if buf[-2:] != B_CRLF: - if buf[-1] in B_CRLF: buf = buf[:-1] - buf = buf + B_CRLF - conn.sendall(buf) - if callback: callback(buf) - conn.close() + with self.transfercmd(cmd) as conn: + while 1: + buf = fp.readline() + if not buf: break + if buf[-2:] != B_CRLF: + if buf[-1] in B_CRLF: buf = buf[:-1] + buf = buf + B_CRLF + conn.sendall(buf) + if callback: callback(buf) return self.voidresp() def acct(self, password): @@ -524,7 +530,7 @@ class FTP: def delete(self, filename): '''Delete a file.''' resp = self.sendcmd('DELE ' + filename) - if resp[:3] in ('250', '200'): + if resp[:3] in {'250', '200'}: return resp else: raise error_reply(resp) @@ -555,7 +561,11 @@ class FTP: def mkd(self, dirname): '''Make a directory, return its full pathname.''' - resp = self.sendcmd('MKD ' + dirname) + resp = self.voidcmd('MKD ' + dirname) + # fix around non-compliant implementations such as IIS shipped + # with Windows server 2003 + if not resp.startswith('257'): + return '' return parse257(resp) def rmd(self, dirname): @@ -564,7 +574,11 @@ class FTP: def pwd(self): '''Return current working directory.''' - resp = self.sendcmd('PWD') + resp = self.voidcmd('PWD') + # fix around non-compliant implementations such as IIS shipped + # with Windows server 2003 + if not resp.startswith('257'): + return '' return parse257(resp) def quit(self): @@ -581,6 +595,196 @@ class FTP: self.file = self.sock = None +try: + import ssl +except ImportError: + pass +else: + class FTP_TLS(FTP): + '''A FTP subclass which adds TLS support to FTP as described + in RFC-4217. + + Connect as usual to port 21 implicitly securing the FTP control + connection before authenticating. + + Securing the data connection requires user to explicitly ask + for it by calling prot_p() method. + + Usage example: + >>> from ftplib import FTP_TLS + >>> ftps = FTP_TLS('ftp.python.org') + >>> ftps.login() # login anonymously previously securing control channel + '230 Guest login ok, access restrictions apply.' + >>> ftps.prot_p() # switch to secure data connection + '200 Protection level set to P' + >>> ftps.retrlines('LIST') # list directory content securely + total 9 + drwxr-xr-x 8 root wheel 1024 Jan 3 1994 . + drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .. + drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin + drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc + d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming + drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib + drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub + drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr + -rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg + '226 Transfer complete.' + >>> ftps.quit() + '221 Goodbye.' + >>> + ''' + ssl_version = ssl.PROTOCOL_TLSv1 + + def __init__(self, host='', user='', passwd='', acct='', keyfile=None, + certfile=None, context=None, + timeout=_GLOBAL_DEFAULT_TIMEOUT): + if context is not None and keyfile is not None: + raise ValueError("context and keyfile arguments are mutually " + "exclusive") + if context is not None and certfile is not None: + raise ValueError("context and certfile arguments are mutually " + "exclusive") + self.keyfile = keyfile + self.certfile = certfile + self.context = context + self._prot_p = False + FTP.__init__(self, host, user, passwd, acct, timeout) + + def login(self, user='', passwd='', acct='', secure=True): + if secure and not isinstance(self.sock, ssl.SSLSocket): + self.auth() + return FTP.login(self, user, passwd, acct) + + def auth(self): + '''Set up secure control connection by using TLS/SSL.''' + if isinstance(self.sock, ssl.SSLSocket): + raise ValueError("Already using TLS") + if self.ssl_version == ssl.PROTOCOL_TLSv1: + resp = self.voidcmd('AUTH TLS') + else: + resp = self.voidcmd('AUTH SSL') + if self.context is not None: + self.sock = self.context.wrap_socket(self.sock) + else: + self.sock = ssl.wrap_socket(self.sock, self.keyfile, + self.certfile, + ssl_version=self.ssl_version) + self.file = self.sock.makefile(mode='r', encoding=self.encoding) + return resp + + def prot_p(self): + '''Set up secure data connection.''' + # PROT defines whether or not the data channel is to be protected. + # Though RFC-2228 defines four possible protection levels, + # RFC-4217 only recommends two, Clear and Private. + # Clear (PROT C) means that no security is to be used on the + # data-channel, Private (PROT P) means that the data-channel + # should be protected by TLS. + # PBSZ command MUST still be issued, but must have a parameter of + # '0' to indicate that no buffering is taking place and the data + # connection should not be encapsulated. + self.voidcmd('PBSZ 0') + resp = self.voidcmd('PROT P') + self._prot_p = True + return resp + + def prot_c(self): + '''Set up clear text data connection.''' + resp = self.voidcmd('PROT C') + self._prot_p = False + return resp + + # --- Overridden FTP methods + + def ntransfercmd(self, cmd, rest=None): + conn, size = FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + if self.context is not None: + conn = self.context.wrap_socket(conn) + else: + conn = ssl.wrap_socket(conn, self.keyfile, self.certfile, + ssl_version=self.ssl_version) + return conn, size + + def retrbinary(self, cmd, callback, blocksize=8192, rest=None): + self.voidcmd('TYPE I') + conn = self.transfercmd(cmd, rest) + try: + while 1: + data = conn.recv(blocksize) + if not data: + break + callback(data) + # shutdown ssl layer + if isinstance(conn, ssl.SSLSocket): + conn.unwrap() + finally: + conn.close() + return self.voidresp() + + def retrlines(self, cmd, callback = None): + if callback is None: callback = print_line + resp = self.sendcmd('TYPE A') + conn = self.transfercmd(cmd) + fp = conn.makefile('r', encoding=self.encoding) + try: + while 1: + line = fp.readline() + if self.debugging > 2: print('*retr*', repr(line)) + if not line: + break + if line[-2:] == CRLF: + line = line[:-2] + elif line[-1:] == '\n': + line = line[:-1] + callback(line) + # shutdown ssl layer + if isinstance(conn, ssl.SSLSocket): + conn.unwrap() + finally: + fp.close() + conn.close() + return self.voidresp() + + def storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None): + self.voidcmd('TYPE I') + conn = self.transfercmd(cmd, rest) + try: + while 1: + buf = fp.read(blocksize) + if not buf: break + conn.sendall(buf) + if callback: callback(buf) + # shutdown ssl layer + if isinstance(conn, ssl.SSLSocket): + conn.unwrap() + finally: + conn.close() + return self.voidresp() + + def storlines(self, cmd, fp, callback=None): + self.voidcmd('TYPE A') + conn = self.transfercmd(cmd) + try: + while 1: + buf = fp.readline() + if not buf: break + if buf[-2:] != B_CRLF: + if buf[-1] in B_CRLF: buf = buf[:-1] + buf = buf + B_CRLF + conn.sendall(buf) + if callback: callback(buf) + # shutdown ssl layer + if isinstance(conn, ssl.SSLSocket): + conn.unwrap() + finally: + conn.close() + return self.voidresp() + + __all__.append('FTP_TLS') + all_errors = (Error, IOError, EOFError, ssl.SSLError) + + _150_re = None def parse150(resp): @@ -689,9 +893,9 @@ def ftpcp(source, sourcename, target, targetname = '', type = 'I'): # transfer request. # So: STOR before RETR, because here the target is a "user". treply = target.sendcmd('STOR ' + targetname) - if treply[:3] not in ('125', '150'): raise error_proto # RFC 959 + if treply[:3] not in {'125', '150'}: raise error_proto # RFC 959 sreply = source.sendcmd('RETR ' + sourcename) - if sreply[:3] not in ('125', '150'): raise error_proto # RFC 959 + if sreply[:3] not in {'125', '150'}: raise error_proto # RFC 959 source.voidresp() target.voidresp() |