diff options
Diffstat (limited to 'Lib/ftplib.py')
-rw-r--r-- | Lib/ftplib.py | 341 |
1 files changed, 200 insertions, 141 deletions
diff --git a/Lib/ftplib.py b/Lib/ftplib.py index a62218b333..741e64794e 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -38,13 +38,7 @@ python ftplib.py -d localhost -l -p -l 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"] @@ -72,6 +66,7 @@ all_errors = (Error, IOError, EOFError) # Line terminators (we always output CRLF, but accept any of CRLF, CR, LF) CRLF = '\r\n' +B_CRLF = b'\r\n' # The class itself class FTP: @@ -96,7 +91,7 @@ class FTP: below for details). The download/upload functions first issue appropriate TYPE and PORT or PASV commands. -''' + ''' debugging = 0 host = '' @@ -105,23 +100,41 @@ class FTP: file = None welcome = None passiveserver = 1 + encoding = "latin-1" # Initialization method (called by class instantiation). # Initialize host to localhost, port to standard ftp port # Optional arguments are host (for connect()), # and user, passwd, acct (for login()) def __init__(self, host='', user='', passwd='', acct='', - timeout=_GLOBAL_DEFAULT_TIMEOUT): + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + self.source_address = source_address self.timeout = timeout if host: self.connect(host) if user: self.login(user, passwd, acct) - def connect(self, host='', port=0, timeout=-999): + 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, source_address=None): '''Connect to host. Arguments are: - host: hostname to connect to (string, default previous host) - port: port to connect to (integer, default previous port) + - source_address: a 2-tuple (host, port) for the socket to bind + to as its source address before connecting. ''' if host != '': self.host = host @@ -129,9 +142,12 @@ class FTP: self.port = port if timeout != -999: self.timeout = timeout - self.sock = socket.create_connection((self.host, self.port), self.timeout) + if source_address is not None: + self.source_address = source_address + self.sock = socket.create_connection((self.host, self.port), self.timeout, + source_address=self.source_address) self.af = self.sock.family - self.file = self.sock.makefile('rb') + self.file = self.sock.makefile('r', encoding=self.encoding) self.welcome = self.getresp() return self.welcome @@ -139,7 +155,7 @@ class FTP: '''Get the welcome message from the server. (this is read and squirreled away by connect())''' if self.debugging: - print '*welcome*', self.sanitize(self.welcome) + print('*welcome*', self.sanitize(self.welcome)) return self.welcome def set_debuglevel(self, level): @@ -159,22 +175,20 @@ class FTP: # Internal: "sanitize" a string for printing 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': - i = i-1 + if s[:5] in {'pass ', 'PASS '}: + i = len(s.rstrip('\r\n')) s = s[:5] + '*'*(i-5) + s[i:] return repr(s) # Internal: send one line to the server, appending CRLF def putline(self, line): line = line + CRLF - if self.debugging > 1: print '*put*', self.sanitize(line) - self.sock.sendall(line) + if self.debugging > 1: print('*put*', self.sanitize(line)) + self.sock.sendall(line.encode(self.encoding)) # Internal: send one command to the server (through putline()) def putcmd(self, line): - if self.debugging: print '*cmd*', self.sanitize(line) + if self.debugging: print('*cmd*', self.sanitize(line)) self.putline(line) # Internal: return one line from the server, stripping CRLF. @@ -182,7 +196,7 @@ class FTP: def getline(self): line = self.file.readline() if self.debugging > 1: - print '*get*', self.sanitize(line) + print('*get*', self.sanitize(line)) if not line: raise EOFError if line[-2:] == CRLF: line = line[:-2] elif line[-1:] in CRLF: line = line[:-1] @@ -208,22 +222,22 @@ class FTP: # Raise various errors if the response indicates an error def getresp(self): resp = self.getmultiline() - if self.debugging: print '*resp*', self.sanitize(resp) + 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 + raise error_temp(resp) if c == '5': - raise error_perm, resp - raise error_proto, resp + raise error_perm(resp) + raise error_proto(resp) def voidresp(self): """Expect a response beginning with '2'.""" resp = self.getresp() if resp[:1] != '2': - raise error_reply, resp + raise error_reply(resp) return resp def abort(self): @@ -231,12 +245,13 @@ class FTP: This does not follow the procedure from the RFC to send Telnet IP and Synch; that doesn't seem to work with the servers I've tried. Instead, just send the ABOR command as OOB data.''' - line = 'ABOR' + CRLF - if self.debugging > 1: print '*put urgent*', self.sanitize(line) + line = b'ABOR' + B_CRLF + 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'): - raise error_proto, resp + if resp[:3] not in {'426', '225', '226'}: + raise error_proto(resp) + return resp def sendcmd(self, cmd): '''Send a command and return the response.''' @@ -266,7 +281,7 @@ class FTP: if self.af == socket.AF_INET6: af = 2 if af == 0: - raise error_proto, 'unsupported address family' + raise error_proto('unsupported address family') fields = ['', repr(af), host, repr(port), ''] cmd = 'EPRT ' + '|'.join(fields) return self.voidcmd(cmd) @@ -280,7 +295,8 @@ class FTP: try: sock = socket.socket(af, socktype, proto) sock.bind(sa) - except socket.error, err: + except socket.error as _: + err = _ if sock: sock.close() sock = None @@ -291,6 +307,7 @@ class FTP: raise err else: raise socket.error("getaddrinfo returns an empty list") + raise socket.error(msg) sock.listen(1) port = sock.getsockname()[1] # Get proper port host = self.sock.getsockname()[0] # Get proper host @@ -327,7 +344,8 @@ class FTP: size = None if self.passiveserver: host, port = self.makepasv() - conn = socket.create_connection((host, port), self.timeout) + conn = socket.create_connection((host, port), self.timeout, + source_address=self.source_address) try: if rest is not None: self.sendcmd("REST %s" % rest) @@ -341,13 +359,12 @@ class FTP: if resp[0] == '2': resp = self.getresp() if resp[0] != '1': - raise error_reply, resp + raise error_reply(resp) except: conn.close() raise else: - sock = self.makeport() - try: + with self.makeport() as sock: if rest is not None: self.sendcmd("REST %s" % rest) resp = self.sendcmd(cmd) @@ -355,12 +372,10 @@ class FTP: if resp[0] == '2': resp = self.getresp() if resp[0] != '1': - raise error_reply, resp + raise error_reply(resp) conn, sockaddr = sock.accept() if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT: conn.settimeout(self.timeout) - finally: - sock.close() if resp[:3] == '150': # this is conditional in case we received a 125 size = parse150(resp) @@ -375,7 +390,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: @@ -388,7 +403,7 @@ class FTP: if resp[0] == '3': resp = self.sendcmd('PASS ' + passwd) if resp[0] == '3': resp = self.sendcmd('ACCT ' + acct) if resp[0] != '2': - raise error_reply, resp + raise error_reply(resp) return resp def retrbinary(self, cmd, callback, blocksize=8192, rest=None): @@ -406,20 +421,19 @@ 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): """Retrieve data in line mode. A new port is created for you. Args: - cmd: A RETR, LIST, NLST, or MLSD command. + cmd: A RETR, LIST, or NLST command. callback: An optional single parameter callable that is called for each line with the trailing CRLF stripped. [default: print_line()] @@ -429,20 +443,18 @@ class FTP: """ if callback is None: callback = print_line resp = self.sendcmd('TYPE A') - conn = self.transfercmd(cmd) - fp = conn.makefile('rb') - 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, rest=None): @@ -461,13 +473,12 @@ class FTP: The response code. """ self.voidcmd('TYPE I') - conn = self.transfercmd(cmd, rest) - 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): @@ -483,16 +494,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:] != CRLF: - if buf[-1] in CRLF: buf = buf[:-1] - buf = buf + 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,27 +534,55 @@ class FTP: cmd = cmd + (' ' + arg) self.retrlines(cmd, func) + def mlsd(self, path="", facts=[]): + '''List a directory in a standardized format by using MLSD + command (RFC-3659). If path is omitted the current directory + is assumed. "facts" is a list of strings representing the type + of information desired (e.g. ["type", "size", "perm"]). + + Return a generator object yielding a tuple of two elements + for every file found in path. + First element is the file name, the second one is a dictionary + including a variable number of "facts" depending on the server + and whether "facts" argument has been provided. + ''' + if facts: + self.sendcmd("OPTS MLST " + ";".join(facts) + ";") + if path: + cmd = "MLSD %s" % path + else: + cmd = "MLSD" + lines = [] + self.retrlines(cmd, lines.append) + for line in lines: + facts_found, _, name = line.rstrip(CRLF).partition(' ') + entry = {} + for fact in facts_found[:-1].split(";"): + key, _, value = fact.partition("=") + entry[key.lower()] = value + yield (name, entry) + def rename(self, fromname, toname): '''Rename a file.''' resp = self.sendcmd('RNFR ' + fromname) if resp[0] != '3': - raise error_reply, resp + raise error_reply(resp) return self.voidcmd('RNTO ' + toname) 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 + raise error_reply(resp) def cwd(self, dirname): '''Change to a directory.''' if dirname == '..': try: return self.voidcmd('CDUP') - except error_perm, msg: + except error_perm as msg: if msg.args[0][:3] != '500': raise elif dirname == '': @@ -558,14 +596,15 @@ class FTP: resp = self.sendcmd('SIZE ' + filename) if resp[:3] == '213': s = resp[3:].strip() - try: - return int(s) - except (OverflowError, ValueError): - return long(s) + return int(s) 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): @@ -574,7 +613,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): @@ -632,11 +675,19 @@ else: ssl_version = ssl.PROTOCOL_TLSv1 def __init__(self, host='', user='', passwd='', acct='', keyfile=None, - certfile=None, timeout=_GLOBAL_DEFAULT_TIMEOUT): + certfile=None, context=None, + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + 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) + FTP.__init__(self, host, user, passwd, acct, timeout, source_address) def login(self, user='', passwd='', acct='', secure=True): if secure and not isinstance(self.sock, ssl.SSLSocket): @@ -651,9 +702,21 @@ else: resp = self.voidcmd('AUTH TLS') else: resp = self.voidcmd('AUTH SSL') - self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, - ssl_version=self.ssl_version) - self.file = self.sock.makefile(mode='rb') + 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 ccc(self): + '''Switch back to a clear-text control connection.''' + if not isinstance(self.sock, ssl.SSLSocket): + raise ValueError("not using TLS") + resp = self.voidcmd('CCC') + self.sock = self.sock.unwrap() return resp def prot_p(self): @@ -683,14 +746,16 @@ else: def ntransfercmd(self, cmd, rest=None): conn, size = FTP.ntransfercmd(self, cmd, rest) if self._prot_p: - conn = ssl.wrap_socket(conn, self.keyfile, self.certfile, - ssl_version=self.ssl_version) + 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: + with self.transfercmd(cmd, rest) as conn: while 1: data = conn.recv(blocksize) if not data: @@ -699,19 +764,17 @@ else: # 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('rb') - try: + fp = conn.makefile('r', encoding=self.encoding) + with fp, conn: while 1: line = fp.readline() - if self.debugging > 2: print '*retr*', repr(line) + if self.debugging > 2: print('*retr*', repr(line)) if not line: break if line[-2:] == CRLF: @@ -722,15 +785,11 @@ else: # 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: + with self.transfercmd(cmd, rest) as conn: while 1: buf = fp.read(blocksize) if not buf: break @@ -739,29 +798,33 @@ else: # 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: + with self.transfercmd(cmd) as conn: while 1: buf = fp.readline() if not buf: break - if buf[-2:] != CRLF: - if buf[-1] in CRLF: buf = buf[:-1] - buf = buf + CRLF + 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() + def abort(self): + # overridden as we can't pass MSG_OOB flag to sendall() + line = b'ABOR' + B_CRLF + self.sock.sendall(line) + resp = self.getmultiline() + if resp[:3] not in {'426', '225', '226'}: + raise error_proto(resp) + return resp + __all__.append('FTP_TLS') all_errors = (Error, IOError, EOFError, ssl.SSLError) @@ -774,19 +837,16 @@ def parse150(resp): be present in the 150 message. ''' if resp[:3] != '150': - raise error_reply, resp + raise error_reply(resp) global _150_re if _150_re is None: import re - _150_re = re.compile("150 .* \((\d+) bytes\)", re.IGNORECASE) + _150_re = re.compile( + "150 .* \((\d+) bytes\)", re.IGNORECASE | re.ASCII) m = _150_re.match(resp) if not m: return None - s = m.group(1) - try: - return int(s) - except (OverflowError, ValueError): - return long(s) + return int(m.group(1)) _227_re = None @@ -797,14 +857,14 @@ def parse227(resp): Return ('host.addr.as.numbers', port#) tuple.''' if resp[:3] != '227': - raise error_reply, resp + raise error_reply(resp) global _227_re if _227_re is None: import re - _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)') + _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)', re.ASCII) m = _227_re.search(resp) if not m: - raise error_proto, resp + raise error_proto(resp) numbers = m.groups() host = '.'.join(numbers[:4]) port = (int(numbers[4]) << 8) + int(numbers[5]) @@ -817,17 +877,17 @@ def parse229(resp, peer): Return ('host.addr.as.numbers', port#) tuple.''' if resp[:3] != '229': - raise error_reply, resp + raise error_reply(resp) left = resp.find('(') - if left < 0: raise error_proto, resp + if left < 0: raise error_proto(resp) right = resp.find(')', left + 1) if right < 0: - raise error_proto, resp # should contain '(|||port|)' + raise error_proto(resp) # should contain '(|||port|)' if resp[left + 1] != resp[right - 1]: - raise error_proto, resp + raise error_proto(resp) parts = resp[left + 1:right].split(resp[left+1]) if len(parts) != 5: - raise error_proto, resp + raise error_proto(resp) host = peer[0] port = int(parts[3]) return host, port @@ -839,7 +899,7 @@ def parse257(resp): Returns the directoryname in the 257 reply.''' if resp[:3] != '257': - raise error_reply, resp + raise error_reply(resp) if resp[3:5] != ' "': return '' # Not compliant to RFC 959, but UNIX ftpd does this dirname = '' @@ -858,7 +918,7 @@ def parse257(resp): def print_line(line): '''Default retrlines callback to print a line.''' - print line + print(line) def ftpcp(source, sourcename, target, targetname = '', type = 'I'): @@ -873,9 +933,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() @@ -898,8 +958,7 @@ class Netrc: filename = os.path.join(os.environ["HOME"], ".netrc") else: - raise IOError, \ - "specify file to load or set $HOME" + raise IOError("specify file to load or set $HOME") self.__hosts = {} self.__macros = {} fp = open(filename, "r") @@ -997,7 +1056,7 @@ def test(): ''' if len(sys.argv) < 2: - print test.__doc__ + print(test.__doc__) sys.exit(0) debugging = 0 |