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()
551 def handle_expt(self):
552 self.cmd_channel.respond("425 Can't connect to specified address.")
555 def handle_error(self):
556 """Called to handle any uncaught exceptions."""
559 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
564 logerror(traceback.format_exc())
565 self.cmd_channel.respond("425 Can't connect to specified address.")
568 class DTPHandler(asyncore.dispatcher):
569 """Class handling server-data-transfer-process (server-DTP, see
570 RFC-959) managing data-transfer operations involving sending
573 Instance attributes defined in this class, initialized when
576 - (instance) cmd_channel: the command channel class instance.
577 - (file) file_obj: the file transferred (if any).
578 - (bool) receive: True if channel is used for receiving data.
579 - (bool) transfer_finished: True if transfer completed successfully.
580 - (int) tot_bytes_sent: the total bytes sent.
581 - (int) tot_bytes_received: the total bytes received.
583 DTPHandler implementation note:
585 When a producer is consumed and close_when_done() has been called
586 previously, refill_buffer() erroneously calls close() instead of
587 handle_close() - (see: http://bugs.python.org/issue1740572)
589 To avoid this problem DTPHandler is implemented as a subclass of
590 asyncore.dispatcher instead of asynchat.async_chat.
591 This implementation follows the same approach that asynchat module
592 should use in Python 2.6.
594 The most important change in the implementation is related to
595 producer_fifo, which is a pure deque object instead of a
596 producer_fifo instance.
598 Since we don't want to break backward compatibily with older python
599 versions (deque has been introduced in Python 2.4), if deque is not
600 available we use a list instead.
603 ac_in_buffer_size = 8192
604 ac_out_buffer_size = 8192
606 def __init__(self, sock_obj, cmd_channel):
607 """Initialize the command channel.
609 - (instance) sock_obj: the socket object instance of the newly
610 established connection.
611 - (instance) cmd_channel: the command channel class instance.
613 asyncore.dispatcher.__init__(self, sock_obj)
614 # we toss the use of the asynchat's "simple producer" and
615 # replace it with a pure deque, which the original fifo
617 self.producer_fifo = deque()
619 self.cmd_channel = cmd_channel
622 self.transfer_finished = False
623 self.tot_bytes_sent = 0
624 self.tot_bytes_received = 0
625 self.data_wrapper = lambda x: x
627 # --- utility methods
629 def enable_receiving(self, type):
630 """Enable receiving of data over the channel. Depending on the
631 TYPE currently in use it creates an appropriate wrapper for the
634 - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
637 self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
639 self.data_wrapper = lambda x: x
641 raise TypeError, "Unsupported type"
644 def get_transmitted_bytes(self):
645 "Return the number of transmitted bytes."
646 return self.tot_bytes_sent + self.tot_bytes_received
648 def transfer_in_progress(self):
649 "Return True if a transfer is in progress, else False."
650 return self.get_transmitted_bytes() != 0
654 def handle_read(self):
655 """Called when there is data waiting to be read."""
657 chunk = self.recv(self.ac_in_buffer_size)
661 self.tot_bytes_received += len(chunk)
663 self.transfer_finished = True
664 #self.close() # <-- asyncore.recv() already do that...
666 # while we're writing on the file an exception could occur
667 # in case that filesystem gets full; if this happens we
668 # let handle_error() method handle this exception, providing
669 # a detailed error message.
670 self.file_obj.write(self.data_wrapper(chunk))
672 def handle_write(self):
673 """Called when data is ready to be written, initiates send."""
676 def push(self, data):
677 """Push data onto the deque and initiate send."""
678 sabs = self.ac_out_buffer_size
680 for i in xrange(0, len(data), sabs):
681 self.producer_fifo.append(data[i:i+sabs])
683 self.producer_fifo.append(data)
686 def push_with_producer(self, producer):
687 """Push data using a producer and initiate send."""
688 self.producer_fifo.append(producer)
692 """Predicate for inclusion in the readable for select()."""
696 """Predicate for inclusion in the writable for select()."""
697 return self.producer_fifo or (not self.connected)
699 def close_when_done(self):
700 """Automatically close this channel once the outgoing queue is empty."""
701 self.producer_fifo.append(None)
703 def initiate_send(self):
704 """Attempt to send data in fifo order."""
705 while self.producer_fifo and self.connected:
706 first = self.producer_fifo[0]
707 # handle empty string/buffer or None entry
709 del self.producer_fifo[0]
711 self.transfer_finished = True
715 # handle classic producer behavior
716 obs = self.ac_out_buffer_size
718 data = buffer(first, 0, obs)
722 self.producer_fifo.appendleft(data)
724 del self.producer_fifo[0]
729 num_sent = self.send(data)
735 self.tot_bytes_sent += num_sent
736 if num_sent < len(data) or obs < len(first):
737 self.producer_fifo[0] = first[num_sent:]
739 del self.producer_fifo[0]
740 # we tried to send some actual data
743 def handle_expt(self):
744 """Called on "exceptional" data events."""
745 self.cmd_channel.respond("426 Connection error; transfer aborted.")
748 def handle_error(self):
749 """Called when an exception is raised and not otherwise handled."""
752 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
754 except socket.error, err:
755 # fix around asyncore bug (http://bugs.python.org/issue1736101)
756 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
762 # an error could occur in case we fail reading / writing
763 # from / to file (e.g. file system gets full)
764 except EnvironmentError, err:
765 error = _strerror(err)
767 # some other exception occurred; we don't want to provide
768 # confidential error messages
769 logerror(traceback.format_exc())
770 error = "Internal error"
771 self.cmd_channel.respond("426 %s; transfer aborted." %error)
774 def handle_close(self):
775 """Called when the socket is closed."""
776 # If we used channel for receiving we assume that transfer is
777 # finished when client close connection , if we used channel
778 # for sending we have to check that all data has been sent
779 # (responding with 226) or not (responding with 426).
781 self.transfer_finished = True
785 if self.transfer_finished:
786 self.cmd_channel.respond("226 Transfer complete.")
788 fname = self.file_obj.name
789 self.cmd_channel.log('"%s" %s.' %(fname, action))
791 tot_bytes = self.get_transmitted_bytes()
792 msg = "Transfer aborted; %d bytes transmitted." %tot_bytes
793 self.cmd_channel.respond("426 " + msg)
794 self.cmd_channel.log(msg)
798 """Close the data channel, first attempting to close any remaining
800 if self.file_obj and not self.file_obj.closed:
801 self.file_obj.close()
802 asyncore.dispatcher.close(self)
803 self.cmd_channel.on_dtp_close()
809 """Producer wrapper for file[-like] objects."""
813 def __init__(self, file, type):
814 """Initialize the producer with a data_wrapper appropriate to TYPE.
816 - (file) file: the file[-like] object.
817 - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
822 self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
824 self.data_wrapper = lambda x: x
826 raise TypeError, "Unsupported type"
829 """Attempt a chunk of data of size self.buffer_size."""
832 data = self.data_wrapper(self.file.read(self.buffer_size))
835 if not self.file.closed:
840 class IteratorProducer:
841 """Producer for iterator objects."""
843 def __init__(self, iterator):
844 self.iterator = iterator
847 """Attempt a chunk of data from iterator by calling its next()
851 return self.iterator.next()
852 except StopIteration:
856 class BufferedIteratorProducer:
857 """Producer for iterator objects with buffer capabilities."""
858 # how many times iterator.next() will be called before
859 # returning some data
862 def __init__(self, iterator):
863 self.iterator = iterator
866 """Attempt a chunk of data from iterator by calling
867 its next() method different times.
870 for x in xrange(self.loops):
872 buffer.append(self.iterator.next())
873 except StopIteration:
875 return ''.join(buffer)
881 """A class used to interact with the file system, providing a high
882 level, cross-platform interface compatible with both Windows and
883 UNIX style filesystems.
885 It provides some utility methods and some wraps around operations
886 involved in file creation and file system operations like moving
887 files or removing directories.
890 - (str) root: the user home directory.
891 - (str) cwd: the current working directory.
892 - (str) rnfr: source file to be renamed.
900 # --- Pathname / conversion utilities
902 def ftpnorm(self, ftppath):
903 """Normalize a "virtual" ftp pathname (tipically the raw string
904 coming from client) depending on the current working directory.
906 Example (having "/foo" as current working directory):
909 Note: directory separators are system independent ("/").
910 Pathname returned is always absolutized.
912 if os.path.isabs(ftppath):
913 p = os.path.normpath(ftppath)
915 p = os.path.normpath(os.path.join(self.cwd, ftppath))
916 # normalize string in a standard web-path notation having '/'
918 p = p.replace("\\", "/")
919 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
920 # don't need them. In case we get an UNC path we collapse
921 # redundant separators appearing at the beginning of the string
924 # Anti path traversal: don't trust user input, in the event
925 # that self.cwd is not absolute, return "/" as a safety measure.
926 # This is for extra protection, maybe not really necessary.
927 if not os.path.isabs(p):
931 def ftp2fs(self, ftppath):
932 """Translate a "virtual" ftp pathname (tipically the raw string
933 coming from client) into equivalent absolute "real" filesystem
936 Example (having "/home/user" as root directory):
937 'x' -> '/home/user/x'
939 Note: directory separators are system dependent.
941 # as far as I know, it should always be path traversal safe...
942 if os.path.normpath(self.root) == os.sep:
943 return os.path.normpath(self.ftpnorm(ftppath))
945 p = self.ftpnorm(ftppath)[1:]
946 return os.path.normpath(os.path.join(self.root, p))
948 def fs2ftp(self, fspath):
949 """Translate a "real" filesystem pathname into equivalent
950 absolute "virtual" ftp pathname depending on the user's
953 Example (having "/home/user" as root directory):
954 '/home/user/x' -> '/x'
956 As for ftpnorm, directory separators are system independent
957 ("/") and pathname returned is always absolutized.
959 On invalid pathnames escaping from user's root directory
960 (e.g. "/home" when root is "/home/user") always return "/".
962 if os.path.isabs(fspath):
963 p = os.path.normpath(fspath)
965 p = os.path.normpath(os.path.join(self.root, fspath))
966 if not self.validpath(p):
968 p = p.replace(os.sep, "/")
969 p = p[len(self.root):]
970 if not p.startswith('/'):
974 # alias for backward compatibility with 0.2.0
978 def validpath(self, path):
979 """Check whether the path belongs to user's home directory.
980 Expected argument is a "real" filesystem pathname.
982 If path is a symbolic link it is resolved to check its real
985 Pathnames escaping from user's root directory are considered
988 root = self.realpath(self.root)
989 path = self.realpath(path)
990 if not self.root.endswith(os.sep):
991 root = self.root + os.sep
992 if not path.endswith(os.sep):
994 if path[0:len(root)] == root:
998 # --- Wrapper methods around open() and tempfile.mkstemp
1000 def open(self, filename, mode):
1001 """Open a file returning its handler."""
1002 return open(filename, mode)
1004 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
1005 """A wrap around tempfile.mkstemp creating a file with a unique
1006 name. Unlike mkstemp it returns an object with a file-like
1010 def __init__(self, fd, name):
1013 def __getattr__(self, attr):
1014 return getattr(self.file, attr)
1016 text = not 'b' in mode
1017 # max number of tries to find out a unique file name
1018 tempfile.TMP_MAX = 50
1019 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
1020 file = os.fdopen(fd, mode)
1021 return FileWrapper(file, name)
1023 # --- Wrapper methods around os.*
1025 def chdir(self, path):
1026 """Change the current directory."""
1027 # temporarily join the specified directory to see if we have
1028 # permissions to do so
1029 basedir = os.getcwd()
1036 self.cwd = self.fs2ftp(path)
1038 def mkdir(self, path, basename):
1039 """Create the specified directory."""
1040 os.mkdir(os.path.join(path, basename))
1042 def listdir(self, path):
1043 """List the content of a directory."""
1044 return os.listdir(path)
1046 def rmdir(self, path):
1047 """Remove the specified directory."""
1050 def remove(self, path):
1051 """Remove the specified file."""
1054 def rename(self, src, dst):
1055 """Rename the specified src file to the dst filename."""
1058 def stat(self, path):
1059 """Perform a stat() system call on the given path."""
1060 return os.stat(path)
1062 def lstat(self, path):
1063 """Like stat but does not follow symbolic links."""
1064 return os.lstat(path)
1066 if not hasattr(os, 'lstat'):
1069 # --- Wrapper methods around os.path.*
1071 def isfile(self, path):
1072 """Return True if path is a file."""
1073 return os.path.isfile(path)
1075 def islink(self, path):
1076 """Return True if path is a symbolic link."""
1077 return os.path.islink(path)
1079 def isdir(self, path):
1080 """Return True if path is a directory."""
1081 return os.path.isdir(path)
1083 def getsize(self, path):
1084 """Return the size of the specified file in bytes."""
1085 return os.path.getsize(path)
1087 def getmtime(self, path):
1088 """Return the last modified time as a number of seconds since
1090 return os.path.getmtime(path)
1092 def realpath(self, path):
1093 """Return the canonical version of path eliminating any
1094 symbolic links encountered in the path (if they are
1095 supported by the operating system).
1097 return os.path.realpath(path)
1099 def lexists(self, path):
1100 """Return True if path refers to an existing path, including
1101 a broken or circular symbolic link.
1103 if hasattr(os.path, 'lexists'):
1104 return os.path.lexists(path)
1105 # grant backward compatibility with python 2.3
1106 elif hasattr(os, 'lstat'):
1114 return os.path.exists(path)
1116 exists = lexists # alias for backward compatibility with 0.2.0
1118 def glob1(self, dirname, pattern):
1119 """Return a list of files matching a dirname pattern
1122 Unlike glob.glob1 raises exception if os.listdir() fails.
1124 names = self.listdir(dirname)
1125 if pattern[0] != '.':
1126 names = filter(lambda x: x[0] != '.', names)
1127 return fnmatch.filter(names, pattern)
1129 # --- Listing utilities
1131 # note: the following operations are no more blocking
1133 def get_list_dir(self, datacr):
1134 """"Return an iterator object that yields a directory listing
1135 in a form suitable for LIST command.
1137 raise DeprecationWarning()
1139 def get_stat_dir(self, rawline):
1140 """Return an iterator object that yields a list of files
1141 matching a dirname pattern non-recursively in a form
1142 suitable for STAT command.
1144 - (str) rawline: the raw string passed by client as command
1147 ftppath = self.ftpnorm(rawline)
1148 if not glob.has_magic(ftppath):
1149 return self.get_list_dir(self.ftp2fs(rawline))
1151 basedir, basename = os.path.split(ftppath)
1152 if glob.has_magic(basedir):
1153 return iter(['Directory recursion not supported.\r\n'])
1155 basedir = self.ftp2fs(basedir)
1156 listing = self.glob1(basedir, basename)
1159 return self.format_list(basedir, listing)
1161 def format_list(self, basedir, listing, ignore_err=True):
1162 """Return an iterator object that yields the entries of given
1163 directory emulating the "/bin/ls -lA" UNIX command output.
1165 - (str) basedir: the absolute dirname.
1166 - (list) listing: the names of the entries in basedir
1167 - (bool) ignore_err: when False raise exception if os.lstat()
1170 On platforms which do not support the pwd and grp modules (such
1171 as Windows), ownership is printed as "owner" and "group" as a
1172 default, and number of hard links is always "1". On UNIX
1173 systems, the actual owner, group, and number of links are
1176 This is how output appears to client:
1178 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
1179 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
1180 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
1182 for basename in listing:
1183 file = os.path.join(basedir, basename)
1185 st = self.lstat(file)
1190 perms = filemode(st.st_mode) # permissions
1191 nlinks = st.st_nlink # number of links to inode
1192 if not nlinks: # non-posix system, let's use a bogus value
1194 size = st.st_size # file size
1195 uname = st.st_uid or "owner"
1196 gname = st.st_gid or "group"
1198 # stat.st_mtime could fail (-1) if last mtime is too old
1199 # in which case we return the local time as last mtime
1201 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1203 mtime = time.strftime("%b %d %H:%M")
1204 # if the file is a symlink, resolve it, e.g. "symlink -> realfile"
1205 if stat.S_ISLNK(st.st_mode):
1206 basename = basename + " -> " + os.readlink(file)
1208 # formatting is matched with proftpd ls output
1209 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
1210 size, mtime, basename)
1212 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
1213 """Return an iterator object that yields the entries of a given
1214 directory or of a single file in a form suitable with MLSD and
1217 Every entry includes a list of "facts" referring the listed
1218 element. See RFC-3659, chapter 7, to see what every single
1221 - (str) basedir: the absolute dirname.
1222 - (list) listing: the names of the entries in basedir
1223 - (str) perms: the string referencing the user permissions.
1224 - (str) facts: the list of "facts" to be returned.
1225 - (bool) ignore_err: when False raise exception if os.stat()
1228 Note that "facts" returned may change depending on the platform
1229 and on what user specified by using the OPTS command.
1231 This is how output could appear to the client issuing
1234 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
1235 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
1236 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
1238 permdir = ''.join([x for x in perms if x not in 'arw'])
1239 permfile = ''.join([x for x in perms if x not in 'celmp'])
1240 if ('w' in perms) or ('a' in perms) or ('f' in perms):
1244 type = size = perm = modify = create = unique = mode = uid = gid = ""
1245 for basename in listing:
1246 file = os.path.join(basedir, basename)
1248 st = self.stat(file)
1254 if stat.S_ISDIR(st.st_mode):
1258 elif basename == '..':
1263 perm = 'perm=%s;' %permdir
1268 perm = 'perm=%s;' %permfile
1270 size = 'size=%s;' %st.st_size # file size
1271 # last modification time
1272 if 'modify' in facts:
1274 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1275 time.localtime(st.st_mtime))
1277 # stat.st_mtime could fail (-1) if last mtime is too old
1279 if 'create' in facts:
1280 # on Windows we can provide also the creation time
1282 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1283 time.localtime(st.st_ctime))
1287 if 'unix.mode' in facts:
1288 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
1289 if 'unix.uid' in facts:
1290 uid = 'unix.uid=%s;' %st.st_uid
1291 if 'unix.gid' in facts:
1292 gid = 'unix.gid=%s;' %st.st_gid
1293 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
1294 # posix platforms only; we get it by mixing st_dev and
1295 # st_ino values which should be enough for granting an
1296 # uniqueness for the file listed.
1297 # The same approach is used by pure-ftpd.
1298 # Implementors who want to provide unique fact on other
1299 # platforms should use some platform-specific method (e.g.
1300 # on Windows NTFS filesystems MTF records could be used).
1301 if 'unique' in facts:
1302 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
1304 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
1305 mode, uid, gid, unique, basename)
1310 class FTPExceptionSent(Exception):
1311 """An FTP exception that FTPHandler has processed
1315 class FTPHandler(asynchat.async_chat):
1316 """Implements the FTP server Protocol Interpreter (see RFC-959),
1317 handling commands received from the client on the control channel.
1319 All relevant session information is stored in class attributes
1320 reproduced below and can be modified before instantiating this
1323 - (str) banner: the string sent when client connects.
1325 - (int) max_login_attempts:
1326 the maximum number of wrong authentications before disconnecting
1327 the client (default 3).
1329 - (bool)permit_foreign_addresses:
1330 FTP site-to-site transfer feature: also referenced as "FXP" it
1331 permits for transferring a file between two remote FTP servers
1332 without the transfer going through the client's host (not
1333 recommended for security reasons as described in RFC-2577).
1334 Having this attribute set to False means that all data
1335 connections from/to remote IP addresses which do not match the
1336 client's IP address will be dropped (defualt False).
1338 - (bool) permit_privileged_ports:
1339 set to True if you want to permit active data connections (PORT)
1340 over privileged ports (not recommended, defaulting to False).
1342 - (str) masquerade_address:
1343 the "masqueraded" IP address to provide along PASV reply when
1344 pyftpdlib is running behind a NAT or other types of gateways.
1345 When configured pyftpdlib will hide its local address and
1346 instead use the public address of your NAT (default None).
1348 - (list) passive_ports:
1349 what ports ftpd will use for its passive data transfers.
1350 Value expected is a list of integers (e.g. range(60000, 65535)).
1351 When configured pyftpdlib will no longer use kernel-assigned
1352 random ports (default None).
1355 All relevant instance attributes initialized when client connects
1356 are reproduced below. You may be interested in them in case you
1357 want to subclass the original FTPHandler.
1359 - (bool) authenticated: True if client authenticated himself.
1360 - (str) username: the name of the connected user (if any).
1361 - (int) attempted_logins: number of currently attempted logins.
1362 - (str) current_type: the current transfer type (default "a")
1363 - (int) af: the address family (IPv4/IPv6)
1364 - (instance) server: the FTPServer class instance.
1365 - (instance) data_server: the data server instance (if any).
1366 - (instance) data_channel: the data channel instance (if any).
1368 # these are overridable defaults
1371 authorizer = DummyAuthorizer()
1372 active_dtp = ActiveDTP
1373 passive_dtp = PassiveDTP
1374 dtp_handler = DTPHandler
1375 abstracted_fs = AbstractedFS
1377 # session attributes (explained in the docstring)
1378 banner = "pyftpdlib %s ready." %__ver__
1379 max_login_attempts = 3
1380 permit_foreign_addresses = False
1381 permit_privileged_ports = False
1382 masquerade_address = None
1383 passive_ports = None
1385 def __init__(self, conn, server):
1386 """Initialize the command channel.
1388 - (instance) conn: the socket object instance of the newly
1389 established connection.
1390 - (instance) server: the ftp server class instance.
1393 asynchat.async_chat.__init__(self, conn=conn) # python2.5
1395 asynchat.async_chat.__init__(self, sock=conn) # python2.6
1396 self.server = server
1397 self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1399 self.in_buffer_len = 0
1400 self.set_terminator("\r\n")
1402 # session attributes
1403 self.fs = self.abstracted_fs()
1404 self.authenticated = False
1407 self.attempted_logins = 0
1408 self.current_type = 'a'
1409 self.restart_position = 0
1410 self.quit_pending = False
1411 self._epsvall = False
1412 self.__in_dtp_queue = None
1413 self.__out_dtp_queue = None
1415 self.__errno_responses = {
1423 # mlsx facts attributes
1424 self.current_facts = ['type', 'perm', 'size', 'modify']
1425 self.current_facts.append('unique')
1426 self.available_facts = self.current_facts[:]
1427 self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1428 self.available_facts.append('create')
1431 self.data_server = None
1432 self.data_channel = None
1434 if hasattr(self.socket, 'family'):
1435 self.af = self.socket.family
1436 else: # python < 2.5
1437 ip, port = self.socket.getsockname()[:2]
1438 self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
1439 socket.SOCK_STREAM)[0][0]
1442 """Return a 220 'Ready' response to the client over the command
1445 if len(self.banner) <= 75:
1446 self.respond("220 %s" %str(self.banner))
1448 self.push('220-%s\r\n' %str(self.banner))
1449 self.respond('220 ')
1451 def handle_max_cons(self):
1452 """Called when limit for maximum number of connections is reached."""
1453 msg = "Too many connections. Service temporary unavailable."
1454 self.respond("421 %s" %msg)
1456 # If self.push is used, data could not be sent immediately in
1457 # which case a new "loop" will occur exposing us to the risk of
1458 # accepting new connections. Since this could cause asyncore to
1459 # run out of fds (...and exposes the server to DoS attacks), we
1460 # immediately close the channel by using close() instead of
1461 # close_when_done(). If data has not been sent yet client will
1462 # be silently disconnected.
1465 def handle_max_cons_per_ip(self):
1466 """Called when too many clients are connected from the same IP."""
1467 msg = "Too many connections from the same IP address."
1468 self.respond("421 %s" %msg)
1470 self.close_when_done()
1472 # --- asyncore / asynchat overridden methods
1475 # if there's a quit pending we stop reading data from socket
1476 return not self.quit_pending
1478 def collect_incoming_data(self, data):
1479 """Read incoming data and append to the input buffer."""
1480 self.in_buffer.append(data)
1481 self.in_buffer_len += len(data)
1482 # Flush buffer if it gets too long (possible DoS attacks).
1483 # RFC-959 specifies that a 500 response could be given in
1486 if self.in_buffer_len > buflimit:
1487 self.respond('500 Command too long.')
1488 self.log('Command received exceeded buffer limit of %s.' %(buflimit))
1490 self.in_buffer_len = 0
1492 # commands accepted before authentication
1493 unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1495 # commands needing an argument
1496 arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
1497 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
1498 'TYPE','USER','XMKD','XRMD')
1500 # commands needing no argument
1501 unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1502 'SYST','XCUP','XPWD')
1504 def found_terminator(self):
1505 r"""Called when the incoming data stream matches the \r\n
1508 Depending on the command received it calls the command's
1509 corresponding method (e.g. for received command "MKD pathname",
1510 ftp_MKD() method is called with "pathname" as the argument).
1512 line = ''.join(self.in_buffer)
1514 self.in_buffer_len = 0
1516 cmd = line.split(' ')[0].upper()
1517 space = line.find(' ')
1519 arg = line[space + 1:]
1524 self.logline("<== %s" %line)
1526 self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1528 # let's check if user provided an argument for those commands
1530 if not arg and cmd in self.arg_cmds:
1531 self.respond("501 Syntax error: command needs an argument.")
1534 # let's do the same for those commands requiring no argument.
1535 elif arg and cmd in self.unarg_cmds:
1536 self.respond("501 Syntax error: command does not accept arguments.")
1539 # provide a limited set of commands if user isn't
1541 if (not self.authenticated):
1542 if cmd in self.unauth_cmds:
1543 # we permit STAT during this phase but we don't want
1544 # STAT to return a directory LISTing if the user is
1545 # not authenticated yet (this could happen if STAT
1546 # is used with an argument)
1547 if (cmd == 'STAT') and arg:
1548 self.respond("530 Log in with USER and PASS first.")
1550 method = getattr(self, 'ftp_' + cmd)
1551 method(arg) # call the proper ftp_* method
1552 elif cmd in proto_cmds:
1553 self.respond("530 Log in with USER and PASS first.")
1555 self.respond('500 Command "%s" not understood.' %line)
1557 # provide full command set
1558 elif (self.authenticated) and (cmd in proto_cmds):
1559 if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
1561 method = getattr(self, 'ftp_' + cmd)
1562 method(arg) # call the proper ftp_* method
1565 # recognize those commands having "special semantics"
1572 self.respond('500 Command "%s" not understood.' %line)
1574 def __check_path(self, cmd, line):
1575 """Check whether a path is valid."""
1577 # Always true, we will only check later, once we have a cursor
1580 def __check_perm(self, cmd, line, datacr):
1581 """Check permissions depending on issued command."""
1582 map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
1583 'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
1586 'DELE':'d', 'RMD':'d', 'XRMD':'d',
1588 'MKD':'m', 'XMKD':'m',
1590 raise NotImplementedError
1592 if cmd == 'STAT' and not line:
1595 if not line and (cmd in ('LIST','NLST','MLSD')):
1596 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1598 path = self.fs.ftp2fs(line, datacr)
1599 if not self.authorizer.has_perm(self.username, perm, path):
1600 self.log('FAIL %s "%s". Not enough privileges.' \
1601 %(cmd, self.fs.ftpnorm(line)))
1602 self.respond("550 Can't %s. Not enough privileges." %cmd)
1606 def handle_expt(self):
1607 """Called when there is out of band (OOB) data for the socket
1608 connection. This could happen in case of such commands needing
1609 "special action" (typically STAT and ABOR) in which case we
1610 append OOB data to incoming buffer.
1612 if hasattr(socket, 'MSG_OOB'):
1614 data = self.socket.recv(1024, socket.MSG_OOB)
1615 except socket.error:
1618 self.in_buffer.append(data)
1620 self.log("Can't handle OOB data.")
1623 def handle_error(self):
1626 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1628 except socket.error, err:
1629 # fix around asyncore bug (http://bugs.python.org/issue1736101)
1630 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
1631 errno.ECONNABORTED):
1635 logerror(traceback.format_exc())
1637 logerror(traceback.format_exc())
1640 def handle_close(self):
1645 """Close the current channel disconnecting the client."""
1646 if not self._closed:
1648 if self.data_server:
1649 self.data_server.close()
1650 del self.data_server
1652 if self.data_channel:
1653 self.data_channel.close()
1654 del self.data_channel
1656 del self.__out_dtp_queue
1657 del self.__in_dtp_queue
1659 # remove client IP address from ip map
1660 self.server.ip_map.remove(self.remote_ip)
1661 asynchat.async_chat.close(self)
1662 self.log("Disconnected.")
1666 def on_dtp_connection(self):
1667 """Called every time data channel connects (either active or
1670 Incoming and outgoing queues are checked for pending data.
1671 If outbound data is pending, it is pushed into the data channel.
1672 If awaiting inbound data, the data channel is enabled for
1675 if self.data_server:
1676 self.data_server.close()
1677 self.data_server = None
1679 # check for data to send
1680 if self.__out_dtp_queue:
1681 data, isproducer, file = self.__out_dtp_queue
1683 self.data_channel.file_obj = file
1685 self.data_channel.push(data)
1687 self.data_channel.push_with_producer(data)
1688 if self.data_channel:
1689 self.data_channel.close_when_done()
1690 self.__out_dtp_queue = None
1692 # check for data to receive
1693 elif self.__in_dtp_queue:
1694 self.data_channel.file_obj = self.__in_dtp_queue
1695 self.data_channel.enable_receiving(self.current_type)
1696 self.__in_dtp_queue = None
1698 def on_dtp_close(self):
1699 """Called every time the data channel is closed."""
1700 self.data_channel = None
1701 if self.quit_pending:
1702 self.close_when_done()
1706 def respond(self, resp):
1707 """Send a response to the client using the command channel."""
1708 self.push(resp + '\r\n')
1709 self.logline('==> %s' % resp)
1711 def push_dtp_data(self, data, isproducer=False, file=None):
1712 """Pushes data into the data channel.
1714 It is usually called for those commands requiring some data to
1715 be sent over the data channel (e.g. RETR).
1716 If data channel does not exist yet, it queues the data to send
1717 later; data will then be pushed into data channel when
1718 on_dtp_connection() will be called.
1720 - (str/classobj) data: the data to send which may be a string
1721 or a producer object).
1722 - (bool) isproducer: whether treat data as a producer.
1723 - (file) file: the file[-like] object to send (if any).
1725 if self.data_channel:
1726 self.respond("125 Data connection already open. Transfer starting.")
1728 self.data_channel.file_obj = file
1730 self.data_channel.push(data)
1732 self.data_channel.push_with_producer(data)
1733 if self.data_channel:
1734 self.data_channel.close_when_done()
1736 self.respond("150 File status okay. About to open data connection.")
1737 self.__out_dtp_queue = (data, isproducer, file)
1740 """Log a message, including additional identifying session data."""
1741 log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
1742 self.remote_port, msg))
1744 def logline(self, msg):
1745 """Log a line including additional indentifying session data."""
1746 logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg))
1748 def flush_account(self):
1749 """Flush account information by clearing attributes that need
1750 to be reset on a REIN or new USER command.
1752 if self.data_channel:
1753 if not self.data_channel.transfer_in_progress():
1754 self.data_channel.close()
1755 self.data_channel = None
1756 if self.data_server:
1757 self.data_server.close()
1758 self.data_server = None
1761 self.authenticated = False
1764 self.attempted_logins = 0
1765 self.current_type = 'a'
1766 self.restart_position = 0
1767 self.quit_pending = False
1768 self.__in_dtp_queue = None
1769 self.__out_dtp_queue = None
1771 def run_as_current_user(self, function, *args, **kwargs):
1772 """Execute a function impersonating the current logged-in user."""
1773 self.authorizer.impersonate_user(self.username, self.password)
1775 return function(*args, **kwargs)
1777 self.authorizer.terminate_impersonation()
1781 def try_as_current_user(self, function, args=None, kwargs=None, line=None, errno_resp=None):
1782 """run function as current user, auto-respond in exceptions
1783 @param args,kwargs the arguments, in list and dict respectively
1784 @param errno_resp a dictionary of responses to IOError, OSError
1787 eresp = self.__errno_responses.copy()
1788 eresp.update(errno_resp)
1790 eresp = self.__errno_responses
1794 uline = ' "%s"' % _to_unicode(line)
1800 return self.run_as_current_user(function, *args, **kwargs)
1801 except NotImplementedError, err:
1802 cmdname = function.__name__
1803 why = err.args[0] or 'Not implemented'
1804 self.log('FAIL %s() not implemented: %s.' %(cmdname, why))
1805 self.respond('502 %s.' %why)
1806 raise FTPExceptionSent(why)
1807 except EnvironmentError, err:
1808 cmdname = function.__name__
1810 logline(traceback.format_exc())
1813 ret_code = eresp.get(err.errno, '451')
1814 why = (err.strerror) or 'Error in command'
1815 self.log('FAIL %s() %s errno=%s: %s.' %(cmdname, uline, err.errno, why))
1816 self.respond('%s %s.' % (str(ret_code), why))
1818 raise FTPExceptionSent(why)
1819 except Exception, err:
1820 cmdname = function.__name__
1822 logerror(traceback.format_exc())
1825 why = (err.args and err.args[0]) or 'Exception'
1826 self.log('FAIL %s() %s Exception: %s.' %(cmdname, uline, why))
1827 self.respond('451 %s.' % why)
1828 raise FTPExceptionSent(why)
1830 def get_crdata2(self, *args, **kwargs):
1831 return self.try_as_current_user(self.fs.get_crdata, args, kwargs, line=args[0])
1833 def _make_eport(self, ip, port):
1834 """Establish an active data channel with remote client which
1835 issued a PORT or EPRT command.
1837 # FTP bounce attacks protection: according to RFC-2577 it's
1838 # recommended to reject PORT if IP address specified in it
1839 # does not match client IP address.
1840 if not self.permit_foreign_addresses:
1841 if ip != self.remote_ip:
1842 self.log("Rejected data connection to foreign address %s:%s."
1844 self.respond("501 Can't connect to a foreign address.")
1847 # ...another RFC-2577 recommendation is rejecting connections
1848 # to privileged ports (< 1024) for security reasons.
1849 if not self.permit_privileged_ports:
1851 self.log('PORT against the privileged port "%s" refused.' %port)
1852 self.respond("501 Can't connect over a privileged port.")
1855 # close existent DTP-server instance, if any.
1856 if self.data_server:
1857 self.data_server.close()
1858 self.data_server = None
1859 if self.data_channel:
1860 self.data_channel.close()
1861 self.data_channel = None
1863 # make sure we are not hitting the max connections limit
1864 if self.server.max_cons:
1865 if len(self._map) >= self.server.max_cons:
1866 msg = "Too many connections. Can't open data channel."
1867 self.respond("425 %s" %msg)
1872 self.active_dtp(ip, port, self)
1874 def _make_epasv(self, extmode=False):
1875 """Initialize a passive data channel with remote client which
1876 issued a PASV or EPSV command.
1877 If extmode argument is False we assume that client issued EPSV in
1878 which case extended passive mode will be used (see RFC-2428).
1880 # close existing DTP-server instance, if any
1881 if self.data_server:
1882 self.data_server.close()
1883 self.data_server = None
1885 if self.data_channel:
1886 self.data_channel.close()
1887 self.data_channel = None
1889 # make sure we are not hitting the max connections limit
1890 if self.server.max_cons:
1891 if len(self._map) >= self.server.max_cons:
1892 msg = "Too many connections. Can't open data channel."
1893 self.respond("425 %s" %msg)
1898 self.data_server = self.passive_dtp(self, extmode)
1900 def ftp_PORT(self, line):
1901 """Start an active data channel by using IPv4."""
1903 self.respond("501 PORT not allowed after EPSV ALL.")
1905 if self.af != socket.AF_INET:
1906 self.respond("425 You cannot use PORT on IPv6 connections. "
1907 "Use EPRT instead.")
1909 # Parse PORT request for getting IP and PORT.
1910 # Request comes in as:
1911 # > h1,h2,h3,h4,p1,p2
1912 # ...where the client's IP address is h1.h2.h3.h4 and the TCP
1913 # port number is (p1 * 256) + p2.
1915 addr = map(int, line.split(','))
1916 assert len(addr) == 6
1918 assert 0 <= x <= 255
1919 ip = '%d.%d.%d.%d' %tuple(addr[:4])
1920 port = (addr[4] * 256) + addr[5]
1921 assert 0 <= port <= 65535
1922 except (AssertionError, ValueError, OverflowError):
1923 self.respond("501 Invalid PORT format.")
1925 self._make_eport(ip, port)
1927 def ftp_EPRT(self, line):
1928 """Start an active data channel by choosing the network protocol
1929 to use (IPv4/IPv6) as defined in RFC-2428.
1932 self.respond("501 EPRT not allowed after EPSV ALL.")
1934 # Parse EPRT request for getting protocol, IP and PORT.
1935 # Request comes in as:
1936 # # <d>proto<d>ip<d>port<d>
1937 # ...where <d> is an arbitrary delimiter character (usually "|") and
1938 # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
1940 af, ip, port = line.split(line[0])[1:-1]
1942 assert 0 <= port <= 65535
1943 except (AssertionError, ValueError, IndexError, OverflowError):
1944 self.respond("501 Invalid EPRT format.")
1948 if self.af != socket.AF_INET:
1949 self.respond('522 Network protocol not supported (use 2).')
1952 octs = map(int, ip.split('.'))
1953 assert len(octs) == 4
1955 assert 0 <= x <= 255
1956 except (AssertionError, ValueError, OverflowError):
1957 self.respond("501 Invalid EPRT format.")
1959 self._make_eport(ip, port)
1961 if self.af == socket.AF_INET:
1962 self.respond('522 Network protocol not supported (use 1).')
1964 self._make_eport(ip, port)
1966 if self.af == socket.AF_INET:
1967 self.respond('501 Unknown network protocol (use 1).')
1969 self.respond('501 Unknown network protocol (use 2).')
1971 def ftp_PASV(self, line):
1972 """Start a passive data channel by using IPv4."""
1974 self.respond("501 PASV not allowed after EPSV ALL.")
1976 if self.af != socket.AF_INET:
1977 self.respond("425 You cannot use PASV on IPv6 connections. "
1978 "Use EPSV instead.")
1980 self._make_epasv(extmode=False)
1982 def ftp_EPSV(self, line):
1983 """Start a passive data channel by using IPv4 or IPv6 as defined
1986 # RFC-2428 specifies that if an optional parameter is given,
1987 # we have to determine the address family from that otherwise
1988 # use the same address family used on the control connection.
1989 # In such a scenario a client may use IPv4 on the control channel
1990 # and choose to use IPv6 for the data channel.
1991 # But how could we use IPv6 on the data channel without knowing
1992 # which IPv6 address to use for binding the socket?
1993 # Unfortunately RFC-2428 does not provide satisfing information
1994 # on how to do that. The assumption is that we don't have any way
1995 # to know which address to use, hence we just use the same address
1996 # family used on the control connection.
1998 self._make_epasv(extmode=True)
2000 if self.af != socket.AF_INET:
2001 self.respond('522 Network protocol not supported (use 2).')
2003 self._make_epasv(extmode=True)
2005 if self.af == socket.AF_INET:
2006 self.respond('522 Network protocol not supported (use 1).')
2008 self._make_epasv(extmode=True)
2009 elif line.lower() == 'all':
2010 self._epsvall = True
2011 self.respond('220 Other commands other than EPSV are now disabled.')
2013 if self.af == socket.AF_INET:
2014 self.respond('501 Unknown network protocol (use 1).')
2016 self.respond('501 Unknown network protocol (use 2).')
2018 def ftp_QUIT(self, line):
2019 """Quit the current session."""
2021 # This command terminates a USER and if file transfer is not
2022 # in progress, the server closes the control connection.
2023 # If file transfer is in progress, the connection will remain
2024 # open for result response and the server will then close it.
2025 if self.authenticated:
2026 msg_quit = self.authorizer.get_msg_quit(self.username)
2028 msg_quit = "Goodbye."
2029 if len(msg_quit) <= 75:
2030 self.respond("221 %s" %msg_quit)
2032 self.push("221-%s\r\n" %msg_quit)
2033 self.respond("221 ")
2035 if not self.data_channel:
2036 self.close_when_done()
2038 # tell the cmd channel to stop responding to commands.
2039 self.quit_pending = True
2042 # --- data transferring
2044 def ftp_LIST(self, line):
2045 """Return a list of files in the specified directory to the
2048 # - If no argument, fall back on cwd as default.
2049 # - Some older FTP clients erroneously issue /bin/ls-like LIST
2050 # formats in which case we fall back on cwd as default.
2051 if not line or line.lower() in ('-a', '-l', '-al', '-la'):
2055 datacr = self.get_crdata2(line, mode='list')
2056 iterator = self.try_as_current_user(self.fs.get_list_dir, (datacr,))
2057 except FTPExceptionSent:
2058 self.fs.close_cr(datacr)
2062 self.log('OK LIST "%s". Transfer starting.' % line)
2063 producer = BufferedIteratorProducer(iterator)
2064 self.push_dtp_data(producer, isproducer=True)
2066 self.fs.close_cr(datacr)
2069 def ftp_NLST(self, line):
2070 """Return a list of files in the specified directory in a
2071 compact form to the client.
2078 datacr = self.get_crdata2(line, mode='list')
2080 datacr = ( None, None, None )
2081 if self.fs.isdir(datacr[1]):
2082 nodelist = self.try_as_current_user(self.fs.listdir, (datacr,))
2084 # if path is a file we just list its name
2085 nodelist = [datacr[1],]
2089 if isinstance(nl.path, (list, tuple)):
2090 listing.append(nl.path[-1])
2092 listing.append(nl.path) # assume string
2093 except FTPExceptionSent:
2094 self.fs.close_cr(datacr)
2097 self.fs.close_cr(datacr)
2101 data = ''.join([ _to_decode(x) + '\r\n' for x in listing ])
2102 self.log('OK NLST "%s". Transfer starting.' %line)
2103 self.push_dtp_data(data)
2105 # --- MLST and MLSD commands
2107 # The MLST and MLSD commands are intended to standardize the file and
2108 # directory information returned by the server-FTP process. These
2109 # commands differ from the LIST command in that the format of the
2110 # replies is strictly defined although extensible.
2112 def ftp_MLST(self, line):
2113 """Return information about a pathname in a machine-processable
2114 form as defined in RFC-3659.
2116 # if no argument, fall back on cwd as default
2121 datacr = self.get_crdata2(line, mode='list')
2122 perms = self.authorizer.get_perms(self.username)
2123 iterator = self.try_as_current_user(self.fs.format_mlsx, (datacr[0], datacr[1].parent,
2124 [datacr[1],], perms, self.current_facts), {'ignore_err':False})
2125 data = ''.join(iterator)
2126 except FTPExceptionSent:
2127 self.fs.close_cr(datacr)
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.get_crdata2(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.try_as_current_user(self.fs.listdir, (datacr,))
2158 except FTPExceptionSent:
2159 self.fs.close_cr(datacr)
2162 self.fs.close_cr(datacr)
2163 perms = self.authorizer.get_perms(self.username)
2164 iterator = self.fs.format_mlsx(datacr[0], datacr[1], listing, perms,
2166 producer = BufferedIteratorProducer(iterator)
2167 self.log('OK MLSD "%s". Transfer starting.' %line)
2168 self.push_dtp_data(producer, isproducer=True)
2170 def ftp_RETR(self, line):
2171 """Retrieve the specified file (transfer from the server to the
2176 datacr = self.get_crdata2(line, mode='file')
2177 fd = self.try_as_current_user(self.fs.open, (datacr, 'rb'))
2178 except FTPExceptionSent:
2179 self.fs.close_cr(datacr)
2182 if self.restart_position:
2183 # Make sure that the requested offset is valid (within the
2184 # size of the file being resumed).
2185 # According to RFC-1123 a 554 reply may result in case that
2186 # the existing file cannot be repositioned as specified in
2190 assert not self.restart_position > self.fs.getsize(datacr)
2191 fd.seek(self.restart_position)
2193 except AssertionError:
2194 why = "Invalid REST parameter"
2195 except IOError, err:
2196 why = _strerror(err)
2197 self.restart_position = 0
2199 self.respond('554 %s' %why)
2200 self.log('FAIL RETR "%s". %s.' %(line, why))
2201 self.fs.close_cr(datacr)
2203 self.log('OK RETR "%s". Download starting.' %line)
2204 producer = FileProducer(fd, self.current_type)
2205 self.push_dtp_data(producer, isproducer=True, file=fd)
2206 self.fs.close_cr(datacr)
2208 def ftp_STOR(self, line, mode='w'):
2209 """Store a file (transfer from the client to the server)."""
2210 # A resume could occur in case of APPE or REST commands.
2211 # In that case we have to open file object in different ways:
2214 # REST: mode = 'r+' (to permit seeking on file object)
2222 datacr = self.get_crdata2(line,mode='create')
2223 if self.restart_position:
2225 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 size = self.try_as_current_user(self.fs.getsize,(datacr,), line=line)
2536 except FTPExceptionSent:
2537 self.fs.close_cr(datacr)
2540 self.respond("213 %s" %size)
2541 self.log('OK SIZE "%s".' %line)
2542 self.fs.close_cr(datacr)
2544 def ftp_MDTM(self, line):
2545 """Return last modification time of file to the client as an ISO
2546 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2551 if line.find('/', 1) < 0:
2552 # root or db, just return local
2555 datacr = self.get_crdata2(line)
2557 raise IOError(errno.ENOENT, "%s is not retrievable" %line)
2559 lmt = self.try_as_current_user(self.fs.getmtime, (datacr,), line=line)
2560 lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
2561 self.respond("213 %s" %lmt)
2562 self.log('OK MDTM "%s".' %line)
2563 except FTPExceptionSent:
2566 self.fs.close_cr(datacr)
2568 def ftp_MKD(self, line):
2569 """Create the specified directory."""
2571 datacr = self.get_crdata2(line, mode='create')
2572 self.try_as_current_user(self.fs.mkdir, (datacr, datacr[2]), line=line)
2573 except FTPExceptionSent:
2574 self.fs.close_cr(datacr)
2577 self.log('OK MKD "%s".' %line)
2578 self.respond("257 Directory created.")
2579 self.fs.close_cr(datacr)
2581 def ftp_RMD(self, line):
2582 """Remove the specified directory."""
2585 datacr = self.get_crdata2(line, mode='delete')
2587 msg = "Can't remove root directory."
2588 self.respond("553 %s" %msg)
2589 self.log('FAIL MKD "/". %s' %msg)
2590 self.fs.close_cr(datacr)
2592 self.try_as_current_user(self.fs.rmdir, (datacr,), line=line)
2593 self.log('OK RMD "%s".' %line)
2594 self.respond("250 Directory removed.")
2595 except FTPExceptionSent:
2597 self.fs.close_cr(datacr)
2599 def ftp_DELE(self, line):
2600 """Delete the specified file."""
2603 datacr = self.get_crdata2(line, mode='delete')
2604 self.try_as_current_user(self.fs.remove, (datacr,), line=line)
2605 self.log('OK DELE "%s".' %line)
2606 self.respond("250 File removed.")
2607 except FTPExceptionSent:
2609 self.fs.close_cr(datacr)
2611 def ftp_RNFR(self, line):
2612 """Rename the specified (only the source name is specified
2613 here, see RNTO command)"""
2616 datacr = self.get_crdata2(line, mode='rfnr')
2618 self.respond("550 No such file or directory.")
2620 self.respond("553 Can't rename the home directory.")
2622 self.fs.rnfr = datacr[1]
2623 self.respond("350 Ready for destination name.")
2624 except FTPExceptionSent:
2626 self.fs.close_cr(datacr)
2628 def ftp_RNTO(self, line):
2629 """Rename file (destination name only, source is specified with
2632 if not self.fs.rnfr:
2633 self.respond("503 Bad sequence of commands: use RNFR first.")
2637 datacr = self.get_crdata2(line,'create')
2638 oldname = self.fs.rnfr.path
2639 if isinstance(oldname, (list, tuple)):
2640 oldname = '/'.join(oldname)
2641 self.try_as_current_user(self.fs.rename, (self.fs.rnfr, datacr), line=line)
2643 self.log('OK RNFR/RNTO "%s ==> %s".' % \
2644 (_to_unicode(oldname), _to_unicode(line)))
2645 self.respond("250 Renaming ok.")
2646 except FTPExceptionSent:
2650 self.fs.close_cr(datacr)
2655 def ftp_TYPE(self, line):
2656 """Set current type data type to binary/ascii"""
2658 if line in ("A", "AN", "A N"):
2659 self.respond("200 Type set to: ASCII.")
2660 self.current_type = 'a'
2661 elif line in ("I", "L8", "L 8"):
2662 self.respond("200 Type set to: Binary.")
2663 self.current_type = 'i'
2665 self.respond('504 Unsupported type "%s".' %line)
2667 def ftp_STRU(self, line):
2668 """Set file structure (obsolete)."""
2669 # obsolete (backward compatibility with older ftp clients)
2670 if line in ('f','F'):
2671 self.respond('200 File transfer structure set to: F.')
2673 self.respond('504 Unimplemented STRU type.')
2675 def ftp_MODE(self, line):
2676 """Set data transfer mode (obsolete)"""
2677 # obsolete (backward compatibility with older ftp clients)
2678 if line in ('s', 'S'):
2679 self.respond('200 Transfer mode set to: S')
2681 self.respond('504 Unimplemented MODE type.')
2683 def ftp_STAT(self, line):
2684 """Return statistics about current ftp session. If an argument
2685 is provided return directory listing over command channel.
2687 Implementation note:
2689 RFC-959 do not explicitly mention globbing; this means that FTP
2690 servers are not required to support globbing in order to be
2691 compliant. However, many FTP servers do support globbing as a
2692 measure of convenience for FTP clients and users.
2694 In order to search for and match the given globbing expression,
2695 the code has to search (possibly) many directories, examine
2696 each contained filename, and build a list of matching files in
2697 memory. Since this operation can be quite intensive, both CPU-
2698 and memory-wise, we limit the search to only one directory
2699 non-recursively, as LIST does.
2701 # return STATus information about ftpd
2704 s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2705 if self.authenticated:
2706 s.append('Logged in as: %s' %self.username)
2708 if not self.username:
2709 s.append("Waiting for username.")
2711 s.append("Waiting for password.")
2712 if self.current_type == 'a':
2716 s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
2717 if self.data_server:
2718 s.append('Passive data channel waiting for connection.')
2719 elif self.data_channel:
2720 bytes_sent = self.data_channel.tot_bytes_sent
2721 bytes_recv = self.data_channel.tot_bytes_received
2722 s.append('Data connection open:')
2723 s.append('Total bytes sent: %s' %bytes_sent)
2724 s.append('Total bytes received: %s' %bytes_recv)
2726 s.append('Data connection closed.')
2728 self.push('211-FTP server status:\r\n')
2729 self.push(''.join([' %s\r\n' %item for item in s]))
2730 self.respond('211 End of status.')
2731 # return directory LISTing over the command channel
2735 datacr = self.fs.get_cr(line)
2736 iterator = self.try_as_current_user(self.fs.get_stat_dir, (line, datacr), line=line)
2737 except FTPExceptionSent:
2740 self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
2741 self.push_with_producer(BufferedIteratorProducer(iterator))
2742 self.respond('213 End of status.')
2743 self.fs.close_cr(datacr)
2745 def ftp_FEAT(self, line):
2746 """List all new features supported as defined in RFC-2398."""
2747 features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
2749 for fact in self.available_facts:
2750 if fact in self.current_facts:
2754 features.append('MLST ' + s)
2756 self.push("211-Features supported:\r\n")
2757 self.push("".join([" %s\r\n" %x for x in features]))
2758 self.respond('211 End FEAT.')
2760 def ftp_OPTS(self, line):
2761 """Specify options for FTP commands as specified in RFC-2389."""
2763 assert (not line.count(' ') > 1), 'Invalid number of arguments'
2765 cmd, arg = line.split(' ')
2766 assert (';' in arg), 'Invalid argument'
2769 # actually the only command able to accept options is MLST
2770 assert (cmd.upper() == 'MLST'), 'Unsupported command "%s"' %cmd
2771 except AssertionError, err:
2772 self.respond('501 %s.' %err)
2774 facts = [x.lower() for x in arg.split(';')]
2775 self.current_facts = [x for x in facts if x in self.available_facts]
2776 f = ''.join([x + ';' for x in self.current_facts])
2777 self.respond('200 MLST OPTS ' + f)
2779 def ftp_NOOP(self, line):
2781 self.respond("200 I successfully done nothin'.")
2783 def ftp_SYST(self, line):
2784 """Return system type (always returns UNIX type: L8)."""
2785 # This command is used to find out the type of operating system
2786 # at the server. The reply shall have as its first word one of
2787 # the system names listed in RFC-943.
2788 # Since that we always return a "/bin/ls -lA"-like output on
2789 # LIST we prefer to respond as if we would on Unix in any case.
2790 self.respond("215 UNIX Type: L8")
2792 def ftp_ALLO(self, line):
2793 """Allocate bytes for storage (obsolete)."""
2794 # obsolete (always respond with 202)
2795 self.respond("202 No storage allocation necessary.")
2797 def ftp_HELP(self, line):
2798 """Return help text to the client."""
2800 if line.upper() in proto_cmds:
2801 self.respond("214 %s" %proto_cmds[line.upper()])
2803 self.respond("501 Unrecognized command.")
2805 # provide a compact list of recognized commands
2806 def formatted_help():
2808 keys = proto_cmds.keys()
2811 elems = tuple((keys[0:8]))
2812 cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2814 return ''.join(cmds)
2816 self.push("214-The following commands are recognized:\r\n")
2817 self.push(formatted_help())
2818 self.respond("214 Help command successful.")
2821 # --- support for deprecated cmds
2823 # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
2824 # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
2825 # Such commands are obsoleted but some ftp clients (e.g. Windows
2826 # ftp.exe) still use them.
2828 def ftp_XCUP(self, line):
2829 """Change to the parent directory. Synonym for CDUP. Deprecated."""
2832 def ftp_XCWD(self, line):
2833 """Change the current working directory. Synonym for CWD. Deprecated."""
2836 def ftp_XMKD(self, line):
2837 """Create the specified directory. Synonym for MKD. Deprecated."""
2840 def ftp_XPWD(self, line):
2841 """Return the current working directory. Synonym for PWD. Deprecated."""
2844 def ftp_XRMD(self, line):
2845 """Remove the specified directory. Synonym for RMD. Deprecated."""
2849 class FTPServer(asyncore.dispatcher):
2850 """This class is an asyncore.disptacher subclass. It creates a FTP
2851 socket listening on <address>, dispatching the requests to a <handler>
2852 (typically FTPHandler class).
2854 Depending on the type of address specified IPv4 or IPv6 connections
2855 (or both, depending from the underlying system) will be accepted.
2857 All relevant session information is stored in class attributes
2859 Overriding them is strongly recommended to avoid running out of
2860 file descriptors (DoS)!
2863 number of maximum simultaneous connections accepted (defaults
2866 - (int) max_cons_per_ip:
2867 number of maximum connections accepted for the same IP address
2868 (defaults to 0 == unlimited).
2874 def __init__(self, address, handler):
2875 """Initiate the FTP server opening listening on address.
2877 - (tuple) address: the host:port pair on which the command
2878 channel will listen.
2880 - (classobj) handler: the handler class to use.
2882 asyncore.dispatcher.__init__(self)
2883 self.handler = handler
2885 host, port = address
2887 # AF_INET or AF_INET6 socket
2888 # Get the correct address family for our host (allows IPv6 addresses)
2890 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
2891 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
2892 except socket.gaierror:
2893 # Probably a DNS issue. Assume IPv4.
2894 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2895 self.set_reuse_addr()
2896 self.bind((host, port))
2899 af, socktype, proto, canonname, sa = res
2901 self.create_socket(af, socktype)
2902 self.set_reuse_addr()
2904 except socket.error, msg:
2911 raise socket.error, msg
2914 def set_reuse_addr(self):
2915 # Overridden for convenience. Avoid to reuse address on Windows.
2916 if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'):
2918 asyncore.dispatcher.set_reuse_addr(self)
2920 def serve_forever(self, **kwargs):
2921 """A wrap around asyncore.loop(); starts the asyncore polling
2924 The keyword arguments in kwargs are the same expected by
2925 asyncore.loop() function: timeout, use_poll, map and count.
2927 if not 'count' in kwargs:
2928 log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2930 # backward compatibility for python < 2.4
2931 if not hasattr(self, '_map'):
2932 if not 'map' in kwargs:
2933 map = asyncore.socket_map
2936 self._map = self.handler._map = map
2940 # use_poll specifies whether to use select module's poll()
2941 # with asyncore or whether to use asyncore's own poll()
2942 # method Python versions < 2.4 need use_poll set to False
2943 # This breaks on OS X systems if use_poll is set to True.
2944 # All systems seem to work fine with it set to False
2945 # (tested on Linux, Windows, and OS X platforms)
2947 asyncore.loop(**kwargs)
2949 asyncore.loop(timeout=1.0, use_poll=False)
2950 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
2951 log("Shutting down FTPd.")
2954 def handle_accept(self):
2955 """Called when remote client initiates a connection."""
2956 sock_obj, addr = self.accept()
2957 log("[]%s:%s Connected." %addr[:2])
2959 handler = self.handler(sock_obj, self)
2961 self.ip_map.append(ip)
2963 # For performance and security reasons we should always set a
2964 # limit for the number of file descriptors that socket_map
2965 # should contain. When we're running out of such limit we'll
2966 # use the last available channel for sending a 421 response
2967 # to the client before disconnecting it.
2969 if len(self._map) > self.max_cons:
2970 handler.handle_max_cons()
2973 # accept only a limited number of connections from the same
2975 if self.max_cons_per_ip:
2976 if self.ip_map.count(ip) > self.max_cons_per_ip:
2977 handler.handle_max_cons_per_ip()
2985 def handle_error(self):
2986 """Called to handle any uncaught exceptions."""
2989 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
2991 logerror(traceback.format_exc())
2994 def close_all(self, map=None, ignore_all=False):
2995 """Stop serving; close all existent connections disconnecting
2999 A dictionary whose items are the channels to close.
3000 If map is omitted, the default asyncore.socket_map is used.
3002 - (bool) ignore_all:
3003 having it set to False results in raising exception in case
3004 of unexpected errors.
3006 Implementation note:
3008 Instead of using the current asyncore.close_all() function
3009 which only close sockets, we iterate over all existent channels
3010 calling close() method for each one of them, avoiding memory
3013 This is how asyncore.close_all() function should work in
3018 for x in map.values():
3022 if x[0] == errno.EBADF:
3024 elif not ignore_all:
3026 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3035 # cmd line usage (provide a read-only anonymous ftp server):
3036 # python -m pyftpdlib.FTPServer
3037 authorizer = DummyAuthorizer()
3038 authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
3039 FTPHandler.authorizer = authorizer
3040 address = ('', 8021)
3041 ftpd = FTPServer(address, FTPHandler)
3042 ftpd.serve_forever()
3044 if __name__ == '__main__':
3047 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: