Skip to content

Commit ccd5e02

Browse files
committed
Issue #2054: ftplib now provides an FTP_TLS class to do secure FTP using
TLS or SSL. Patch by Giampaolo Rodola'.
1 parent 82864d1 commit ccd5e02

File tree

4 files changed

+450
-5
lines changed

4 files changed

+450
-5
lines changed

Doc/library/ftplib.rst

+58
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,41 @@ The module defines the following items:
4949
.. versionchanged:: 2.6
5050
*timeout* was added.
5151

52+
.. class:: FTP_TLS([host[, user[, passwd[, acct[, keyfile[, certfile[, timeout]]]]]]])
53+
54+
A :class:`FTP` subclass which adds TLS support to FTP as described in
55+
:rfc:`4217`.
56+
Connect as usual to port 21 implicitly securing the FTP control connection
57+
before authenticating. Securing the data connection requires user to
58+
explicitly ask for it by calling :exc:`prot_p()` method.
59+
*keyfile* and *certfile* are optional - they can contain a PEM formatted
60+
private key and certificate chain file for the SSL connection.
61+
62+
.. versionadded:: 2.7 Contributed by Giampaolo Rodola'
63+
64+
65+
Here's a sample session using :class:`FTP_TLS` class:
66+
67+
>>> from ftplib import FTP_TLS
68+
>>> ftps = FTP_TLS('ftp.python.org')
69+
>>> ftps.login() # login anonimously previously securing control channel
70+
>>> ftps.prot_p() # switch to secure data connection
71+
>>> ftps.retrlines('LIST') # list directory content securely
72+
total 9
73+
drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .
74+
drwxr-xr-x 8 root wheel 1024 Jan 3 1994 ..
75+
drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin
76+
drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc
77+
d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming
78+
drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib
79+
drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub
80+
drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr
81+
-rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg
82+
'226 Transfer complete.'
83+
>>> ftps.quit()
84+
>>>
85+
86+
5287

5388
.. attribute:: all_errors
5489

@@ -329,3 +364,26 @@ followed by ``lines`` for the text version or ``binary`` for the binary version.
329364
:meth:`close` or :meth:`quit` you cannot reopen the connection by issuing
330365
another :meth:`login` method).
331366

367+
368+
FTP_TLS Objects
369+
---------------
370+
371+
:class:`FTP_TLS` class inherits from :class:`FTP`, defining these additional objects:
372+
373+
.. attribute:: FTP_TLS.ssl_version
374+
375+
The SSL version to use (defaults to *TLSv1*).
376+
377+
.. method:: FTP_TLS.auth()
378+
379+
Set up secure control connection by using TLS or SSL, depending on what specified in :meth:`ssl_version` attribute.
380+
381+
.. method:: FTP_TLS.prot_p()
382+
383+
Set up secure data connection.
384+
385+
.. method:: FTP_TLS.prot_c()
386+
387+
Set up clear text data connection.
388+
389+

Lib/ftplib.py

+176
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# Modified by Jack to work on the mac.
3434
# Modified by Siebren to support docstrings and PASV.
3535
# Modified by Phil Schwartz to add storbinary and storlines callbacks.
36+
# Modified by Giampaolo Rodola' to add TLS support.
3637
#
3738

3839
import os
@@ -575,6 +576,181 @@ def close(self):
575576
self.file = self.sock = None
576577

577578

