2 # -*- encoding: utf-8 -*-
5 # pyftpdlib is released under the MIT license, reproduced below:
6 # ======================================================================
7 # Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>
8 # Hacked by Fabien Pinckaers (C) 2008 <fp@tinyerp.com>
12 # Permission to use, copy, modify, and distribute this software and
13 # its documentation for any purpose and without fee is hereby
14 # granted, provided that the above copyright notice appear in all
15 # copies and that both that copyright notice and this permission
16 # notice appear in supporting documentation, and that the name of
17 # Giampaolo Rodola' not be used in advertising or publicity pertaining to
18 # distribution of the software without specific, written prior
21 # Giampaolo Rodola' DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
22 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
23 # NO EVENT Giampaolo Rodola' BE LIABLE FOR ANY SPECIAL, INDIRECT OR
24 # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
25 # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
26 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
27 # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
28 # ======================================================================
31 """pyftpdlib: RFC-959 asynchronous FTP server.
33 pyftpdlib implements a fully functioning asynchronous FTP server as
34 defined in RFC-959. A hierarchy of classes outlined below implement
35 the backend functionality for the FTPd:
37 [FTPServer] - the base class for the backend.
39 [FTPHandler] - a class representing the server-protocol-interpreter
40 (server-PI, see RFC-959). Each time a new connection occurs
41 FTPServer will create a new FTPHandler instance to handle the
44 [ActiveDTP], [PassiveDTP] - base classes for active/passive-DTP
47 [DTPHandler] - this class handles processing of data transfer
48 operations (server-DTP, see RFC-959).
50 [DummyAuthorizer] - an "authorizer" is a class handling FTPd
51 authentications and permissions. It is used inside FTPHandler class
52 to verify user passwords, to get user's home directory and to get
53 permissions when a filesystem read/write occurs. "DummyAuthorizer"
54 is the base authorizer class providing a platform independent
55 interface for managing virtual users.
57 [AbstractedFS] - class used to interact with the file system,
58 providing a high level, cross-platform interface compatible
59 with both Windows and UNIX style filesystems.
61 [AuthorizerError] - base class for authorizers exceptions.
64 pyftpdlib also provides 3 different logging streams through 3 functions
65 which can be overridden to allow for custom logging.
67 [log] - the main logger that logs the most important messages for
68 the end user regarding the FTPd.
70 [logline] - this function is used to log commands and responses
71 passing through the control FTP channel.
73 [logerror] - log traceback outputs occurring in case of errors.
78 >>> from pyftpdlib import ftpserver
79 >>> authorizer = ftpserver.DummyAuthorizer()
80 >>> authorizer.add_user('user', 'password', '/home/user', perm='elradfmw')
81 >>> authorizer.add_anonymous('/home/nobody')
82 >>> ftp_handler = ftpserver.FTPHandler
83 >>> ftp_handler.authorizer = authorizer
84 >>> address = ("127.0.0.1", 21)
85 >>> ftpd = ftpserver.FTPServer(address, ftp_handler)
86 >>> ftpd.serve_forever()
87 Serving FTP on 127.0.0.1:21
88 []127.0.0.1:2503 connected.
89 127.0.0.1:2503 ==> 220 Ready.
90 127.0.0.1:2503 <== USER anonymous
91 127.0.0.1:2503 ==> 331 Username ok, send password.
92 127.0.0.1:2503 <== PASS ******
93 127.0.0.1:2503 ==> 230 Login successful.
94 [anonymous]@127.0.0.1:2503 User anonymous logged in.
95 127.0.0.1:2503 <== TYPE A
96 127.0.0.1:2503 ==> 200 Type set to: ASCII.
97 127.0.0.1:2503 <== PASV
98 127.0.0.1:2503 ==> 227 Entering passive mode (127,0,0,1,9,201).
99 127.0.0.1:2503 <== LIST
100 127.0.0.1:2503 ==> 150 File status okay. About to open data connection.
101 [anonymous]@127.0.0.1:2503 OK LIST "/". Transfer starting.
102 127.0.0.1:2503 ==> 226 Transfer complete.
103 [anonymous]@127.0.0.1:2503 Transfer complete. 706 bytes transmitted.
104 127.0.0.1:2503 <== QUIT
105 127.0.0.1:2503 ==> 221 Goodbye.
106 [anonymous]@127.0.0.1:2503 Disconnected.
124 from tarfile import filemode
135 __all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer',
136 'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler',
137 'FileProducer', 'IteratorProducer', 'BufferedIteratorProducer',
141 __pname__ = 'Python FTP server library (pyftpdlib)'
143 __date__ = '2008-05-16'
144 __author__ = "Giampaolo Rodola' <g.rodola@gmail.com>"
145 __web__ = 'http://code.google.com/p/pyftpdlib/'
149 'ABOR': 'Syntax: ABOR (abort transfer).',
150 'ALLO': 'Syntax: ALLO <SP> bytes (obsolete; allocate storage).',
151 'APPE': 'Syntax: APPE <SP> file-name (append data to an existent file).',
152 'CDUP': 'Syntax: CDUP (go to parent directory).',
153 'CWD' : 'Syntax: CWD <SP> dir-name (change current working directory).',
154 'DELE': 'Syntax: DELE <SP> file-name (delete file).',
155 'EPRT': 'Syntax: EPRT <SP> |proto|ip|port| (set server in extended active mode).',
156 'EPSV': 'Syntax: EPSV [<SP> proto/"ALL"] (set server in extended passive mode).',
157 'FEAT': 'Syntax: FEAT (list all new features supported).',
158 'HELP': 'Syntax: HELP [<SP> cmd] (show help).',
159 'LIST': 'Syntax: LIST [<SP> path-name] (list files).',
160 'MDTM': 'Syntax: MDTM <SP> file-name (get last modification time).',
161 'MLSD': 'Syntax: MLSD [<SP> dir-name] (list files in a machine-processable form)',
162 'MLST': 'Syntax: MLST [<SP> path-name] (show a path in a machine-processable form)',
163 'MODE': 'Syntax: MODE <SP> mode (obsolete; set data transfer mode).',
164 'MKD' : 'Syntax: MDK <SP> dir-name (create directory).',
165 'NLST': 'Syntax: NLST [<SP> path-name] (list files in a compact form).',
166 'NOOP': 'Syntax: NOOP (just do nothing).',
167 'OPTS': 'Syntax: OPTS <SP> ftp-command [<SP> option] (specify options for FTP commands)',
168 'PASS': 'Syntax: PASS <SP> user-name (set user password).',
169 'PASV': 'Syntax: PASV (set server in passive mode).',
170 'PORT': 'Syntax: PORT <sp> h1,h2,h3,h4,p1,p2 (set server in active mode).',
171 'PWD' : 'Syntax: PWD (get current working directory).',
172 'QUIT': 'Syntax: QUIT (quit current session).',
173 'REIN': 'Syntax: REIN (reinitialize / flush account).',
174 'REST': 'Syntax: REST <SP> marker (restart file position).',
175 'RETR': 'Syntax: RETR <SP> file-name (retrieve a file).',
176 'RMD' : 'Syntax: RMD <SP> dir-name (remove directory).',
177 'RNFR': 'Syntax: RNFR <SP> file-name (file renaming (source name)).',
178 'RNTO': 'Syntax: RNTO <SP> file-name (file renaming (destination name)).',
179 'SIZE': 'Syntax: HELP <SP> file-name (get file size).',
180 'STAT': 'Syntax: STAT [<SP> path name] (status information [list files]).',
181 'STOR': 'Syntax: STOR <SP> file-name (store a file).',
182 'STOU': 'Syntax: STOU [<SP> file-name] (store a file with a unique name).',
183 'STRU': 'Syntax: STRU <SP> type (obsolete; set file structure).',
184 'SYST': 'Syntax: SYST (get operating system type).',
185 'TYPE': 'Syntax: TYPE <SP> [A | I] (set transfer type).',
186 'USER': 'Syntax: USER <SP> user-name (set username).',
187 'XCUP': 'Syntax: XCUP (obsolete; go to parent directory).',
188 'XCWD': 'Syntax: XCWD <SP> dir-name (obsolete; change current directory).',
189 'XMKD': 'Syntax: XMDK <SP> dir-name (obsolete; create directory).',
190 'XPWD': 'Syntax: XPWD (obsolete; get current dir).',
191 'XRMD': 'Syntax: XRMD <SP> dir-name (obsolete; remove directory).',
195 # hack around format_exc function of traceback module to grant
196 # backward compatibility with python < 2.4
197 if not hasattr(traceback, 'format_exc'):
199 import cStringIO as StringIO
204 f = StringIO.StringIO()
205 traceback.print_exc(file=f)
210 traceback.format_exc = _format_exc
214 """A wrap around os.strerror() which may be not available on all
215 platforms (e.g. pythonCE).
217 - (instance) err: an EnvironmentError or derived class instance.
219 if hasattr(os, 'strerror'):
220 return os.strerror(err.errno)
225 # --- library defined exceptions
227 class Error(Exception):
228 """Base class for module exceptions."""
230 class AuthorizerError(Error):
231 """Base class for authorizer exceptions."""
237 """Log messages intended for the end user."""
242 """Log commands and responses passing through the command channel."""
247 """Log traceback outputs occurring in case of errors."""
248 sys.stderr.write(str(msg) + '\n')
254 class DummyAuthorizer:
255 """Basic "dummy" authorizer class, suitable for subclassing to
256 create your own custom authorizers.
258 An "authorizer" is a class handling authentications and permissions
259 of the FTP server. It is used inside FTPHandler class for verifying
260 user's password, getting users home directory, checking user
261 permissions when a file read/write event occurs and changing user
262 before accessing the filesystem.
264 DummyAuthorizer is the base authorizer, providing a platform
265 independent interface for managing "virtual" FTP users. System
266 dependent authorizers can by written by subclassing this base
267 class and overriding appropriate methods as necessary.
271 write_perms = "adfmw"
276 def add_user(self, username, password, homedir, perm='elr',
277 msg_login="Login successful.", msg_quit="Goodbye."):
278 """Add a user to the virtual users table.
280 AuthorizerError exceptions raised on error conditions such as
281 invalid permissions, missing home directory or duplicate usernames.
283 Optional perm argument is a string referencing the user's
284 permissions explained below:
287 - "e" = change directory (CWD command)
288 - "l" = list files (LIST, NLST, MLSD commands)
289 - "r" = retrieve file from the server (RETR command)
292 - "a" = append data to an existing file (APPE command)
293 - "d" = delete file or directory (DELE, RMD commands)
294 - "f" = rename file or directory (RNFR, RNTO commands)
295 - "m" = create directory (MKD command)
296 - "w" = store a file to the server (STOR, STOU commands)
298 Optional msg_login and msg_quit arguments can be specified to
299 provide customized response strings when user log-in and quit.
301 if self.has_user(username):
302 raise AuthorizerError('User "%s" already exists' %username)
303 homedir = os.path.realpath(homedir)
304 if not os.path.isdir(homedir):
305 raise AuthorizerError('No such directory: "%s"' %homedir)
307 if p not in 'elradfmw':
308 raise AuthorizerError('No such permission "%s"' %p)
310 if (p in self.write_perms) and (username == 'anonymous'):
311 warnings.warn("write permissions assigned to anonymous user.",
314 dic = {'pwd': str(password),
317 'msg_login': str(msg_login),
318 'msg_quit': str(msg_quit)
320 self.user_table[username] = dic
322 def add_anonymous(self, homedir, **kwargs):
323 """Add an anonymous user to the virtual users table.
325 AuthorizerError exception raised on error conditions such as
326 invalid permissions, missing home directory, or duplicate
329 The keyword arguments in kwargs are the same expected by
330 add_user method: "perm", "msg_login" and "msg_quit".
332 The optional "perm" keyword argument is a string defaulting to
333 "elr" referencing "read-only" anonymous user's permissions.
335 Using write permission values ("adfmw") results in a
338 DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
340 def remove_user(self, username):
341 """Remove a user from the virtual users table."""
342 del self.user_table[username]
344 def validate_authentication(self, username, password):
345 """Return True if the supplied username and password match the
346 stored credentials."""
347 return self.user_table[username]['pwd'] == password
349 def impersonate_user(self, username, password):
350 """Impersonate another user (noop).
352 It is always called before accessing the filesystem.
353 By default it does nothing. The subclass overriding this
354 method is expected to provide a mechanism to change the
358 def terminate_impersonation(self):
359 """Terminate impersonation (noop).
361 It is always called after having accessed the filesystem.
362 By default it does nothing. The subclass overriding this
363 method is expected to provide a mechanism to switch back
364 to the original user.
367 def has_user(self, username):
368 """Whether the username exists in the virtual users table."""
369 return username in self.user_table
371 def has_perm(self, username, perm, path=None):
372 """Whether the user has permission over path (an absolute
373 pathname of a file or a directory).
375 Expected perm argument is one of the following letters:
378 return perm in self.user_table[username]['perm']
380 def get_perms(self, username):
381 """Return current user permissions."""
382 return self.user_table[username]['perm']
384 def get_home_dir(self, username):
385 """Return the user's home directory."""
386 return self.user_table[username]['home']
388 def get_msg_login(self, username):
389 """Return the user's login message."""
390 return self.user_table[username]['msg_login']
392 def get_msg_quit(self, username):
393 """Return the user's quitting message."""
394 return self.user_table[username]['msg_quit']
399 class PassiveDTP(asyncore.dispatcher):
400 """This class is an asyncore.disptacher subclass. It creates a
401 socket listening on a local port, dispatching the resultant
402 connection to DTPHandler.
405 def __init__(self, cmd_channel, extmode=False):
406 """Initialize the passive data server.
408 - (instance) cmd_channel: the command channel class instance.
409 - (bool) extmode: wheter use extended passive mode response type.
411 asyncore.dispatcher.__init__(self)
412 self.cmd_channel = cmd_channel
414 ip = self.cmd_channel.getsockname()[0]
415 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
417 if not self.cmd_channel.passive_ports:
418 # By using 0 as port number value we let kernel choose a free
419 # unprivileged random port.
422 ports = list(self.cmd_channel.passive_ports)
424 port = ports.pop(random.randint(0, len(ports) -1))
426 self.bind((ip, port))
427 except socket.error, why:
428 if why[0] == errno.EADDRINUSE: # port already in use
431 # If cannot use one of the ports in the configured
432 # range we'll use a kernel-assigned port, and log
433 # a message reporting the issue.
434 # By using 0 as port number value we let kernel
435 # choose a free unprivileged random port.
438 self.cmd_channel.log(
439 "Can't find a valid passive port in the "
440 "configured range. A random kernel-assigned "
448 port = self.socket.getsockname()[1]
450 if self.cmd_channel.masquerade_address:
451 ip = self.cmd_channel.masquerade_address
452 # The format of 227 response in not standardized.
453 # This is the most expected:
454 self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' %(
455 ip.replace('.', ','), port / 256, port % 256))
457 self.cmd_channel.respond('229 Entering extended passive mode '
460 # --- connection / overridden
462 def handle_accept(self):
463 """Called when remote client initiates a connection."""
464 sock, addr = self.accept()
466 # Check the origin of data connection. If not expressively
467 # configured we drop the incoming data connection if remote
468 # IP address does not match the client's IP address.
469 if (self.cmd_channel.remote_ip != addr[0]):
470 if not self.cmd_channel.permit_foreign_addresses:
475 msg = 'Rejected data connection from foreign address %s:%s.' \
477 self.cmd_channel.respond("425 %s" %msg)
478 self.cmd_channel.log(msg)
479 # do not close listening socket: it couldn't be client's blame
482 # site-to-site FTP allowed
483 msg = 'Established data connection with foreign address %s:%s.'\
485 self.cmd_channel.log(msg)
486 # Immediately close the current channel (we accept only one
487 # connection at time) and avoid running out of max connections
490 # delegate such connection to DTP handler
491 handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
492 self.cmd_channel.data_channel = handler
493 self.cmd_channel.on_dtp_connection()
498 def handle_error(self):
499 """Called to handle any uncaught exceptions."""
502 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
504 logerror(traceback.format_exc())
507 def handle_close(self):
508 """Called on closing the data connection."""
512 class ActiveDTP(asyncore.dispatcher):
513 """This class is an asyncore.disptacher subclass. It creates a
514 socket resulting from the connection to a remote user-port,
515 dispatching it to DTPHandler.
518 def __init__(self, ip, port, cmd_channel):
519 """Initialize the active data channel attemping to connect
520 to remote data socket.
522 - (str) ip: the remote IP address.
523 - (int) port: the remote port.
524 - (instance) cmd_channel: the command channel class instance.
526 asyncore.dispatcher.__init__(self)
527 self.cmd_channel = cmd_channel
528 self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
530 self.connect((ip, port))
531 except socket.gaierror:
532 self.cmd_channel.respond("425 Can't connect to specified address.")
535 # --- connection / overridden
537 def handle_write(self):
538 """NOOP, must be overridden to prevent unhandled write event."""
540 def handle_connect(self):
541 """Called when connection is established."""
542 self.cmd_channel.respond('200 Active data connection established.')
543 # delegate such connection to DTP handler
544 handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
545 self.cmd_channel.data_channel = handler
546 self.cmd_channel.on_dtp_connection()
547 #self.close() # <-- (done automatically)
549 def handle_expt(self):
550 self.cmd_channel.respond("425 Can't connect to specified address.")
553 def handle_error(self):
554 """Called to handle any uncaught exceptions."""
557 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
562 logerror(traceback.format_exc())
563 self.cmd_channel.respond("425 Can't connect to specified address.")
568 from collections import deque
570 # backward compatibility with Python < 2.4 by replacing deque with a list
572 def appendleft(self, obj):
573 list.insert(self, 0, obj)
576 class DTPHandler(asyncore.dispatcher):
577 """Class handling server-data-transfer-process (server-DTP, see
578 RFC-959) managing data-transfer operations involving sending
581 Instance attributes defined in this class, initialized when
584 - (instance) cmd_channel: the command channel class instance.
585 - (file) file_obj: the file transferred (if any).
586 - (bool) receive: True if channel is used for receiving data.
587 - (bool) transfer_finished: True if transfer completed successfully.
588 - (int) tot_bytes_sent: the total bytes sent.
589 - (int) tot_bytes_received: the total bytes received.
591 DTPHandler implementation note:
593 When a producer is consumed and close_when_done() has been called
594 previously, refill_buffer() erroneously calls close() instead of
595 handle_close() - (see: http://bugs.python.org/issue1740572)
597 To avoid this problem DTPHandler is implemented as a subclass of
598 asyncore.dispatcher instead of asynchat.async_chat.
599 This implementation follows the same approach that asynchat module
600 should use in Python 2.6.
602 The most important change in the implementation is related to
603 producer_fifo, which is a pure deque object instead of a
604 producer_fifo instance.
606 Since we don't want to break backward compatibily with older python
607 versions (deque has been introduced in Python 2.4), if deque is not
608 available we use a list instead.
611 ac_in_buffer_size = 8192
612 ac_out_buffer_size = 8192
614 def __init__(self, sock_obj, cmd_channel):
615 """Initialize the command channel.
617 - (instance) sock_obj: the socket object instance of the newly
618 established connection.
619 - (instance) cmd_channel: the command channel class instance.
621 asyncore.dispatcher.__init__(self, sock_obj)
622 # we toss the use of the asynchat's "simple producer" and
623 # replace it with a pure deque, which the original fifo
625 self.producer_fifo = deque()
627 self.cmd_channel = cmd_channel
630 self.transfer_finished = False
631 self.tot_bytes_sent = 0
632 self.tot_bytes_received = 0
633 self.data_wrapper = lambda x: x
635 # --- utility methods
637 def enable_receiving(self, type):
638 """Enable receiving of data over the channel. Depending on the
639 TYPE currently in use it creates an appropriate wrapper for the
642 - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
645 self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
647 self.data_wrapper = lambda x: x
649 raise TypeError, "Unsupported type"
652 def get_transmitted_bytes(self):
653 "Return the number of transmitted bytes."
654 return self.tot_bytes_sent + self.tot_bytes_received
656 def transfer_in_progress(self):
657 "Return True if a transfer is in progress, else False."
658 return self.get_transmitted_bytes() != 0
662 def handle_read(self):
663 """Called when there is data waiting to be read."""
665 chunk = self.recv(self.ac_in_buffer_size)
669 self.tot_bytes_received += len(chunk)
671 self.transfer_finished = True
672 #self.close() # <-- asyncore.recv() already do that...
674 # while we're writing on the file an exception could occur
675 # in case that filesystem gets full; if this happens we
676 # let handle_error() method handle this exception, providing
677 # a detailed error message.
678 self.file_obj.write(self.data_wrapper(chunk))
680 def handle_write(self):
681 """Called when data is ready to be written, initiates send."""
684 def push(self, data):
685 """Push data onto the deque and initiate send."""
686 sabs = self.ac_out_buffer_size
688 for i in xrange(0, len(data), sabs):
689 self.producer_fifo.append(data[i:i+sabs])
691 self.producer_fifo.append(data)
694 def push_with_producer(self, producer):
695 """Push data using a producer and initiate send."""
696 self.producer_fifo.append(producer)
700 """Predicate for inclusion in the readable for select()."""
704 """Predicate for inclusion in the writable for select()."""
705 return self.producer_fifo or (not self.connected)
707 def close_when_done(self):
708 """Automatically close this channel once the outgoing queue is empty."""
709 self.producer_fifo.append(None)
711 def initiate_send(self):
712 """Attempt to send data in fifo order."""
713 while self.producer_fifo and self.connected:
714 first = self.producer_fifo[0]
715 # handle empty string/buffer or None entry
717 del self.producer_fifo[0]
719 self.transfer_finished = True
723 # handle classic producer behavior
724 obs = self.ac_out_buffer_size
726 data = buffer(first, 0, obs)
730 self.producer_fifo.appendleft(data)
732 del self.producer_fifo[0]
737 num_sent = self.send(data)
743 self.tot_bytes_sent += num_sent
744 if num_sent < len(data) or obs < len(first):
745 self.producer_fifo[0] = first[num_sent:]
747 del self.producer_fifo[0]
748 # we tried to send some actual data
751 def handle_expt(self):
752 """Called on "exceptional" data events."""
753 self.cmd_channel.respond("426 Connection error; transfer aborted.")
756 def handle_error(self):
757 """Called when an exception is raised and not otherwise handled."""
760 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
762 except socket.error, err:
763 # fix around asyncore bug (http://bugs.python.org/issue1736101)
764 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
770 # an error could occur in case we fail reading / writing
771 # from / to file (e.g. file system gets full)
772 except EnvironmentError, err:
773 error = _strerror(err)
775 # some other exception occurred; we don't want to provide
776 # confidential error messages
777 logerror(traceback.format_exc())
778 error = "Internal error"
779 self.cmd_channel.respond("426 %s; transfer aborted." %error)
782 def handle_close(self):
783 """Called when the socket is closed."""
784 # If we used channel for receiving we assume that transfer is
785 # finished when client close connection , if we used channel
786 # for sending we have to check that all data has been sent
787 # (responding with 226) or not (responding with 426).
789 self.transfer_finished = True
793 if self.transfer_finished:
794 self.cmd_channel.respond("226 Transfer complete.")
796 fname = self.file_obj.name
797 self.cmd_channel.log('"%s" %s.' %(fname, action))
799 tot_bytes = self.get_transmitted_bytes()
800 msg = "Transfer aborted; %d bytes transmitted." %tot_bytes
801 self.cmd_channel.respond("426 " + msg)
802 self.cmd_channel.log(msg)
806 """Close the data channel, first attempting to close any remaining
808 if self.file_obj and not self.file_obj.closed:
809 self.file_obj.close()
810 asyncore.dispatcher.close(self)
811 self.cmd_channel.on_dtp_close()
817 """Producer wrapper for file[-like] objects."""
821 def __init__(self, file, type):
822 """Initialize the producer with a data_wrapper appropriate to TYPE.
824 - (file) file: the file[-like] object.
825 - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
830 self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
832 self.data_wrapper = lambda x: x
834 raise TypeError, "Unsupported type"
837 """Attempt a chunk of data of size self.buffer_size."""
840 data = self.data_wrapper(self.file.read(self.buffer_size))
843 if not self.file.closed:
848 class IteratorProducer:
849 """Producer for iterator objects."""
851 def __init__(self, iterator):
852 self.iterator = iterator
855 """Attempt a chunk of data from iterator by calling its next()
859 return self.iterator.next()
860 except StopIteration:
864 class BufferedIteratorProducer:
865 """Producer for iterator objects with buffer capabilities."""
866 # how many times iterator.next() will be called before
867 # returning some data
870 def __init__(self, iterator):
871 self.iterator = iterator
874 """Attempt a chunk of data from iterator by calling
875 its next() method different times.
878 for x in xrange(self.loops):
880 buffer.append(self.iterator.next())
881 except StopIteration:
883 return ''.join(buffer)
889 """A class used to interact with the file system, providing a high
890 level, cross-platform interface compatible with both Windows and
891 UNIX style filesystems.
893 It provides some utility methods and some wraps around operations
894 involved in file creation and file system operations like moving
895 files or removing directories.
898 - (str) root: the user home directory.
899 - (str) cwd: the current working directory.
900 - (str) rnfr: source file to be renamed.
908 # --- Pathname / conversion utilities
910 def ftpnorm(self, ftppath):
911 """Normalize a "virtual" ftp pathname (tipically the raw string
912 coming from client) depending on the current working directory.
914 Example (having "/foo" as current working directory):
917 Note: directory separators are system independent ("/").
918 Pathname returned is always absolutized.
920 if os.path.isabs(ftppath):
921 p = os.path.normpath(ftppath)
923 p = os.path.normpath(os.path.join(self.cwd, ftppath))
924 # normalize string in a standard web-path notation having '/'
926 p = p.replace("\\", "/")
927 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
928 # don't need them. In case we get an UNC path we collapse
929 # redundant separators appearing at the beginning of the string
932 # Anti path traversal: don't trust user input, in the event
933 # that self.cwd is not absolute, return "/" as a safety measure.
934 # This is for extra protection, maybe not really necessary.
935 if not os.path.isabs(p):
939 def ftp2fs(self, ftppath):
940 """Translate a "virtual" ftp pathname (tipically the raw string
941 coming from client) into equivalent absolute "real" filesystem
944 Example (having "/home/user" as root directory):
945 'x' -> '/home/user/x'
947 Note: directory separators are system dependent.
949 # as far as I know, it should always be path traversal safe...
950 if os.path.normpath(self.root) == os.sep:
951 return os.path.normpath(self.ftpnorm(ftppath))
953 p = self.ftpnorm(ftppath)[1:]
954 return os.path.normpath(os.path.join(self.root, p))
956 def fs2ftp(self, fspath):
957 """Translate a "real" filesystem pathname into equivalent
958 absolute "virtual" ftp pathname depending on the user's
961 Example (having "/home/user" as root directory):
962 '/home/user/x' -> '/x'
964 As for ftpnorm, directory separators are system independent
965 ("/") and pathname returned is always absolutized.
967 On invalid pathnames escaping from user's root directory
968 (e.g. "/home" when root is "/home/user") always return "/".
970 if os.path.isabs(fspath):
971 p = os.path.normpath(fspath)
973 p = os.path.normpath(os.path.join(self.root, fspath))
974 if not self.validpath(p):
976 p = p.replace(os.sep, "/")
977 p = p[len(self.root):]
978 if not p.startswith('/'):
982 # alias for backward compatibility with 0.2.0
986 def validpath(self, path):
987 """Check whether the path belongs to user's home directory.
988 Expected argument is a "real" filesystem pathname.
990 If path is a symbolic link it is resolved to check its real
993 Pathnames escaping from user's root directory are considered
996 root = self.realpath(self.root)
997 path = self.realpath(path)
998 if not self.root.endswith(os.sep):
999 root = self.root + os.sep
1000 if not path.endswith(os.sep):
1001 path = path + os.sep
1002 if path[0:len(root)] == root:
1006 # --- Wrapper methods around open() and tempfile.mkstemp
1008 def open(self, filename, mode):
1009 """Open a file returning its handler."""
1010 return open(filename, mode)
1012 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
1013 """A wrap around tempfile.mkstemp creating a file with a unique
1014 name. Unlike mkstemp it returns an object with a file-like
1018 def __init__(self, fd, name):
1021 def __getattr__(self, attr):
1022 return getattr(self.file, attr)
1024 text = not 'b' in mode
1025 # max number of tries to find out a unique file name
1026 tempfile.TMP_MAX = 50
1027 fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
1028 file = os.fdopen(fd, mode)
1029 return FileWrapper(file, name)
1031 # --- Wrapper methods around os.*
1033 def chdir(self, path):
1034 """Change the current directory."""
1035 # temporarily join the specified directory to see if we have
1036 # permissions to do so
1037 basedir = os.getcwd()
1044 self.cwd = self.fs2ftp(path)
1046 def mkdir(self, path, basename):
1047 """Create the specified directory."""
1048 os.mkdir(os.path.join(path, basename))
1050 def listdir(self, path):
1051 """List the content of a directory."""
1052 return os.listdir(path)
1054 def rmdir(self, path):
1055 """Remove the specified directory."""
1058 def remove(self, path):
1059 """Remove the specified file."""
1062 def rename(self, src, dst):
1063 """Rename the specified src file to the dst filename."""
1066 def stat(self, path):
1067 """Perform a stat() system call on the given path."""
1068 return os.stat(path)
1070 def lstat(self, path):
1071 """Like stat but does not follow symbolic links."""
1072 return os.lstat(path)
1074 if not hasattr(os, 'lstat'):
1077 # --- Wrapper methods around os.path.*
1079 def isfile(self, path):
1080 """Return True if path is a file."""
1081 return os.path.isfile(path)
1083 def islink(self, path):
1084 """Return True if path is a symbolic link."""
1085 return os.path.islink(path)
1087 def isdir(self, path):
1088 """Return True if path is a directory."""
1089 return os.path.isdir(path)
1091 def getsize(self, path):
1092 """Return the size of the specified file in bytes."""
1093 return os.path.getsize(path)
1095 def getmtime(self, path):
1096 """Return the last modified time as a number of seconds since
1098 return os.path.getmtime(path)
1100 def realpath(self, path):
1101 """Return the canonical version of path eliminating any
1102 symbolic links encountered in the path (if they are
1103 supported by the operating system).
1105 return os.path.realpath(path)
1107 def lexists(self, path):
1108 """Return True if path refers to an existing path, including
1109 a broken or circular symbolic link.
1111 if hasattr(os.path, 'lexists'):
1112 return os.path.lexists(path)
1113 # grant backward compatibility with python 2.3
1114 elif hasattr(os, 'lstat'):
1122 return os.path.exists(path)
1124 exists = lexists # alias for backward compatibility with 0.2.0
1126 def glob1(self, dirname, pattern):
1127 """Return a list of files matching a dirname pattern
1130 Unlike glob.glob1 raises exception if os.listdir() fails.
1132 names = self.listdir(dirname)
1133 if pattern[0] != '.':
1134 names = filter(lambda x: x[0] != '.', names)
1135 return fnmatch.filter(names, pattern)
1137 # --- Listing utilities
1139 # note: the following operations are no more blocking
1141 def get_list_dir(self, path):
1142 """"Return an iterator object that yields a directory listing
1143 in a form suitable for LIST command.
1145 if self.isdir(path):
1146 listing = self.listdir(path)
1148 return self.format_list(path, listing)
1149 # if path is a file or a symlink we return information about it
1151 basedir, filename = os.path.split(path)
1152 self.lstat(path) # raise exc in case of problems
1153 return self.format_list(basedir, [filename])
1155 def get_stat_dir(self, rawline):
1156 """Return an iterator object that yields a list of files
1157 matching a dirname pattern non-recursively in a form
1158 suitable for STAT command.
1160 - (str) rawline: the raw string passed by client as command
1163 ftppath = self.ftpnorm(rawline)
1164 if not glob.has_magic(ftppath):
1165 return self.get_list_dir(self.ftp2fs(rawline))
1167 basedir, basename = os.path.split(ftppath)
1168 if glob.has_magic(basedir):
1169 return iter(['Directory recursion not supported.\r\n'])
1171 basedir = self.ftp2fs(basedir)
1172 listing = self.glob1(basedir, basename)
1175 return self.format_list(basedir, listing)
1177 def format_list(self, basedir, listing, ignore_err=True):
1178 """Return an iterator object that yields the entries of given
1179 directory emulating the "/bin/ls -lA" UNIX command output.
1181 - (str) basedir: the absolute dirname.
1182 - (list) listing: the names of the entries in basedir
1183 - (bool) ignore_err: when False raise exception if os.lstat()
1186 On platforms which do not support the pwd and grp modules (such
1187 as Windows), ownership is printed as "owner" and "group" as a
1188 default, and number of hard links is always "1". On UNIX
1189 systems, the actual owner, group, and number of links are
1192 This is how output appears to client:
1194 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
1195 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
1196 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
1198 for basename in listing:
1199 file = os.path.join(basedir, basename)
1201 st = self.lstat(file)
1206 perms = filemode(st.st_mode) # permissions
1207 nlinks = st.st_nlink # number of links to inode
1208 if not nlinks: # non-posix system, let's use a bogus value
1210 size = st.st_size # file size
1212 # get user and group name, else just use the raw uid/gid
1214 uname = pwd.getpwuid(st.st_uid).pw_name
1218 gname = grp.getgrgid(st.st_gid).gr_name
1222 # on non-posix systems the only chance we use default
1223 # bogus values for owner and group
1226 # stat.st_mtime could fail (-1) if last mtime is too old
1227 # in which case we return the local time as last mtime
1229 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1231 mtime = time.strftime("%b %d %H:%M")
1232 # if the file is a symlink, resolve it, e.g. "symlink -> realfile"
1233 if stat.S_ISLNK(st.st_mode):
1234 basename = basename + " -> " + os.readlink(file)
1236 # formatting is matched with proftpd ls output
1237 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
1238 size, mtime, basename)
1240 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
1241 """Return an iterator object that yields the entries of a given
1242 directory or of a single file in a form suitable with MLSD and
1245 Every entry includes a list of "facts" referring the listed
1246 element. See RFC-3659, chapter 7, to see what every single
1249 - (str) basedir: the absolute dirname.
1250 - (list) listing: the names of the entries in basedir
1251 - (str) perms: the string referencing the user permissions.
1252 - (str) facts: the list of "facts" to be returned.
1253 - (bool) ignore_err: when False raise exception if os.stat()
1256 Note that "facts" returned may change depending on the platform
1257 and on what user specified by using the OPTS command.
1259 This is how output could appear to the client issuing
1262 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
1263 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
1264 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
1266 permdir = ''.join([x for x in perms if x not in 'arw'])
1267 permfile = ''.join([x for x in perms if x not in 'celmp'])
1268 if ('w' in perms) or ('a' in perms) or ('f' in perms):
1272 type = size = perm = modify = create = unique = mode = uid = gid = ""
1273 for basename in listing:
1274 file = os.path.join(basedir, basename)
1276 st = self.stat(file)
1282 if stat.S_ISDIR(st.st_mode):
1286 elif basename == '..':
1291 perm = 'perm=%s;' %permdir
1296 perm = 'perm=%s;' %permfile
1298 size = 'size=%s;' %st.st_size # file size
1299 # last modification time
1300 if 'modify' in facts:
1302 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1303 time.localtime(st.st_mtime))
1305 # stat.st_mtime could fail (-1) if last mtime is too old
1307 if 'create' in facts:
1308 # on Windows we can provide also the creation time
1310 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1311 time.localtime(st.st_ctime))
1315 if 'unix.mode' in facts:
1316 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
1317 if 'unix.uid' in facts:
1318 uid = 'unix.uid=%s;' %st.st_uid
1319 if 'unix.gid' in facts:
1320 gid = 'unix.gid=%s;' %st.st_gid
1321 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
1322 # posix platforms only; we get it by mixing st_dev and
1323 # st_ino values which should be enough for granting an
1324 # uniqueness for the file listed.
1325 # The same approach is used by pure-ftpd.
1326 # Implementors who want to provide unique fact on other
1327 # platforms should use some platform-specific method (e.g.
1328 # on Windows NTFS filesystems MTF records could be used).
1329 if 'unique' in facts:
1330 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
1332 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
1333 mode, uid, gid, unique, basename)
1338 class FTPHandler(asynchat.async_chat):
1339 """Implements the FTP server Protocol Interpreter (see RFC-959),
1340 handling commands received from the client on the control channel.
1342 All relevant session information is stored in class attributes
1343 reproduced below and can be modified before instantiating this
1346 - (str) banner: the string sent when client connects.
1348 - (int) max_login_attempts:
1349 the maximum number of wrong authentications before disconnecting
1350 the client (default 3).
1352 - (bool)permit_foreign_addresses:
1353 FTP site-to-site transfer feature: also referenced as "FXP" it
1354 permits for transferring a file between two remote FTP servers
1355 without the transfer going through the client's host (not
1356 recommended for security reasons as described in RFC-2577).
1357 Having this attribute set to False means that all data
1358 connections from/to remote IP addresses which do not match the
1359 client's IP address will be dropped (defualt False).
1361 - (bool) permit_privileged_ports:
1362 set to True if you want to permit active data connections (PORT)
1363 over privileged ports (not recommended, defaulting to False).
1365 - (str) masquerade_address:
1366 the "masqueraded" IP address to provide along PASV reply when
1367 pyftpdlib is running behind a NAT or other types of gateways.
1368 When configured pyftpdlib will hide its local address and
1369 instead use the public address of your NAT (default None).
1371 - (list) passive_ports:
1372 what ports ftpd will use for its passive data transfers.
1373 Value expected is a list of integers (e.g. range(60000, 65535)).
1374 When configured pyftpdlib will no longer use kernel-assigned
1375 random ports (default None).
1378 All relevant instance attributes initialized when client connects
1379 are reproduced below. You may be interested in them in case you
1380 want to subclass the original FTPHandler.
1382 - (bool) authenticated: True if client authenticated himself.
1383 - (str) username: the name of the connected user (if any).
1384 - (int) attempted_logins: number of currently attempted logins.
1385 - (str) current_type: the current transfer type (default "a")
1386 - (int) af: the address family (IPv4/IPv6)
1387 - (instance) server: the FTPServer class instance.
1388 - (instance) data_server: the data server instance (if any).
1389 - (instance) data_channel: the data channel instance (if any).
1391 # these are overridable defaults
1394 authorizer = DummyAuthorizer()
1395 active_dtp = ActiveDTP
1396 passive_dtp = PassiveDTP
1397 dtp_handler = DTPHandler
1398 abstracted_fs = AbstractedFS
1400 # session attributes (explained in the docstring)
1401 banner = "pyftpdlib %s ready." %__ver__
1402 max_login_attempts = 3
1403 permit_foreign_addresses = False
1404 permit_privileged_ports = False
1405 masquerade_address = None
1406 passive_ports = None
1408 def __init__(self, conn, server):
1409 """Initialize the command channel.
1411 - (instance) conn: the socket object instance of the newly
1412 established connection.
1413 - (instance) server: the ftp server class instance.
1416 asynchat.async_chat.__init__(self, conn=conn) # python2.5
1417 except TypeError, e:
1418 asynchat.async_chat.__init__(self, sock=conn) # python2.6
1419 self.server = server
1420 self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1422 self.in_buffer_len = 0
1423 self.set_terminator("\r\n")
1425 # session attributes
1426 self.fs = self.abstracted_fs()
1427 self.authenticated = False
1430 self.attempted_logins = 0
1431 self.current_type = 'a'
1432 self.restart_position = 0
1433 self.quit_pending = False
1434 self._epsvall = False
1435 self.__in_dtp_queue = None
1436 self.__out_dtp_queue = None
1438 # mlsx facts attributes
1439 self.current_facts = ['type', 'perm', 'size', 'modify']
1440 if os.name == 'posix':
1441 self.current_facts.append('unique')
1442 self.available_facts = self.current_facts[:]
1444 self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1446 self.available_facts.append('create')
1449 self.data_server = None
1450 self.data_channel = None
1452 if hasattr(self.socket, 'family'):
1453 self.af = self.socket.family
1454 else: # python < 2.5
1455 ip, port = self.socket.getsockname()[:2]
1456 self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
1457 socket.SOCK_STREAM)[0][0]
1460 """Return a 220 'Ready' response to the client over the command
1463 if len(self.banner) <= 75:
1464 self.respond("220 %s" %str(self.banner))
1466 self.push('220-%s\r\n' %str(self.banner))
1467 self.respond('220 ')
1469 def handle_max_cons(self):
1470 """Called when limit for maximum number of connections is reached."""
1471 msg = "Too many connections. Service temporary unavailable."
1472 self.respond("421 %s" %msg)
1474 # If self.push is used, data could not be sent immediately in
1475 # which case a new "loop" will occur exposing us to the risk of
1476 # accepting new connections. Since this could cause asyncore to
1477 # run out of fds (...and exposes the server to DoS attacks), we
1478 # immediately close the channel by using close() instead of
1479 # close_when_done(). If data has not been sent yet client will
1480 # be silently disconnected.
1483 def handle_max_cons_per_ip(self):
1484 """Called when too many clients are connected from the same IP."""
1485 msg = "Too many connections from the same IP address."
1486 self.respond("421 %s" %msg)
1488 self.close_when_done()
1490 # --- asyncore / asynchat overridden methods
1493 # if there's a quit pending we stop reading data from socket
1494 return not self.quit_pending
1496 def collect_incoming_data(self, data):
1497 """Read incoming data and append to the input buffer."""
1498 self.in_buffer.append(data)
1499 self.in_buffer_len += len(data)
1500 # Flush buffer if it gets too long (possible DoS attacks).
1501 # RFC-959 specifies that a 500 response could be given in
1504 if self.in_buffer_len > buflimit:
1505 self.respond('500 Command too long.')
1506 self.log('Command received exceeded buffer limit of %s.' %(buflimit))
1508 self.in_buffer_len = 0
1510 # commands accepted before authentication
1511 unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1513 # commands needing an argument
1514 arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
1515 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
1516 'TYPE','USER','XMKD','XRMD')
1518 # commands needing no argument
1519 unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1520 'SYST','XCUP','XPWD')
1522 def found_terminator(self):
1523 r"""Called when the incoming data stream matches the \r\n
1526 Depending on the command received it calls the command's
1527 corresponding method (e.g. for received command "MKD pathname",
1528 ftp_MKD() method is called with "pathname" as the argument).
1530 line = ''.join(self.in_buffer)
1532 self.in_buffer_len = 0
1534 cmd = line.split(' ')[0].upper()
1535 space = line.find(' ')
1537 arg = line[space + 1:]
1542 self.logline("<== %s" %line)
1544 self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1546 # let's check if user provided an argument for those commands
1548 if not arg and cmd in self.arg_cmds:
1549 self.respond("501 Syntax error: command needs an argument.")
1552 # let's do the same for those commands requiring no argument.
1553 elif arg and cmd in self.unarg_cmds:
1554 self.respond("501 Syntax error: command does not accept arguments.")
1557 # provide a limited set of commands if user isn't
1559 if (not self.authenticated):
1560 if cmd in self.unauth_cmds:
1561 # we permit STAT during this phase but we don't want
1562 # STAT to return a directory LISTing if the user is
1563 # not authenticated yet (this could happen if STAT
1564 # is used with an argument)
1565 if (cmd == 'STAT') and arg:
1566 self.respond("530 Log in with USER and PASS first.")
1568 method = getattr(self, 'ftp_' + cmd)
1569 method(arg) # call the proper ftp_* method
1570 elif cmd in proto_cmds:
1571 self.respond("530 Log in with USER and PASS first.")
1573 self.respond('500 Command "%s" not understood.' %line)
1575 # provide full command set
1576 elif (self.authenticated) and (cmd in proto_cmds):
1577 if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
1579 method = getattr(self, 'ftp_' + cmd)
1580 method(arg) # call the proper ftp_* method
1583 # recognize those commands having "special semantics"
1590 self.respond('500 Command "%s" not understood.' %line)
1592 def __check_path(self, cmd, line):
1593 """Check whether a path is valid."""
1594 # For the following commands we have to make sure that the real
1595 # path destination belongs to the user's root directory.
1596 # If provided path is a symlink we follow its final destination
1598 if cmd in ('APPE','CWD','DELE','MDTM','NLST','MLSD','MLST','RETR',
1599 'RMD','SIZE','STOR','XCWD','XRMD'):
1601 datacr = self.fs.get_cr(line)
1603 if not self.fs.validpath(self.fs.ftp2fs(line, datacr)):
1604 line = self.fs.ftpnorm(line)
1605 err = '"%s" points to a path which is outside ' \
1606 "the user's root directory" %line
1607 self.respond("550 %s." %err)
1608 self.log('FAIL %s "%s". %s.' %(cmd, line, err))
1609 self.fs.close_cr(datacr)
1613 self.fs.close_cr(datacr)
1616 def __check_perm(self, cmd, line, datacr):
1617 """Check permissions depending on issued command."""
1618 map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
1619 'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
1622 'DELE':'d', 'RMD':'d', 'XRMD':'d',
1624 'MKD':'m', 'XMKD':'m',
1627 if cmd == 'STAT' and not line:
1630 if not line and (cmd in ('LIST','NLST','MLSD')):
1631 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1633 path = self.fs.ftp2fs(line, datacr)
1634 if not self.authorizer.has_perm(self.username, perm, path):
1635 self.log('FAIL %s "%s". Not enough privileges.' \
1636 %(cmd, self.fs.ftpnorm(line)))
1637 self.respond("550 Can't %s. Not enough privileges." %cmd)
1641 def handle_expt(self):
1642 """Called when there is out of band (OOB) data for the socket
1643 connection. This could happen in case of such commands needing
1644 "special action" (typically STAT and ABOR) in which case we
1645 append OOB data to incoming buffer.
1647 if hasattr(socket, 'MSG_OOB'):
1649 data = self.socket.recv(1024, socket.MSG_OOB)
1650 except socket.error:
1653 self.in_buffer.append(data)
1655 self.log("Can't handle OOB data.")
1658 def handle_error(self):
1661 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1663 except socket.error, err:
1664 # fix around asyncore bug (http://bugs.python.org/issue1736101)
1665 if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
1666 errno.ECONNABORTED):
1670 logerror(traceback.format_exc())
1672 logerror(traceback.format_exc())
1675 def handle_close(self):
1680 """Close the current channel disconnecting the client."""
1681 if not self._closed:
1683 if self.data_server:
1684 self.data_server.close()
1685 del self.data_server
1687 if self.data_channel:
1688 self.data_channel.close()
1689 del self.data_channel
1691 del self.__out_dtp_queue
1692 del self.__in_dtp_queue
1694 # remove client IP address from ip map
1695 self.server.ip_map.remove(self.remote_ip)
1696 asynchat.async_chat.close(self)
1697 self.log("Disconnected.")
1701 def on_dtp_connection(self):
1702 """Called every time data channel connects (either active or
1705 Incoming and outgoing queues are checked for pending data.
1706 If outbound data is pending, it is pushed into the data channel.
1707 If awaiting inbound data, the data channel is enabled for
1710 if self.data_server:
1711 self.data_server.close()
1712 self.data_server = None
1714 # check for data to send
1715 if self.__out_dtp_queue:
1716 data, isproducer, file = self.__out_dtp_queue
1718 self.data_channel.file_obj = file
1720 self.data_channel.push(data)
1722 self.data_channel.push_with_producer(data)
1723 if self.data_channel:
1724 self.data_channel.close_when_done()
1725 self.__out_dtp_queue = None
1727 # check for data to receive
1728 elif self.__in_dtp_queue:
1729 self.data_channel.file_obj = self.__in_dtp_queue
1730 self.data_channel.enable_receiving(self.current_type)
1731 self.__in_dtp_queue = None
1733 def on_dtp_close(self):
1734 """Called every time the data channel is closed."""
1735 self.data_channel = None
1736 if self.quit_pending:
1737 self.close_when_done()
1741 def respond(self, resp):
1742 """Send a response to the client using the command channel."""
1743 self.push(resp + '\r\n')
1744 self.logline('==> %s' % resp)
1746 def push_dtp_data(self, data, isproducer=False, file=None):
1747 """Pushes data into the data channel.
1749 It is usually called for those commands requiring some data to
1750 be sent over the data channel (e.g. RETR).
1751 If data channel does not exist yet, it queues the data to send
1752 later; data will then be pushed into data channel when
1753 on_dtp_connection() will be called.
1755 - (str/classobj) data: the data to send which may be a string
1756 or a producer object).
1757 - (bool) isproducer: whether treat data as a producer.
1758 - (file) file: the file[-like] object to send (if any).
1760 if self.data_channel:
1761 self.respond("125 Data connection already open. Transfer starting.")
1763 self.data_channel.file_obj = file
1765 self.data_channel.push(data)
1767 self.data_channel.push_with_producer(data)
1768 if self.data_channel:
1769 self.data_channel.close_when_done()
1771 self.respond("150 File status okay. About to open data connection.")
1772 self.__out_dtp_queue = (data, isproducer, file)
1775 """Log a message, including additional identifying session data."""
1776 log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
1777 self.remote_port, msg))
1779 def logline(self, msg):
1780 """Log a line including additional indentifying session data."""
1781 logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg))
1783 def flush_account(self):
1784 """Flush account information by clearing attributes that need
1785 to be reset on a REIN or new USER command.
1787 if self.data_channel:
1788 if not self.data_channel.transfer_in_progress():
1789 self.data_channel.close()
1790 self.data_channel = None
1791 if self.data_server:
1792 self.data_server.close()
1793 self.data_server = None
1796 self.authenticated = False
1799 self.attempted_logins = 0
1800 self.current_type = 'a'
1801 self.restart_position = 0
1802 self.quit_pending = False
1803 self.__in_dtp_queue = None
1804 self.__out_dtp_queue = None
1806 def run_as_current_user(self, function, *args, **kwargs):
1807 """Execute a function impersonating the current logged-in user."""
1808 self.authorizer.impersonate_user(self.username, self.password)
1810 return function(*args, **kwargs)
1812 self.authorizer.terminate_impersonation()
1816 def _make_eport(self, ip, port):
1817 """Establish an active data channel with remote client which
1818 issued a PORT or EPRT command.
1820 # FTP bounce attacks protection: according to RFC-2577 it's
1821 # recommended to reject PORT if IP address specified in it
1822 # does not match client IP address.
1823 if not self.permit_foreign_addresses:
1824 if ip != self.remote_ip:
1825 self.log("Rejected data connection to foreign address %s:%s."
1827 self.respond("501 Can't connect to a foreign address.")
1830 # ...another RFC-2577 recommendation is rejecting connections
1831 # to privileged ports (< 1024) for security reasons.
1832 if not self.permit_privileged_ports:
1834 self.log('PORT against the privileged port "%s" refused.' %port)
1835 self.respond("501 Can't connect over a privileged port.")
1838 # close existent DTP-server instance, if any.
1839 if self.data_server:
1840 self.data_server.close()
1841 self.data_server = None
1842 if self.data_channel:
1843 self.data_channel.close()
1844 self.data_channel = None
1846 # make sure we are not hitting the max connections limit
1847 if self.server.max_cons:
1848 if len(self._map) >= self.server.max_cons:
1849 msg = "Too many connections. Can't open data channel."
1850 self.respond("425 %s" %msg)
1855 self.active_dtp(ip, port, self)
1857 def _make_epasv(self, extmode=False):
1858 """Initialize a passive data channel with remote client which
1859 issued a PASV or EPSV command.
1860 If extmode argument is False we assume that client issued EPSV in
1861 which case extended passive mode will be used (see RFC-2428).
1863 # close existing DTP-server instance, if any
1864 if self.data_server:
1865 self.data_server.close()
1866 self.data_server = None
1868 if self.data_channel:
1869 self.data_channel.close()
1870 self.data_channel = None
1872 # make sure we are not hitting the max connections limit
1873 if self.server.max_cons:
1874 if len(self._map) >= self.server.max_cons:
1875 msg = "Too many connections. Can't open data channel."
1876 self.respond("425 %s" %msg)
1881 self.data_server = self.passive_dtp(self, extmode)
1883 def ftp_PORT(self, line):
1884 """Start an active data channel by using IPv4."""
1886 self.respond("501 PORT not allowed after EPSV ALL.")
1888 if self.af != socket.AF_INET:
1889 self.respond("425 You cannot use PORT on IPv6 connections. "
1890 "Use EPRT instead.")
1892 # Parse PORT request for getting IP and PORT.
1893 # Request comes in as:
1894 # > h1,h2,h3,h4,p1,p2
1895 # ...where the client's IP address is h1.h2.h3.h4 and the TCP
1896 # port number is (p1 * 256) + p2.
1898 addr = map(int, line.split(','))
1899 assert len(addr) == 6
1901 assert 0 <= x <= 255
1902 ip = '%d.%d.%d.%d' %tuple(addr[:4])
1903 port = (addr[4] * 256) + addr[5]
1904 assert 0 <= port <= 65535
1905 except (AssertionError, ValueError, OverflowError):
1906 self.respond("501 Invalid PORT format.")
1908 self._make_eport(ip, port)
1910 def ftp_EPRT(self, line):
1911 """Start an active data channel by choosing the network protocol
1912 to use (IPv4/IPv6) as defined in RFC-2428.
1915 self.respond("501 EPRT not allowed after EPSV ALL.")
1917 # Parse EPRT request for getting protocol, IP and PORT.
1918 # Request comes in as:
1919 # # <d>proto<d>ip<d>port<d>
1920 # ...where <d> is an arbitrary delimiter character (usually "|") and
1921 # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
1923 af, ip, port = line.split(line[0])[1:-1]
1925 assert 0 <= port <= 65535
1926 except (AssertionError, ValueError, IndexError, OverflowError):
1927 self.respond("501 Invalid EPRT format.")
1931 if self.af != socket.AF_INET:
1932 self.respond('522 Network protocol not supported (use 2).')
1935 octs = map(int, ip.split('.'))
1936 assert len(octs) == 4
1938 assert 0 <= x <= 255
1939 except (AssertionError, ValueError, OverflowError), err:
1940 self.respond("501 Invalid EPRT format.")
1942 self._make_eport(ip, port)
1944 if self.af == socket.AF_INET:
1945 self.respond('522 Network protocol not supported (use 1).')
1947 self._make_eport(ip, port)
1949 if self.af == socket.AF_INET:
1950 self.respond('501 Unknown network protocol (use 1).')
1952 self.respond('501 Unknown network protocol (use 2).')
1954 def ftp_PASV(self, line):
1955 """Start a passive data channel by using IPv4."""
1957 self.respond("501 PASV not allowed after EPSV ALL.")
1959 if self.af != socket.AF_INET:
1960 self.respond("425 You cannot use PASV on IPv6 connections. "
1961 "Use EPSV instead.")
1963 self._make_epasv(extmode=False)
1965 def ftp_EPSV(self, line):
1966 """Start a passive data channel by using IPv4 or IPv6 as defined
1969 # RFC-2428 specifies that if an optional parameter is given,
1970 # we have to determine the address family from that otherwise
1971 # use the same address family used on the control connection.
1972 # In such a scenario a client may use IPv4 on the control channel
1973 # and choose to use IPv6 for the data channel.
1974 # But how could we use IPv6 on the data channel without knowing
1975 # which IPv6 address to use for binding the socket?
1976 # Unfortunately RFC-2428 does not provide satisfing information
1977 # on how to do that. The assumption is that we don't have any way
1978 # to know which address to use, hence we just use the same address
1979 # family used on the control connection.
1981 self._make_epasv(extmode=True)
1983 if self.af != socket.AF_INET:
1984 self.respond('522 Network protocol not supported (use 2).')
1986 self._make_epasv(extmode=True)
1988 if self.af == socket.AF_INET:
1989 self.respond('522 Network protocol not supported (use 1).')
1991 self._make_epasv(extmode=True)
1992 elif line.lower() == 'all':
1993 self._epsvall = True
1994 self.respond('220 Other commands other than EPSV are now disabled.')
1996 if self.af == socket.AF_INET:
1997 self.respond('501 Unknown network protocol (use 1).')
1999 self.respond('501 Unknown network protocol (use 2).')
2001 def ftp_QUIT(self, line):
2002 """Quit the current session."""
2004 # This command terminates a USER and if file transfer is not
2005 # in progress, the server closes the control connection.
2006 # If file transfer is in progress, the connection will remain
2007 # open for result response and the server will then close it.
2008 if self.authenticated:
2009 msg_quit = self.authorizer.get_msg_quit(self.username)
2011 msg_quit = "Goodbye."
2012 if len(msg_quit) <= 75:
2013 self.respond("221 %s" %msg_quit)
2015 self.push("221-%s\r\n" %msg_quit)
2016 self.respond("221 ")
2018 if not self.data_channel:
2019 self.close_when_done()
2021 # tell the cmd channel to stop responding to commands.
2022 self.quit_pending = True
2025 # --- data transferring
2027 def ftp_LIST(self, line):
2028 """Return a list of files in the specified directory to the
2031 # - If no argument, fall back on cwd as default.
2032 # - Some older FTP clients erroneously issue /bin/ls-like LIST
2033 # formats in which case we fall back on cwd as default.
2034 if not line or line.lower() in ('-a', '-l', '-al', '-la'):
2038 data = self.fs.get_cr(line)
2039 path = self.fs.ftp2fs(line, data)
2040 line = self.fs.ftpnorm(line)
2041 iterator = self.run_as_current_user(self.fs.get_list_dir, path)
2042 except OSError, err:
2043 self.fs.close_cr(data)
2044 why = _strerror(err)
2045 self.log('FAIL LIST "%s". %s.' %(line, why))
2046 self.respond('550 %s.' %why)
2048 self.fs.close_cr(data)
2049 self.log('OK LIST "%s". Transfer starting.' %line)
2050 producer = BufferedIteratorProducer(iterator)
2051 self.push_dtp_data(producer, isproducer=True)
2053 def ftp_NLST(self, line):
2054 """Return a list of files in the specified directory in a
2055 compact form to the client.
2061 data = self.fs.get_cr(line)
2062 path = self.fs.ftp2fs(line, data)
2063 line = self.fs.ftpnorm(line)
2064 if self.fs.isdir(path):
2065 listing = self.run_as_current_user(self.fs.listdir, path)
2066 listing = map(lambda x:os.path.split(x.path)[1], listing)
2068 # if path is a file we just list its name
2069 self.fs.lstat(path) # raise exc in case of problems
2070 basedir, filename = os.path.split(line)
2071 listing = [filename]
2072 except OSError, err:
2073 self.fs.close_cr(data)
2074 why = _strerror(err)
2075 self.log('FAIL NLST "%s". %s.' %(line, why))
2076 self.respond('550 %s.' %why)
2078 self.fs.close_cr(data)
2082 data = '\r\n'.join(listing) + '\r\n'
2083 self.log('OK NLST "%s". Transfer starting.' %line)
2084 self.push_dtp_data(data)
2086 # --- MLST and MLSD commands
2088 # The MLST and MLSD commands are intended to standardize the file and
2089 # directory information returned by the server-FTP process. These
2090 # commands differ from the LIST command in that the format of the
2091 # replies is strictly defined although extensible.
2093 def ftp_MLST(self, line):
2094 """Return information about a pathname in a machine-processable
2095 form as defined in RFC-3659.
2097 # if no argument, fall back on cwd as default
2102 datacr = self.fs.get_cr(line)
2103 path = self.fs.ftp2fs(line, datacr)
2104 line = self.fs.ftpnorm(line)
2105 basedir, basename = os.path.split(path)
2106 perms = self.authorizer.get_perms(self.username)
2107 iterator = self.run_as_current_user(self.fs.format_mlsx, basedir,
2108 [basename], perms, self.current_facts, ignore_err=False)
2109 data = ''.join(iterator)
2110 except OSError, err:
2111 self.fs.close_cr(datacr)
2112 why = _strerror(err)
2113 self.log('FAIL MLST "%s". %s.' %(line, why))
2114 self.respond('550 %s.' %why)
2116 self.fs.close_cr(datacr)
2117 # since TVFS is supported (see RFC-3659 chapter 6), a fully
2118 # qualified pathname should be returned
2119 data = data.split(' ')[0] + ' %s\r\n' %line
2120 # response is expected on the command channel
2121 self.push('250-Listing "%s":\r\n' %line)
2122 # the fact set must be preceded by a space
2123 self.push(' ' + data)
2124 self.respond('250 End MLST.')
2126 def ftp_MLSD(self, line):
2127 """Return contents of a directory in a machine-processable form
2128 as defined in RFC-3659.
2130 # if no argument, fall back on cwd as default
2135 datacr = self.fs.get_cr(line)
2136 path = self.fs.ftp2fs(line, datacr)
2137 line = self.fs.ftpnorm(line)
2138 # RFC-3659 requires 501 response code if path is not a directory
2139 if not self.fs.isdir(path):
2140 err = 'No such directory'
2141 self.log('FAIL MLSD "%s". %s.' %(line, err))
2142 self.respond("501 %s." %err)
2144 listing = self.run_as_current_user(self.fs.listdir, path)
2145 except OSError, err:
2146 self.fs.close_cr(datacr)
2147 why = _strerror(err)
2148 self.log('FAIL MLSD "%s". %s.' %(line, why))
2149 self.respond('550 %s.' %why)
2151 self.fs.close_cr(datacr)
2152 perms = self.authorizer.get_perms(self.username)
2153 iterator = self.fs.format_mlsx(path, listing, perms,
2155 producer = BufferedIteratorProducer(iterator)
2156 self.log('OK MLSD "%s". Transfer starting.' %line)
2157 self.push_dtp_data(producer, isproducer=True)
2159 def ftp_RETR(self, line):
2160 """Retrieve the specified file (transfer from the server to the
2165 datacr = self.fs.get_cr(line)
2166 file = self.fs.ftp2fs(line, datacr)
2167 line = self.fs.ftpnorm(line)
2168 fd = self.run_as_current_user(self.fs.open, file, 'rb')
2169 except OSError, err:
2170 self.fs.close_cr(datacr)
2171 why = _strerror(err)
2172 self.log('FAIL RETR "%s". %s.' %(line, why))
2173 self.respond('550 %s.' %why)
2175 except IOError, err:
2176 self.fs.close_cr(datacr)
2177 why = _strerror(err)
2178 self.log('FAIL RETR "%s". %s.' %(line, why))
2179 self.respond('550 %s.' %why)
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(file)
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)
2220 line = self.fs.ftpnorm(line)
2221 basedir,basename = os.path.split(line)
2225 datacr = self.fs.get_cr(line)
2226 file = self.fs.ftp2fs(basedir, datacr)
2228 except OSError, err:
2229 self.fs.close_cr(datacr)
2230 why = _strerror(err)
2231 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2232 self.respond('550 %s.' %why)
2235 if self.restart_position:
2238 fd = self.run_as_current_user(self.fs.create, file, basename, mode + 'b')
2239 except IOError, err:
2240 self.fs.close_cr(datacr)
2241 why = _strerror(err)
2242 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2243 self.respond('550 %s.' %why)
2246 if self.restart_position:
2247 # Make sure that the requested offset is valid (within the
2248 # size of the file being resumed).
2249 # According to RFC-1123 a 554 reply may result in case
2250 # that the existing file cannot be repositioned as
2251 # specified in the REST.
2254 assert not self.restart_position > self.fs.getsize(self.fs.ftp2fs(line, datacr))
2255 fd.seek(self.restart_position)
2257 except AssertionError:
2258 why = "Invalid REST parameter"
2259 except IOError, err:
2260 why = _strerror(err)
2261 self.restart_position = 0
2263 self.fs.close_cr(datacr)
2264 self.respond('554 %s' %why)
2265 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2268 self.log('OK %s "%s". Upload starting.' %(cmd, line))
2269 if self.data_channel:
2270 self.respond("125 Data connection already open. Transfer starting.")
2271 self.data_channel.file_obj = fd
2272 self.data_channel.enable_receiving(self.current_type)
2274 self.respond("150 File status okay. About to open data connection.")
2275 self.__in_dtp_queue = fd
2276 self.fs.close_cr(datacr)
2279 def ftp_STOU(self, line):
2280 """Store a file on the server with a unique name."""
2281 # Note 1: RFC-959 prohibited STOU parameters, but this
2282 # prohibition is obsolete.
2283 # Note 2: 250 response wanted by RFC-959 has been declared
2284 # incorrect in RFC-1123 that wants 125/150 instead.
2285 # Note 3: RFC-1123 also provided an exact output format
2286 # defined to be as follow:
2288 # ...where pppp represents the unique path name of the
2289 # file that will be written.
2291 # watch for STOU preceded by REST, which makes no sense.
2292 if self.restart_position:
2293 self.respond("450 Can't STOU while REST request is pending.")
2297 datacr = self.fs.get_cr(line)
2300 line = self.fs.ftpnorm(line)
2301 basedir,prefix = os.path.split(line)
2302 basedir = self.fs.ftp2fs(basedir, datacr)
2303 #prefix = prefix + '.'
2305 basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
2308 fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
2310 except IOError, err:
2311 # hitted the max number of tries to find out file with
2313 if err.errno == errno.EEXIST:
2314 why = 'No usable unique file name found'
2315 # something else happened
2317 why = _strerror(err)
2318 self.respond("450 %s." %why)
2319 self.log('FAIL STOU "%s". %s.' %(self.fs.ftpnorm(line), why))
2320 self.fs.close_cr(datacr)
2324 if not self.authorizer.has_perm(self.username, 'w', filename):
2325 self.log('FAIL STOU "%s". Not enough privileges'
2326 %self.fs.ftpnorm(line))
2327 self.respond("550 Can't STOU: not enough privileges.")
2328 self.fs.close_cr(datacr)
2331 # now just acts like STOR except that restarting isn't allowed
2332 self.log('OK STOU "%s". Upload starting.' %filename)
2333 if self.data_channel:
2334 self.respond("125 FILE: %s" %filename)
2335 self.data_channel.file_obj = fd
2336 self.data_channel.enable_receiving(self.current_type)
2338 self.respond("150 FILE: %s" %filename)
2339 self.__in_dtp_queue = fd
2340 self.fs.close_cr(datacr)
2343 def ftp_APPE(self, line):
2344 """Append data to an existing file on the server."""
2345 # watch for APPE preceded by REST, which makes no sense.
2346 if self.restart_position:
2347 self.respond("550 Can't APPE while REST request is pending.")
2349 self.ftp_STOR(line, mode='a')
2351 def ftp_REST(self, line):
2352 """Restart a file transfer from a previous mark."""
2357 except (ValueError, OverflowError):
2358 self.respond("501 Invalid parameter.")
2360 self.respond("350 Restarting at position %s. " \
2361 "Now use RETR/STOR for resuming." %marker)
2362 self.log("OK REST %s." %marker)
2363 self.restart_position = marker
2365 def ftp_ABOR(self, line):
2366 """Abort the current data transfer."""
2368 # ABOR received while no data channel exists
2369 if (self.data_server is None) and (self.data_channel is None):
2370 resp = "225 No transfer to abort."
2372 # a PASV was received but connection wasn't made yet
2373 if self.data_server:
2374 self.data_server.close()
2375 self.data_server = None
2376 resp = "225 ABOR command successful; data channel closed."
2378 # If a data transfer is in progress the server must first
2379 # close the data connection, returning a 426 reply to
2380 # indicate that the transfer terminated abnormally, then it
2381 # must send a 226 reply, indicating that the abort command
2382 # was successfully processed.
2383 # If no data has been transmitted we just respond with 225
2384 # indicating that no transfer was in progress.
2385 if self.data_channel:
2386 if self.data_channel.transfer_in_progress():
2387 self.data_channel.close()
2388 self.data_channel = None
2389 self.respond("426 Connection closed; transfer aborted.")
2390 self.log("OK ABOR. Transfer aborted, data channel closed.")
2391 resp = "226 ABOR command successful."
2393 self.data_channel.close()
2394 self.data_channel = None
2395 self.log("OK ABOR. Data channel closed.")
2396 resp = "225 ABOR command successful; data channel closed."
2400 # --- authentication
2402 def ftp_USER(self, line):
2403 """Set the username for the current session."""
2404 # we always treat anonymous user as lower-case string.
2405 if line.lower() == "anonymous":
2408 # RFC-959 specifies a 530 response to the USER command if the
2409 # username is not valid. If the username is valid is required
2410 # ftpd returns a 331 response instead. In order to prevent a
2411 # malicious client from determining valid usernames on a server,
2412 # it is suggested by RFC-2577 that a server always return 331 to
2413 # the USER command and then reject the combination of username
2414 # and password for an invalid username when PASS is provided later.
2415 if not self.authenticated:
2416 self.respond('331 Username ok, send password.')
2418 # a new USER command could be entered at any point in order
2419 # to change the access control flushing any user, password,
2420 # and account information already supplied and beginning the
2421 # login sequence again.
2422 self.flush_account()
2423 msg = 'Previous account information was flushed'
2424 self.log('OK USER "%s". %s.' %(line, msg))
2425 self.respond('331 %s, send password.' %msg)
2426 self.username = line
2428 def ftp_PASS(self, line):
2429 """Check username's password against the authorizer."""
2431 if self.authenticated:
2432 self.respond("503 User already authenticated.")
2434 if not self.username:
2435 self.respond("503 Login with USER first.")
2439 if self.authorizer.has_user(self.username):
2440 if self.username == 'anonymous' \
2441 or self.authorizer.validate_authentication(self.username, line):
2442 msg_login = self.authorizer.get_msg_login(self.username)
2443 if len(msg_login) <= 75:
2444 self.respond('230 %s' %msg_login)
2446 self.push("230-%s\r\n" %msg_login)
2447 self.respond("230 ")
2449 self.authenticated = True
2450 self.password = line
2451 self.attempted_logins = 0
2452 self.fs.root = self.authorizer.get_home_dir(self.username)
2453 self.fs.username=self.username
2454 self.fs.password=line
2455 self.log("User %s logged in." %self.username)
2457 self.attempted_logins += 1
2458 if self.attempted_logins >= self.max_login_attempts:
2459 self.respond("530 Maximum login attempts. Disconnecting.")
2462 self.respond("530 Authentication failed.")
2463 self.log('Authentication failed (user: "%s").' %self.username)
2468 self.attempted_logins += 1
2469 if self.attempted_logins >= self.max_login_attempts:
2470 self.log('Authentication failed: unknown username "%s".'
2472 self.respond("530 Maximum login attempts. Disconnecting.")
2474 elif self.username.lower() == 'anonymous':
2475 self.respond("530 Anonymous access not allowed.")
2476 self.log('Authentication failed: anonymous access not allowed.')
2478 self.respond("530 Authentication failed.")
2479 self.log('Authentication failed: unknown username "%s".'
2483 def ftp_REIN(self, line):
2484 """Reinitialize user's current session."""
2486 # REIN command terminates a USER, flushing all I/O and account
2487 # information, except to allow any transfer in progress to be
2488 # completed. All parameters are reset to the default settings
2489 # and the control connection is left open. This is identical
2490 # to the state in which a user finds himself immediately after
2491 # the control connection is opened.
2492 self.log("OK REIN. Flushing account information.")
2493 self.flush_account()
2494 # Note: RFC-959 erroneously mention "220" as the correct response
2495 # code to be given in this case, but this is wrong...
2496 self.respond("230 Ready for new user.")
2499 # --- filesystem operations
2501 def ftp_PWD(self, line):
2502 """Return the name of the current working directory to the client."""
2503 self.respond('257 "%s" is the current directory.' %self.fs.cwd)
2505 def ftp_CWD(self, line):
2506 """Change the current working directory."""
2507 # TODO: a lot of FTP servers go back to root directory if no
2508 # arg is provided but this is not specified in RFC-959.
2509 # Search for official references about this behaviour.
2514 datacr = self.fs.get_cr(line)
2515 path = self.fs.ftp2fs(line, datacr)
2516 self.run_as_current_user(self.fs.chdir, path)
2517 except OSError, err:
2519 why = 'Authentication Required or Failed'
2520 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2521 self.respond('530 %s.' %why)
2523 why = _strerror(err)
2524 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2525 self.respond('550 %s.' %why)
2527 self.log('OK CWD "%s".' %self.fs.cwd)
2528 self.respond('250 "%s" is the current directory.' %self.fs.cwd)
2529 self.fs.close_cr(datacr)
2531 def ftp_CDUP(self, line):
2532 """Change into the parent directory."""
2533 # Note: RFC-959 says that code 200 is required but it also says
2534 # that CDUP uses the same codes as CWD.
2537 def ftp_SIZE(self, line):
2538 """Return size of file in a format suitable for using with
2539 RESTart as defined in RFC-3659.
2541 Implementation note:
2542 properly handling the SIZE command when TYPE ASCII is used would
2543 require to scan the entire file to perform the ASCII translation
2544 logic (file.read().replace(os.linesep, '\r\n')) and then
2545 calculating the len of such data which may be different than
2546 the actual size of the file on the server. Considering that
2547 calculating such result could be very resource-intensive it
2548 could be easy for a malicious client to try a DoS attack, thus
2549 we do not perform the ASCII translation.
2551 However, clients in general should not be resuming downloads in
2552 ASCII mode. Resuming downloads in binary mode is the recommended
2553 way as specified in RFC-3659.
2557 datacr = self.fs.get_cr(line)
2558 path = self.fs.ftp2fs(line, datacr)
2559 line = self.fs.ftpnorm(line)
2560 if self.fs.isdir(path):
2561 why = "%s is not retrievable" %line
2562 self.log('FAIL SIZE "%s". %s.' %(line, why))
2563 self.respond("550 %s." %why)
2564 self.fs.close_cr(datacr)
2566 size = self.run_as_current_user(self.fs.getsize, path)
2567 except OSError, err:
2568 why = _strerror(err)
2569 self.log('FAIL SIZE "%s". %s.' %(line, why))
2570 self.respond('550 %s.' %why)
2572 self.respond("213 %s" %size)
2573 self.log('OK SIZE "%s".' %line)
2574 self.fs.close_cr(datacr)
2576 def ftp_MDTM(self, line):
2577 """Return last modification time of file to the client as an ISO
2578 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2582 datacr = self.fs.get_cr(line)
2583 path = self.fs.ftp2fs(line, datacr)
2584 line = self.fs.ftpnorm(line)
2585 if not self.fs.isfile(self.fs.realpath(path)):
2586 why = "%s is not retrievable" %line
2587 self.log('FAIL MDTM "%s". %s.' %(line, why))
2588 self.respond("550 %s." %why)
2589 self.fs.close_cr(datacr)
2591 lmt = self.run_as_current_user(self.fs.getmtime, path)
2592 except OSError, err:
2593 why = _strerror(err)
2594 self.log('FAIL MDTM "%s". %s.' %(line, why))
2595 self.respond('550 %s.' %why)
2597 lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
2598 self.respond("213 %s" %lmt)
2599 self.log('OK MDTM "%s".' %line)
2600 self.fs.close_cr(datacr)
2602 def ftp_MKD(self, line):
2603 """Create the specified directory."""
2605 line = self.fs.ftpnorm(line)
2606 basedir,basename = os.path.split(line)
2608 datacr = self.fs.get_cr(line)
2609 path = self.fs.ftp2fs(basedir, datacr)
2610 self.run_as_current_user(self.fs.mkdir, path, basename)
2611 except OSError, err:
2612 why = _strerror(err)
2613 self.log('FAIL MKD "%s". %s.' %(line, why))
2614 self.respond('550 %s.' %why)
2616 self.log('OK MKD "%s".' %line)
2617 self.respond("257 Directory created.")
2618 self.fs.close_cr(datacr)
2620 def ftp_RMD(self, line):
2621 """Remove the specified directory."""
2624 datacr = self.fs.get_cr(line)
2625 path = self.fs.ftp2fs(line, datacr)
2626 line = self.fs.ftpnorm(line)
2627 if self.fs.realpath(path) == self.fs.realpath(self.fs.root):
2628 msg = "Can't remove root directory."
2629 self.respond("550 %s" %msg)
2630 self.log('FAIL MKD "/". %s' %msg)
2631 self.fs.close_cr(datacr)
2633 self.run_as_current_user(self.fs.rmdir, path)
2634 except OSError, err:
2635 why = _strerror(err)
2636 self.log('FAIL RMD "%s". %s.' %(line, why))
2637 self.respond('550 %s.' %why)
2639 self.log('OK RMD "%s".' %line)
2640 self.respond("250 Directory removed.")
2641 self.fs.close_cr(datacr)
2643 def ftp_DELE(self, line):
2644 """Delete the specified file."""
2647 datacr = self.fs.get_cr(line)
2648 path = self.fs.ftp2fs(line, datacr)
2649 line = self.fs.ftpnorm(line)
2650 self.run_as_current_user(self.fs.remove, path)
2651 except OSError, err:
2652 why = _strerror(err)
2653 self.log('FAIL DELE "%s". %s.' %(line, why))
2654 self.respond('550 %s.' %why)
2656 self.log('OK DELE "%s".' %line)
2657 self.respond("250 File removed.")
2658 self.fs.close_cr(datacr)
2660 def ftp_RNFR(self, line):
2661 """Rename the specified (only the source name is specified
2662 here, see RNTO command)"""
2665 datacr = self.fs.get_cr(line)
2666 line = self.fs.ftpnorm(line)
2667 path = self.fs.ftp2fs(line, datacr)
2668 if not self.fs.lexists(path):
2669 self.respond("550 No such file or directory.")
2670 elif self.fs.realpath(path) == self.fs.realpath(self.fs.root):
2671 self.respond("550 Can't rename the home directory.")
2674 self.respond("350 Ready for destination name.")
2676 self.respond("550 Can't find the file or directory.")
2677 self.fs.close_cr(datacr)
2679 def ftp_RNTO(self, line):
2680 """Rename file (destination name only, source is specified with
2683 if not self.fs.rnfr:
2684 self.respond("503 Bad sequence of commands: use RNFR first.")
2689 datacr = self.fs.get_cr(line)
2690 src = self.fs.ftp2fs(self.fs.rnfr, datacr)
2691 line = self.fs.ftpnorm(line)
2692 basedir,basename = os.path.split(line)
2693 dst = self.fs.ftp2fs(basedir, datacr)
2694 self.run_as_current_user(self.fs.rename, src, dst,basename)
2695 except OSError, err:
2696 why = _strerror(err)
2697 self.log('FAIL RNFR/RNTO "%s ==> %s". %s.' \
2698 %(self.fs.ftpnorm(self.fs.rnfr), line, why))
2699 self.respond('550 %s.' %why)
2701 self.log('OK RNFR/RNTO "%s ==> %s".' \
2702 %(self.fs.ftpnorm(self.fs.rnfr), line))
2703 self.respond("250 Renaming ok.")
2706 self.fs.close_cr(datacr)
2711 def ftp_TYPE(self, line):
2712 """Set current type data type to binary/ascii"""
2714 if line in ("A", "AN", "A N"):
2715 self.respond("200 Type set to: ASCII.")
2716 self.current_type = 'a'
2717 elif line in ("I", "L8", "L 8"):
2718 self.respond("200 Type set to: Binary.")
2719 self.current_type = 'i'
2721 self.respond('504 Unsupported type "%s".' %line)
2723 def ftp_STRU(self, line):
2724 """Set file structure (obsolete)."""
2725 # obsolete (backward compatibility with older ftp clients)
2726 if line in ('f','F'):
2727 self.respond('200 File transfer structure set to: F.')
2729 self.respond('504 Unimplemented STRU type.')
2731 def ftp_MODE(self, line):
2732 """Set data transfer mode (obsolete)"""
2733 # obsolete (backward compatibility with older ftp clients)
2734 if line in ('s', 'S'):
2735 self.respond('200 Transfer mode set to: S')
2737 self.respond('504 Unimplemented MODE type.')
2739 def ftp_STAT(self, line):
2740 """Return statistics about current ftp session. If an argument
2741 is provided return directory listing over command channel.
2743 Implementation note:
2745 RFC-959 do not explicitly mention globbing; this means that FTP
2746 servers are not required to support globbing in order to be
2747 compliant. However, many FTP servers do support globbing as a
2748 measure of convenience for FTP clients and users.
2750 In order to search for and match the given globbing expression,
2751 the code has to search (possibly) many directories, examine
2752 each contained filename, and build a list of matching files in
2753 memory. Since this operation can be quite intensive, both CPU-
2754 and memory-wise, we limit the search to only one directory
2755 non-recursively, as LIST does.
2757 # return STATus information about ftpd
2760 s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2761 if self.authenticated:
2762 s.append('Logged in as: %s' %self.username)
2764 if not self.username:
2765 s.append("Waiting for username.")
2767 s.append("Waiting for password.")
2768 if self.current_type == 'a':
2772 s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
2773 if self.data_server:
2774 s.append('Passive data channel waiting for connection.')
2775 elif self.data_channel:
2776 bytes_sent = self.data_channel.tot_bytes_sent
2777 bytes_recv = self.data_channel.tot_bytes_received
2778 s.append('Data connection open:')
2779 s.append('Total bytes sent: %s' %bytes_sent)
2780 s.append('Total bytes received: %s' %bytes_recv)
2782 s.append('Data connection closed.')
2784 self.push('211-FTP server status:\r\n')
2785 self.push(''.join([' %s\r\n' %item for item in s]))
2786 self.respond('211 End of status.')
2787 # return directory LISTing over the command channel
2791 datacr = self.fs.get_cr(line)
2792 iterator = self.run_as_current_user(self.fs.get_stat_dir, line, datacr)
2793 except OSError, err:
2794 self.respond('550 %s.' %_strerror(err))
2796 self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
2797 self.push_with_producer(BufferedIteratorProducer(iterator))
2798 self.respond('213 End of status.')
2799 self.fs.close_cr(datacr)
2801 def ftp_FEAT(self, line):
2802 """List all new features supported as defined in RFC-2398."""
2803 features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
2805 for fact in self.available_facts:
2806 if fact in self.current_facts:
2810 features.append('MLST ' + s)
2812 self.push("211-Features supported:\r\n")
2813 self.push("".join([" %s\r\n" %x for x in features]))
2814 self.respond('211 End FEAT.')
2816 def ftp_OPTS(self, line):
2817 """Specify options for FTP commands as specified in RFC-2389."""
2819 assert (not line.count(' ') > 1), 'Invalid number of arguments'
2821 cmd, arg = line.split(' ')
2822 assert (';' in arg), 'Invalid argument'
2825 # actually the only command able to accept options is MLST
2826 assert (cmd.upper() == 'MLST'), 'Unsupported command "%s"' %cmd
2827 except AssertionError, err:
2828 self.respond('501 %s.' %err)
2830 facts = [x.lower() for x in arg.split(';')]
2831 self.current_facts = [x for x in facts if x in self.available_facts]
2832 f = ''.join([x + ';' for x in self.current_facts])
2833 self.respond('200 MLST OPTS ' + f)
2835 def ftp_NOOP(self, line):
2837 self.respond("200 I successfully done nothin'.")
2839 def ftp_SYST(self, line):
2840 """Return system type (always returns UNIX type: L8)."""
2841 # This command is used to find out the type of operating system
2842 # at the server. The reply shall have as its first word one of
2843 # the system names listed in RFC-943.
2844 # Since that we always return a "/bin/ls -lA"-like output on
2845 # LIST we prefer to respond as if we would on Unix in any case.
2846 self.respond("215 UNIX Type: L8")
2848 def ftp_ALLO(self, line):
2849 """Allocate bytes for storage (obsolete)."""
2850 # obsolete (always respond with 202)
2851 self.respond("202 No storage allocation necessary.")
2853 def ftp_HELP(self, line):
2854 """Return help text to the client."""
2856 if line.upper() in proto_cmds:
2857 self.respond("214 %s" %proto_cmds[line.upper()])
2859 self.respond("501 Unrecognized command.")
2861 # provide a compact list of recognized commands
2862 def formatted_help():
2864 keys = proto_cmds.keys()
2867 elems = tuple((keys[0:8]))
2868 cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2870 return ''.join(cmds)
2872 self.push("214-The following commands are recognized:\r\n")
2873 self.push(formatted_help())
2874 self.respond("214 Help command successful.")
2877 # --- support for deprecated cmds
2879 # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
2880 # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
2881 # Such commands are obsoleted but some ftp clients (e.g. Windows
2882 # ftp.exe) still use them.
2884 def ftp_XCUP(self, line):
2885 """Change to the parent directory. Synonym for CDUP. Deprecated."""
2888 def ftp_XCWD(self, line):
2889 """Change the current working directory. Synonym for CWD. Deprecated."""
2892 def ftp_XMKD(self, line):
2893 """Create the specified directory. Synonym for MKD. Deprecated."""
2896 def ftp_XPWD(self, line):
2897 """Return the current working directory. Synonym for PWD. Deprecated."""
2900 def ftp_XRMD(self, line):
2901 """Remove the specified directory. Synonym for RMD. Deprecated."""
2905 class FTPServer(asyncore.dispatcher):
2906 """This class is an asyncore.disptacher subclass. It creates a FTP
2907 socket listening on <address>, dispatching the requests to a <handler>
2908 (typically FTPHandler class).
2910 Depending on the type of address specified IPv4 or IPv6 connections
2911 (or both, depending from the underlying system) will be accepted.
2913 All relevant session information is stored in class attributes
2915 Overriding them is strongly recommended to avoid running out of
2916 file descriptors (DoS)!
2919 number of maximum simultaneous connections accepted (defaults
2922 - (int) max_cons_per_ip:
2923 number of maximum connections accepted for the same IP address
2924 (defaults to 0 == unlimited).
2930 def __init__(self, address, handler):
2931 """Initiate the FTP server opening listening on address.
2933 - (tuple) address: the host:port pair on which the command
2934 channel will listen.
2936 - (classobj) handler: the handler class to use.
2938 asyncore.dispatcher.__init__(self)
2939 self.handler = handler
2941 host, port = address
2943 # AF_INET or AF_INET6 socket
2944 # Get the correct address family for our host (allows IPv6 addresses)
2946 info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
2947 socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
2948 except socket.gaierror:
2949 # Probably a DNS issue. Assume IPv4.
2950 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2951 self.set_reuse_addr()
2952 self.bind((host, port))
2955 af, socktype, proto, canonname, sa = res
2957 self.create_socket(af, socktype)
2958 self.set_reuse_addr()
2960 except socket.error, msg:
2967 raise socket.error, msg
2970 def set_reuse_addr(self):
2971 # Overridden for convenience. Avoid to reuse address on Windows.
2972 if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'):
2974 asyncore.dispatcher.set_reuse_addr(self)
2976 def serve_forever(self, **kwargs):
2977 """A wrap around asyncore.loop(); starts the asyncore polling
2980 The keyword arguments in kwargs are the same expected by
2981 asyncore.loop() function: timeout, use_poll, map and count.
2983 if not 'count' in kwargs:
2984 log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2986 # backward compatibility for python < 2.4
2987 if not hasattr(self, '_map'):
2988 if not 'map' in kwargs:
2989 map = asyncore.socket_map
2992 self._map = self.handler._map = map
2996 # use_poll specifies whether to use select module's poll()
2997 # with asyncore or whether to use asyncore's own poll()
2998 # method Python versions < 2.4 need use_poll set to False
2999 # This breaks on OS X systems if use_poll is set to True.
3000 # All systems seem to work fine with it set to False
3001 # (tested on Linux, Windows, and OS X platforms)
3003 asyncore.loop(**kwargs)
3005 asyncore.loop(timeout=1.0, use_poll=False)
3006 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3007 log("Shutting down FTPd.")
3010 def handle_accept(self):
3011 """Called when remote client initiates a connection."""
3012 sock_obj, addr = self.accept()
3013 log("[]%s:%s Connected." %addr[:2])
3015 handler = self.handler(sock_obj, self)
3017 self.ip_map.append(ip)
3019 # For performance and security reasons we should always set a
3020 # limit for the number of file descriptors that socket_map
3021 # should contain. When we're running out of such limit we'll
3022 # use the last available channel for sending a 421 response
3023 # to the client before disconnecting it.
3025 if len(self._map) > self.max_cons:
3026 handler.handle_max_cons()
3029 # accept only a limited number of connections from the same
3031 if self.max_cons_per_ip:
3032 if self.ip_map.count(ip) > self.max_cons_per_ip:
3033 handler.handle_max_cons_per_ip()
3041 def handle_error(self):
3042 """Called to handle any uncaught exceptions."""
3045 except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3047 logerror(traceback.format_exc())
3050 def close_all(self, map=None, ignore_all=False):
3051 """Stop serving; close all existent connections disconnecting
3055 A dictionary whose items are the channels to close.
3056 If map is omitted, the default asyncore.socket_map is used.
3058 - (bool) ignore_all:
3059 having it set to False results in raising exception in case
3060 of unexpected errors.
3062 Implementation note:
3064 Instead of using the current asyncore.close_all() function
3065 which only close sockets, we iterate over all existent channels
3066 calling close() method for each one of them, avoiding memory
3069 This is how asyncore.close_all() function should work in
3074 for x in map.values():
3078 if x[0] == errno.EBADF:
3080 elif not ignore_all:
3082 except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3091 # cmd line usage (provide a read-only anonymous ftp server):
3092 # python -m pyftpdlib.FTPServer
3093 authorizer = DummyAuthorizer()
3094 authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
3095 FTPHandler.authorizer = authorizer
3096 address = ('', 8021)
3097 ftpd = FTPServer(address, FTPHandler)
3098 ftpd.serve_forever()
3100 if __name__ == '__main__':
3103 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: