diff -r 63c1fbc4de4b Doc/library/ftplib.rst --- a/Doc/library/ftplib.rst Mon Nov 25 23:19:58 2013 +0100 +++ b/Doc/library/ftplib.rst Tue Nov 26 00:39:37 2013 +0100 @@ -74,7 +74,7 @@ *source_address* parameter was added. -.. class:: FTP_TLS(host='', user='', passwd='', acct='', keyfile=None, certfile=None, context=None, timeout=None, source_address=None) +.. class:: FTP_TLS(host='', user='', passwd='', acct='', keyfile=None, certfile=None, context=None, timeout=None, source_address=None, check_hostname=False) A :class:`FTP` subclass which adds TLS support to FTP as described in :rfc:`4217`. @@ -87,13 +87,17 @@ bundling SSL configuration options, certificates and private keys into a single (potentially long-lived) structure. *source_address* is a 2-tuple ``(host, port)`` for the socket to bind to as its source address before - connecting. + connecting. If *check_hostname* is set, then host is matched against the + host name(s) allowed by the server cert. .. versionadded:: 3.2 .. versionchanged:: 3.3 *source_address* parameter was added. + .. versionchanged:: 3.4 + *check_hostname* and :data:`ssl.HAS_SNI` support + Here's a sample session using the :class:`FTP_TLS` class: >>> from ftplib import FTP_TLS diff -r 63c1fbc4de4b Lib/ftplib.py --- a/Lib/ftplib.py Mon Nov 25 23:19:58 2013 +0100 +++ b/Lib/ftplib.py Tue Nov 26 00:39:37 2013 +0100 @@ -718,7 +718,8 @@ def __init__(self, host='', user='', passwd='', acct='', keyfile=None, certfile=None, context=None, - timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None, + check_hostname=False): if context is not None and keyfile is not None: raise ValueError("context and keyfile arguments are mutually " "exclusive") @@ -731,7 +732,14 @@ context = ssl._create_stdlib_context(self.ssl_version, certfile=certfile, keyfile=keyfile) + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") self.context = context + self._check_hostname = check_hostname self._prot_p = False FTP.__init__(self, host, user, passwd, acct, timeout, source_address) @@ -748,7 +756,16 @@ resp = self.voidcmd('AUTH TLS') else: resp = self.voidcmd('AUTH SSL') - self.sock = self.context.wrap_socket(self.sock) + server_hostname = self.host if ssl.HAS_SNI else None + self.sock = self.context.wrap_socket(self.sock, + server_hostname=server_hostname) + if self._check_hostname: + try: + ssl.match_hostname(self.sock.getpeercert(), self.host) + except Exception: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + raise self.file = self.sock.makefile(mode='r', encoding=self.encoding) return resp @@ -787,7 +804,16 @@ def ntransfercmd(self, cmd, rest=None): conn, size = FTP.ntransfercmd(self, cmd, rest) if self._prot_p: - conn = self.context.wrap_socket(conn) + server_hostname = self.host if ssl.HAS_SNI else None + conn = self.context.wrap_socket(conn, + server_hostname=server_hostname) + if self._check_hostname: + try: + ssl.match_hostname(conn.getpeercert(), self.host) + except Exception: + conn.shutdown(socket.SHUT_RDWR) + conn.close() + raise return conn, size def abort(self): diff -r 63c1fbc4de4b Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Mon Nov 25 23:19:58 2013 +0100 +++ b/Lib/test/test_ftplib.py Tue Nov 26 00:39:37 2013 +0100 @@ -301,7 +301,8 @@ if ssl is not None: - CERTFILE = os.path.join(os.path.dirname(__file__), "keycert.pem") + CERTFILE = os.path.join(os.path.dirname(__file__), "keycert3.pem") + CAFILE = os.path.join(os.path.dirname(__file__), "pycacert.pem") class SSLConnection(asyncore.dispatcher): """An asyncore.dispatcher subclass supporting TLS/SSL.""" @@ -923,6 +924,36 @@ self.client.ccc() self.assertRaises(ValueError, self.client.sock.unwrap) + def test_check_hostname(self): + self.client.quit() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + ctx.verify_mode = ssl.CERT_REQUIRED + ctx.load_verify_locations(CAFILE) + self.client = ftplib.FTP_TLS(context=ctx, timeout=TIMEOUT, + check_hostname=True) + + # 127.0.0.1 doesn't match SAN + self.client.connect(self.server.host, self.server.port) + with self.assertRaises(ssl.CertificateError): + self.client.auth() + # exception quits connection + + self.client.connect(self.server.host, self.server.port) + self.client.prot_p() + with self.assertRaises(ssl.CertificateError): + with self.client.transfercmd("list") as sock: + pass + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.auth() + self.client.quit() + + self.client.connect("localhost", self.server.port) + self.client.prot_p() + with self.client.transfercmd("list") as sock: + pass + class TestTimeouts(TestCase):