579+
try:
580+
import ssl
581+
except ImportError:
582+
pass
583+
else:
584+
class FTP_TLS(FTP):
585+
'''A FTP subclass which adds TLS support to FTP as described
586+
in RFC-4217.
587+
588+
Connect as usual to port 21 implicitly securing the FTP control
589+
connection before authenticating.
590+
591+
Securing the data connection requires user to explicitly ask
592+
for it by calling prot_p() method.
593+
594+
Usage example:
595+
>>> from ftplib import FTP_TLS
596+
>>> ftps = FTP_TLS('ftp.python.org')
597+
>>> ftps.login() # login anonimously previously securing control channel
598+
'230 Guest login ok, access restrictions apply.'
599+
>>> ftps.prot_p() # switch to secure data connection
600+
'200 Protection level set to P'
601+
>>> ftps.retrlines('LIST') # list directory content securely
602+
total 9
603+
drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .
604+
drwxr-xr-x 8 root wheel 1024 Jan 3 1994 ..
605+
drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin
606+
drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc
607+
d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming
608+
drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib
609+
drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub
610+
drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr
611+
-rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg
612+
'226 Transfer complete.'
613+
>>> ftps.quit()
614+
'221 Goodbye.'
615+
>>>
616+
'''
617+
ssl_version = ssl.PROTOCOL_TLSv1
618+
619+
def __init__(self, host='', user='', passwd='', acct='', keyfile=None,
620+
certfile=None, timeout=_GLOBAL_DEFAULT_TIMEOUT):
621+
self.keyfile = keyfile
622+
self.certfile = certfile
623+
self._prot_p = False
624+
FTP.__init__(self, host, user, passwd, acct, timeout)
625+
626+
def login(self, user='', passwd='', acct='', secure=True):
627+
if secure and not isinstance(self.sock, ssl.SSLSocket):
628+
self.auth()
629+
return FTP.login(self, user, passwd, acct)
630+
631+
def auth(self):
632+
'''Set up secure control connection by using TLS/SSL.'''
633+
if isinstance(self.sock, ssl.SSLSocket):
634+
raise ValueError("Already using TLS")
635+
if self.ssl_version == ssl.PROTOCOL_TLSv1:
636+
resp = self.voidcmd('AUTH TLS')
637+
else:
638+
resp = self.voidcmd('AUTH SSL')
639+
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile,
640+
ssl_version=self.ssl_version)
641+
self.file = self.sock.makefile(mode='rb')
642+
return resp
643+
644+
def prot_p(self):
645+
'''Set up secure data connection.'''
646+
# PROT defines whether or not the data channel is to be protected.
647+
# Though RFC-2228 defines four possible protection levels,
648+
# RFC-4217 only recommends two, Clear and Private.
649+
# Clear (PROT C) means that no security is to be used on the
650+
# data-channel, Private (PROT P) means that the data-channel
651+
# should be protected by TLS.
652+
# PBSZ command MUST still be issued, but must have a parameter of
653+
# '0' to indicate that no buffering is taking place and the data
654+
# connection should not be encapsulated.
655+
self.voidcmd('PBSZ 0')
656+
resp = self.voidcmd('PROT P')
657+
self._prot_p = True
658+
return resp
659+
660+
def prot_c(self):
661+
'''Set up clear text data connection.'''
662+
resp = self.voidcmd('PROT C')
663+
self._prot_p = False
664+
return resp
665+
666+
# --- Overridden FTP methods
667+
668+
def ntransfercmd(self, cmd, rest=None):
669+
conn, size = FTP.ntransfercmd(self, cmd, rest)
670+
if self._prot_p:
671+
conn = ssl.wrap_socket(conn, self.keyfile, self.certfile,
672+
ssl_version=self.ssl_version)
673+
return conn, size
674+
675+
def retrbinary(self, cmd, callback, blocksize=8192, rest=None):
676+
self.voidcmd('TYPE I')
677+
conn = self.transfercmd(cmd, rest)
678+
try:
679+
while 1:
680+
data = conn.recv(blocksize)
681+
if not data:
682+
break
683+
callback(data)
684+
# shutdown ssl layer
685+
if isinstance(conn, ssl.SSLSocket):
686+
conn.unwrap()
687+
finally:
688+
conn.close()
689+
return self.voidresp()
690+
691+
def retrlines(self, cmd, callback = None):
692+
if callback is None: callback = print_line
693+
resp = self.sendcmd('TYPE A')
694+
conn = self.transfercmd(cmd)
695+
fp = conn.makefile('rb')
696+
try:
697+
while 1:
698+
line = fp.readline()
699+
if self.debugging > 2: print '*retr*', repr(line)
700+
if not line:
701+
break
702+
if line[-2:] == CRLF:
703+
line = line[:-2]
704+
elif line[-1:] == '\n':
705+
line = line[:-1]
706+
callback(line)
707+
# shutdown ssl layer
708+
if isinstance(conn, ssl.SSLSocket):
709+
conn.unwrap()
710+
finally:
711+
fp.close()
712+
conn.close()
713+
return self.voidresp()
714+
715+
def storbinary(self, cmd, fp, blocksize=8192, callback=None):
716+
self.voidcmd('TYPE I')
717+
conn = self.transfercmd(cmd)
718+
try:
719+
while 1:
720+
buf = fp.read(blocksize)
721+
if not buf: break
722+
conn.sendall(buf)
723+
if callback: callback(buf)
724+
# shutdown ssl layer
725+
if isinstance(conn, ssl.SSLSocket):
726+
conn.unwrap()
727+
finally:
728+
conn.close()
729+
return self.voidresp()
730+
731+
def storlines(self, cmd, fp, callback=None):
732+
self.voidcmd('TYPE A')
733+
conn = self.transfercmd(cmd)
734+
try:
735+
while 1:
736+
buf = fp.readline()
737+
if not buf: break
738+
if buf[-2:] != CRLF:
739+
if buf[-1] in CRLF: buf = buf[:-1]
740+
buf = buf + CRLF
741+
conn.sendall(buf)
742+
if callback: callback(buf)
743+
# shutdown ssl layer
744+
if isinstance(conn, ssl.SSLSocket):
745+
conn.unwrap()
746+
finally:
747+
conn.close()
748+
return self.voidresp()
749+
750+
__all__.append('FTP_TLS')
751+
all_errors = (Error, IOError, EOFError, ssl.SSLError)
752+
753+
578754
_150_re = None
579755

580756
def parse150(resp):

0 commit comments

Comments
 (0)