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 collections import deque
125 from tarfile import filemode
129 __all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer',
130 'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler',
131 'FileProducer', 'IteratorProducer', 'BufferedIteratorProducer',
135 __pname__ = 'Python FTP server library (pyftpdlib)'
137 __date__ = '2008-05-16'
138 __author__ = "Giampaolo Rodola' <g.rodola@gmail.com>"
139 __web__ = 'http://code.google.com/p/pyftpdlib/'
143 'ABOR': 'Syntax: ABOR (abort transfer).',
144 'ALLO': 'Syntax: ALLO <SP> bytes (obsolete; allocate storage).',
145 'APPE': 'Syntax: APPE <SP> file-name (append data to an existent file).',
146 'CDUP': 'Syntax: CDUP (go to parent directory).',
147 'CWD' : 'Syntax: CWD <SP> dir-name (change current working directory).',
148 'DELE': 'Syntax: DELE <SP> file-name (delete file).',
149 'EPRT': 'Syntax: EPRT <SP> |proto|ip|port| (set server in extended active mode).',
150 'EPSV': 'Syntax: EPSV [<SP> proto/"ALL"] (set server in extended passive mode).',
151 'FEAT': 'Syntax: FEAT (list all new features supported).',
152 'HELP': 'Syntax: HELP [<SP> cmd] (show help).',
153 'LIST': 'Syntax: LIST [<SP> path-name] (list files).',
154 'MDTM': 'Syntax: MDTM <SP> file-name (get last modification time).',
155 'MLSD': 'Syntax: MLSD [<SP> dir-name] (list files in a machine-processable form)',
156 'MLST': 'Syntax: MLST [<SP> path-name] (show a path in a machine-processable form)',
157 'MODE': 'Syntax: MODE <SP> mode (obsolete; set data transfer mode).',
158 'MKD' : 'Syntax: MDK <SP> dir-name (create directory).',
159 'NLST': 'Syntax: NLST [<SP> path-name] (list files in a compact form).',
160 'NOOP': 'Syntax: NOOP (just do nothing).',
161 'OPTS': 'Syntax: OPTS <SP> ftp-command [<SP> option] (specify options for FTP commands)',
162 'PASS': 'Syntax: PASS <SP> user-name (set user password).',
163 'PASV': 'Syntax: PASV (set server in passive mode).',
164 'PORT': 'Syntax: PORT <sp> h1,h2,h3,h4,p1,p2 (set server in active mode).',
165 'PWD' : 'Syntax: PWD (get current working directory).',
166 'QUIT': 'Syntax: QUIT (quit current session).',
167 'REIN': 'Syntax: REIN (reinitialize / flush account).',
168 'REST': 'Syntax: REST <SP> marker (restart file position).',
169 'RETR': 'Syntax: RETR <SP> file-name (retrieve a file).',
170 'RMD' : 'Syntax: RMD <SP> dir-name (remove directory).',
171 'RNFR': 'Syntax: RNFR <SP> file-name (file renaming (source name)).',
172 'RNTO': 'Syntax: RNTO <SP> file-name (file renaming (destination name)).',
173 'SIZE': 'Syntax: HELP <SP> file-name (get file size).',
174 'STAT': 'Syntax: STAT [<SP> path name] (status information [list files]).',
175 'STOR': 'Syntax: STOR <SP> file-name (store a file).',
176 'STOU': 'Syntax: STOU [<SP> file-name] (store a file with a unique name).',
177 'STRU': 'Syntax: STRU <SP> type (obsolete; set file structure).',
178 'SYST': 'Syntax: SYST (get operating system type).',
179 'TYPE': 'Syntax: TYPE <SP> [A | I] (set transfer type).',
180 'USER': 'Syntax: USER <SP> user-name (set username).',
181 'XCUP': 'Syntax: XCUP (obsolete; go to parent directory).',
182 'XCWD': 'Syntax: XCWD <SP> dir-name (obsolete; change current directory).',
183 'XMKD': 'Syntax: XMDK <SP> dir-name (obsolete; create directory).',
184 'XPWD': 'Syntax: XPWD (obsolete; get current dir).',
185 'XRMD': 'Syntax: XRMD <SP> dir-name (obsolete; remove directory).',
190 """A wrap around os.strerror() which may be not available on all
191 platforms (e.g. pythonCE).
193 - (instance) err: an EnvironmentError or derived class instance.
195 if hasattr(os, 'strerror'):
196 return os.strerror(err.errno)
202 return s.decode('utf-8')
206 return s.decode('latin')
210 return s.encode('ascii')
216 return s.encode('utf-8')
220 return s.encode('latin')
224 return s.decode('ascii')
228 # --- library defined exceptions
230 class Error(Exception):
231 """Base class for module exceptions."""
233 class AuthorizerError(Error):
234 """Base class for authorizer exceptions."""
240 """Log messages intended for the end user."""
245 """Log commands and responses passing through the command channel."""
250 """Log traceback outputs occurring in case of errors."""
251 sys.stderr.write(str(msg) + '\n')
257 class DummyAuthorizer:
258 """Basic "dummy" authorizer class, suitable for subclassing to
259 create your own custom authorizers.
261 An "authorizer" is a class handling authentications and permissions
262 of the FTP server. It is used inside FTPHandler class for verifying
263 user's password, getting users home directory, checking user
264 permissions when a file read/write event occurs and changing user
265 before accessing the filesystem.
267 DummyAuthorizer is the base authorizer, providing a platform
268 independent interface for managing "virtual" FTP users. System
269 dependent authorizers can by written by subclassing this base
270 class and overriding appropriate methods as necessary.
274 write_perms = "adfmw"
279 def add_user(self, username, password, homedir, perm='elr',
280 msg_login="Login successful.", msg_quit="Goodbye."):
281 """Add a user to the virtual users table.
283 AuthorizerError exceptions raised on error conditions such as
284 invalid permissions, missing home directory or duplicate usernames.
286 Optional perm argument is a string referencing the user's
287 permissions explained below:
290 - "e" = change directory (CWD command)
291 - "l" = list files (LIST, NLST, MLSD commands)
292 - "r" = retrieve file from the server (RETR command)
295 - "a" = append data to an existing file (APPE command)
296 - "d" = delete file or directory (DELE, RMD commands)
297 - "f" = rename file or directory (RNFR, RNTO commands)
298 - "m" = create directory (MKD command)
299 - "w" = store a file to the server (STOR, STOU commands)
301 Optional msg_login and msg_quit arguments can be specified to
302 provide customized response strings when user log-in and quit.
304 if self.has_user(username):
305 raise AuthorizerError('User "%s" already exists' %username)
306 homedir = os.path.realpath(homedir)
307 if not os.path.isdir(homedir):
308 raise AuthorizerError('No such directory: "%s"' %homedir)
310 if p not in 'elradfmw':
311 raise AuthorizerError('No such permission "%s"' %p)
313 if (p in self.write_perms) and (username == 'anonymous'):
314 warnings.warn("write permissions assigned to anonymous user.",
317 dic = {'pwd': str(password),
320 'msg_login': str(msg_login),
321 'msg_quit': str(msg_quit)
323 self.user_table[username] = dic
325 def add_anonymous(self, homedir, **kwargs):
326 """Add an anonymous user to the virtual users table.
328 AuthorizerError exception raised on error conditions such as
329 invalid permissions, missing home directory, or duplicate
332 The keyword arguments in kwargs are the same expected by
333 add_user method: "perm", "msg_login" and "msg_quit".
335 The optional "perm" keyword argument is a string defaulting to
336 "elr" referencing "read-only" anonymous user's permissions.
338 Using write permission values ("adfmw") results in a
341 DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
343 def remove_user(self, username):
344 """Remove a user from the virtual users table."""
345 del self.user_table[username]
347 def validate_authentication(self, username, password):
348 """Return True if the supplied username and password match the
349 stored credentials."""
350 return self.user_table[username]['pwd'] == password
352 def impersonate_user(self, username, password):
353 """Impersonate another user (noop).
355 It is always called before accessing the filesystem.
356 By default it does nothing. The subclass overriding this
357 method is expected to provide a mechanism to change the
361 def terminate_impersonation(self):
362 """Terminate impersonation (noop).
364 It is always called after having accessed the filesystem.
365 By default it does nothing. The subclass overriding this
366 method is expected to provide a mechanism to switch back
367 to the original user.
370 def has_user(self, username):
371 """Whether the username exists in the virtual users table."""
372 return username in self.user_table
374 def has_perm(self, username, perm, path=None):
375 """Whether the user has permission over path (an absolute
376 pathname of a file or a directory).
378 Expected perm argument is one of the following letters:
381 return perm in self.user_table[username]['perm']
383 def get_perms(self, username):
384 """Return current user permissions."""
385 return self.user_table[username]['perm']
387 def get_home_dir(self, username):
388 """Return the user's home directory."""
389 return self.user_table[username]['home']
391 def get_msg_login(self, username):
392 """Return the user's login message."""
393 return self.user_table[username]['msg_login']
395 def get_msg_quit(self, username):
396 """Return the user's quitting message."""
397 return self.user_table[username]['msg_quit']
402 class PassiveDTP(asyncore.dispatcher):
403 """This class is an asyncore.disptacher subclass. It creates a
404 socket listening on a local port, dispatching the resultant
405 connection to DTPHandler.
408 def __init__(self, cmd_channel, extmode=False):
409 """Initialize the passive data server.
411 - (instance) cmd_channel: the command channel class instance.
412 - (bool) extmode: wheter use extended passive mode response type.
414 asyncore.dispatcher.__init__(self)
415 self.cmd_channel = cmd_channel
417 ip = self.cmd_channel.getsockname()[0]
418 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
420 if not self.cmd_channel.passive_ports:
421 # By using 0 as port number value we let kernel choose a free
422 # unprivileged random port.
425 ports = list(self.cmd_channel.passive_ports)
427 port = ports.pop(random.randint(0, len(ports) -1))
429 self.bind((ip, port))
430 except socket.error, why:
431 if why[0] == errno.EADDRINUSE: # port already in use
434 # If cannot use one of the ports in the configured
435 # range we'll use a kernel-assigned port, and log
436 # a message reporting the issue.
437 # By using 0 as port number value we let kernel
438 # choose a free unprivileged random port.
441 self.cmd_channel.log(
442 "Can't find a valid passive port in the "
443 "configured range. A random kernel-assigned "
451 port = self.socket.getsockname()[1]
453 if self.cmd_channel.masquerade_address:
454 ip = self.cmd_channel.masquerade_address
455 # The format of 227 response in not standardized.
456 # This is the most expected:
457 self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' %(
458 ip.replace('.', ','), port / 256, port % 256))
460 self.cmd_channel.respond('229 Entering extended passive mode '
463 # --- connection / overridden
465 def handle_accept(self):
466 """Called when remote client initiates a connection."""
467 sock, addr = self.accept()
469 # Check the origin of data connection. If not expressively
470 # configured we drop the incoming data connection if remote
471 # IP address does not match the client's IP address.
472 if (self.cmd_channel.remote_ip != addr[0]):
473 if not self.cmd_channel.permit_foreign_addresses:
478 msg = 'Rejected data connection from foreign address %s:%s.' \
480 self.cmd_channel.respond("425 %s" %msg)
481 self.cmd_channel.log(msg)
482 # do not close listening socket: it couldn't be client's blame
485 # site-to-site FTP allowed
486 msg = 'Established data connection with foreign address %s:%s.'\
488 self.cmd_channel.log(msg)
489 # Immediately close the current channel (we accept only one
490 # connection at time) and avoid running out of max connections
493 # delegate such connection to DTP handler
494 handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
495 self.cmd_channel.data_channel = handler
496 self.cmd_channel.on_dtp_connection()
501 def handle_error(self):
502 """Called to handle any uncaught exceptions."""
505 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
507 logerror(traceback.format_exc())
510 def handle_close(self):
511 """Called on closing the data connection."""
515 class ActiveDTP(asyncore.dispatcher):
516 """This class is an asyncore.disptacher subclass. It creates a
517 socket resulting from the connection to a remote user-port,
518 dispatching it to DTPHandler.
521 def __init__(self, ip, port, cmd_channel):
522 """Initialize the active data channel attemping to connect
523 to remote data socket.
525 - (str) ip: the remote IP address.
526 - (int) port: the remote port.
527 - (instance) cmd_channel: the command channel class instance.
529 asyncore.dispatcher.__init__(self)
530 self.cmd_channel = cmd_channel
531 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
533 self.connect((ip, port))
534 except socket.gaierror:
535 self.cmd_channel.respond("425 Can't connect to specified address.")
538 # --- connection / overridden
540 def handle_write(self):
541 """NOOP, must be overridden to prevent unhandled write event."""
543 def handle_connect(self):
544 """Called when connection is established."""
545 self.cmd_channel.respond('200 Active data connection established.')
546 # delegate such connection to DTP handler
547 handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
548 self.cmd_channel.data_channel = handler
549 self.cmd_channel.on_dtp_connection()
550 #self.close() # <-- (done automatically)
552 def handle_expt(self):
553 self.cmd_channel.respond("425 Can't connect to specified address.")
556 def handle_error(self):
557 """Called to handle any uncaught exceptions."""
560 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
565 logerror(traceback.format_exc())
566 self.cmd_channel.respond("425 Can't connect to specified address.")
569 class DTPHandler(asyncore.dispatcher):
570 """Class handling server-data-transfer-process (server-DTP, see
571 RFC-959) managing data-transfer operations involving sending
574 Instance attributes defined in this class, initialized when
577 - (instance) cmd_channel: the command channel class instance.
578 - (file) file_obj: the file transferred (if any).
579 - (bool) receive: True if channel is used for receiving data.
580 - (bool) transfer_finished: True if transfer completed successfully.
581 - (int) tot_bytes_sent: the total bytes sent.
582 - (int) tot_bytes_received: the total bytes received.
584 DTPHandler implementation note:
586 When a producer is consumed and close_when_done() has been called
587 previously, refill_buffer() erroneously calls close() instead of
588 handle_close() - (see: http://bugs.python.org/issue1740572)
590 To avoid this problem DTPHandler is implemented as a subclass of
591 asyncore.dispatcher instead of asynchat.async_chat.
592 This implementation follows the same approach that asynchat module
593 should use in Python 2.6.
595 The most important change in the implementation is related to
596 producer_fifo, which is a pure deque object instead of a
597 producer_fifo instance.
599 Since we don't want to break backward compatibily with older python
600 versions (deque has been introduced in Python 2.4), if deque is not
601 available we use a list instead.
604 ac_in_buffer_size = 8192
605 ac_out_buffer_size = 8192
607 def __init__(self, sock_obj, cmd_channel):
608 """Initialize the command channel.
610 - (instance) sock_obj: the socket object instance of the newly
611 established connection.
612 - (instance) cmd_channel: the command channel class instance.
614 asyncore.dispatcher.__init__(self, sock_obj)
615 # we toss the use of the asynchat's "simple producer" and
616 # replace it with a pure deque, which the original fifo
618 self.producer_fifo = deque()
620 self.cmd_channel = cmd_channel
623 self.transfer_finished = False
624 self.tot_bytes_sent = 0
625 self.tot_bytes_received = 0
626 self.data_wrapper = lambda x: x
628 # --- utility methods
630 def enable_receiving(self, type):
631 """Enable receiving of data over the channel. Depending on the
632 TYPE currently in use it creates an appropriate wrapper for the
635 - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
638 self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
640 self.data_wrapper = lambda x: x
642 raise TypeError, "Unsupported type"
645 def get_transmitted_bytes(self):
646 "Return the number of transmitted bytes."
647 return self.tot_bytes_sent + self.tot_bytes_received
649 def transfer_in_progress(self):
650 "Return True if a transfer is in progress, else False."
651 return self.get_transmitted_bytes() != 0
655 def handle_read(self):
656 """Called when there is data waiting to be read."""
658 chunk = self.recv(self.ac_in_buffer_size)
662 self.tot_bytes_received += len(chunk)
664 self.transfer_finished = True
665 #self.close() # <-- asyncore.recv() already do that...
667 # while we're writing on the file an exception could occur
668 # in case that filesystem gets full; if this happens we
669 # let handle_error() method handle this exception, providing
670 # a detailed error message.
671 self.file_obj.write(self.data_wrapper(chunk))
673 def handle_write(self):
674 """Called when data is ready to be written, initiates send."""
677 def push(self, data):
678 """Push data onto the deque and initiate send."""
679 sabs = self.ac_out_buffer_size
681 for i in xrange(0, len(data), sabs):
682 self.producer_fifo.append(data[i:i+sabs])
684 self.producer_fifo.append(data)
687 def push_with_producer(self, producer):
688 """Push data using a producer and initiate send."""
689 self.producer_fifo.append(producer)
693 """Predicate for inclusion in the readable for select()."""
697 """Predicate for inclusion in the writable for select()."""
698 return self.producer_fifo or (not self.connected)
700 def close_when_done(self):
701 """Automatically close this channel once the outgoing queue is empty."""
702 self.producer_fifo.append(None)
704 def initiate_send(self):
705 """Attempt to send data in fifo order."""
706 while self.producer_fifo and self.connected:
707 first = self.producer_fifo[0]
708 # handle empty string/buffer or None entry
710 del self.producer_fifo[0]
712 self.transfer_finished = True
716 # handle classic producer behavior
717 obs = self.ac_out_buffer_size
719 data = buffer(first, 0, obs)
723 self.producer_fifo.appendleft(data)
725 del self.producer_fifo[0]
730 num_sent = self.send(data)
736 self.tot_bytes_sent += num_sent
737 if num_sent < len(data) or obs < len(first):
738 self.producer_fifo[0] = first[num_sent:]
740 del self.producer_fifo[0]
741 # we tried to send some actual data
744 def handle_expt(self):
745 """Called on "exceptional" data events."""
746 self.cmd_channel.respond("426 Connection error; transfer aborted.")
749 def handle_error(self):
750 """Called when an exception is raised and not otherwise handled."""
753 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
755 except socket.error, err:
756 # fix around asyncore bug (http://bugs.python.org/issue1736101)
757 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
763 # an error could occur in case we fail reading / writing
764 # from / to file (e.g. file system gets full)
765 except EnvironmentError, err:
766 error = _strerror(err)
768 # some other exception occurred; we don't want to provide
769 # confidential error messages
770 logerror(traceback.format_exc())
771 error = "Internal error"
772 self.cmd_channel.respond("426 %s; transfer aborted." %error)
775 def handle_close(self):
776 """Called when the socket is closed."""
777 # If we used channel for receiving we assume that transfer is
778 # finished when client close connection , if we used channel
779 # for sending we have to check that all data has been sent
780 # (responding with 226) or not (responding with 426).
782 self.transfer_finished = True
786 if self.transfer_finished:
787 self.cmd_channel.respond("226 Transfer complete.")
789 fname = self.file_obj.name
790 self.cmd_channel.log('"%s" %s.' %(fname, action))
792 tot_bytes = self.get_transmitted_bytes()
793 msg = "Transfer aborted; %d bytes transmitted." %tot_bytes
794 self.cmd_channel.respond("426 " + msg)
795 self.cmd_channel.log(msg)
799 """Close the data channel, first attempting to close any remaining
801 if self.file_obj and not self.file_obj.closed:
802 self.file_obj.close()
803 asyncore.dispatcher.close(self)
804 self.cmd_channel.on_dtp_close()
810 """Producer wrapper for file[-like] objects."""
814 def __init__(self, file, type):
815 """Initialize the producer with a data_wrapper appropriate to TYPE.
817 - (file) file: the file[-like] object.
818 - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
823 self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
825 self.data_wrapper = lambda x: x
827 raise TypeError, "Unsupported type"
830 """Attempt a chunk of data of size self.buffer_size."""
833 data = self.data_wrapper(self.file.read(self.buffer_size))
836 if not self.file.closed:
841 class IteratorProducer:
842 """Producer for iterator objects."""
844 def __init__(self, iterator):
845 self.iterator = iterator
848 """Attempt a chunk of data from iterator by calling its next()
852 return self.iterator.next()
853 except StopIteration:
857 class BufferedIteratorProducer:
858 """Producer for iterator objects with buffer capabilities."""
859 # how many times iterator.next() will be called before
860 # returning some data
863 def __init__(self, iterator):
864 self.iterator = iterator
867 """Attempt a chunk of data from iterator by calling
868 its next() method different times.
871 for x in xrange(self.loops):
873 buffer.append(self.iterator.next())
874 except StopIteration:
876 return ''.join(buffer)
882 """A class used to interact with the file system, providing a high
883 level, cross-platform interface compatible with both Windows and
884 UNIX style filesystems.
886 It provides some utility methods and some wraps around operations
887 involved in file creation and file system operations like moving
888 files or removing directories.
891 - (str) root: the user home directory.
892 - (str) cwd: the current working directory.
893 - (str) rnfr: source file to be renamed.
901 # --- Pathname / conversion utilities
903 def ftpnorm(self, ftppath):
904 """Normalize a "virtual" ftp pathname (tipically the raw string
905 coming from client) depending on the current working directory.
907 Example (having "/foo" as current working directory):
910 Note: directory separators are system independent ("/").
911 Pathname returned is always absolutized.
913 if os.path.isabs(ftppath):
914 p = os.path.normpath(ftppath)
916 p = os.path.normpath(os.path.join(self.cwd, ftppath))
917 # normalize string in a standard web-path notation having '/'
919 p = p.replace("\\", "/")
920 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
921 # don't need them. In case we get an UNC path we collapse
922 # redundant separators appearing at the beginning of the string
925 # Anti path traversal: don't trust user input, in the event
926 # that self.cwd is not absolute, return "/" as a safety measure.
927 # This is for extra protection, maybe not really necessary.
928 if not os.path.isabs(p):
932 def ftp2fs(self, ftppath):
933 """Translate a "virtual" ftp pathname (tipically the raw string
934 coming from client) into equivalent absolute "real" filesystem
937 Example (having "/home/user" as root directory):
938 'x' -> '/home/user/x'
940 Note: directory separators are system dependent.
942 # as far as I know, it should always be path traversal safe...
943 if os.path.normpath(self.root) == os.sep:
944 return os.path.normpath(self.ftpnorm(ftppath))
946 p = self.ftpnorm(ftppath)[1:]
947 return os.path.normpath(os.path.join(self.root, p))
949 def fs2ftp(self, fspath):
950 """Translate a "real" filesystem pathname into equivalent
951 absolute "virtual" ftp pathname depending on the user's
954 Example (having "/home/user" as root directory):
955 '/home/user/x' -> '/x'
957 As for ftpnorm, directory separators are system independent
958 ("/") and pathname returned is always absolutized.
960 On invalid pathnames escaping from user's root directory
961 (e.g. "/home" when root is "/home/user") always return "/".
963 if os.path.isabs(fspath):
964 p = os.path.normpath(fspath)
966 p = os.path.normpath(os.path.join(self.root, fspath))
967 if not self.validpath(p):
969 p = p.replace(os.sep, "/")
970 p = p[len(self.root):]
971 if not p.startswith('/'):
975 # alias for backward compatibility with 0.2.0
979 def validpath(self, path):
980 """Check whether the path belongs to user's home directory.
981 Expected argument is a "real" filesystem pathname.
983 If path is a symbolic link it is resolved to check its real
986 Pathnames escaping from user's root directory are considered
989 root = self.realpath(self.root)
990 path = self.realpath(path)
991 if not self.root.endswith(os.sep):
992 root = self.root + os.sep
993 if not path.endswith(os.sep):
995 if path[0:len(root)] == root:
999 # --- Wrapper methods around open() and tempfile.mkstemp
1001 def open(self, filename, mode):
1002 """Open a file returning its handler."""
1003 return open(filename, mode)
1005 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
1006 """A wrap around tempfile.mkstemp creating a file with a unique
1007 name. Unlike mkstemp it returns an object with a file-like
1011 def __init__(self, fd, name):
1014 def __getattr__(self, attr):
1015 return getattr(self.file, attr)
1017 text = not 'b' in mode
1018 # max number of tries to find out a unique file name
1019 tempfile.TMP_MAX = 50
1020 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
1021 file = os.fdopen(fd, mode)
1022 return FileWrapper(file, name)
1024 # --- Wrapper methods around os.*
1026 def chdir(self, path):
1027 """Change the current directory."""
1028 # temporarily join the specified directory to see if we have
1029 # permissions to do so
1030 basedir = os.getcwd()
1037 self.cwd = self.fs2ftp(path)
1039 def mkdir(self, path, basename):
1040 """Create the specified directory."""
1041 os.mkdir(os.path.join(path, basename))
1043 def listdir(self, path):
1044 """List the content of a directory."""
1045 return os.listdir(path)
1047 def rmdir(self, path):
1048 """Remove the specified directory."""
1051 def remove(self, path):
1052 """Remove the specified file."""
1055 def rename(self, src, dst):
1056 """Rename the specified src file to the dst filename."""
1059 def stat(self, path):
1060 """Perform a stat() system call on the given path."""
1061 return os.stat(path)
1063 def lstat(self, path):
1064 """Like stat but does not follow symbolic links."""
1065 return os.lstat(path)
1067 if not hasattr(os, 'lstat'):
1070 # --- Wrapper methods around os.path.*
1072 def isfile(self, path):
1073 """Return True if path is a file."""
1074 return os.path.isfile(path)
1076 def islink(self, path):
1077 """Return True if path is a symbolic link."""
1078 return os.path.islink(path)
1080 def isdir(self, path):
1081 """Return True if path is a directory."""
1082 return os.path.isdir(path)
1084 def getsize(self, path):
1085 """Return the size of the specified file in bytes."""
1086 return os.path.getsize(path)
1088 def getmtime(self, path):
1089 """Return the last modified time as a number of seconds since
1091 return os.path.getmtime(path)
1093 def realpath(self, path):
1094 """Return the canonical version of path eliminating any
1095 symbolic links encountered in the path (if they are
1096 supported by the operating system).
1098 return os.path.realpath(path)
1100 def lexists(self, path):
1101 """Return True if path refers to an existing path, including
1102 a broken or circular symbolic link.
1104 if hasattr(os.path, 'lexists'):
1105 return os.path.lexists(path)
1106 # grant backward compatibility with python 2.3
1107 elif hasattr(os, 'lstat'):
1115 return os.path.exists(path)
1117 exists = lexists # alias for backward compatibility with 0.2.0
1119 def glob1(self, dirname, pattern):
1120 """Return a list of files matching a dirname pattern
1123 Unlike glob.glob1 raises exception if os.listdir() fails.
1125 names = self.listdir(dirname)
1126 if pattern[0] != '.':
1127 names = filter(lambda x: x[0] != '.', names)
1128 return fnmatch.filter(names, pattern)
1130 # --- Listing utilities
1132 # note: the following operations are no more blocking
1134 def get_list_dir(self, datacr):
1135 """"Return an iterator object that yields a directory listing
1136 in a form suitable for LIST command.
1138 raise DeprecationWarning()
1140 def get_stat_dir(self, rawline):
1141 """Return an iterator object that yields a list of files
1142 matching a dirname pattern non-recursively in a form
1143 suitable for STAT command.
1145 - (str) rawline: the raw string passed by client as command
1148 ftppath = self.ftpnorm(rawline)
1149 if not glob.has_magic(ftppath):
1150 return self.get_list_dir(self.ftp2fs(rawline))
1152 basedir, basename = os.path.split(ftppath)
1153 if glob.has_magic(basedir):
1154 return iter(['Directory recursion not supported.\r\n'])
1156 basedir = self.ftp2fs(basedir)
1157 listing = self.glob1(basedir, basename)
1160 return self.format_list(basedir, listing)
1162 def format_list(self, basedir, listing, ignore_err=True):
1163 """Return an iterator object that yields the entries of given
1164 directory emulating the "/bin/ls -lA" UNIX command output.
1166 - (str) basedir: the absolute dirname.
1167 - (list) listing: the names of the entries in basedir
1168 - (bool) ignore_err: when False raise exception if os.lstat()
1171 On platforms which do not support the pwd and grp modules (such
1172 as Windows), ownership is printed as "owner" and "group" as a
1173 default, and number of hard links is always "1". On UNIX
1174 systems, the actual owner, group, and number of links are
1177 This is how output appears to client:
1179 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
1180 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
1181 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
1183 for basename in listing:
1184 file = os.path.join(basedir, basename)
1186 st = self.lstat(file)
1191 perms = filemode(st.st_mode) # permissions
1192 nlinks = st.st_nlink # number of links to inode
1193 if not nlinks: # non-posix system, let's use a bogus value
1195 size = st.st_size # file size
1196 uname = st.st_uid or "owner"
1197 gname = st.st_gid or "group"
1199 # stat.st_mtime could fail (-1) if last mtime is too old
1200 # in which case we return the local time as last mtime
1202 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1204 mtime = time.strftime("%b %d %H:%M")
1205 # if the file is a symlink, resolve it, e.g. "symlink -> realfile"
1206 if stat.S_ISLNK(st.st_mode):
1207 basename = basename + " -> " + os.readlink(file)
1209 # formatting is matched with proftpd ls output
1210 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
1211 size, mtime, basename)
1213 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
1214 """Return an iterator object that yields the entries of a given
1215 directory or of a single file in a form suitable with MLSD and
1218 Every entry includes a list of "facts" referring the listed
1219 element. See RFC-3659, chapter 7, to see what every single
1222 - (str) basedir: the absolute dirname.
1223 - (list) listing: the names of the entries in basedir
1224 - (str) perms: the string referencing the user permissions.
1225 - (str) facts: the list of "facts" to be returned.
1226 - (bool) ignore_err: when False raise exception if os.stat()
1229 Note that "facts" returned may change depending on the platform
1230 and on what user specified by using the OPTS command.
1232 This is how output could appear to the client issuing
1235 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
1236 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
1237 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
1239 permdir = ''.join([x for x in perms if x not in 'arw'])
1240 permfile = ''.join([x for x in perms if x not in 'celmp'])
1241 if ('w' in perms) or ('a' in perms) or ('f' in perms):
1245 type = size = perm = modify = create = unique = mode = uid = gid = ""
1246 for basename in listing:
1247 file = os.path.join(basedir, basename)
1249 st = self.stat(file)
1255 if stat.S_ISDIR(st.st_mode):
1259 elif basename == '..':
1264 perm = 'perm=%s;' %permdir
1269 perm = 'perm=%s;' %permfile
1271 size = 'size=%s;' %st.st_size # file size
1272 # last modification time
1273 if 'modify' in facts:
1275 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1276 time.localtime(st.st_mtime))
1278 # stat.st_mtime could fail (-1) if last mtime is too old
1280 if 'create' in facts:
1281 # on Windows we can provide also the creation time
1283 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1284 time.localtime(st.st_ctime))
1288 if 'unix.mode' in facts:
1289 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
1290 if 'unix.uid' in facts:
1291 uid = 'unix.uid=%s;' %st.st_uid
1292 if 'unix.gid' in facts:
1293 gid = 'unix.gid=%s;' %st.st_gid
1294 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
1295 # posix platforms only; we get it by mixing st_dev and
1296 # st_ino values which should be enough for granting an
1297 # uniqueness for the file listed.
1298 # The same approach is used by pure-ftpd.
1299 # Implementors who want to provide unique fact on other
1300 # platforms should use some platform-specific method (e.g.
1301 # on Windows NTFS filesystems MTF records could be used).
1302 if 'unique' in facts:
1303 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
1305 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
1306 mode, uid, gid, unique, basename)
1311 class FTPExceptionSent(Exception):
1312 """An FTP exception that FTPHandler has processed
1316 class FTPHandler(asynchat.async_chat):
1317 """Implements the FTP server Protocol Interpreter (see RFC-959),
1318 handling commands received from the client on the control channel.
1320 All relevant session information is stored in class attributes
1321 reproduced below and can be modified before instantiating this
1324 - (str) banner: the string sent when client connects.
1326 - (int) max_login_attempts:
1327 the maximum number of wrong authentications before disconnecting
1328 the client (default 3).
1330 - (bool)permit_foreign_addresses:
1331 FTP site-to-site transfer feature: also referenced as "FXP" it
1332 permits for transferring a file between two remote FTP servers
1333 without the transfer going through the client's host (not
1334 recommended for security reasons as described in RFC-2577).
1335 Having this attribute set to False means that all data
1336 connections from/to remote IP addresses which do not match the
1337 client's IP address will be dropped (defualt False).
1339 - (bool) permit_privileged_ports:
1340 set to True if you want to permit active data connections (PORT)
1341 over privileged ports (not recommended, defaulting to False).
1343 - (str) masquerade_address:
1344 the "masqueraded" IP address to provide along PASV reply when
1345 pyftpdlib is running behind a NAT or other types of gateways.
1346 When configured pyftpdlib will hide its local address and
1347 instead use the public address of your NAT (default None).
1349 - (list) passive_ports:
1350 what ports ftpd will use for its passive data transfers.
1351 Value expected is a list of integers (e.g. range(60000, 65535)).
1352 When configured pyftpdlib will no longer use kernel-assigned
1353 random ports (default None).
1356 All relevant instance attributes initialized when client connects
1357 are reproduced below. You may be interested in them in case you
1358 want to subclass the original FTPHandler.
1360 - (bool) authenticated: True if client authenticated himself.
1361 - (str) username: the name of the connected user (if any).
1362 - (int) attempted_logins: number of currently attempted logins.
1363 - (str) current_type: the current transfer type (default "a")
1364 - (int) af: the address family (IPv4/IPv6)
1365 - (instance) server: the FTPServer class instance.
1366 - (instance) data_server: the data server instance (if any).
1367 - (instance) data_channel: the data channel instance (if any).
1369 # these are overridable defaults
1372 authorizer = DummyAuthorizer()
1373 active_dtp = ActiveDTP
1374 passive_dtp = PassiveDTP
1375 dtp_handler = DTPHandler
1376 abstracted_fs = AbstractedFS
1378 # session attributes (explained in the docstring)
1379 banner = "pyftpdlib %s ready." %__ver__
1380 max_login_attempts = 3
1381 permit_foreign_addresses = False
1382 permit_privileged_ports = False
1383 masquerade_address = None
1384 passive_ports = None
1386 def __init__(self, conn, server):
1387 """Initialize the command channel.
1389 - (instance) conn: the socket object instance of the newly
1390 established connection.
1391 - (instance) server: the ftp server class instance.
1394 asynchat.async_chat.__init__(self, conn=conn) # python2.5
1396 asynchat.async_chat.__init__(self, sock=conn) # python2.6
1397 self.server = server
1398 self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1400 self.in_buffer_len = 0
1401 self.set_terminator("\r\n")
1403 # session attributes
1404 self.fs = self.abstracted_fs()
1405 self.authenticated = False
1408 self.attempted_logins = 0
1409 self.current_type = 'a'
1410 self.restart_position = 0
1411 self.quit_pending = False
1412 self._epsvall = False
1413 self.__in_dtp_queue = None
1414 self.__out_dtp_queue = None
1416 self.__errno_responses = {
1424 # mlsx facts attributes
1425 self.current_facts = ['type', 'perm', 'size', 'modify']
1426 self.current_facts.append('unique')
1427 self.available_facts = self.current_facts[:]
1428 self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1429 self.available_facts.append('create')
1432 self.data_server = None
1433 self.data_channel = None
1435 if hasattr(self.socket, 'family'):
1436 self.af = self.socket.family
1437 else: # python < 2.5
1438 ip, port = self.socket.getsockname()[:2]
1439 self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
1440 socket.SOCK_STREAM)[0][0]
1443 """Return a 220 'Ready' response to the client over the command
1446 if len(self.banner) <= 75:
1447 self.respond("220 %s" %str(self.banner))
1449 self.push('220-%s\r\n' %str(self.banner))
1450 self.respond('220 ')
1452 def handle_max_cons(self):
1453 """Called when limit for maximum number of connections is reached."""
1454 msg = "Too many connections. Service temporary unavailable."
1455 self.respond("421 %s" %msg)
1457 # If self.push is used, data could not be sent immediately in
1458 # which case a new "loop" will occur exposing us to the risk of
1459 # accepting new connections. Since this could cause asyncore to
1460 # run out of fds (...and exposes the server to DoS attacks), we
1461 # immediately close the channel by using close() instead of
1462 # close_when_done(). If data has not been sent yet client will
1463 # be silently disconnected.
1466 def handle_max_cons_per_ip(self):
1467 """Called when too many clients are connected from the same IP."""
1468 msg = "Too many connections from the same IP address."
1469 self.respond("421 %s" %msg)
1471 self.close_when_done()
1473 # --- asyncore / asynchat overridden methods
1476 # if there's a quit pending we stop reading data from socket
1477 return not self.quit_pending
1479 def collect_incoming_data(self, data):
1480 """Read incoming data and append to the input buffer."""
1481 self.in_buffer.append(data)
1482 self.in_buffer_len += len(data)
1483 # Flush buffer if it gets too long (possible DoS attacks).
1484 # RFC-959 specifies that a 500 response could be given in
1487 if self.in_buffer_len > buflimit:
1488 self.respond('500 Command too long.')
1489 self.log('Command received exceeded buffer limit of %s.' %(buflimit))
1491 self.in_buffer_len = 0
1493 # commands accepted before authentication
1494 unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1496 # commands needing an argument
1497 arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
1498 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
1499 'TYPE','USER','XMKD','XRMD')
1501 # commands needing no argument
1502 unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1503 'SYST','XCUP','XPWD')
1505 def found_terminator(self):
1506 r"""Called when the incoming data stream matches the \r\n
1509 Depending on the command received it calls the command's
1510 corresponding method (e.g. for received command "MKD pathname",
1511 ftp_MKD() method is called with "pathname" as the argument).
1513 line = ''.join(self.in_buffer)
1515 self.in_buffer_len = 0
1517 cmd = line.split(' ')[0].upper()
1518 space = line.find(' ')
1520 arg = line[space + 1:]
1525 self.logline("<== %s" %line)
1527 self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1529 # let's check if user provided an argument for those commands
1531 if not arg and cmd in self.arg_cmds:
1532 self.respond("501 Syntax error: command needs an argument.")
1535 # let's do the same for those commands requiring no argument.
1536 elif arg and cmd in self.unarg_cmds:
1537 self.respond("501 Syntax error: command does not accept arguments.")
1540 # provide a limited set of commands if user isn't
1542 if (not self.authenticated):
1543 if cmd in self.unauth_cmds:
1544 # we permit STAT during this phase but we don't want
1545 # STAT to return a directory LISTing if the user is
1546 # not authenticated yet (this could happen if STAT
1547 # is used with an argument)
1548 if (cmd == 'STAT') and arg:
1549 self.respond("530 Log in with USER and PASS first.")
1551 method = getattr(self, 'ftp_' + cmd)
1552 method(arg) # call the proper ftp_* method
1553 elif cmd in proto_cmds:
1554 self.respond("530 Log in with USER and PASS first.")
1556 self.respond('500 Command "%s" not understood.' %line)
1558 # provide full command set
1559 elif (self.authenticated) and (cmd in proto_cmds):
1560 if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
1562 method = getattr(self, 'ftp_' + cmd)
1563 method(arg) # call the proper ftp_* method
1566 # recognize those commands having "special semantics"
1573 self.respond('500 Command "%s" not understood.' %line)
1575 def __check_path(self, cmd, line):
1576 """Check whether a path is valid."""
1578 # Always true, we will only check later, once we have a cursor
1581 def __check_perm(self, cmd, line, datacr):
1582 """Check permissions depending on issued command."""
1583 map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
1584 'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
1587 'DELE':'d', 'RMD':'d', 'XRMD':'d',
1589 'MKD':'m', 'XMKD':'m',
1591 raise NotImplementedError
1593 if cmd == 'STAT' and not line:
1596 if not line and (cmd in ('LIST','NLST','MLSD')):
1597 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1599 path = self.fs.ftp2fs(line, datacr)
1600 if not self.authorizer.has_perm(self.username, perm, path):
1601 self.log('FAIL %s "%s". Not enough privileges.' \
1602 %(cmd, self.fs.ftpnorm(line)))
1603 self.respond("550 Can't %s. Not enough privileges." %cmd)
1607 def handle_expt(self):
1608 """Called when there is out of band (OOB) data for the socket
1609 connection. This could happen in case of such commands needing
1610 "special action" (typically STAT and ABOR) in which case we
1611 append OOB data to incoming buffer.
1613 if hasattr(socket, 'MSG_OOB'):
1615 data = self.socket.recv(1024, socket.MSG_OOB)
1616 except socket.error:
1619 self.in_buffer.append(data)
1621 self.log("Can't handle OOB data.")
1624 def handle_error(self):
1627 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1629 except socket.error, err:
1630 # fix around asyncore bug (http://bugs.python.org/issue1736101)
1631 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
1632 errno.ECONNABORTED):
1636 logerror(traceback.format_exc())
1638 logerror(traceback.format_exc())
1641 def handle_close(self):
1646 """Close the current channel disconnecting the client."""
1647 if not self._closed:
1649 if self.data_server:
1650 self.data_server.close()
1651 del self.data_server
1653 if self.data_channel:
1654 self.data_channel.close()
1655 del self.data_channel
1657 del self.__out_dtp_queue
1658 del self.__in_dtp_queue
1660 # remove client IP address from ip map
1661 self.server.ip_map.remove(self.remote_ip)
1662 asynchat.async_chat.close(self)
1663 self.log("Disconnected.")
1667 def on_dtp_connection(self):
1668 """Called every time data channel connects (either active or
1671 Incoming and outgoing queues are checked for pending data.
1672 If outbound data is pending, it is pushed into the data channel.
1673 If awaiting inbound data, the data channel is enabled for
1676 if self.data_server:
1677 self.data_server.close()
1678 self.data_server = None
1680 # check for data to send
1681 if self.__out_dtp_queue:
1682 data, isproducer, file = self.__out_dtp_queue
1684 self.data_channel.file_obj = file
1686 self.data_channel.push(data)
1688 self.data_channel.push_with_producer(data)
1689 if self.data_channel:
1690 self.data_channel.close_when_done()
1691 self.__out_dtp_queue = None
1693 # check for data to receive
1694 elif self.__in_dtp_queue:
1695 self.data_channel.file_obj = self.__in_dtp_queue
1696 self.data_channel.enable_receiving(self.current_type)
1697 self.__in_dtp_queue = None
1699 def on_dtp_close(self):
1700 """Called every time the data channel is closed."""
1701 self.data_channel = None
1702 if self.quit_pending:
1703 self.close_when_done()
1707 def respond(self, resp):
1708 """Send a response to the client using the command channel."""
1709 self.push(resp + '\r\n')
1710 self.logline('==> %s' % resp)
1712 def push_dtp_data(self, data, isproducer=False, file=None):
1713 """Pushes data into the data channel.
1715 It is usually called for those commands requiring some data to
1716 be sent over the data channel (e.g. RETR).
1717 If data channel does not exist yet, it queues the data to send
1718 later; data will then be pushed into data channel when
1719 on_dtp_connection() will be called.
1721 - (str/classobj) data: the data to send which may be a string
1722 or a producer object).
1723 - (bool) isproducer: whether treat data as a producer.
1724 - (file) file: the file[-like] object to send (if any).
1726 if self.data_channel:
1727 self.respond("125 Data connection already open. Transfer starting.")
1729 self.data_channel.file_obj = file
1731 self.data_channel.push(data)
1733 self.data_channel.push_with_producer(data)
1734 if self.data_channel:
1735 self.data_channel.close_when_done()
1737 self.respond("150 File status okay. About to open data connection.")
1738 self.__out_dtp_queue = (data, isproducer, file)
1741 """Log a message, including additional identifying session data."""
1742 log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
1743 self.remote_port, msg))
1745 def logline(self, msg):
1746 """Log a line including additional indentifying session data."""
1747 logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg))
1749 def flush_account(self):
1750 """Flush account information by clearing attributes that need
1751 to be reset on a REIN or new USER command.
1753 if self.data_channel:
1754 if not self.data_channel.transfer_in_progress():
1755 self.data_channel.close()
1756 self.data_channel = None
1757 if self.data_server:
1758 self.data_server.close()
1759 self.data_server = None
1762 self.authenticated = False
1765 self.attempted_logins = 0
1766 self.current_type = 'a'
1767 self.restart_position = 0
1768 self.quit_pending = False
1769 self.__in_dtp_queue = None
1770 self.__out_dtp_queue = None
1772 def run_as_current_user(self, function, *args, **kwargs):
1773 """Execute a function impersonating the current logged-in user."""
1774 self.authorizer.impersonate_user(self.username, self.password)
1776 return function(*args, **kwargs)
1778 self.authorizer.terminate_impersonation()
1782 def try_as_current_user(self, function, args=None, kwargs=None, line=None, errno_resp=None):
1783 """run function as current user, auto-respond in exceptions
1784 @param args,kwargs the arguments, in list and dict respectively
1785 @param errno_resp a dictionary of responses to IOError, OSError
1788 eresp = self.__errno_responses.copy()
1789 eresp.update(errno_resp)
1791 eresp = self.__errno_responses
1795 uline = ' "%s"' % _to_unicode(line)
1801 return self.run_as_current_user(function, *args, **kwargs)
1802 except NotImplementedError, err:
1803 cmdname = function.__name__
1804 why = err.args[0] or 'Not implemented'
1805 self.log('FAIL %s() not implemented: %s.' %(cmdname, why))
1806 self.respond('502 %s.' %why)
1807 raise FTPExceptionSent(why)
1808 except EnvironmentError, err:
1809 cmdname = function.__name__
1811 logline(traceback.format_exc())
1814 ret_code = eresp.get(err.errno, '451')
1815 why = (err.strerror) or 'Error in command'
1816 self.log('FAIL %s() %s errno=%s: %s.' %(cmdname, uline, err.errno, why))
1817 self.respond('%s %s.' % (str(ret_code), why))
1819 raise FTPExceptionSent(why)
1820 except Exception, err:
1821 cmdname = function.__name__
1823 logerror(traceback.format_exc())
1826 why = (err.args and err.args[0]) or 'Exception'
1827 self.log('FAIL %s() %s Exception: %s.' %(cmdname, uline, why))
1828 self.respond('451 %s.' % why)
1829 raise FTPExceptionSent(why)
1831 def get_crdata2(self, *args, **kwargs):
1832 return self.try_as_current_user(self.fs.get_crdata, args, kwargs, line=args[0])
1834 def _make_eport(self, ip, port):
1835 """Establish an active data channel with remote client which
1836 issued a PORT or EPRT command.
1838 # FTP bounce attacks protection: according to RFC-2577 it's
1839 # recommended to reject PORT if IP address specified in it
1840 # does not match client IP address.
1841 if not self.permit_foreign_addresses:
1842 if ip != self.remote_ip:
1843 self.log("Rejected data connection to foreign address %s:%s."
1845 self.respond("501 Can't connect to a foreign address.")
1848 # ...another RFC-2577 recommendation is rejecting connections
1849 # to privileged ports (< 1024) for security reasons.
1850 if not self.permit_privileged_ports:
1852 self.log('PORT against the privileged port "%s" refused.' %port)
1853 self.respond("501 Can't connect over a privileged port.")
1856 # close existent DTP-server instance, if any.
1857 if self.data_server:
1858 self.data_server.close()
1859 self.data_server = None
1860 if self.data_channel:
1861 self.data_channel.close()
1862 self.data_channel = None
1864 # make sure we are not hitting the max connections limit
1865 if self.server.max_cons:
1866 if len(self._map) >= self.server.max_cons:
1867 msg = "Too many connections. Can't open data channel."
1868 self.respond("425 %s" %msg)
1873 self.active_dtp(ip, port, self)
1875 def _make_epasv(self, extmode=False):
1876 """Initialize a passive data channel with remote client which
1877 issued a PASV or EPSV command.
1878 If extmode argument is False we assume that client issued EPSV in
1879 which case extended passive mode will be used (see RFC-2428).
1881 # close existing DTP-server instance, if any
1882 if self.data_server:
1883 self.data_server.close()
1884 self.data_server = None
1886 if self.data_channel:
1887 self.data_channel.close()
1888 self.data_channel = None
1890 # make sure we are not hitting the max connections limit
1891 if self.server.max_cons:
1892 if len(self._map) >= self.server.max_cons:
1893 msg = "Too many connections. Can't open data channel."
1894 self.respond("425 %s" %msg)
1899 self.data_server = self.passive_dtp(self, extmode)
1901 def ftp_PORT(self, line):
1902 """Start an active data channel by using IPv4."""
1904 self.respond("501 PORT not allowed after EPSV ALL.")
1906 if self.af != socket.AF_INET:
1907 self.respond("425 You cannot use PORT on IPv6 connections. "
1908 "Use EPRT instead.")
1910 # Parse PORT request for getting IP and PORT.
1911 # Request comes in as:
1912 # > h1,h2,h3,h4,p1,p2
1913 # ...where the client's IP address is h1.h2.h3.h4 and the TCP
1914 # port number is (p1 * 256) + p2.
1916 addr = map(int, line.split(','))
1917 assert len(addr) == 6
1919 assert 0 <= x <= 255
1920 ip = '%d.%d.%d.%d' %tuple(addr[:4])
1921 port = (addr[4] * 256) + addr[5]
1922 assert 0 <= port <= 65535
1923 except (AssertionError, ValueError, OverflowError):
1924 self.respond("501 Invalid PORT format.")
1926 self._make_eport(ip, port)
1928 def ftp_EPRT(self, line):
1929 """Start an active data channel by choosing the network protocol
1930 to use (IPv4/IPv6) as defined in RFC-2428.
1933 self.respond("501 EPRT not allowed after EPSV ALL.")
1935 # Parse EPRT request for getting protocol, IP and PORT.
1936 # Request comes in as:
1937 # # <d>proto<d>ip<d>port<d>
1938 # ...where <d> is an arbitrary delimiter character (usually "|") and
1939 # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
1941 af, ip, port = line.split(line[0])[1:-1]
1943 assert 0 <= port <= 65535
1944 except (AssertionError, ValueError, IndexError, OverflowError):
1945 self.respond("501 Invalid EPRT format.")
1949 if self.af != socket.AF_INET:
1950 self.respond('522 Network protocol not supported (use 2).')
1953 octs = map(int, ip.split('.'))
1954 assert len(octs) == 4
1956 assert 0 <= x <= 255
1957 except (AssertionError, ValueError, OverflowError):
1958 self.respond("501 Invalid EPRT format.")
1960 self._make_eport(ip, port)
1962 if self.af == socket.AF_INET:
1963 self.respond('522 Network protocol not supported (use 1).')
1965 self._make_eport(ip, port)
1967 if self.af == socket.AF_INET:
1968 self.respond('501 Unknown network protocol (use 1).')
1970 self.respond('501 Unknown network protocol (use 2).')
1972 def ftp_PASV(self, line):
1973 """Start a passive data channel by using IPv4."""
1975 self.respond("501 PASV not allowed after EPSV ALL.")
1977 if self.af != socket.AF_INET:
1978 self.respond("425 You cannot use PASV on IPv6 connections. "
1979 "Use EPSV instead.")
1981 self._make_epasv(extmode=False)
1983 def ftp_EPSV(self, line):
1984 """Start a passive data channel by using IPv4 or IPv6 as defined
1987 # RFC-2428 specifies that if an optional parameter is given,
1988 # we have to determine the address family from that otherwise
1989 # use the same address family used on the control connection.
1990 # In such a scenario a client may use IPv4 on the control channel
1991 # and choose to use IPv6 for the data channel.
1992 # But how could we use IPv6 on the data channel without knowing
1993 # which IPv6 address to use for binding the socket?
1994 # Unfortunately RFC-2428 does not provide satisfing information
1995 # on how to do that. The assumption is that we don't have any way
1996 # to know which address to use, hence we just use the same address
1997 # family used on the control connection.
1999 self._make_epasv(extmode=True)
2001 if self.af != socket.AF_INET:
2002 self.respond('522 Network protocol not supported (use 2).')
2004 self._make_epasv(extmode=True)
2006 if self.af == socket.AF_INET:
2007 self.respond('522 Network protocol not supported (use 1).')
2009 self._make_epasv(extmode=True)
2010 elif line.lower() == 'all':
2011 self._epsvall = True
2012 self.respond('220 Other commands other than EPSV are now disabled.')
2014 if self.af == socket.AF_INET:
2015 self.respond('501 Unknown network protocol (use 1).')
2017 self.respond('501 Unknown network protocol (use 2).')
2019 def ftp_QUIT(self, line):
2020 """Quit the current session."""
2022 # This command terminates a USER and if file transfer is not
2023 # in progress, the server closes the control connection.
2024 # If file transfer is in progress, the connection will remain
2025 # open for result response and the server will then close it.
2026 if self.authenticated:
2027 msg_quit = self.authorizer.get_msg_quit(self.username)
2029 msg_quit = "Goodbye."
2030 if len(msg_quit) <= 75:
2031 self.respond("221 %s" %msg_quit)
2033 self.push("221-%s\r\n" %msg_quit)
2034 self.respond("221 ")
2036 if not self.data_channel:
2037 self.close_when_done()
2039 # tell the cmd channel to stop responding to commands.
2040 self.quit_pending = True
2043 # --- data transferring
2045 def ftp_LIST(self, line):
2046 """Return a list of files in the specified directory to the
2049 # - If no argument, fall back on cwd as default.
2050 # - Some older FTP clients erroneously issue /bin/ls-like LIST
2051 # formats in which case we fall back on cwd as default.
2052 if not line or line.lower() in ('-a', '-l', '-al', '-la'):
2056 datacr = self.get_crdata2(line, mode='list')
2057 iterator = self.try_as_current_user(self.fs.get_list_dir, (datacr,))
2058 except FTPExceptionSent:
2059 self.fs.close_cr(datacr)
2063 self.log('OK LIST "%s". Transfer starting.' % line)
2064 producer = BufferedIteratorProducer(iterator)
2065 self.push_dtp_data(producer, isproducer=True)
2067 self.fs.close_cr(datacr)
2070 def ftp_NLST(self, line):
2071 """Return a list of files in the specified directory in a
2072 compact form to the client.
2079 datacr = self.get_crdata2(line, mode='list')
2081 datacr = ( None, None, None )
2082 if self.fs.isdir(datacr[1]):
2083 nodelist = self.try_as_current_user(self.fs.listdir, (datacr,))
2085 # if path is a file we just list its name
2086 nodelist = [datacr[1],]
2090 if isinstance(nl.path, (list, tuple)):
2091 listing.append(nl.path[-1])
2093 listing.append(nl.path) # assume string
2094 except FTPExceptionSent:
2095 self.fs.close_cr(datacr)
2098 self.fs.close_cr(datacr)
2102 data = ''.join([ _to_decode(x) + '\r\n' for x in listing ])
2103 self.log('OK NLST "%s". Transfer starting.' %line)
2104 self.push_dtp_data(data)
2106 # --- MLST and MLSD commands
2108 # The MLST and MLSD commands are intended to standardize the file and
2109 # directory information returned by the server-FTP process. These
2110 # commands differ from the LIST command in that the format of the
2111 # replies is strictly defined although extensible.
2113 def ftp_MLST(self, line):
2114 """Return information about a pathname in a machine-processable
2115 form as defined in RFC-3659.
2117 # if no argument, fall back on cwd as default
2122 datacr = self.get_crdata2(line, mode='list')
2123 perms = self.authorizer.get_perms(self.username)
2124 iterator = self.try_as_current_user(self.fs.format_mlsx, (datacr[0], datacr[1].parent,
2125 [datacr[1],], perms, self.current_facts), {'ignore_err':False})
2126 data = ''.join(iterator)
2127 except FTPExceptionSent:
2128 self.fs.close_cr(datacr)
2131 self.fs.close_cr(datacr)
2132 # since TVFS is supported (see RFC-3659 chapter 6), a fully
2133 # qualified pathname should be returned
2134 data = data.split(' ')[0] + ' %s\r\n' %line
2135 # response is expected on the command channel
2136 self.push('250-Listing "%s":\r\n' %line)
2137 # the fact set must be preceded by a space
2138 self.push(' ' + data)
2139 self.respond('250 End MLST.')
2141 def ftp_MLSD(self, line):
2142 """Return contents of a directory in a machine-processable form
2143 as defined in RFC-3659.
2145 # if no argument, fall back on cwd as default
2151 datacr = self.get_crdata2(line, mode='list')
2152 # RFC-3659 requires 501 response code if path is not a directory
2153 if not self.fs.isdir(datacr[1]):
2154 err = 'No such directory'
2155 self.log('FAIL MLSD "%s". %s.' %(line, err))
2156 self.respond("501 %s." %err)
2158 listing = self.try_as_current_user(self.fs.listdir, (datacr,))
2159 except FTPExceptionSent:
2160 self.fs.close_cr(datacr)
2163 self.fs.close_cr(datacr)
2164 perms = self.authorizer.get_perms(self.username)
2165 iterator = self.fs.format_mlsx(datacr[0], datacr[1], listing, perms,
2167 producer = BufferedIteratorProducer(iterator)
2168 self.log('OK MLSD "%s". Transfer starting.' %line)
2169 self.push_dtp_data(producer, isproducer=True)
2171 def ftp_RETR(self, line):
2172 """Retrieve the specified file (transfer from the server to the
2177 datacr = self.get_crdata2(line, mode='file')
2178 fd = self.try_as_current_user(self.fs.open, (datacr, 'rb'))
2179 except FTPExceptionSent:
2180 self.fs.close_cr(datacr)
2183 if self.restart_position:
2184 # Make sure that the requested offset is valid (within the
2185 # size of the file being resumed).
2186 # According to RFC-1123 a 554 reply may result in case that
2187 # the existing file cannot be repositioned as specified in
2191 assert not self.restart_position > self.fs.getsize(datacr)
2192 fd.seek(self.restart_position)
2194 except AssertionError:
2195 why = "Invalid REST parameter"
2196 except IOError, err:
2197 why = _strerror(err)
2198 self.restart_position = 0
2200 self.respond('554 %s' %why)
2201 self.log('FAIL RETR "%s". %s.' %(line, why))
2202 self.fs.close_cr(datacr)
2204 self.log('OK RETR "%s". Download starting.' %line)
2205 producer = FileProducer(fd, self.current_type)
2206 self.push_dtp_data(producer, isproducer=True, file=fd)
2207 self.fs.close_cr(datacr)
2209 def ftp_STOR(self, line, mode='w'):
2210 """Store a file (transfer from the client to the server)."""
2211 # A resume could occur in case of APPE or REST commands.
2212 # In that case we have to open file object in different ways:
2215 # REST: mode = 'r+' (to permit seeking on file object)
2223 datacr = self.get_crdata2(line,mode='create')
2224 if self.restart_position:
2226 fd = self.try_as_current_user(self.fs.create, (datacr, datacr[2], mode + 'b'))
2227 except FTPExceptionSent:
2228 self.fs.close_cr(datacr)
2231 if self.restart_position:
2232 # Make sure that the requested offset is valid (within the
2233 # size of the file being resumed).
2234 # According to RFC-1123 a 554 reply may result in case
2235 # that the existing file cannot be repositioned as
2236 # specified in the REST.
2239 assert not self.restart_position > self.fs.getsize(datacr)
2240 fd.seek(self.restart_position)
2242 except AssertionError:
2243 why = "Invalid REST parameter"
2244 except IOError, err:
2245 why = _strerror(err)
2246 self.restart_position = 0
2248 self.fs.close_cr(datacr)
2249 self.respond('554 %s' %why)
2250 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2253 self.log('OK %s "%s". Upload starting.' %(cmd, line))
2254 if self.data_channel:
2255 self.respond("125 Data connection already open. Transfer starting.")
2256 self.data_channel.file_obj = fd
2257 self.data_channel.enable_receiving(self.current_type)
2259 self.respond("150 File status okay. About to open data connection.")
2260 self.__in_dtp_queue = fd
2261 self.fs.close_cr(datacr)
2264 def ftp_STOU(self, line):
2265 """Store a file on the server with a unique name."""
2266 # Note 1: RFC-959 prohibited STOU parameters, but this
2267 # prohibition is obsolete.
2268 # Note 2: 250 response wanted by RFC-959 has been declared
2269 # incorrect in RFC-1123 that wants 125/150 instead.
2270 # Note 3: RFC-1123 also provided an exact output format
2271 # defined to be as follow:
2273 # ...where pppp represents the unique path name of the
2274 # file that will be written.
2276 # watch for STOU preceded by REST, which makes no sense.
2277 if self.restart_position:
2278 self.respond("450 Can't STOU while REST request is pending.")
2283 datacr = self.get_crdata2(line, mode='create')
2287 basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
2290 fd = self.try_as_current_user(self.fs.mkstemp, kwargs={'prefix':prefix,
2291 'dir': basedir}, line=line )
2292 except FTPExceptionSent:
2293 self.fs.close_cr(datacr)
2295 except IOError, err: # TODO
2296 # hitted the max number of tries to find out file with
2298 if err.errno == errno.EEXIST:
2299 why = 'No usable unique file name found'
2300 # something else happened
2302 why = _strerror(err)
2303 self.respond("450 %s." %why)
2304 self.log('FAIL STOU "%s". %s.' %(self.fs.ftpnorm(line), why))
2305 self.fs.close_cr(datacr)
2309 if not self.authorizer.has_perm(self.username, 'w', filename):
2310 self.log('FAIL STOU "%s". Not enough privileges'
2311 %self.fs.ftpnorm(line))
2312 self.respond("550 Can't STOU: not enough privileges.")
2313 self.fs.close_cr(datacr)
2316 # now just acts like STOR except that restarting isn't allowed
2317 self.log('OK STOU "%s". Upload starting.' %filename)
2318 if self.data_channel:
2319 self.respond("125 FILE: %s" %filename)
2320 self.data_channel.file_obj = fd
2321 self.data_channel.enable_receiving(self.current_type)
2323 self.respond("150 FILE: %s" %filename)
2324 self.__in_dtp_queue = fd
2325 self.fs.close_cr(datacr)
2328 def ftp_APPE(self, line):
2329 """Append data to an existing file on the server."""
2330 # watch for APPE preceded by REST, which makes no sense.
2331 if self.restart_position:
2332 self.respond("550 Can't APPE while REST request is pending.")
2334 self.ftp_STOR(line, mode='a')
2336 def ftp_REST(self, line):
2337 """Restart a file transfer from a previous mark."""
2342 except (ValueError, OverflowError):
2343 self.respond("501 Invalid parameter.")
2345 self.respond("350 Restarting at position %s. " \
2346 "Now use RETR/STOR for resuming." %marker)
2347 self.log("OK REST %s." %marker)
2348 self.restart_position = marker
2350 def ftp_ABOR(self, line):
2351 """Abort the current data transfer."""
2353 # ABOR received while no data channel exists
2354 if (self.data_server is None) and (self.data_channel is None):
2355 resp = "225 No transfer to abort."
2357 # a PASV was received but connection wasn't made yet
2358 if self.data_server:
2359 self.data_server.close()
2360 self.data_server = None
2361 resp = "225 ABOR command successful; data channel closed."
2363 # If a data transfer is in progress the server must first
2364 # close the data connection, returning a 426 reply to
2365 # indicate that the transfer terminated abnormally, then it
2366 # must send a 226 reply, indicating that the abort command
2367 # was successfully processed.
2368 # If no data has been transmitted we just respond with 225
2369 # indicating that no transfer was in progress.
2370 if self.data_channel:
2371 if self.data_channel.transfer_in_progress():
2372 self.data_channel.close()
2373 self.data_channel = None
2374 self.respond("426 Connection closed; transfer aborted.")
2375 self.log("OK ABOR. Transfer aborted, data channel closed.")
2376 resp = "226 ABOR command successful."
2378 self.data_channel.close()
2379 self.data_channel = None
2380 self.log("OK ABOR. Data channel closed.")
2381 resp = "225 ABOR command successful; data channel closed."
2385 # --- authentication
2387 def ftp_USER(self, line):
2388 """Set the username for the current session."""
2389 # we always treat anonymous user as lower-case string.
2390 if line.lower() == "anonymous":
2393 # RFC-959 specifies a 530 response to the USER command if the
2394 # username is not valid. If the username is valid is required
2395 # ftpd returns a 331 response instead. In order to prevent a
2396 # malicious client from determining valid usernames on a server,
2397 # it is suggested by RFC-2577 that a server always return 331 to
2398 # the USER command and then reject the combination of username
2399 # and password for an invalid username when PASS is provided later.
2400 if not self.authenticated:
2401 self.respond('331 Username ok, send password.')
2403 # a new USER command could be entered at any point in order
2404 # to change the access control flushing any user, password,
2405 # and account information already supplied and beginning the
2406 # login sequence again.
2407 self.flush_account()
2408 msg = 'Previous account information was flushed'
2409 self.log('OK USER "%s". %s.' %(line, msg))
2410 self.respond('331 %s, send password.' %msg)
2411 self.username = line
2413 def ftp_PASS(self, line):
2414 """Check username's password against the authorizer."""
2416 if self.authenticated:
2417 self.respond("503 User already authenticated.")
2419 if not self.username:
2420 self.respond("503 Login with USER first.")
2424 if self.authorizer.has_user(self.username):
2425 if self.username == 'anonymous' \
2426 or self.authorizer.validate_authentication(self.username, line):
2427 msg_login = self.authorizer.get_msg_login(self.username)
2428 if len(msg_login) <= 75:
2429 self.respond('230 %s' %msg_login)
2431 self.push("230-%s\r\n" %msg_login)
2432 self.respond("230 ")
2434 self.authenticated = True
2435 self.password = line
2436 self.attempted_logins = 0
2437 self.fs.root = self.authorizer.get_home_dir(self.username)
2438 self.fs.username=self.username
2439 self.fs.password=line
2440 self.log("User %s logged in." %self.username)
2442 self.attempted_logins += 1
2443 if self.attempted_logins >= self.max_login_attempts:
2444 self.respond("530 Maximum login attempts. Disconnecting.")
2447 self.respond("530 Authentication failed.")
2448 self.log('Authentication failed (user: "%s").' %self.username)
2453 self.attempted_logins += 1
2454 if self.attempted_logins >= self.max_login_attempts:
2455 self.log('Authentication failed: unknown username "%s".'
2457 self.respond("530 Maximum login attempts. Disconnecting.")
2459 elif self.username.lower() == 'anonymous':
2460 self.respond("530 Anonymous access not allowed.")
2461 self.log('Authentication failed: anonymous access not allowed.')
2463 self.respond("530 Authentication failed.")
2464 self.log('Authentication failed: unknown username "%s".'
2468 def ftp_REIN(self, line):
2469 """Reinitialize user's current session."""
2471 # REIN command terminates a USER, flushing all I/O and account
2472 # information, except to allow any transfer in progress to be
2473 # completed. All parameters are reset to the default settings
2474 # and the control connection is left open. This is identical
2475 # to the state in which a user finds himself immediately after
2476 # the control connection is opened.
2477 self.log("OK REIN. Flushing account information.")
2478 self.flush_account()
2479 # Note: RFC-959 erroneously mention "220" as the correct response
2480 # code to be given in this case, but this is wrong...
2481 self.respond("230 Ready for new user.")
2484 # --- filesystem operations
2486 def ftp_PWD(self, line):
2487 """Return the name of the current working directory to the client."""
2488 cwd = self.fs.get_cwd()
2489 self.respond('257 "%s" is the current directory.' % cwd)
2491 def ftp_CWD(self, line):
2492 """Change the current working directory."""
2493 # check: a lot of FTP servers go back to root directory if no
2494 # arg is provided but this is not specified in RFC-959.
2495 # Search for official references about this behaviour.
2498 datacr = self.get_crdata2(line,'cwd')
2499 self.try_as_current_user(self.fs.chdir, (datacr,), line=line, errno_resp={2: 530})
2500 cwd = self.fs.get_cwd()
2501 self.log('OK CWD "%s".' % cwd)
2502 self.respond('250 "%s" is the current directory.' % cwd)
2503 except FTPExceptionSent:
2506 self.fs.close_cr(datacr)
2508 def ftp_CDUP(self, line):
2509 """Change into the parent directory."""
2510 # Note: RFC-959 says that code 200 is required but it also says
2511 # that CDUP uses the same codes as CWD.
2514 def ftp_SIZE(self, line):
2515 """Return size of file in a format suitable for using with
2516 RESTart as defined in RFC-3659.
2518 Implementation note:
2519 properly handling the SIZE command when TYPE ASCII is used would
2520 require to scan the entire file to perform the ASCII translation
2521 logic (file.read().replace(os.linesep, '\r\n')) and then
2522 calculating the len of such data which may be different than
2523 the actual size of the file on the server. Considering that
2524 calculating such result could be very resource-intensive it
2525 could be easy for a malicious client to try a DoS attack, thus
2526 we do not perform the ASCII translation.
2528 However, clients in general should not be resuming downloads in
2529 ASCII mode. Resuming downloads in binary mode is the recommended
2530 way as specified in RFC-3659.
2534 datacr = self.get_crdata2(line, mode='file')
2535 #if self.fs.isdir(datacr[1]):
2536 # why = "%s is not retrievable" %line
2537 # self.log('FAIL SIZE "%s". %s.' %(line, why))
2538 # self.respond("550 %s." %why)
2539 # self.fs.close_cr(datacr)
2541 size = self.try_as_current_user(self.fs.getsize,(datacr,), line=line)
2542 except FTPExceptionSent:
2543 self.fs.close_cr(datacr)
2546 self.respond("213 %s" %size)
2547 self.log('OK SIZE "%s".' %line)
2548 self.fs.close_cr(datacr)
2550 def ftp_MDTM(self, line):
2551 """Return last modification time of file to the client as an ISO
2552 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2557 if line.find('/', 1) < 0:
2558 # root or db, just return local
2561 datacr = self.get_crdata2(line)
2563 raise IOError(errno.ENOENT, "%s is not retrievable" %line)
2564 #if not self.fs.isfile(datacr[1]):
2565 # raise IOError(errno.EPERM, "%s is not a regular file" % line)
2567 lmt = self.try_as_current_user(self.fs.getmtime, (datacr,), line=line)
2568 lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
2569 self.respond("213 %s" %lmt)
2570 self.log('OK MDTM "%s".' %line)
2571 except FTPExceptionSent:
2574 self.fs.close_cr(datacr)
2576 def ftp_MKD(self, line):
2577 """Create the specified directory."""
2579 datacr = self.get_crdata2(line, mode='create')
2580 self.try_as_current_user(self.fs.mkdir, (datacr, datacr[2]), line=line)
2581 except FTPExceptionSent:
2582 self.fs.close_cr(datacr)
2585 self.log('OK MKD "%s".' %line)
2586 self.respond("257 Directory created.")
2587 self.fs.close_cr(datacr)
2589 def ftp_RMD(self, line):
2590 """Remove the specified directory."""
2593 datacr = self.get_crdata2(line, mode='delete')
2595 msg = "Can't remove root directory."
2596 self.respond("553 %s" %msg)
2597 self.log('FAIL MKD "/". %s' %msg)
2598 self.fs.close_cr(datacr)
2600 self.try_as_current_user(self.fs.rmdir, (datacr,), line=line)
2601 self.log('OK RMD "%s".' %line)
2602 self.respond("250 Directory removed.")
2603 except FTPExceptionSent:
2605 self.fs.close_cr(datacr)
2607 def ftp_DELE(self, line):
2608 """Delete the specified file."""
2611 datacr = self.get_crdata2(line, mode='delete')
2612 self.try_as_current_user(self.fs.remove, (datacr,), line=line)
2613 self.log('OK DELE "%s".' %line)
2614 self.respond("250 File removed.")
2615 except FTPExceptionSent:
2617 self.fs.close_cr(datacr)
2619 def ftp_RNFR(self, line):
2620 """Rename the specified (only the source name is specified
2621 here, see RNTO command)"""
2624 datacr = self.get_crdata2(line, mode='rfnr')
2626 self.respond("550 No such file or directory.")
2628 self.respond("553 Can't rename the home directory.")
2630 self.fs.rnfr = datacr[1]
2631 self.respond("350 Ready for destination name.")
2632 except FTPExceptionSent:
2634 self.fs.close_cr(datacr)
2636 def ftp_RNTO(self, line):
2637 """Rename file (destination name only, source is specified with
2640 if not self.fs.rnfr:
2641 self.respond("503 Bad sequence of commands: use RNFR first.")
2645 datacr = self.get_crdata2(line,'create')
2646 oldname = self.fs.rnfr.path
2647 if isinstance(oldname, (list, tuple)):
2648 oldname = '/'.join(oldname)
2649 self.try_as_current_user(self.fs.rename, (self.fs.rnfr, datacr), line=line)
2651 self.log('OK RNFR/RNTO "%s ==> %s".' % \
2652 (_to_unicode(oldname), _to_unicode(line)))
2653 self.respond("250 Renaming ok.")
2654 except FTPExceptionSent:
2658 self.fs.close_cr(datacr)
2663 def ftp_TYPE(self, line):
2664 """Set current type data type to binary/ascii"""
2666 if line in ("A", "AN", "A N"):
2667 self.respond("200 Type set to: ASCII.")
2668 self.current_type = 'a'
2669 elif line in ("I", "L8", "L 8"):
2670 self.respond("200 Type set to: Binary.")
2671 self.current_type = 'i'
2673 self.respond('504 Unsupported type "%s".' %line)
2675 def ftp_STRU(self, line):
2676 """Set file structure (obsolete)."""
2677 # obsolete (backward compatibility with older ftp clients)
2678 if line in ('f','F'):
2679 self.respond('200 File transfer structure set to: F.')
2681 self.respond('504 Unimplemented STRU type.')
2683 def ftp_MODE(self, line):
2684 """Set data transfer mode (obsolete)"""
2685 # obsolete (backward compatibility with older ftp clients)
2686 if line in ('s', 'S'):
2687 self.respond('200 Transfer mode set to: S')
2689 self.respond('504 Unimplemented MODE type.')
2691 def ftp_STAT(self, line):
2692 """Return statistics about current ftp session. If an argument
2693 is provided return directory listing over command channel.
2695 Implementation note:
2697 RFC-959 do not explicitly mention globbing; this means that FTP
2698 servers are not required to support globbing in order to be
2699 compliant. However, many FTP servers do support globbing as a
2700 measure of convenience for FTP clients and users.
2702 In order to search for and match the given globbing expression,
2703 the code has to search (possibly) many directories, examine
2704 each contained filename, and build a list of matching files in
2705 memory. Since this operation can be quite intensive, both CPU-
2706 and memory-wise, we limit the search to only one directory
2707 non-recursively, as LIST does.
2709 # return STATus information about ftpd
2712 s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2713 if self.authenticated:
2714 s.append('Logged in as: %s' %self.username)
2716 if not self.username:
2717 s.append("Waiting for username.")
2719 s.append("Waiting for password.")
2720 if self.current_type == 'a':
2724 s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
2725 if self.data_server:
2726 s.append('Passive data channel waiting for connection.')
2727 elif self.data_channel:
2728 bytes_sent = self.data_channel.tot_bytes_sent
2729 bytes_recv = self.data_channel.tot_bytes_received
2730 s.append('Data connection open:')
2731 s.append('Total bytes sent: %s' %bytes_sent)
2732 s.append('Total bytes received: %s' %bytes_recv)
2734 s.append('Data connection closed.')
2736 self.push('211-FTP server status:\r\n')
2737 self.push(''.join([' %s\r\n' %item for item in s]))
2738 self.respond('211 End of status.')
2739 # return directory LISTing over the command channel
2743 datacr = self.fs.get_cr(line)
2744 iterator = self.try_as_current_user(self.fs.get_stat_dir, (line, datacr), line=line)
2745 except FTPExceptionSent:
2748 self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
2749 self.push_with_producer(BufferedIteratorProducer(iterator))
2750 self.respond('213 End of status.')
2751 self.fs.close_cr(datacr)
2753 def ftp_FEAT(self, line):
2754 """List all new features supported as defined in RFC-2398."""
2755 features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
2757 for fact in self.available_facts:
2758 if fact in self.current_facts:
2762 features.append('MLST ' + s)
2764 self.push("211-Features supported:\r\n")
2765 self.push("".join([" %s\r\n" %x for x in features]))
2766 self.respond('211 End FEAT.')
2768 def ftp_OPTS(self, line):
2769 """Specify options for FTP commands as specified in RFC-2389."""
2771 assert (not line.count(' ') > 1), 'Invalid number of arguments'
2773 cmd, arg = line.split(' ')
2774 assert (';' in arg), 'Invalid argument'
2777 # actually the only command able to accept options is MLST
2778 assert (cmd.upper() == 'MLST'), 'Unsupported command "%s"' %cmd
2779 except AssertionError, err:
2780 self.respond('501 %s.' %err)
2782 facts = [x.lower() for x in arg.split(';')]
2783 self.current_facts = [x for x in facts if x in self.available_facts]
2784 f = ''.join([x + ';' for x in self.current_facts])
2785 self.respond('200 MLST OPTS ' + f)
2787 def ftp_NOOP(self, line):
2789 self.respond("200 I successfully done nothin'.")
2791 def ftp_SYST(self, line):
2792 """Return system type (always returns UNIX type: L8)."""
2793 # This command is used to find out the type of operating system
2794 # at the server. The reply shall have as its first word one of
2795 # the system names listed in RFC-943.
2796 # Since that we always return a "/bin/ls -lA"-like output on
2797 # LIST we prefer to respond as if we would on Unix in any case.
2798 self.respond("215 UNIX Type: L8")
2800 def ftp_ALLO(self, line):
2801 """Allocate bytes for storage (obsolete)."""
2802 # obsolete (always respond with 202)
2803 self.respond("202 No storage allocation necessary.")
2805 def ftp_HELP(self, line):
2806 """Return help text to the client."""
2808 if line.upper() in proto_cmds:
2809 self.respond("214 %s" %proto_cmds[line.upper()])
2811 self.respond("501 Unrecognized command.")
2813 # provide a compact list of recognized commands
2814 def formatted_help():
2816 keys = proto_cmds.keys()
2819 elems = tuple((keys[0:8]))
2820 cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2822 return ''.join(cmds)
2824 self.push("214-The following commands are recognized:\r\n")
2825 self.push(formatted_help())
2826 self.respond("214 Help command successful.")
2829 # --- support for deprecated cmds
2831 # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
2832 # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
2833 # Such commands are obsoleted but some ftp clients (e.g. Windows
2834 # ftp.exe) still use them.
2836 def ftp_XCUP(self, line):
2837 """Change to the parent directory. Synonym for CDUP. Deprecated."""
2840 def ftp_XCWD(self, line):
2841 """Change the current working directory. Synonym for CWD. Deprecated."""
2844 def ftp_XMKD(self, line):
2845 """Create the specified directory. Synonym for MKD. Deprecated."""
2848 def ftp_XPWD(self, line):
2849 """Return the current working directory. Synonym for PWD. Deprecated."""
2852 def ftp_XRMD(self, line):
2853 """Remove the specified directory. Synonym for RMD. Deprecated."""
2857 class FTPServer(asyncore.dispatcher):
2858 """This class is an asyncore.disptacher subclass. It creates a FTP
2859 socket listening on <address>, dispatching the requests to a <handler>
2860 (typically FTPHandler class).
2862 Depending on the type of address specified IPv4 or IPv6 connections
2863 (or both, depending from the underlying system) will be accepted.
2865 All relevant session information is stored in class attributes
2867 Overriding them is strongly recommended to avoid running out of
2868 file descriptors (DoS)!
2871 number of maximum simultaneous connections accepted (defaults
2874 - (int) max_cons_per_ip:
2875 number of maximum connections accepted for the same IP address
2876 (defaults to 0 == unlimited).
2882 def __init__(self, address, handler):
2883 """Initiate the FTP server opening listening on address.
2885 - (tuple) address: the host:port pair on which the command
2886 channel will listen.
2888 - (classobj) handler: the handler class to use.
2890 asyncore.dispatcher.__init__(self)
2891 self.handler = handler
2893 host, port = address
2895 # AF_INET or AF_INET6 socket
2896 # Get the correct address family for our host (allows IPv6 addresses)
2898 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
2899 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
2900 except socket.gaierror:
2901 # Probably a DNS issue. Assume IPv4.
2902 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2903 self.set_reuse_addr()
2904 self.bind((host, port))
2907 af, socktype, proto, canonname, sa = res
2909 self.create_socket(af, socktype)
2910 self.set_reuse_addr()
2912 except socket.error, msg:
2919 raise socket.error, msg
2922 def set_reuse_addr(self):
2923 # Overridden for convenience. Avoid to reuse address on Windows.
2924 if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'):
2926 asyncore.dispatcher.set_reuse_addr(self)
2928 def serve_forever(self, **kwargs):
2929 """A wrap around asyncore.loop(); starts the asyncore polling
2932 The keyword arguments in kwargs are the same expected by
2933 asyncore.loop() function: timeout, use_poll, map and count.
2935 if not 'count' in kwargs:
2936 log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2938 # backward compatibility for python < 2.4
2939 if not hasattr(self, '_map'):
2940 if not 'map' in kwargs:
2941 map = asyncore.socket_map
2944 self._map = self.handler._map = map
2948 # use_poll specifies whether to use select module's poll()
2949 # with asyncore or whether to use asyncore's own poll()
2950 # method Python versions < 2.4 need use_poll set to False
2951 # This breaks on OS X systems if use_poll is set to True.
2952 # All systems seem to work fine with it set to False
2953 # (tested on Linux, Windows, and OS X platforms)
2955 asyncore.loop(**kwargs)
2957 asyncore.loop(timeout=1.0, use_poll=False)
2958 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
2959 log("Shutting down FTPd.")
2962 def handle_accept(self):
2963 """Called when remote client initiates a connection."""
2964 sock_obj, addr = self.accept()
2965 log("[]%s:%s Connected." %addr[:2])
2967 handler = self.handler(sock_obj, self)
2969 self.ip_map.append(ip)
2971 # For performance and security reasons we should always set a
2972 # limit for the number of file descriptors that socket_map
2973 # should contain. When we're running out of such limit we'll
2974 # use the last available channel for sending a 421 response
2975 # to the client before disconnecting it.
2977 if len(self._map) > self.max_cons:
2978 handler.handle_max_cons()
2981 # accept only a limited number of connections from the same
2983 if self.max_cons_per_ip:
2984 if self.ip_map.count(ip) > self.max_cons_per_ip:
2985 handler.handle_max_cons_per_ip()
2993 def handle_error(self):
2994 """Called to handle any uncaught exceptions."""
2997 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
2999 logerror(traceback.format_exc())
3002 def close_all(self, map=None, ignore_all=False):
3003 """Stop serving; close all existent connections disconnecting
3007 A dictionary whose items are the channels to close.
3008 If map is omitted, the default asyncore.socket_map is used.
3010 - (bool) ignore_all:
3011 having it set to False results in raising exception in case
3012 of unexpected errors.
3014 Implementation note:
3016 Instead of using the current asyncore.close_all() function
3017 which only close sockets, we iterate over all existent channels
3018 calling close() method for each one of them, avoiding memory
3021 This is how asyncore.close_all() function should work in
3026 for x in map.values():
3030 if x[0] == errno.EBADF:
3032 elif not ignore_all:
3034 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3043 # cmd line usage (provide a read-only anonymous ftp server):
3044 # python -m pyftpdlib.FTPServer
3045 authorizer = DummyAuthorizer()
3046 authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
3047 FTPHandler.authorizer = authorizer
3048 address = ('', 8021)
3049 ftpd = FTPServer(address, FTPHandler)
3050 ftpd.serve_forever()
3052 if __name__ == '__main__':
3055 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: