2 # -*- encoding: utf-8 -*-
5 # pyftpdlib is released under the MIT license, reproduced below:
6 # ======================================================================
7 # Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>
8 # Hacked by Fabien Pinckaers (C) 2008 <fp@tinyerp.com>
12 # Permission to use, copy, modify, and distribute this software and
13 # its documentation for any purpose and without fee is hereby
14 # granted, provided that the above copyright notice appear in all
15 # copies and that both that copyright notice and this permission
16 # notice appear in supporting documentation, and that the name of
17 # Giampaolo Rodola' not be used in advertising or publicity pertaining to
18 # distribution of the software without specific, written prior
21 # Giampaolo Rodola' DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
22 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
23 # NO EVENT Giampaolo Rodola' BE LIABLE FOR ANY SPECIAL, INDIRECT OR
24 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
25 # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
26 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
27 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
28 # ======================================================================
31 """pyftpdlib: RFC-959 asynchronous FTP server.
33 pyftpdlib implements a fully functioning asynchronous FTP server as
34 defined in RFC-959. A hierarchy of classes outlined below implement
35 the backend functionality for the FTPd:
37 [FTPServer] - the base class for the backend.
39 [FTPHandler] - a class representing the server-protocol-interpreter
40 (server-PI, see RFC-959). Each time a new connection occurs
41 FTPServer will create a new FTPHandler instance to handle the
44 [ActiveDTP], [PassiveDTP] - base classes for active/passive-DTP
47 [DTPHandler] - this class handles processing of data transfer
48 operations (server-DTP, see RFC-959).
50 [DummyAuthorizer] - an "authorizer" is a class handling FTPd
51 authentications and permissions. It is used inside FTPHandler class
52 to verify user passwords, to get user's home directory and to get
53 permissions when a filesystem read/write occurs. "DummyAuthorizer"
54 is the base authorizer class providing a platform independent
55 interface for managing virtual users.
57 [AbstractedFS] - class used to interact with the file system,
58 providing a high level, cross-platform interface compatible
59 with both Windows and UNIX style filesystems.
61 [AuthorizerError] - base class for authorizers exceptions.
64 pyftpdlib also provides 3 different logging streams through 3 functions
65 which can be overridden to allow for custom logging.
67 [log] - the main logger that logs the most important messages for
68 the end user regarding the FTPd.
70 [logline] - this function is used to log commands and responses
71 passing through the control FTP channel.
73 [logerror] - log traceback outputs occurring in case of errors.
78 >>> from pyftpdlib import ftpserver
79 >>> authorizer = ftpserver.DummyAuthorizer()
80 >>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmw')
81 >>> authorizer.add_anonymous('/home/nobody')
82 >>> ftp_handler = ftpserver.FTPHandler
83 >>> ftp_handler.authorizer = authorizer
84 >>> address = ("127.0.0.1", 21)
85 >>> ftpd = ftpserver.FTPServer(address, ftp_handler)
86 >>> ftpd.serve_forever()
87 Serving FTP on 127.0.0.1:21
88 []127.0.0.1:2503 connected.
89 127.0.0.1:2503 ==> 220 Ready.
90 127.0.0.1:2503 <== USER anonymous
91 127.0.0.1:2503 ==> 331 Username ok, send password.
92 127.0.0.1:2503 <== PASS ******
93 127.0.0.1:2503 ==> 230 Login successful.
94 [anonymous]@127.0.0.1:2503 User anonymous logged in.
95 127.0.0.1:2503 <== TYPE A
96 127.0.0.1:2503 ==> 200 Type set to: ASCII.
97 127.0.0.1:2503 <== PASV
98 127.0.0.1:2503 ==> 227 Entering passive mode (127,0,0,1,9,201).
99 127.0.0.1:2503 <== LIST
100 127.0.0.1:2503 ==> 150 File status okay. About to open data connection.
101 [anonymous]@127.0.0.1:2503 OK LIST "/". Transfer starting.
102 127.0.0.1:2503 ==> 226 Transfer complete.
103 [anonymous]@127.0.0.1:2503 Transfer complete. 706 bytes transmitted.
104 127.0.0.1:2503 <== QUIT
105 127.0.0.1:2503 ==> 221 Goodbye.
106 [anonymous]@127.0.0.1:2503 Disconnected.
124 from tarfile import filemode
135 __all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer',
136 'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler',
137 'FileProducer', 'IteratorProducer', 'BufferedIteratorProducer',
141 __pname__ = 'Python FTP server library (pyftpdlib)'
143 __date__ = '2008-05-16'
144 __author__ = "Giampaolo Rodola' <g.rodola@gmail.com>"
145 __web__ = 'http://code.google.com/p/pyftpdlib/'
149 'ABOR': 'Syntax: ABOR (abort transfer).',
150 'ALLO': 'Syntax: ALLO <SP> bytes (obsolete; allocate storage).',
151 'APPE': 'Syntax: APPE <SP> file-name (append data to an existent file).',
152 'CDUP': 'Syntax: CDUP (go to parent directory).',
153 'CWD' : 'Syntax: CWD <SP> dir-name (change current working directory).',
154 'DELE': 'Syntax: DELE <SP> file-name (delete file).',
155 'EPRT': 'Syntax: EPRT <SP> |proto|ip|port| (set server in extended active mode).',
156 'EPSV': 'Syntax: EPSV [<SP> proto/"ALL"] (set server in extended passive mode).',
157 'FEAT': 'Syntax: FEAT (list all new features supported).',
158 'HELP': 'Syntax: HELP [<SP> cmd] (show help).',
159 'LIST': 'Syntax: LIST [<SP> path-name] (list files).',
160 'MDTM': 'Syntax: MDTM <SP> file-name (get last modification time).',
161 'MLSD': 'Syntax: MLSD [<SP> dir-name] (list files in a machine-processable form)',
162 'MLST': 'Syntax: MLST [<SP> path-name] (show a path in a machine-processable form)',
163 'MODE': 'Syntax: MODE <SP> mode (obsolete; set data transfer mode).',
164 'MKD' : 'Syntax: MDK <SP> dir-name (create directory).',
165 'NLST': 'Syntax: NLST [<SP> path-name] (list files in a compact form).',
166 'NOOP': 'Syntax: NOOP (just do nothing).',
167 'OPTS': 'Syntax: OPTS <SP> ftp-command [<SP> option] (specify options for FTP commands)',
168 'PASS': 'Syntax: PASS <SP> user-name (set user password).',
169 'PASV': 'Syntax: PASV (set server in passive mode).',
170 'PORT': 'Syntax: PORT <sp> h1,h2,h3,h4,p1,p2 (set server in active mode).',
171 'PWD' : 'Syntax: PWD (get current working directory).',
172 'QUIT': 'Syntax: QUIT (quit current session).',
173 'REIN': 'Syntax: REIN (reinitialize / flush account).',
174 'REST': 'Syntax: REST <SP> marker (restart file position).',
175 'RETR': 'Syntax: RETR <SP> file-name (retrieve a file).',
176 'RMD' : 'Syntax: RMD <SP> dir-name (remove directory).',
177 'RNFR': 'Syntax: RNFR <SP> file-name (file renaming (source name)).',
178 'RNTO': 'Syntax: RNTO <SP> file-name (file renaming (destination name)).',
179 'SIZE': 'Syntax: HELP <SP> file-name (get file size).',
180 'STAT': 'Syntax: STAT [<SP> path name] (status information [list files]).',
181 'STOR': 'Syntax: STOR <SP> file-name (store a file).',
182 'STOU': 'Syntax: STOU [<SP> file-name] (store a file with a unique name).',
183 'STRU': 'Syntax: STRU <SP> type (obsolete; set file structure).',
184 'SYST': 'Syntax: SYST (get operating system type).',
185 'TYPE': 'Syntax: TYPE <SP> [A | I] (set transfer type).',
186 'USER': 'Syntax: USER <SP> user-name (set username).',
187 'XCUP': 'Syntax: XCUP (obsolete; go to parent directory).',
188 'XCWD': 'Syntax: XCWD <SP> dir-name (obsolete; change current directory).',
189 'XMKD': 'Syntax: XMDK <SP> dir-name (obsolete; create directory).',
190 'XPWD': 'Syntax: XPWD (obsolete; get current dir).',
191 'XRMD': 'Syntax: XRMD <SP> dir-name (obsolete; remove directory).',
195 # hack around format_exc function of traceback module to grant
196 # backward compatibility with python < 2.4
197 if not hasattr(traceback, 'format_exc'):
199 import cStringIO as StringIO
204 f = StringIO.StringIO()
205 traceback.print_exc(file=f)
210 traceback.format_exc = _format_exc
214 """A wrap around os.strerror() which may be not available on all
215 platforms (e.g. pythonCE).
217 - (instance) err: an EnvironmentError or derived class instance.
219 if hasattr(os, 'strerror'):
220 return os.strerror(err.errno)
226 return s.decode('utf-8')
230 return s.decode('latin')
234 return s.encode('ascii')
240 return s.encode('utf-8')
244 return s.encode('latin')
248 return s.decode('ascii')
252 # --- library defined exceptions
254 class Error(Exception):
255 """Base class for module exceptions."""
257 class AuthorizerError(Error):
258 """Base class for authorizer exceptions."""
264 """Log messages intended for the end user."""
269 """Log commands and responses passing through the command channel."""
274 """Log traceback outputs occurring in case of errors."""
275 sys.stderr.write(str(msg) + '\n')
281 class DummyAuthorizer:
282 """Basic "dummy" authorizer class, suitable for subclassing to
283 create your own custom authorizers.
285 An "authorizer" is a class handling authentications and permissions
286 of the FTP server. It is used inside FTPHandler class for verifying
287 user's password, getting users home directory, checking user
288 permissions when a file read/write event occurs and changing user
289 before accessing the filesystem.
291 DummyAuthorizer is the base authorizer, providing a platform
292 independent interface for managing "virtual" FTP users. System
293 dependent authorizers can by written by subclassing this base
294 class and overriding appropriate methods as necessary.
298 write_perms = "adfmw"
303 def add_user(self, username, password, homedir, perm='elr',
304 msg_login="Login successful.", msg_quit="Goodbye."):
305 """Add a user to the virtual users table.
307 AuthorizerError exceptions raised on error conditions such as
308 invalid permissions, missing home directory or duplicate usernames.
310 Optional perm argument is a string referencing the user's
311 permissions explained below:
314 - "e" = change directory (CWD command)
315 - "l" = list files (LIST, NLST, MLSD commands)
316 - "r" = retrieve file from the server (RETR command)
319 - "a" = append data to an existing file (APPE command)
320 - "d" = delete file or directory (DELE, RMD commands)
321 - "f" = rename file or directory (RNFR, RNTO commands)
322 - "m" = create directory (MKD command)
323 - "w" = store a file to the server (STOR, STOU commands)
325 Optional msg_login and msg_quit arguments can be specified to
326 provide customized response strings when user log-in and quit.
328 if self.has_user(username):
329 raise AuthorizerError('User "%s" already exists' %username)
330 homedir = os.path.realpath(homedir)
331 if not os.path.isdir(homedir):
332 raise AuthorizerError('No such directory: "%s"' %homedir)
334 if p not in 'elradfmw':
335 raise AuthorizerError('No such permission "%s"' %p)
337 if (p in self.write_perms) and (username == 'anonymous'):
338 warnings.warn("write permissions assigned to anonymous user.",
341 dic = {'pwd': str(password),
344 'msg_login': str(msg_login),
345 'msg_quit': str(msg_quit)
347 self.user_table[username] = dic
349 def add_anonymous(self, homedir, **kwargs):
350 """Add an anonymous user to the virtual users table.
352 AuthorizerError exception raised on error conditions such as
353 invalid permissions, missing home directory, or duplicate
356 The keyword arguments in kwargs are the same expected by
357 add_user method: "perm", "msg_login" and "msg_quit".
359 The optional "perm" keyword argument is a string defaulting to
360 "elr" referencing "read-only" anonymous user's permissions.
362 Using write permission values ("adfmw") results in a
365 DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
367 def remove_user(self, username):
368 """Remove a user from the virtual users table."""
369 del self.user_table[username]
371 def validate_authentication(self, username, password):
372 """Return True if the supplied username and password match the
373 stored credentials."""
374 return self.user_table[username]['pwd'] == password
376 def impersonate_user(self, username, password):
377 """Impersonate another user (noop).
379 It is always called before accessing the filesystem.
380 By default it does nothing. The subclass overriding this
381 method is expected to provide a mechanism to change the
385 def terminate_impersonation(self):
386 """Terminate impersonation (noop).
388 It is always called after having accessed the filesystem.
389 By default it does nothing. The subclass overriding this
390 method is expected to provide a mechanism to switch back
391 to the original user.
394 def has_user(self, username):
395 """Whether the username exists in the virtual users table."""
396 return username in self.user_table
398 def has_perm(self, username, perm, path=None):
399 """Whether the user has permission over path (an absolute
400 pathname of a file or a directory).
402 Expected perm argument is one of the following letters:
405 return perm in self.user_table[username]['perm']
407 def get_perms(self, username):
408 """Return current user permissions."""
409 return self.user_table[username]['perm']
411 def get_home_dir(self, username):
412 """Return the user's home directory."""
413 return self.user_table[username]['home']
415 def get_msg_login(self, username):
416 """Return the user's login message."""
417 return self.user_table[username]['msg_login']
419 def get_msg_quit(self, username):
420 """Return the user's quitting message."""
421 return self.user_table[username]['msg_quit']
426 class PassiveDTP(asyncore.dispatcher):
427 """This class is an asyncore.disptacher subclass. It creates a
428 socket listening on a local port, dispatching the resultant
429 connection to DTPHandler.
432 def __init__(self, cmd_channel, extmode=False):
433 """Initialize the passive data server.
435 - (instance) cmd_channel: the command channel class instance.
436 - (bool) extmode: wheter use extended passive mode response type.
438 asyncore.dispatcher.__init__(self)
439 self.cmd_channel = cmd_channel
441 ip = self.cmd_channel.getsockname()[0]
442 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
444 if not self.cmd_channel.passive_ports:
445 # By using 0 as port number value we let kernel choose a free
446 # unprivileged random port.
449 ports = list(self.cmd_channel.passive_ports)
451 port = ports.pop(random.randint(0, len(ports) -1))
453 self.bind((ip, port))
454 except socket.error, why:
455 if why[0] == errno.EADDRINUSE: # port already in use
458 # If cannot use one of the ports in the configured
459 # range we'll use a kernel-assigned port, and log
460 # a message reporting the issue.
461 # By using 0 as port number value we let kernel
462 # choose a free unprivileged random port.
465 self.cmd_channel.log(
466 "Can't find a valid passive port in the "
467 "configured range. A random kernel-assigned "
475 port = self.socket.getsockname()[1]
477 if self.cmd_channel.masquerade_address:
478 ip = self.cmd_channel.masquerade_address
479 # The format of 227 response in not standardized.
480 # This is the most expected:
481 self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' %(
482 ip.replace('.', ','), port / 256, port % 256))
484 self.cmd_channel.respond('229 Entering extended passive mode '
487 # --- connection / overridden
489 def handle_accept(self):
490 """Called when remote client initiates a connection."""
491 sock, addr = self.accept()
493 # Check the origin of data connection. If not expressively
494 # configured we drop the incoming data connection if remote
495 # IP address does not match the client's IP address.
496 if (self.cmd_channel.remote_ip != addr[0]):
497 if not self.cmd_channel.permit_foreign_addresses:
502 msg = 'Rejected data connection from foreign address %s:%s.' \
504 self.cmd_channel.respond("425 %s" %msg)
505 self.cmd_channel.log(msg)
506 # do not close listening socket: it couldn't be client's blame
509 # site-to-site FTP allowed
510 msg = 'Established data connection with foreign address %s:%s.'\
512 self.cmd_channel.log(msg)
513 # Immediately close the current channel (we accept only one
514 # connection at time) and avoid running out of max connections
517 # delegate such connection to DTP handler
518 handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
519 self.cmd_channel.data_channel = handler
520 self.cmd_channel.on_dtp_connection()
525 def handle_error(self):
526 """Called to handle any uncaught exceptions."""
529 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
531 logerror(traceback.format_exc())
534 def handle_close(self):
535 """Called on closing the data connection."""
539 class ActiveDTP(asyncore.dispatcher):
540 """This class is an asyncore.disptacher subclass. It creates a
541 socket resulting from the connection to a remote user-port,
542 dispatching it to DTPHandler.
545 def __init__(self, ip, port, cmd_channel):
546 """Initialize the active data channel attemping to connect
547 to remote data socket.
549 - (str) ip: the remote IP address.
550 - (int) port: the remote port.
551 - (instance) cmd_channel: the command channel class instance.
553 asyncore.dispatcher.__init__(self)
554 self.cmd_channel = cmd_channel
555 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
557 self.connect((ip, port))
558 except socket.gaierror:
559 self.cmd_channel.respond("425 Can't connect to specified address.")
562 # --- connection / overridden
564 def handle_write(self):
565 """NOOP, must be overridden to prevent unhandled write event."""
567 def handle_connect(self):
568 """Called when connection is established."""
569 self.cmd_channel.respond('200 Active data connection established.')
570 # delegate such connection to DTP handler
571 handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
572 self.cmd_channel.data_channel = handler
573 self.cmd_channel.on_dtp_connection()
574 #self.close() # <-- (done automatically)
576 def handle_expt(self):
577 self.cmd_channel.respond("425 Can't connect to specified address.")
580 def handle_error(self):
581 """Called to handle any uncaught exceptions."""
584 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
589 logerror(traceback.format_exc())
590 self.cmd_channel.respond("425 Can't connect to specified address.")
595 from collections import deque
597 # backward compatibility with Python < 2.4 by replacing deque with a list
599 def appendleft(self, obj):
600 list.insert(self, 0, obj)
603 class DTPHandler(asyncore.dispatcher):
604 """Class handling server-data-transfer-process (server-DTP, see
605 RFC-959) managing data-transfer operations involving sending
608 Instance attributes defined in this class, initialized when
611 - (instance) cmd_channel: the command channel class instance.
612 - (file) file_obj: the file transferred (if any).
613 - (bool) receive: True if channel is used for receiving data.
614 - (bool) transfer_finished: True if transfer completed successfully.
615 - (int) tot_bytes_sent: the total bytes sent.
616 - (int) tot_bytes_received: the total bytes received.
618 DTPHandler implementation note:
620 When a producer is consumed and close_when_done() has been called
621 previously, refill_buffer() erroneously calls close() instead of
622 handle_close() - (see: http://bugs.python.org/issue1740572)
624 To avoid this problem DTPHandler is implemented as a subclass of
625 asyncore.dispatcher instead of asynchat.async_chat.
626 This implementation follows the same approach that asynchat module
627 should use in Python 2.6.
629 The most important change in the implementation is related to
630 producer_fifo, which is a pure deque object instead of a
631 producer_fifo instance.
633 Since we don't want to break backward compatibily with older python
634 versions (deque has been introduced in Python 2.4), if deque is not
635 available we use a list instead.
638 ac_in_buffer_size = 8192
639 ac_out_buffer_size = 8192
641 def __init__(self, sock_obj, cmd_channel):
642 """Initialize the command channel.
644 - (instance) sock_obj: the socket object instance of the newly
645 established connection.
646 - (instance) cmd_channel: the command channel class instance.
648 asyncore.dispatcher.__init__(self, sock_obj)
649 # we toss the use of the asynchat's "simple producer" and
650 # replace it with a pure deque, which the original fifo
652 self.producer_fifo = deque()
654 self.cmd_channel = cmd_channel
657 self.transfer_finished = False
658 self.tot_bytes_sent = 0
659 self.tot_bytes_received = 0
660 self.data_wrapper = lambda x: x
662 # --- utility methods
664 def enable_receiving(self, type):
665 """Enable receiving of data over the channel. Depending on the
666 TYPE currently in use it creates an appropriate wrapper for the
669 - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
672 self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
674 self.data_wrapper = lambda x: x
676 raise TypeError, "Unsupported type"
679 def get_transmitted_bytes(self):
680 "Return the number of transmitted bytes."
681 return self.tot_bytes_sent + self.tot_bytes_received
683 def transfer_in_progress(self):
684 "Return True if a transfer is in progress, else False."
685 return self.get_transmitted_bytes() != 0
689 def handle_read(self):
690 """Called when there is data waiting to be read."""
692 chunk = self.recv(self.ac_in_buffer_size)
696 self.tot_bytes_received += len(chunk)
698 self.transfer_finished = True
699 #self.close() # <-- asyncore.recv() already do that...
701 # while we're writing on the file an exception could occur
702 # in case that filesystem gets full; if this happens we
703 # let handle_error() method handle this exception, providing
704 # a detailed error message.
705 self.file_obj.write(self.data_wrapper(chunk))
707 def handle_write(self):
708 """Called when data is ready to be written, initiates send."""
711 def push(self, data):
712 """Push data onto the deque and initiate send."""
713 sabs = self.ac_out_buffer_size
715 for i in xrange(0, len(data), sabs):
716 self.producer_fifo.append(data[i:i+sabs])
718 self.producer_fifo.append(data)
721 def push_with_producer(self, producer):
722 """Push data using a producer and initiate send."""
723 self.producer_fifo.append(producer)
727 """Predicate for inclusion in the readable for select()."""
731 """Predicate for inclusion in the writable for select()."""
732 return self.producer_fifo or (not self.connected)
734 def close_when_done(self):
735 """Automatically close this channel once the outgoing queue is empty."""
736 self.producer_fifo.append(None)
738 def initiate_send(self):
739 """Attempt to send data in fifo order."""
740 while self.producer_fifo and self.connected:
741 first = self.producer_fifo[0]
742 # handle empty string/buffer or None entry
744 del self.producer_fifo[0]
746 self.transfer_finished = True
750 # handle classic producer behavior
751 obs = self.ac_out_buffer_size
753 data = buffer(first, 0, obs)
757 self.producer_fifo.appendleft(data)
759 del self.producer_fifo[0]
764 num_sent = self.send(data)
770 self.tot_bytes_sent += num_sent
771 if num_sent < len(data) or obs < len(first):
772 self.producer_fifo[0] = first[num_sent:]
774 del self.producer_fifo[0]
775 # we tried to send some actual data
778 def handle_expt(self):
779 """Called on "exceptional" data events."""
780 self.cmd_channel.respond("426 Connection error; transfer aborted.")
783 def handle_error(self):
784 """Called when an exception is raised and not otherwise handled."""
787 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
789 except socket.error, err:
790 # fix around asyncore bug (http://bugs.python.org/issue1736101)
791 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
797 # an error could occur in case we fail reading / writing
798 # from / to file (e.g. file system gets full)
799 except EnvironmentError, err:
800 error = _strerror(err)
802 # some other exception occurred; we don't want to provide
803 # confidential error messages
804 logerror(traceback.format_exc())
805 error = "Internal error"
806 self.cmd_channel.respond("426 %s; transfer aborted." %error)
809 def handle_close(self):
810 """Called when the socket is closed."""
811 # If we used channel for receiving we assume that transfer is
812 # finished when client close connection , if we used channel
813 # for sending we have to check that all data has been sent
814 # (responding with 226) or not (responding with 426).
816 self.transfer_finished = True
820 if self.transfer_finished:
821 self.cmd_channel.respond("226 Transfer complete.")
823 fname = self.file_obj.name
824 self.cmd_channel.log('"%s" %s.' %(fname, action))
826 tot_bytes = self.get_transmitted_bytes()
827 msg = "Transfer aborted; %d bytes transmitted." %tot_bytes
828 self.cmd_channel.respond("426 " + msg)
829 self.cmd_channel.log(msg)
833 """Close the data channel, first attempting to close any remaining
835 if self.file_obj and not self.file_obj.closed:
836 self.file_obj.close()
837 asyncore.dispatcher.close(self)
838 self.cmd_channel.on_dtp_close()
844 """Producer wrapper for file[-like] objects."""
848 def __init__(self, file, type):
849 """Initialize the producer with a data_wrapper appropriate to TYPE.
851 - (file) file: the file[-like] object.
852 - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
857 self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
859 self.data_wrapper = lambda x: x
861 raise TypeError, "Unsupported type"
864 """Attempt a chunk of data of size self.buffer_size."""
867 data = self.data_wrapper(self.file.read(self.buffer_size))
870 if not self.file.closed:
875 class IteratorProducer:
876 """Producer for iterator objects."""
878 def __init__(self, iterator):
879 self.iterator = iterator
882 """Attempt a chunk of data from iterator by calling its next()
886 return self.iterator.next()
887 except StopIteration:
891 class BufferedIteratorProducer:
892 """Producer for iterator objects with buffer capabilities."""
893 # how many times iterator.next() will be called before
894 # returning some data
897 def __init__(self, iterator):
898 self.iterator = iterator
901 """Attempt a chunk of data from iterator by calling
902 its next() method different times.
905 for x in xrange(self.loops):
907 buffer.append(self.iterator.next())
908 except StopIteration:
910 return ''.join(buffer)
916 """A class used to interact with the file system, providing a high
917 level, cross-platform interface compatible with both Windows and
918 UNIX style filesystems.
920 It provides some utility methods and some wraps around operations
921 involved in file creation and file system operations like moving
922 files or removing directories.
925 - (str) root: the user home directory.
926 - (str) cwd: the current working directory.
927 - (str) rnfr: source file to be renamed.
935 # --- Pathname / conversion utilities
937 def ftpnorm(self, ftppath):
938 """Normalize a "virtual" ftp pathname (tipically the raw string
939 coming from client) depending on the current working directory.
941 Example (having "/foo" as current working directory):
944 Note: directory separators are system independent ("/").
945 Pathname returned is always absolutized.
947 if os.path.isabs(ftppath):
948 p = os.path.normpath(ftppath)
950 p = os.path.normpath(os.path.join(self.cwd, ftppath))
951 # normalize string in a standard web-path notation having '/'
953 p = p.replace("\\", "/")
954 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
955 # don't need them. In case we get an UNC path we collapse
956 # redundant separators appearing at the beginning of the string
959 # Anti path traversal: don't trust user input, in the event
960 # that self.cwd is not absolute, return "/" as a safety measure.
961 # This is for extra protection, maybe not really necessary.
962 if not os.path.isabs(p):
966 def ftp2fs(self, ftppath):
967 """Translate a "virtual" ftp pathname (tipically the raw string
968 coming from client) into equivalent absolute "real" filesystem
971 Example (having "/home/user" as root directory):
972 'x' -> '/home/user/x'
974 Note: directory separators are system dependent.
976 # as far as I know, it should always be path traversal safe...
977 if os.path.normpath(self.root) == os.sep:
978 return os.path.normpath(self.ftpnorm(ftppath))
980 p = self.ftpnorm(ftppath)[1:]
981 return os.path.normpath(os.path.join(self.root, p))
983 def fs2ftp(self, fspath):
984 """Translate a "real" filesystem pathname into equivalent
985 absolute "virtual" ftp pathname depending on the user's
988 Example (having "/home/user" as root directory):
989 '/home/user/x' -> '/x'
991 As for ftpnorm, directory separators are system independent
992 ("/") and pathname returned is always absolutized.
994 On invalid pathnames escaping from user's root directory
995 (e.g. "/home" when root is "/home/user") always return "/".
997 if os.path.isabs(fspath):
998 p = os.path.normpath(fspath)
1000 p = os.path.normpath(os.path.join(self.root, fspath))
1001 if not self.validpath(p):
1003 p = p.replace(os.sep, "/")
1004 p = p[len(self.root):]
1005 if not p.startswith('/'):
1009 # alias for backward compatibility with 0.2.0
1013 def validpath(self, path):
1014 """Check whether the path belongs to user's home directory.
1015 Expected argument is a "real" filesystem pathname.
1017 If path is a symbolic link it is resolved to check its real
1020 Pathnames escaping from user's root directory are considered
1023 root = self.realpath(self.root)
1024 path = self.realpath(path)
1025 if not self.root.endswith(os.sep):
1026 root = self.root + os.sep
1027 if not path.endswith(os.sep):
1028 path = path + os.sep
1029 if path[0:len(root)] == root:
1033 # --- Wrapper methods around open() and tempfile.mkstemp
1035 def open(self, filename, mode):
1036 """Open a file returning its handler."""
1037 return open(filename, mode)
1039 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
1040 """A wrap around tempfile.mkstemp creating a file with a unique
1041 name. Unlike mkstemp it returns an object with a file-like
1045 def __init__(self, fd, name):
1048 def __getattr__(self, attr):
1049 return getattr(self.file, attr)
1051 text = not 'b' in mode
1052 # max number of tries to find out a unique file name
1053 tempfile.TMP_MAX = 50
1054 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
1055 file = os.fdopen(fd, mode)
1056 return FileWrapper(file, name)
1058 # --- Wrapper methods around os.*
1060 def chdir(self, path):
1061 """Change the current directory."""
1062 # temporarily join the specified directory to see if we have
1063 # permissions to do so
1064 basedir = os.getcwd()
1071 self.cwd = self.fs2ftp(path)
1073 def mkdir(self, path, basename):
1074 """Create the specified directory."""
1075 os.mkdir(os.path.join(path, basename))
1077 def listdir(self, path):
1078 """List the content of a directory."""
1079 return os.listdir(path)
1081 def rmdir(self, path):
1082 """Remove the specified directory."""
1085 def remove(self, path):
1086 """Remove the specified file."""
1089 def rename(self, src, dst):
1090 """Rename the specified src file to the dst filename."""
1093 def stat(self, path):
1094 """Perform a stat() system call on the given path."""
1095 return os.stat(path)
1097 def lstat(self, path):
1098 """Like stat but does not follow symbolic links."""
1099 return os.lstat(path)
1101 if not hasattr(os, 'lstat'):
1104 # --- Wrapper methods around os.path.*
1106 def isfile(self, path):
1107 """Return True if path is a file."""
1108 return os.path.isfile(path)
1110 def islink(self, path):
1111 """Return True if path is a symbolic link."""
1112 return os.path.islink(path)
1114 def isdir(self, path):
1115 """Return True if path is a directory."""
1116 return os.path.isdir(path)
1118 def getsize(self, path):
1119 """Return the size of the specified file in bytes."""
1120 return os.path.getsize(path)
1122 def getmtime(self, path):
1123 """Return the last modified time as a number of seconds since
1125 return os.path.getmtime(path)
1127 def realpath(self, path):
1128 """Return the canonical version of path eliminating any
1129 symbolic links encountered in the path (if they are
1130 supported by the operating system).
1132 return os.path.realpath(path)
1134 def lexists(self, path):
1135 """Return True if path refers to an existing path, including
1136 a broken or circular symbolic link.
1138 if hasattr(os.path, 'lexists'):
1139 return os.path.lexists(path)
1140 # grant backward compatibility with python 2.3
1141 elif hasattr(os, 'lstat'):
1149 return os.path.exists(path)
1151 exists = lexists # alias for backward compatibility with 0.2.0
1153 def glob1(self, dirname, pattern):
1154 """Return a list of files matching a dirname pattern
1157 Unlike glob.glob1 raises exception if os.listdir() fails.
1159 names = self.listdir(dirname)
1160 if pattern[0] != '.':
1161 names = filter(lambda x: x[0] != '.', names)
1162 return fnmatch.filter(names, pattern)
1164 # --- Listing utilities
1166 # note: the following operations are no more blocking
1168 def get_list_dir(self, datacr):
1169 """"Return an iterator object that yields a directory listing
1170 in a form suitable for LIST command.
1172 raise DeprecationWarning()
1174 def get_stat_dir(self, rawline):
1175 """Return an iterator object that yields a list of files
1176 matching a dirname pattern non-recursively in a form
1177 suitable for STAT command.
1179 - (str) rawline: the raw string passed by client as command
1182 ftppath = self.ftpnorm(rawline)
1183 if not glob.has_magic(ftppath):
1184 return self.get_list_dir(self.ftp2fs(rawline))
1186 basedir, basename = os.path.split(ftppath)
1187 if glob.has_magic(basedir):
1188 return iter(['Directory recursion not supported.\r\n'])
1190 basedir = self.ftp2fs(basedir)
1191 listing = self.glob1(basedir, basename)
1194 return self.format_list(basedir, listing)
1196 def format_list(self, basedir, listing, ignore_err=True):
1197 """Return an iterator object that yields the entries of given
1198 directory emulating the "/bin/ls -lA" UNIX command output.
1200 - (str) basedir: the absolute dirname.
1201 - (list) listing: the names of the entries in basedir
1202 - (bool) ignore_err: when False raise exception if os.lstat()
1205 On platforms which do not support the pwd and grp modules (such
1206 as Windows), ownership is printed as "owner" and "group" as a
1207 default, and number of hard links is always "1". On UNIX
1208 systems, the actual owner, group, and number of links are
1211 This is how output appears to client:
1213 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
1214 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
1215 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
1217 for basename in listing:
1218 file = os.path.join(basedir, basename)
1220 st = self.lstat(file)
1225 perms = filemode(st.st_mode) # permissions
1226 nlinks = st.st_nlink # number of links to inode
1227 if not nlinks: # non-posix system, let's use a bogus value
1229 size = st.st_size # file size
1231 # get user and group name, else just use the raw uid/gid
1233 uname = pwd.getpwuid(st.st_uid).pw_name
1237 gname = grp.getgrgid(st.st_gid).gr_name
1241 # on non-posix systems the only chance we use default
1242 # bogus values for owner and group
1245 # stat.st_mtime could fail (-1) if last mtime is too old
1246 # in which case we return the local time as last mtime
1248 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1250 mtime = time.strftime("%b %d %H:%M")
1251 # if the file is a symlink, resolve it, e.g. "symlink -> realfile"
1252 if stat.S_ISLNK(st.st_mode):
1253 basename = basename + " -> " + os.readlink(file)
1255 # formatting is matched with proftpd ls output
1256 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
1257 size, mtime, basename)
1259 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
1260 """Return an iterator object that yields the entries of a given
1261 directory or of a single file in a form suitable with MLSD and
1264 Every entry includes a list of "facts" referring the listed
1265 element. See RFC-3659, chapter 7, to see what every single
1268 - (str) basedir: the absolute dirname.
1269 - (list) listing: the names of the entries in basedir
1270 - (str) perms: the string referencing the user permissions.
1271 - (str) facts: the list of "facts" to be returned.
1272 - (bool) ignore_err: when False raise exception if os.stat()
1275 Note that "facts" returned may change depending on the platform
1276 and on what user specified by using the OPTS command.
1278 This is how output could appear to the client issuing
1281 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
1282 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
1283 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
1285 permdir = ''.join([x for x in perms if x not in 'arw'])
1286 permfile = ''.join([x for x in perms if x not in 'celmp'])
1287 if ('w' in perms) or ('a' in perms) or ('f' in perms):
1291 type = size = perm = modify = create = unique = mode = uid = gid = ""
1292 for basename in listing:
1293 file = os.path.join(basedir, basename)
1295 st = self.stat(file)
1301 if stat.S_ISDIR(st.st_mode):
1305 elif basename == '..':
1310 perm = 'perm=%s;' %permdir
1315 perm = 'perm=%s;' %permfile
1317 size = 'size=%s;' %st.st_size # file size
1318 # last modification time
1319 if 'modify' in facts:
1321 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1322 time.localtime(st.st_mtime))
1324 # stat.st_mtime could fail (-1) if last mtime is too old
1326 if 'create' in facts:
1327 # on Windows we can provide also the creation time
1329 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1330 time.localtime(st.st_ctime))
1334 if 'unix.mode' in facts:
1335 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
1336 if 'unix.uid' in facts:
1337 uid = 'unix.uid=%s;' %st.st_uid
1338 if 'unix.gid' in facts:
1339 gid = 'unix.gid=%s;' %st.st_gid
1340 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
1341 # posix platforms only; we get it by mixing st_dev and
1342 # st_ino values which should be enough for granting an
1343 # uniqueness for the file listed.
1344 # The same approach is used by pure-ftpd.
1345 # Implementors who want to provide unique fact on other
1346 # platforms should use some platform-specific method (e.g.
1347 # on Windows NTFS filesystems MTF records could be used).
1348 if 'unique' in facts:
1349 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
1351 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
1352 mode, uid, gid, unique, basename)
1357 class FTPHandler(asynchat.async_chat):
1358 """Implements the FTP server Protocol Interpreter (see RFC-959),
1359 handling commands received from the client on the control channel.
1361 All relevant session information is stored in class attributes
1362 reproduced below and can be modified before instantiating this
1365 - (str) banner: the string sent when client connects.
1367 - (int) max_login_attempts:
1368 the maximum number of wrong authentications before disconnecting
1369 the client (default 3).
1371 - (bool)permit_foreign_addresses:
1372 FTP site-to-site transfer feature: also referenced as "FXP" it
1373 permits for transferring a file between two remote FTP servers
1374 without the transfer going through the client's host (not
1375 recommended for security reasons as described in RFC-2577).
1376 Having this attribute set to False means that all data
1377 connections from/to remote IP addresses which do not match the
1378 client's IP address will be dropped (defualt False).
1380 - (bool) permit_privileged_ports:
1381 set to True if you want to permit active data connections (PORT)
1382 over privileged ports (not recommended, defaulting to False).
1384 - (str) masquerade_address:
1385 the "masqueraded" IP address to provide along PASV reply when
1386 pyftpdlib is running behind a NAT or other types of gateways.
1387 When configured pyftpdlib will hide its local address and
1388 instead use the public address of your NAT (default None).
1390 - (list) passive_ports:
1391 what ports ftpd will use for its passive data transfers.
1392 Value expected is a list of integers (e.g. range(60000, 65535)).
1393 When configured pyftpdlib will no longer use kernel-assigned
1394 random ports (default None).
1397 All relevant instance attributes initialized when client connects
1398 are reproduced below. You may be interested in them in case you
1399 want to subclass the original FTPHandler.
1401 - (bool) authenticated: True if client authenticated himself.
1402 - (str) username: the name of the connected user (if any).
1403 - (int) attempted_logins: number of currently attempted logins.
1404 - (str) current_type: the current transfer type (default "a")
1405 - (int) af: the address family (IPv4/IPv6)
1406 - (instance) server: the FTPServer class instance.
1407 - (instance) data_server: the data server instance (if any).
1408 - (instance) data_channel: the data channel instance (if any).
1410 # these are overridable defaults
1413 authorizer = DummyAuthorizer()
1414 active_dtp = ActiveDTP
1415 passive_dtp = PassiveDTP
1416 dtp_handler = DTPHandler
1417 abstracted_fs = AbstractedFS
1419 # session attributes (explained in the docstring)
1420 banner = "pyftpdlib %s ready." %__ver__
1421 max_login_attempts = 3
1422 permit_foreign_addresses = False
1423 permit_privileged_ports = False
1424 masquerade_address = None
1425 passive_ports = None
1427 def __init__(self, conn, server):
1428 """Initialize the command channel.
1430 - (instance) conn: the socket object instance of the newly
1431 established connection.
1432 - (instance) server: the ftp server class instance.
1435 asynchat.async_chat.__init__(self, conn=conn) # python2.5
1436 except TypeError, e:
1437 asynchat.async_chat.__init__(self, sock=conn) # python2.6
1438 self.server = server
1439 self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1441 self.in_buffer_len = 0
1442 self.set_terminator("\r\n")
1444 # session attributes
1445 self.fs = self.abstracted_fs()
1446 self.authenticated = False
1449 self.attempted_logins = 0
1450 self.current_type = 'a'
1451 self.restart_position = 0
1452 self.quit_pending = False
1453 self._epsvall = False
1454 self.__in_dtp_queue = None
1455 self.__out_dtp_queue = None
1457 # mlsx facts attributes
1458 self.current_facts = ['type', 'perm', 'size', 'modify']
1459 if os.name == 'posix':
1460 self.current_facts.append('unique')
1461 self.available_facts = self.current_facts[:]
1463 self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1465 self.available_facts.append('create')
1468 self.data_server = None
1469 self.data_channel = None
1471 if hasattr(self.socket, 'family'):
1472 self.af = self.socket.family
1473 else: # python < 2.5
1474 ip, port = self.socket.getsockname()[:2]
1475 self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
1476 socket.SOCK_STREAM)[0][0]
1479 """Return a 220 'Ready' response to the client over the command
1482 if len(self.banner) <= 75:
1483 self.respond("220 %s" %str(self.banner))
1485 self.push('220-%s\r\n' %str(self.banner))
1486 self.respond('220 ')
1488 def handle_max_cons(self):
1489 """Called when limit for maximum number of connections is reached."""
1490 msg = "Too many connections. Service temporary unavailable."
1491 self.respond("421 %s" %msg)
1493 # If self.push is used, data could not be sent immediately in
1494 # which case a new "loop" will occur exposing us to the risk of
1495 # accepting new connections. Since this could cause asyncore to
1496 # run out of fds (...and exposes the server to DoS attacks), we
1497 # immediately close the channel by using close() instead of
1498 # close_when_done(). If data has not been sent yet client will
1499 # be silently disconnected.
1502 def handle_max_cons_per_ip(self):
1503 """Called when too many clients are connected from the same IP."""
1504 msg = "Too many connections from the same IP address."
1505 self.respond("421 %s" %msg)
1507 self.close_when_done()
1509 # --- asyncore / asynchat overridden methods
1512 # if there's a quit pending we stop reading data from socket
1513 return not self.quit_pending
1515 def collect_incoming_data(self, data):
1516 """Read incoming data and append to the input buffer."""
1517 self.in_buffer.append(data)
1518 self.in_buffer_len += len(data)
1519 # Flush buffer if it gets too long (possible DoS attacks).
1520 # RFC-959 specifies that a 500 response could be given in
1523 if self.in_buffer_len > buflimit:
1524 self.respond('500 Command too long.')
1525 self.log('Command received exceeded buffer limit of %s.' %(buflimit))
1527 self.in_buffer_len = 0
1529 # commands accepted before authentication
1530 unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1532 # commands needing an argument
1533 arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
1534 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
1535 'TYPE','USER','XMKD','XRMD')
1537 # commands needing no argument
1538 unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1539 'SYST','XCUP','XPWD')
1541 def found_terminator(self):
1542 r"""Called when the incoming data stream matches the \r\n
1545 Depending on the command received it calls the command's
1546 corresponding method (e.g. for received command "MKD pathname",
1547 ftp_MKD() method is called with "pathname" as the argument).
1549 line = ''.join(self.in_buffer)
1551 self.in_buffer_len = 0
1553 cmd = line.split(' ')[0].upper()
1554 space = line.find(' ')
1556 arg = line[space + 1:]
1561 self.logline("<== %s" %line)
1563 self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1565 # let's check if user provided an argument for those commands
1567 if not arg and cmd in self.arg_cmds:
1568 self.respond("501 Syntax error: command needs an argument.")
1571 # let's do the same for those commands requiring no argument.
1572 elif arg and cmd in self.unarg_cmds:
1573 self.respond("501 Syntax error: command does not accept arguments.")
1576 # provide a limited set of commands if user isn't
1578 if (not self.authenticated):
1579 if cmd in self.unauth_cmds:
1580 # we permit STAT during this phase but we don't want
1581 # STAT to return a directory LISTing if the user is
1582 # not authenticated yet (this could happen if STAT
1583 # is used with an argument)
1584 if (cmd == 'STAT') and arg:
1585 self.respond("530 Log in with USER and PASS first.")
1587 method = getattr(self, 'ftp_' + cmd)
1588 method(arg) # call the proper ftp_* method
1589 elif cmd in proto_cmds:
1590 self.respond("530 Log in with USER and PASS first.")
1592 self.respond('500 Command "%s" not understood.' %line)
1594 # provide full command set
1595 elif (self.authenticated) and (cmd in proto_cmds):
1596 if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
1598 method = getattr(self, 'ftp_' + cmd)
1599 method(arg) # call the proper ftp_* method
1602 # recognize those commands having "special semantics"
1609 self.respond('500 Command "%s" not understood.' %line)
1611 def __check_path(self, cmd, line):
1612 """Check whether a path is valid."""
1614 # Always true, we will only check later, once we have a cursor
1617 def __check_perm(self, cmd, line, datacr):
1618 """Check permissions depending on issued command."""
1619 map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
1620 'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
1623 'DELE':'d', 'RMD':'d', 'XRMD':'d',
1625 'MKD':'m', 'XMKD':'m',
1627 raise NotImplementedError
1629 if cmd == 'STAT' and not line:
1632 if not line and (cmd in ('LIST','NLST','MLSD')):
1633 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1635 path = self.fs.ftp2fs(line, datacr)
1636 if not self.authorizer.has_perm(self.username, perm, path):
1637 self.log('FAIL %s "%s". Not enough privileges.' \
1638 %(cmd, self.fs.ftpnorm(line)))
1639 self.respond("550 Can't %s. Not enough privileges." %cmd)
1643 def handle_expt(self):
1644 """Called when there is out of band (OOB) data for the socket
1645 connection. This could happen in case of such commands needing
1646 "special action" (typically STAT and ABOR) in which case we
1647 append OOB data to incoming buffer.
1649 if hasattr(socket, 'MSG_OOB'):
1651 data = self.socket.recv(1024, socket.MSG_OOB)
1652 except socket.error:
1655 self.in_buffer.append(data)
1657 self.log("Can't handle OOB data.")
1660 def handle_error(self):
1663 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1665 except socket.error, err:
1666 # fix around asyncore bug (http://bugs.python.org/issue1736101)
1667 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
1668 errno.ECONNABORTED):
1672 logerror(traceback.format_exc())
1674 logerror(traceback.format_exc())
1677 def handle_close(self):
1682 """Close the current channel disconnecting the client."""
1683 if not self._closed:
1685 if self.data_server:
1686 self.data_server.close()
1687 del self.data_server
1689 if self.data_channel:
1690 self.data_channel.close()
1691 del self.data_channel
1693 del self.__out_dtp_queue
1694 del self.__in_dtp_queue
1696 # remove client IP address from ip map
1697 self.server.ip_map.remove(self.remote_ip)
1698 asynchat.async_chat.close(self)
1699 self.log("Disconnected.")
1703 def on_dtp_connection(self):
1704 """Called every time data channel connects (either active or
1707 Incoming and outgoing queues are checked for pending data.
1708 If outbound data is pending, it is pushed into the data channel.
1709 If awaiting inbound data, the data channel is enabled for
1712 if self.data_server:
1713 self.data_server.close()
1714 self.data_server = None
1716 # check for data to send
1717 if self.__out_dtp_queue:
1718 data, isproducer, file = self.__out_dtp_queue
1720 self.data_channel.file_obj = file
1722 self.data_channel.push(data)
1724 self.data_channel.push_with_producer(data)
1725 if self.data_channel:
1726 self.data_channel.close_when_done()
1727 self.__out_dtp_queue = None
1729 # check for data to receive
1730 elif self.__in_dtp_queue:
1731 self.data_channel.file_obj = self.__in_dtp_queue
1732 self.data_channel.enable_receiving(self.current_type)
1733 self.__in_dtp_queue = None
1735 def on_dtp_close(self):
1736 """Called every time the data channel is closed."""
1737 self.data_channel = None
1738 if self.quit_pending:
1739 self.close_when_done()
1743 def respond(self, resp):
1744 """Send a response to the client using the command channel."""
1745 self.push(resp + '\r\n')
1746 self.logline('==> %s' % resp)
1748 def push_dtp_data(self, data, isproducer=False, file=None):
1749 """Pushes data into the data channel.
1751 It is usually called for those commands requiring some data to
1752 be sent over the data channel (e.g. RETR).
1753 If data channel does not exist yet, it queues the data to send
1754 later; data will then be pushed into data channel when
1755 on_dtp_connection() will be called.
1757 - (str/classobj) data: the data to send which may be a string
1758 or a producer object).
1759 - (bool) isproducer: whether treat data as a producer.
1760 - (file) file: the file[-like] object to send (if any).
1762 if self.data_channel:
1763 self.respond("125 Data connection already open. Transfer starting.")
1765 self.data_channel.file_obj = file
1767 self.data_channel.push(data)
1769 self.data_channel.push_with_producer(data)
1770 if self.data_channel:
1771 self.data_channel.close_when_done()
1773 self.respond("150 File status okay. About to open data connection.")
1774 self.__out_dtp_queue = (data, isproducer, file)
1777 """Log a message, including additional identifying session data."""
1778 log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
1779 self.remote_port, msg))
1781 def logline(self, msg):
1782 """Log a line including additional indentifying session data."""
1783 logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg))
1785 def flush_account(self):
1786 """Flush account information by clearing attributes that need
1787 to be reset on a REIN or new USER command.
1789 if self.data_channel:
1790 if not self.data_channel.transfer_in_progress():
1791 self.data_channel.close()
1792 self.data_channel = None
1793 if self.data_server:
1794 self.data_server.close()
1795 self.data_server = None
1798 self.authenticated = False
1801 self.attempted_logins = 0
1802 self.current_type = 'a'
1803 self.restart_position = 0
1804 self.quit_pending = False
1805 self.__in_dtp_queue = None
1806 self.__out_dtp_queue = None
1808 def run_as_current_user(self, function, *args, **kwargs):
1809 """Execute a function impersonating the current logged-in user."""
1810 self.authorizer.impersonate_user(self.username, self.password)
1812 return function(*args, **kwargs)
1814 self.authorizer.terminate_impersonation()
1818 def _make_eport(self, ip, port):
1819 """Establish an active data channel with remote client which
1820 issued a PORT or EPRT command.
1822 # FTP bounce attacks protection: according to RFC-2577 it's
1823 # recommended to reject PORT if IP address specified in it
1824 # does not match client IP address.
1825 if not self.permit_foreign_addresses:
1826 if ip != self.remote_ip:
1827 self.log("Rejected data connection to foreign address %s:%s."
1829 self.respond("501 Can't connect to a foreign address.")
1832 # ...another RFC-2577 recommendation is rejecting connections
1833 # to privileged ports (< 1024) for security reasons.
1834 if not self.permit_privileged_ports:
1836 self.log('PORT against the privileged port "%s" refused.' %port)
1837 self.respond("501 Can't connect over a privileged port.")
1840 # close existent DTP-server instance, if any.
1841 if self.data_server:
1842 self.data_server.close()
1843 self.data_server = None
1844 if self.data_channel:
1845 self.data_channel.close()
1846 self.data_channel = None
1848 # make sure we are not hitting the max connections limit
1849 if self.server.max_cons:
1850 if len(self._map) >= self.server.max_cons:
1851 msg = "Too many connections. Can't open data channel."
1852 self.respond("425 %s" %msg)
1857 self.active_dtp(ip, port, self)
1859 def _make_epasv(self, extmode=False):
1860 """Initialize a passive data channel with remote client which
1861 issued a PASV or EPSV command.
1862 If extmode argument is False we assume that client issued EPSV in
1863 which case extended passive mode will be used (see RFC-2428).
1865 # close existing DTP-server instance, if any
1866 if self.data_server:
1867 self.data_server.close()
1868 self.data_server = None
1870 if self.data_channel:
1871 self.data_channel.close()
1872 self.data_channel = None
1874 # make sure we are not hitting the max connections limit
1875 if self.server.max_cons:
1876 if len(self._map) >= self.server.max_cons:
1877 msg = "Too many connections. Can't open data channel."
1878 self.respond("425 %s" %msg)
1883 self.data_server = self.passive_dtp(self, extmode)
1885 def ftp_PORT(self, line):
1886 """Start an active data channel by using IPv4."""
1888 self.respond("501 PORT not allowed after EPSV ALL.")
1890 if self.af != socket.AF_INET:
1891 self.respond("425 You cannot use PORT on IPv6 connections. "
1892 "Use EPRT instead.")
1894 # Parse PORT request for getting IP and PORT.
1895 # Request comes in as:
1896 # > h1,h2,h3,h4,p1,p2
1897 # ...where the client's IP address is h1.h2.h3.h4 and the TCP
1898 # port number is (p1 * 256) + p2.
1900 addr = map(int, line.split(','))
1901 assert len(addr) == 6
1903 assert 0 <= x <= 255
1904 ip = '%d.%d.%d.%d' %tuple(addr[:4])
1905 port = (addr[4] * 256) + addr[5]
1906 assert 0 <= port <= 65535
1907 except (AssertionError, ValueError, OverflowError):
1908 self.respond("501 Invalid PORT format.")
1910 self._make_eport(ip, port)
1912 def ftp_EPRT(self, line):
1913 """Start an active data channel by choosing the network protocol
1914 to use (IPv4/IPv6) as defined in RFC-2428.
1917 self.respond("501 EPRT not allowed after EPSV ALL.")
1919 # Parse EPRT request for getting protocol, IP and PORT.
1920 # Request comes in as:
1921 # # <d>proto<d>ip<d>port<d>
1922 # ...where <d> is an arbitrary delimiter character (usually "|") and
1923 # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
1925 af, ip, port = line.split(line[0])[1:-1]
1927 assert 0 <= port <= 65535
1928 except (AssertionError, ValueError, IndexError, OverflowError):
1929 self.respond("501 Invalid EPRT format.")
1933 if self.af != socket.AF_INET:
1934 self.respond('522 Network protocol not supported (use 2).')
1937 octs = map(int, ip.split('.'))
1938 assert len(octs) == 4
1940 assert 0 <= x <= 255
1941 except (AssertionError, ValueError, OverflowError), err:
1942 self.respond("501 Invalid EPRT format.")
1944 self._make_eport(ip, port)
1946 if self.af == socket.AF_INET:
1947 self.respond('522 Network protocol not supported (use 1).')
1949 self._make_eport(ip, port)
1951 if self.af == socket.AF_INET:
1952 self.respond('501 Unknown network protocol (use 1).')
1954 self.respond('501 Unknown network protocol (use 2).')
1956 def ftp_PASV(self, line):
1957 """Start a passive data channel by using IPv4."""
1959 self.respond("501 PASV not allowed after EPSV ALL.")
1961 if self.af != socket.AF_INET:
1962 self.respond("425 You cannot use PASV on IPv6 connections. "
1963 "Use EPSV instead.")
1965 self._make_epasv(extmode=False)
1967 def ftp_EPSV(self, line):
1968 """Start a passive data channel by using IPv4 or IPv6 as defined
1971 # RFC-2428 specifies that if an optional parameter is given,
1972 # we have to determine the address family from that otherwise
1973 # use the same address family used on the control connection.
1974 # In such a scenario a client may use IPv4 on the control channel
1975 # and choose to use IPv6 for the data channel.
1976 # But how could we use IPv6 on the data channel without knowing
1977 # which IPv6 address to use for binding the socket?
1978 # Unfortunately RFC-2428 does not provide satisfing information
1979 # on how to do that. The assumption is that we don't have any way
1980 # to know which address to use, hence we just use the same address
1981 # family used on the control connection.
1983 self._make_epasv(extmode=True)
1985 if self.af != socket.AF_INET:
1986 self.respond('522 Network protocol not supported (use 2).')
1988 self._make_epasv(extmode=True)
1990 if self.af == socket.AF_INET:
1991 self.respond('522 Network protocol not supported (use 1).')
1993 self._make_epasv(extmode=True)
1994 elif line.lower() == 'all':
1995 self._epsvall = True
1996 self.respond('220 Other commands other than EPSV are now disabled.')
1998 if self.af == socket.AF_INET:
1999 self.respond('501 Unknown network protocol (use 1).')
2001 self.respond('501 Unknown network protocol (use 2).')
2003 def ftp_QUIT(self, line):
2004 """Quit the current session."""
2006 # This command terminates a USER and if file transfer is not
2007 # in progress, the server closes the control connection.
2008 # If file transfer is in progress, the connection will remain
2009 # open for result response and the server will then close it.
2010 if self.authenticated:
2011 msg_quit = self.authorizer.get_msg_quit(self.username)
2013 msg_quit = "Goodbye."
2014 if len(msg_quit) <= 75:
2015 self.respond("221 %s" %msg_quit)
2017 self.push("221-%s\r\n" %msg_quit)
2018 self.respond("221 ")
2020 if not self.data_channel:
2021 self.close_when_done()
2023 # tell the cmd channel to stop responding to commands.
2024 self.quit_pending = True
2027 # --- data transferring
2029 def ftp_LIST(self, line):
2030 """Return a list of files in the specified directory to the
2033 # - If no argument, fall back on cwd as default.
2034 # - Some older FTP clients erroneously issue /bin/ls-like LIST
2035 # formats in which case we fall back on cwd as default.
2036 if not line or line.lower() in ('-a', '-l', '-al', '-la'):
2040 datacr = self.fs.get_crdata(line, mode='list')
2041 iterator = self.run_as_current_user(self.fs.get_list_dir, datacr)
2042 except IOError, err:
2043 self.fs.close_cr(datacr)
2044 self.respond('550 %s.'% err.strerror)
2046 except OSError, err:
2047 self.fs.close_cr(datacr)
2048 why = _strerror(err)
2049 self.log('FAIL LIST "%s". %s.' %(line, why))
2050 self.respond('550 %s.' %why)
2053 self.log('OK LIST "%s". Transfer starting.' % line)
2054 producer = BufferedIteratorProducer(iterator)
2055 self.push_dtp_data(producer, isproducer=True)
2057 self.fs.close_cr(datacr)
2060 def ftp_NLST(self, line):
2061 """Return a list of files in the specified directory in a
2062 compact form to the client.
2069 datacr = self.fs.get_crdata(line, mode='list')
2071 datacr = ( None, None, None )
2072 if self.fs.isdir(datacr[1]):
2073 nodelist = self.run_as_current_user(self.fs.listdir, datacr)
2075 # if path is a file we just list its name
2076 nodelist = [datacr[1],]
2080 if isinstance(nl.path, (list, tuple)):
2081 listing.append(nl.path[-1])
2083 listing.append(nl.path) # assume string
2084 except IOError, err:
2085 self.fs.close_cr(datacr)
2086 self.respond('550 %s.'% err.strerror)
2088 except OSError, err:
2089 self.fs.close_cr(datacr)
2090 why = _strerror(err)
2091 self.log('FAIL NLST "%s". %s.' %(line, why))
2092 self.respond('550 %s.' %why)
2095 self.fs.close_cr(datacr)
2099 data = ''.join([ _to_decode(x) + '\r\n' for x in listing ])
2100 self.log('OK NLST "%s". Transfer starting.' %line)
2101 self.push_dtp_data(data)
2103 # --- MLST and MLSD commands
2105 # The MLST and MLSD commands are intended to standardize the file and
2106 # directory information returned by the server-FTP process. These
2107 # commands differ from the LIST command in that the format of the
2108 # replies is strictly defined although extensible.
2110 def ftp_MLST(self, line):
2111 """Return information about a pathname in a machine-processable
2112 form as defined in RFC-3659.
2114 # if no argument, fall back on cwd as default
2119 datacr = self.fs.get_crdata(line, mode='list')
2120 perms = self.authorizer.get_perms(self.username)
2121 iterator = self.run_as_current_user(self.fs.format_mlsx, datacr[0], datacr[1].parent,
2122 [datacr[1],], perms, self.current_facts, ignore_err=False)
2123 data = ''.join(iterator)
2124 except EnvironmentError, err:
2125 self.fs.close_cr(datacr)
2126 why = _strerror(err)
2127 self.log('FAIL MLST "%s". %s.' %(line, why))
2128 self.respond('550 %s.' %why)
2130 self.fs.close_cr(datacr)
2131 # since TVFS is supported (see RFC-3659 chapter 6), a fully
2132 # qualified pathname should be returned
2133 data = data.split(' ')[0] + ' %s\r\n' %line
2134 # response is expected on the command channel
2135 self.push('250-Listing "%s":\r\n' %line)
2136 # the fact set must be preceded by a space
2137 self.push(' ' + data)
2138 self.respond('250 End MLST.')
2140 def ftp_MLSD(self, line):
2141 """Return contents of a directory in a machine-processable form
2142 as defined in RFC-3659.
2144 # if no argument, fall back on cwd as default
2150 datacr = self.fs.get_crdata(line, mode='list')
2151 # RFC-3659 requires 501 response code if path is not a directory
2152 if not self.fs.isdir(datacr[1]):
2153 err = 'No such directory'
2154 self.log('FAIL MLSD "%s". %s.' %(line, err))
2155 self.respond("501 %s." %err)
2157 listing = self.run_as_current_user(self.fs.listdir, datacr)
2158 except OSError, err:
2159 self.fs.close_cr(datacr)
2160 why = _strerror(err)
2161 self.log('FAIL MLSD "%s". %s.' %(line, why))
2162 self.respond('550 %s.' %why)
2164 self.fs.close_cr(datacr)
2165 perms = self.authorizer.get_perms(self.username)
2166 iterator = self.fs.format_mlsx(datacr[0], datacr[1], listing, perms,
2168 producer = BufferedIteratorProducer(iterator)
2169 self.log('OK MLSD "%s". Transfer starting.' %line)
2170 self.push_dtp_data(producer, isproducer=True)
2172 def ftp_RETR(self, line):
2173 """Retrieve the specified file (transfer from the server to the
2178 datacr = self.fs.get_crdata(line, mode='file')
2179 fd = self.run_as_current_user(self.fs.open, datacr, 'rb')
2180 except OSError, err:
2181 self.fs.close_cr(datacr)
2182 why = _strerror(err)
2183 self.log('FAIL RETR "%s". %s.' %(line, why))
2184 self.respond('550 %s.' %why)
2186 except IOError, err:
2187 self.fs.close_cr(datacr)
2189 self.log('FAIL RETR "%s". %s.' %(line, why))
2190 self.respond('550 %s.' %why)
2193 if self.restart_position:
2194 # Make sure that the requested offset is valid (within the
2195 # size of the file being resumed).
2196 # According to RFC-1123 a 554 reply may result in case that
2197 # the existing file cannot be repositioned as specified in
2201 assert not self.restart_position > self.fs.getsize(datacr)
2202 fd.seek(self.restart_position)
2204 except AssertionError:
2205 why = "Invalid REST parameter"
2206 except IOError, err:
2207 why = _strerror(err)
2208 self.restart_position = 0
2210 self.respond('554 %s' %why)
2211 self.log('FAIL RETR "%s". %s.' %(line, why))
2212 self.fs.close_cr(datacr)
2214 self.log('OK RETR "%s". Download starting.' %line)
2215 producer = FileProducer(fd, self.current_type)
2216 self.push_dtp_data(producer, isproducer=True, file=fd)
2217 self.fs.close_cr(datacr)
2219 def ftp_STOR(self, line, mode='w'):
2220 """Store a file (transfer from the client to the server)."""
2221 # A resume could occur in case of APPE or REST commands.
2222 # In that case we have to open file object in different ways:
2225 # REST: mode = 'r+' (to permit seeking on file object)
2233 datacr = self.fs.get_crdata(line,mode='create')
2234 except OSError, err:
2235 self.fs.close_cr(datacr)
2236 why = _strerror(err)
2237 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2238 self.respond('550 %s.' %why)
2241 if self.restart_position:
2244 fd = self.run_as_current_user(self.fs.create, datacr, datacr[2], mode + 'b')
2245 except IOError, err:
2246 self.fs.close_cr(datacr)
2247 why = _strerror(err)
2248 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2249 self.respond('550 %s.' %why)
2252 if self.restart_position:
2253 # Make sure that the requested offset is valid (within the
2254 # size of the file being resumed).
2255 # According to RFC-1123 a 554 reply may result in case
2256 # that the existing file cannot be repositioned as
2257 # specified in the REST.
2260 assert not self.restart_position > self.fs.getsize(datacr)
2261 fd.seek(self.restart_position)
2263 except AssertionError:
2264 why = "Invalid REST parameter"
2265 except IOError, err:
2266 why = _strerror(err)
2267 self.restart_position = 0
2269 self.fs.close_cr(datacr)
2270 self.respond('554 %s' %why)
2271 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2274 self.log('OK %s "%s". Upload starting.' %(cmd, line))
2275 if self.data_channel:
2276 self.respond("125 Data connection already open. Transfer starting.")
2277 self.data_channel.file_obj = fd
2278 self.data_channel.enable_receiving(self.current_type)
2280 self.respond("150 File status okay. About to open data connection.")
2281 self.__in_dtp_queue = fd
2282 self.fs.close_cr(datacr)
2285 def ftp_STOU(self, line):
2286 """Store a file on the server with a unique name."""
2287 # Note 1: RFC-959 prohibited STOU parameters, but this
2288 # prohibition is obsolete.
2289 # Note 2: 250 response wanted by RFC-959 has been declared
2290 # incorrect in RFC-1123 that wants 125/150 instead.
2291 # Note 3: RFC-1123 also provided an exact output format
2292 # defined to be as follow:
2294 # ...where pppp represents the unique path name of the
2295 # file that will be written.
2297 # watch for STOU preceded by REST, which makes no sense.
2298 if self.restart_position:
2299 self.respond("450 Can't STOU while REST request is pending.")
2304 datacr = self.fs.get_crdata(line, mode='create')
2308 basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
2311 fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
2313 except IOError, err:
2314 # hitted the max number of tries to find out file with
2316 if err.errno == errno.EEXIST:
2317 why = 'No usable unique file name found'
2318 # something else happened
2320 why = _strerror(err)
2321 self.respond("450 %s." %why)
2322 self.log('FAIL STOU "%s". %s.' %(self.fs.ftpnorm(line), why))
2323 self.fs.close_cr(datacr)
2327 if not self.authorizer.has_perm(self.username, 'w', filename):
2328 self.log('FAIL STOU "%s". Not enough privileges'
2329 %self.fs.ftpnorm(line))
2330 self.respond("550 Can't STOU: not enough privileges.")
2331 self.fs.close_cr(datacr)
2334 # now just acts like STOR except that restarting isn't allowed
2335 self.log('OK STOU "%s". Upload starting.' %filename)
2336 if self.data_channel:
2337 self.respond("125 FILE: %s" %filename)
2338 self.data_channel.file_obj = fd
2339 self.data_channel.enable_receiving(self.current_type)
2341 self.respond("150 FILE: %s" %filename)
2342 self.__in_dtp_queue = fd
2343 self.fs.close_cr(datacr)
2346 def ftp_APPE(self, line):
2347 """Append data to an existing file on the server."""
2348 # watch for APPE preceded by REST, which makes no sense.
2349 if self.restart_position:
2350 self.respond("550 Can't APPE while REST request is pending.")
2352 self.ftp_STOR(line, mode='a')
2354 def ftp_REST(self, line):
2355 """Restart a file transfer from a previous mark."""
2360 except (ValueError, OverflowError):
2361 self.respond("501 Invalid parameter.")
2363 self.respond("350 Restarting at position %s. " \
2364 "Now use RETR/STOR for resuming." %marker)
2365 self.log("OK REST %s." %marker)
2366 self.restart_position = marker
2368 def ftp_ABOR(self, line):
2369 """Abort the current data transfer."""
2371 # ABOR received while no data channel exists
2372 if (self.data_server is None) and (self.data_channel is None):
2373 resp = "225 No transfer to abort."
2375 # a PASV was received but connection wasn't made yet
2376 if self.data_server:
2377 self.data_server.close()
2378 self.data_server = None
2379 resp = "225 ABOR command successful; data channel closed."
2381 # If a data transfer is in progress the server must first
2382 # close the data connection, returning a 426 reply to
2383 # indicate that the transfer terminated abnormally, then it
2384 # must send a 226 reply, indicating that the abort command
2385 # was successfully processed.
2386 # If no data has been transmitted we just respond with 225
2387 # indicating that no transfer was in progress.
2388 if self.data_channel:
2389 if self.data_channel.transfer_in_progress():
2390 self.data_channel.close()
2391 self.data_channel = None
2392 self.respond("426 Connection closed; transfer aborted.")
2393 self.log("OK ABOR. Transfer aborted, data channel closed.")
2394 resp = "226 ABOR command successful."
2396 self.data_channel.close()
2397 self.data_channel = None
2398 self.log("OK ABOR. Data channel closed.")
2399 resp = "225 ABOR command successful; data channel closed."
2403 # --- authentication
2405 def ftp_USER(self, line):
2406 """Set the username for the current session."""
2407 # we always treat anonymous user as lower-case string.
2408 if line.lower() == "anonymous":
2411 # RFC-959 specifies a 530 response to the USER command if the
2412 # username is not valid. If the username is valid is required
2413 # ftpd returns a 331 response instead. In order to prevent a
2414 # malicious client from determining valid usernames on a server,
2415 # it is suggested by RFC-2577 that a server always return 331 to
2416 # the USER command and then reject the combination of username
2417 # and password for an invalid username when PASS is provided later.
2418 if not self.authenticated:
2419 self.respond('331 Username ok, send password.')
2421 # a new USER command could be entered at any point in order
2422 # to change the access control flushing any user, password,
2423 # and account information already supplied and beginning the
2424 # login sequence again.
2425 self.flush_account()
2426 msg = 'Previous account information was flushed'
2427 self.log('OK USER "%s". %s.' %(line, msg))
2428 self.respond('331 %s, send password.' %msg)
2429 self.username = line
2431 def ftp_PASS(self, line):
2432 """Check username's password against the authorizer."""
2434 if self.authenticated:
2435 self.respond("503 User already authenticated.")
2437 if not self.username:
2438 self.respond("503 Login with USER first.")
2442 if self.authorizer.has_user(self.username):
2443 if self.username == 'anonymous' \
2444 or self.authorizer.validate_authentication(self.username, line):
2445 msg_login = self.authorizer.get_msg_login(self.username)
2446 if len(msg_login) <= 75:
2447 self.respond('230 %s' %msg_login)
2449 self.push("230-%s\r\n" %msg_login)
2450 self.respond("230 ")
2452 self.authenticated = True
2453 self.password = line
2454 self.attempted_logins = 0
2455 self.fs.root = self.authorizer.get_home_dir(self.username)
2456 self.fs.username=self.username
2457 self.fs.password=line
2458 self.log("User %s logged in." %self.username)
2460 self.attempted_logins += 1
2461 if self.attempted_logins >= self.max_login_attempts:
2462 self.respond("530 Maximum login attempts. Disconnecting.")
2465 self.respond("530 Authentication failed.")
2466 self.log('Authentication failed (user: "%s").' %self.username)
2471 self.attempted_logins += 1
2472 if self.attempted_logins >= self.max_login_attempts:
2473 self.log('Authentication failed: unknown username "%s".'
2475 self.respond("530 Maximum login attempts. Disconnecting.")
2477 elif self.username.lower() == 'anonymous':
2478 self.respond("530 Anonymous access not allowed.")
2479 self.log('Authentication failed: anonymous access not allowed.')
2481 self.respond("530 Authentication failed.")
2482 self.log('Authentication failed: unknown username "%s".'
2486 def ftp_REIN(self, line):
2487 """Reinitialize user's current session."""
2489 # REIN command terminates a USER, flushing all I/O and account
2490 # information, except to allow any transfer in progress to be
2491 # completed. All parameters are reset to the default settings
2492 # and the control connection is left open. This is identical
2493 # to the state in which a user finds himself immediately after
2494 # the control connection is opened.
2495 self.log("OK REIN. Flushing account information.")
2496 self.flush_account()
2497 # Note: RFC-959 erroneously mention "220" as the correct response
2498 # code to be given in this case, but this is wrong...
2499 self.respond("230 Ready for new user.")
2502 # --- filesystem operations
2504 def ftp_PWD(self, line):
2505 """Return the name of the current working directory to the client."""
2506 cwd = self.fs.get_cwd()
2507 self.respond('257 "%s" is the current directory.' % cwd)
2509 def ftp_CWD(self, line):
2510 """Change the current working directory."""
2511 # check: a lot of FTP servers go back to root directory if no
2512 # arg is provided but this is not specified in RFC-959.
2513 # Search for official references about this behaviour.
2516 datacr = self.fs.get_crdata(line,'cwd')
2517 self.run_as_current_user(self.fs.chdir, datacr)
2518 cwd = self.fs.get_cwd()
2519 self.log('OK CWD "%s".' % cwd)
2520 self.respond('250 "%s" is the current directory.' % cwd)
2521 except EnvironmentError, err:
2522 self.log("Could not cwd: %s" % err)
2524 why = 'Authentication Required or Failed'
2525 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2526 self.respond('530 %s.' %why)
2528 why = _strerror(err)
2529 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2530 self.respond('550 %s.' %why)
2531 self.fs.close_cr(datacr)
2533 def ftp_CDUP(self, line):
2534 """Change into the parent directory."""
2535 # Note: RFC-959 says that code 200 is required but it also says
2536 # that CDUP uses the same codes as CWD.
2539 def ftp_SIZE(self, line):
2540 """Return size of file in a format suitable for using with
2541 RESTart as defined in RFC-3659.
2543 Implementation note:
2544 properly handling the SIZE command when TYPE ASCII is used would
2545 require to scan the entire file to perform the ASCII translation
2546 logic (file.read().replace(os.linesep, '\r\n')) and then
2547 calculating the len of such data which may be different than
2548 the actual size of the file on the server. Considering that
2549 calculating such result could be very resource-intensive it
2550 could be easy for a malicious client to try a DoS attack, thus
2551 we do not perform the ASCII translation.
2553 However, clients in general should not be resuming downloads in
2554 ASCII mode. Resuming downloads in binary mode is the recommended
2555 way as specified in RFC-3659.
2559 datacr = self.fs.get_crdata(line, mode='file')
2560 #if self.fs.isdir(datacr[1]):
2561 # why = "%s is not retrievable" %line
2562 # self.log('FAIL SIZE "%s". %s.' %(line, why))
2563 # self.respond("550 %s." %why)
2564 # self.fs.close_cr(datacr)
2566 size = self.run_as_current_user(self.fs.getsize, datacr)
2567 except EnvironmentError, err:
2569 self.log('FAIL SIZE "%s". %s.' %(line, why))
2570 if err.errno == errno.ENOENT:
2571 self.respond("404 %s." % why)
2573 self.respond('550 %s.' % why)
2575 self.respond("213 %s" %size)
2576 self.log('OK SIZE "%s".' %line)
2577 self.fs.close_cr(datacr)
2579 def ftp_MDTM(self, line):
2580 """Return last modification time of file to the client as an ISO
2581 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2585 datacr = self.fs.get_crdata(line)
2586 if not self.fs.isfile(datacr):
2587 why = "%s is not retrievable" %line
2588 self.log('FAIL MDTM "%s". %s.' %(line, why))
2589 self.respond("550 %s." %why)
2590 self.fs.close_cr(datacr)
2592 lmt = self.run_as_current_user(self.fs.getmtime, datacr)
2593 lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
2594 self.respond("213 %s" %lmt)
2595 self.log('OK MDTM "%s".' %line)
2596 except OSError, err:
2597 why = _strerror(err)
2598 self.log('FAIL MDTM "%s". %s.' %(line, why))
2599 self.respond('550 %s.' %why)
2600 self.fs.close_cr(datacr)
2602 def ftp_MKD(self, line):
2603 """Create the specified directory."""
2605 datacr = self.fs.get_crdata(line, mode='create')
2606 self.run_as_current_user(self.fs.mkdir, datacr, datacr[2])
2607 except IOError, err:
2608 self.log('FAIL MKD "%s". %s.' %(line, err.strerror))
2609 self.respond('550 %s.' % err.strerror)
2610 except OSError, err:
2611 why = _strerror(err)
2612 self.log('FAIL MKD "%s". %s.' %(line, why))
2613 self.respond('550 %s.' %why)
2615 self.log('OK MKD "%s".' %line)
2616 self.respond("257 Directory created.")
2617 self.fs.close_cr(datacr)
2619 def ftp_RMD(self, line):
2620 """Remove the specified directory."""
2623 datacr = self.fs.get_crdata(line, mode='delete')
2625 msg = "Can't remove root directory."
2626 self.respond("550 %s" %msg)
2627 self.log('FAIL MKD "/". %s' %msg)
2628 self.fs.close_cr(datacr)
2630 self.run_as_current_user(self.fs.rmdir, datacr)
2631 except OSError, err:
2632 why = _strerror(err)
2633 self.log('FAIL RMD "%s". %s.' %(line, why))
2634 self.respond('550 %s.' %why)
2636 self.log('OK RMD "%s".' %line)
2637 self.respond("250 Directory removed.")
2638 self.fs.close_cr(datacr)
2640 def ftp_DELE(self, line):
2641 """Delete the specified file."""
2644 datacr = self.fs.get_crdata(line, mode='delete')
2645 self.run_as_current_user(self.fs.remove, datacr)
2646 except EnvironmentError, err:
2648 self.log('FAIL DELE "%s". %s.' %(line, why))
2649 if err.errno == errno.ENOENT:
2650 self.respond('404 %s.' % why)
2652 self.respond('550 %s.' %why)
2654 self.log('OK DELE "%s".' %line)
2655 self.respond("250 File removed.")
2656 self.fs.close_cr(datacr)
2658 def ftp_RNFR(self, line):
2659 """Rename the specified (only the source name is specified
2660 here, see RNTO command)"""
2663 datacr = self.fs.get_crdata(line, mode='rfnr')
2665 self.respond("550 No such file or directory.")
2667 self.respond("550 Can't rename the home directory.")
2669 self.fs.rnfr = datacr[1]
2670 self.respond("350 Ready for destination name.")
2672 self.respond("550 Can't find the file or directory.")
2673 self.fs.close_cr(datacr)
2675 def ftp_RNTO(self, line):
2676 """Rename file (destination name only, source is specified with
2679 if not self.fs.rnfr:
2680 self.respond("503 Bad sequence of commands: use RNFR first.")
2685 datacr = self.fs.get_crdata(line,'create')
2686 oldname = self.fs.rnfr.path
2687 if isinstance(oldname, (list, tuple)):
2688 oldname = '/'.join(oldname)
2689 self.run_as_current_user(self.fs.rename, self.fs.rnfr, datacr)
2691 self.log('OK RNFR/RNTO "%s ==> %s".' % \
2692 (_to_unicode(oldname), _to_unicode(line)))
2693 self.respond("250 Renaming ok.")
2694 except EnvironmentError, err:
2695 why = _strerror(err)
2696 self.log('FAIL RNFR/RNTO "%s ==> %s". %s.' \
2697 % (_to_unicode(oldname), _to_unicode(line), why))
2698 self.respond('550 %s.' %why)
2701 self.fs.close_cr(datacr)
2706 def ftp_TYPE(self, line):
2707 """Set current type data type to binary/ascii"""
2709 if line in ("A", "AN", "A N"):
2710 self.respond("200 Type set to: ASCII.")
2711 self.current_type = 'a'
2712 elif line in ("I", "L8", "L 8"):
2713 self.respond("200 Type set to: Binary.")
2714 self.current_type = 'i'
2716 self.respond('504 Unsupported type "%s".' %line)
2718 def ftp_STRU(self, line):
2719 """Set file structure (obsolete)."""
2720 # obsolete (backward compatibility with older ftp clients)
2721 if line in ('f','F'):
2722 self.respond('200 File transfer structure set to: F.')
2724 self.respond('504 Unimplemented STRU type.')
2726 def ftp_MODE(self, line):
2727 """Set data transfer mode (obsolete)"""
2728 # obsolete (backward compatibility with older ftp clients)
2729 if line in ('s', 'S'):
2730 self.respond('200 Transfer mode set to: S')
2732 self.respond('504 Unimplemented MODE type.')
2734 def ftp_STAT(self, line):
2735 """Return statistics about current ftp session. If an argument
2736 is provided return directory listing over command channel.
2738 Implementation note:
2740 RFC-959 do not explicitly mention globbing; this means that FTP
2741 servers are not required to support globbing in order to be
2742 compliant. However, many FTP servers do support globbing as a
2743 measure of convenience for FTP clients and users.
2745 In order to search for and match the given globbing expression,
2746 the code has to search (possibly) many directories, examine
2747 each contained filename, and build a list of matching files in
2748 memory. Since this operation can be quite intensive, both CPU-
2749 and memory-wise, we limit the search to only one directory
2750 non-recursively, as LIST does.
2752 # return STATus information about ftpd
2755 s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2756 if self.authenticated:
2757 s.append('Logged in as: %s' %self.username)
2759 if not self.username:
2760 s.append("Waiting for username.")
2762 s.append("Waiting for password.")
2763 if self.current_type == 'a':
2767 s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
2768 if self.data_server:
2769 s.append('Passive data channel waiting for connection.')
2770 elif self.data_channel:
2771 bytes_sent = self.data_channel.tot_bytes_sent
2772 bytes_recv = self.data_channel.tot_bytes_received
2773 s.append('Data connection open:')
2774 s.append('Total bytes sent: %s' %bytes_sent)
2775 s.append('Total bytes received: %s' %bytes_recv)
2777 s.append('Data connection closed.')
2779 self.push('211-FTP server status:\r\n')
2780 self.push(''.join([' %s\r\n' %item for item in s]))
2781 self.respond('211 End of status.')
2782 # return directory LISTing over the command channel
2786 datacr = self.fs.get_cr(line)
2787 iterator = self.run_as_current_user(self.fs.get_stat_dir, line, datacr)
2788 except OSError, err:
2789 self.respond('550 %s.' %_strerror(err))
2791 self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
2792 self.push_with_producer(BufferedIteratorProducer(iterator))
2793 self.respond('213 End of status.')
2794 self.fs.close_cr(datacr)
2796 def ftp_FEAT(self, line):
2797 """List all new features supported as defined in RFC-2398."""
2798 features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
2800 for fact in self.available_facts:
2801 if fact in self.current_facts:
2805 features.append('MLST ' + s)
2807 self.push("211-Features supported:\r\n")
2808 self.push("".join([" %s\r\n" %x for x in features]))
2809 self.respond('211 End FEAT.')
2811 def ftp_OPTS(self, line):
2812 """Specify options for FTP commands as specified in RFC-2389."""
2814 assert (not line.count(' ') > 1), 'Invalid number of arguments'
2816 cmd, arg = line.split(' ')
2817 assert (';' in arg), 'Invalid argument'
2820 # actually the only command able to accept options is MLST
2821 assert (cmd.upper() == 'MLST'), 'Unsupported command "%s"' %cmd
2822 except AssertionError, err:
2823 self.respond('501 %s.' %err)
2825 facts = [x.lower() for x in arg.split(';')]
2826 self.current_facts = [x for x in facts if x in self.available_facts]
2827 f = ''.join([x + ';' for x in self.current_facts])
2828 self.respond('200 MLST OPTS ' + f)
2830 def ftp_NOOP(self, line):
2832 self.respond("200 I successfully done nothin'.")
2834 def ftp_SYST(self, line):
2835 """Return system type (always returns UNIX type: L8)."""
2836 # This command is used to find out the type of operating system
2837 # at the server. The reply shall have as its first word one of
2838 # the system names listed in RFC-943.
2839 # Since that we always return a "/bin/ls -lA"-like output on
2840 # LIST we prefer to respond as if we would on Unix in any case.
2841 self.respond("215 UNIX Type: L8")
2843 def ftp_ALLO(self, line):
2844 """Allocate bytes for storage (obsolete)."""
2845 # obsolete (always respond with 202)
2846 self.respond("202 No storage allocation necessary.")
2848 def ftp_HELP(self, line):
2849 """Return help text to the client."""
2851 if line.upper() in proto_cmds:
2852 self.respond("214 %s" %proto_cmds[line.upper()])
2854 self.respond("501 Unrecognized command.")
2856 # provide a compact list of recognized commands
2857 def formatted_help():
2859 keys = proto_cmds.keys()
2862 elems = tuple((keys[0:8]))
2863 cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2865 return ''.join(cmds)
2867 self.push("214-The following commands are recognized:\r\n")
2868 self.push(formatted_help())
2869 self.respond("214 Help command successful.")
2872 # --- support for deprecated cmds
2874 # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
2875 # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
2876 # Such commands are obsoleted but some ftp clients (e.g. Windows
2877 # ftp.exe) still use them.
2879 def ftp_XCUP(self, line):
2880 """Change to the parent directory. Synonym for CDUP. Deprecated."""
2883 def ftp_XCWD(self, line):
2884 """Change the current working directory. Synonym for CWD. Deprecated."""
2887 def ftp_XMKD(self, line):
2888 """Create the specified directory. Synonym for MKD. Deprecated."""
2891 def ftp_XPWD(self, line):
2892 """Return the current working directory. Synonym for PWD. Deprecated."""
2895 def ftp_XRMD(self, line):
2896 """Remove the specified directory. Synonym for RMD. Deprecated."""
2900 class FTPServer(asyncore.dispatcher):
2901 """This class is an asyncore.disptacher subclass. It creates a FTP
2902 socket listening on <address>, dispatching the requests to a <handler>
2903 (typically FTPHandler class).
2905 Depending on the type of address specified IPv4 or IPv6 connections
2906 (or both, depending from the underlying system) will be accepted.
2908 All relevant session information is stored in class attributes
2910 Overriding them is strongly recommended to avoid running out of
2911 file descriptors (DoS)!
2914 number of maximum simultaneous connections accepted (defaults
2917 - (int) max_cons_per_ip:
2918 number of maximum connections accepted for the same IP address
2919 (defaults to 0 == unlimited).
2925 def __init__(self, address, handler):
2926 """Initiate the FTP server opening listening on address.
2928 - (tuple) address: the host:port pair on which the command
2929 channel will listen.
2931 - (classobj) handler: the handler class to use.
2933 asyncore.dispatcher.__init__(self)
2934 self.handler = handler
2936 host, port = address
2938 # AF_INET or AF_INET6 socket
2939 # Get the correct address family for our host (allows IPv6 addresses)
2941 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
2942 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
2943 except socket.gaierror:
2944 # Probably a DNS issue. Assume IPv4.
2945 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2946 self.set_reuse_addr()
2947 self.bind((host, port))
2950 af, socktype, proto, canonname, sa = res
2952 self.create_socket(af, socktype)
2953 self.set_reuse_addr()
2955 except socket.error, msg:
2962 raise socket.error, msg
2965 def set_reuse_addr(self):
2966 # Overridden for convenience. Avoid to reuse address on Windows.
2967 if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'):
2969 asyncore.dispatcher.set_reuse_addr(self)
2971 def serve_forever(self, **kwargs):
2972 """A wrap around asyncore.loop(); starts the asyncore polling
2975 The keyword arguments in kwargs are the same expected by
2976 asyncore.loop() function: timeout, use_poll, map and count.
2978 if not 'count' in kwargs:
2979 log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2981 # backward compatibility for python < 2.4
2982 if not hasattr(self, '_map'):
2983 if not 'map' in kwargs:
2984 map = asyncore.socket_map
2987 self._map = self.handler._map = map
2991 # use_poll specifies whether to use select module's poll()
2992 # with asyncore or whether to use asyncore's own poll()
2993 # method Python versions < 2.4 need use_poll set to False
2994 # This breaks on OS X systems if use_poll is set to True.
2995 # All systems seem to work fine with it set to False
2996 # (tested on Linux, Windows, and OS X platforms)
2998 asyncore.loop(**kwargs)
3000 asyncore.loop(timeout=1.0, use_poll=False)
3001 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3002 log("Shutting down FTPd.")
3005 def handle_accept(self):
3006 """Called when remote client initiates a connection."""
3007 sock_obj, addr = self.accept()
3008 log("[]%s:%s Connected." %addr[:2])
3010 handler = self.handler(sock_obj, self)
3012 self.ip_map.append(ip)
3014 # For performance and security reasons we should always set a
3015 # limit for the number of file descriptors that socket_map
3016 # should contain. When we're running out of such limit we'll
3017 # use the last available channel for sending a 421 response
3018 # to the client before disconnecting it.
3020 if len(self._map) > self.max_cons:
3021 handler.handle_max_cons()
3024 # accept only a limited number of connections from the same
3026 if self.max_cons_per_ip:
3027 if self.ip_map.count(ip) > self.max_cons_per_ip:
3028 handler.handle_max_cons_per_ip()
3036 def handle_error(self):
3037 """Called to handle any uncaught exceptions."""
3040 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3042 logerror(traceback.format_exc())
3045 def close_all(self, map=None, ignore_all=False):
3046 """Stop serving; close all existent connections disconnecting
3050 A dictionary whose items are the channels to close.
3051 If map is omitted, the default asyncore.socket_map is used.
3053 - (bool) ignore_all:
3054 having it set to False results in raising exception in case
3055 of unexpected errors.
3057 Implementation note:
3059 Instead of using the current asyncore.close_all() function
3060 which only close sockets, we iterate over all existent channels
3061 calling close() method for each one of them, avoiding memory
3064 This is how asyncore.close_all() function should work in
3069 for x in map.values():
3073 if x[0] == errno.EBADF:
3075 elif not ignore_all:
3077 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3086 # cmd line usage (provide a read-only anonymous ftp server):
3087 # python -m pyftpdlib.FTPServer
3088 authorizer = DummyAuthorizer()
3089 authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
3090 FTPHandler.authorizer = authorizer
3091 address = ('', 8021)
3092 ftpd = FTPServer(address, FTPHandler)
3093 ftpd.serve_forever()
3095 if __name__ == '__main__':
3098 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: