Document FTP: adapt to node_descriptors API
[odoo/odoo.git] / addons / document_ftp / ftpserver / ftpserver.py
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3 # ftpserver.py
4 #
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>
9 #
10 #                         All Rights Reserved
11 #
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
19 #  permission.
20 #
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 #  ======================================================================
29
30
31 """pyftpdlib: RFC-959 asynchronous FTP server.
32
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:
36
37     [FTPServer] - the base class for the backend.
38
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
42     current PI session.
43
44     [ActiveDTP], [PassiveDTP] - base classes for active/passive-DTP
45     backends.
46
47     [DTPHandler] - this class handles processing of data transfer
48     operations (server-DTP, see RFC-959).
49
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.
56
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.
60
61     [AuthorizerError] - base class for authorizers exceptions.
62
63
64 pyftpdlib also provides 3 different logging streams through 3 functions
65 which can be overridden to allow for custom logging.
66
67     [log] - the main logger that logs the most important messages for
68     the end user regarding the FTPd.
69
70     [logline] - this function is used to log commands and responses
71     passing through the control FTP channel.
72
73     [logerror] - log traceback outputs occurring in case of errors.
74
75
76 Usage example:
77
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.
107 """
108
109
110 import asyncore
111 import asynchat
112 import socket
113 import os
114 import sys
115 import traceback
116 import errno
117 import time
118 import glob
119 import fnmatch
120 import tempfile
121 import warnings
122 import random
123 import stat
124 from tarfile import filemode
125
126 try:
127     import pwd
128     import grp
129 except ImportError:
130     pwd = grp = None
131
132
133 LOG_ACTIVE = True
134
135 __all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer',
136            'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler',
137            'FileProducer', 'IteratorProducer', 'BufferedIteratorProducer',
138            'AbstractedFS',]
139
140
141 __pname__   = 'Python FTP server library (pyftpdlib)'
142 __ver__     = '0.4.0'
143 __date__    = '2008-05-16'
144 __author__  = "Giampaolo Rodola' <g.rodola@gmail.com>"
145 __web__     = 'http://code.google.com/p/pyftpdlib/'
146
147
148 proto_cmds = {
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).',
192     }
193
194
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'):
198     try:
199         import cStringIO as StringIO
200     except ImportError:
201         import StringIO
202
203     def _format_exc():
204         f = StringIO.StringIO()
205         traceback.print_exc(file=f)
206         data = f.getvalue()
207         f.close()
208         return data
209
210     traceback.format_exc = _format_exc
211
212
213 def _strerror(err):
214     """A wrap around os.strerror() which may be not available on all
215     platforms (e.g. pythonCE).
216
217      - (instance) err: an EnvironmentError or derived class instance.
218     """
219     if hasattr(os, 'strerror'):
220         return os.strerror(err.errno)
221     else:
222         return err.strerror
223
224
225 # --- library defined exceptions
226
227 class Error(Exception):
228     """Base class for module exceptions."""
229
230 class AuthorizerError(Error):
231     """Base class for authorizer exceptions."""
232
233
234 # --- loggers
235
236 def log(msg):
237     """Log messages intended for the end user."""
238     if LOG_ACTIVE:
239         print msg
240
241 def logline(msg):
242     """Log commands and responses passing through the command channel."""
243     if LOG_ACTIVE:
244         print msg
245
246 def logerror(msg):
247     """Log traceback outputs occurring in case of errors."""
248     sys.stderr.write(str(msg) + '\n')
249     sys.stderr.flush()
250
251
252 # --- authorizers
253
254 class DummyAuthorizer:
255     """Basic "dummy" authorizer class, suitable for subclassing to
256     create your own custom authorizers.
257
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.
263
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.
268     """
269
270     read_perms = "elr"
271     write_perms = "adfmw"
272
273     def __init__(self):
274         self.user_table = {}
275
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.
279
280         AuthorizerError exceptions raised on error conditions such as
281         invalid permissions, missing home directory or duplicate usernames.
282
283         Optional perm argument is a string referencing the user's
284         permissions explained below:
285
286         Read permissions:
287          - "e" = change directory (CWD command)
288          - "l" = list files (LIST, NLST, MLSD commands)
289          - "r" = retrieve file from the server (RETR command)
290
291         Write permissions:
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)
297
298         Optional msg_login and msg_quit arguments can be specified to
299         provide customized response strings when user log-in and quit.
300         """
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)
306         for p in perm:
307             if p not in 'elradfmw':
308                 raise AuthorizerError('No such permission "%s"' %p)
309         for p in perm:
310             if (p in self.write_perms) and (username == 'anonymous'):
311                 warnings.warn("write permissions assigned to anonymous user.",
312                               RuntimeWarning)
313                 break
314         dic = {'pwd': str(password),
315                'home': homedir,
316                'perm': perm,
317                'msg_login': str(msg_login),
318                'msg_quit': str(msg_quit)
319                }
320         self.user_table[username] = dic
321
322     def add_anonymous(self, homedir, **kwargs):
323         """Add an anonymous user to the virtual users table.
324
325         AuthorizerError exception raised on error conditions such as
326         invalid permissions, missing home directory, or duplicate
327         anonymous users.
328
329         The keyword arguments in kwargs are the same expected by
330         add_user method: "perm", "msg_login" and "msg_quit".
331
332         The optional "perm" keyword argument is a string defaulting to
333         "elr" referencing "read-only" anonymous user's permissions.
334
335         Using write permission values ("adfmw") results in a
336         RuntimeWarning.
337         """
338         DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
339
340     def remove_user(self, username):
341         """Remove a user from the virtual users table."""
342         del self.user_table[username]
343
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
348
349     def impersonate_user(self, username, password):
350         """Impersonate another user (noop).
351
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
355         current user.
356         """
357
358     def terminate_impersonation(self):
359         """Terminate impersonation (noop).
360
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.
365         """
366
367     def has_user(self, username):
368         """Whether the username exists in the virtual users table."""
369         return username in self.user_table
370
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).
374
375         Expected perm argument is one of the following letters:
376         "elradfmw".
377         """
378         return perm in self.user_table[username]['perm']
379
380     def get_perms(self, username):
381         """Return current user permissions."""
382         return self.user_table[username]['perm']
383
384     def get_home_dir(self, username):
385         """Return the user's home directory."""
386         return self.user_table[username]['home']
387
388     def get_msg_login(self, username):
389         """Return the user's login message."""
390         return self.user_table[username]['msg_login']
391
392     def get_msg_quit(self, username):
393         """Return the user's quitting message."""
394         return self.user_table[username]['msg_quit']
395
396
397 # --- DTP classes
398
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.
403     """
404
405     def __init__(self, cmd_channel, extmode=False):
406         """Initialize the passive data server.
407
408          - (instance) cmd_channel: the command channel class instance.
409          - (bool) extmode: wheter use extended passive mode response type.
410         """
411         asyncore.dispatcher.__init__(self)
412         self.cmd_channel = cmd_channel
413
414         ip = self.cmd_channel.getsockname()[0]
415         self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
416
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.
420             self.bind((ip, 0))
421         else:
422             ports = list(self.cmd_channel.passive_ports)
423             while ports:
424                 port = ports.pop(random.randint(0, len(ports) -1))
425                 try:
426                     self.bind((ip, port))
427                 except socket.error, why:
428                     if why[0] == errno.EADDRINUSE:  # port already in use
429                         if ports:
430                             continue
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.
436                         else:
437                             self.bind((ip, 0))
438                             self.cmd_channel.log(
439                                 "Can't find a valid passive port in the "
440                                 "configured range. A random kernel-assigned "
441                                 "port will be used."
442                                 )
443                     else:
444                         raise
445                 else:
446                     break
447         self.listen(5)
448         port = self.socket.getsockname()[1]
449         if not extmode:
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))
456         else:
457             self.cmd_channel.respond('229 Entering extended passive mode '
458                                      '(|||%d|).' %port)
459
460     # --- connection / overridden
461
462     def handle_accept(self):
463         """Called when remote client initiates a connection."""
464         sock, addr = self.accept()
465
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:
471                 try:
472                     sock.close()
473                 except socket.error:
474                     pass
475                 msg = 'Rejected data connection from foreign address %s:%s.' \
476                         %(addr[0], addr[1])
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
480                 return
481             else:
482                 # site-to-site FTP allowed
483                 msg = 'Established data connection with foreign address %s:%s.'\
484                         %(addr[0], addr[1])
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
488         # limit.
489         self.close()
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()
494
495     def writable(self):
496         return 0
497
498     def handle_error(self):
499         """Called to handle any uncaught exceptions."""
500         try:
501             raise
502         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
503             raise
504         logerror(traceback.format_exc())
505         self.close()
506
507     def handle_close(self):
508         """Called on closing the data connection."""
509         self.close()
510
511
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.
516     """
517
518     def __init__(self, ip, port, cmd_channel):
519         """Initialize the active data channel attemping to connect
520         to remote data socket.
521
522          - (str) ip: the remote IP address.
523          - (int) port: the remote port.
524          - (instance) cmd_channel: the command channel class instance.
525         """
526         asyncore.dispatcher.__init__(self)
527         self.cmd_channel = cmd_channel
528         self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
529         try:
530             self.connect((ip, port))
531         except socket.gaierror:
532             self.cmd_channel.respond("425 Can't connect to specified address.")
533             self.close()
534
535     # --- connection / overridden
536
537     def handle_write(self):
538         """NOOP, must be overridden to prevent unhandled write event."""
539
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)
548
549     def handle_expt(self):
550         self.cmd_channel.respond("425 Can't connect to specified address.")
551         self.close()
552
553     def handle_error(self):
554         """Called to handle any uncaught exceptions."""
555         try:
556             raise
557         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
558             raise
559         except socket.error:
560             pass
561         except:
562             logerror(traceback.format_exc())
563         self.cmd_channel.respond("425 Can't connect to specified address.")
564         self.close()
565
566
567 try:
568     from collections import deque
569 except ImportError:
570     # backward compatibility with Python < 2.4 by replacing deque with a list
571     class deque(list):
572         def appendleft(self, obj):
573             list.insert(self, 0, obj)
574
575
576 class DTPHandler(asyncore.dispatcher):
577     """Class handling server-data-transfer-process (server-DTP, see
578     RFC-959) managing data-transfer operations involving sending
579     and receiving data.
580
581     Instance attributes defined in this class, initialized when
582     channel is opened:
583
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.
590
591     DTPHandler implementation note:
592
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)
596
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.
601
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.
605
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.
609     """
610
611     ac_in_buffer_size = 8192
612     ac_out_buffer_size  = 8192
613
614     def __init__(self, sock_obj, cmd_channel):
615         """Initialize the command channel.
616
617          - (instance) sock_obj: the socket object instance of the newly
618             established connection.
619          - (instance) cmd_channel: the command channel class instance.
620         """
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
624         # was a wrapping of
625         self.producer_fifo = deque()
626
627         self.cmd_channel = cmd_channel
628         self.file_obj = None
629         self.receive = False
630         self.transfer_finished = False
631         self.tot_bytes_sent = 0
632         self.tot_bytes_received = 0
633         self.data_wrapper = lambda x: x
634
635     # --- utility methods
636
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
640         incoming data.
641
642          - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
643         """
644         if type == 'a':
645             self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
646         elif type == 'i':
647             self.data_wrapper = lambda x: x
648         else:
649             raise TypeError, "Unsupported type"
650         self.receive = True
651
652     def get_transmitted_bytes(self):
653         "Return the number of transmitted bytes."
654         return self.tot_bytes_sent + self.tot_bytes_received
655
656     def transfer_in_progress(self):
657         "Return True if a transfer is in progress, else False."
658         return self.get_transmitted_bytes() != 0
659
660     # --- connection
661
662     def handle_read(self):
663         """Called when there is data waiting to be read."""
664         try:
665             chunk = self.recv(self.ac_in_buffer_size)
666         except socket.error:
667             self.handle_error()
668         else:
669             self.tot_bytes_received += len(chunk)
670             if not chunk:
671                 self.transfer_finished = True
672                 #self.close()  # <-- asyncore.recv() already do that...
673                 return
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))
679
680     def handle_write(self):
681         """Called when data is ready to be written, initiates send."""
682         self.initiate_send()
683
684     def push(self, data):
685         """Push data onto the deque and initiate send."""
686         sabs = self.ac_out_buffer_size
687         if len(data) > sabs:
688             for i in xrange(0, len(data), sabs):
689                 self.producer_fifo.append(data[i:i+sabs])
690         else:
691             self.producer_fifo.append(data)
692         self.initiate_send()
693
694     def push_with_producer(self, producer):
695         """Push data using a producer and initiate send."""
696         self.producer_fifo.append(producer)
697         self.initiate_send()
698
699     def readable(self):
700         """Predicate for inclusion in the readable for select()."""
701         return self.receive
702
703     def writable(self):
704         """Predicate for inclusion in the writable for select()."""
705         return self.producer_fifo or (not self.connected)
706
707     def close_when_done(self):
708         """Automatically close this channel once the outgoing queue is empty."""
709         self.producer_fifo.append(None)
710
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
716             if not first:
717                 del self.producer_fifo[0]
718                 if first is None:
719                     self.transfer_finished = True
720                     self.handle_close()
721                     return
722
723             # handle classic producer behavior
724             obs = self.ac_out_buffer_size
725             try:
726                 data = buffer(first, 0, obs)
727             except TypeError:
728                 data = first.more()
729                 if data:
730                     self.producer_fifo.appendleft(data)
731                 else:
732                     del self.producer_fifo[0]
733                 continue
734
735             # send the data
736             try:
737                 num_sent = self.send(data)
738             except socket.error:
739                 self.handle_error()
740                 return
741
742             if num_sent:
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:]
746                 else:
747                     del self.producer_fifo[0]
748             # we tried to send some actual data
749             return
750
751     def handle_expt(self):
752         """Called on "exceptional" data events."""
753         self.cmd_channel.respond("426 Connection error; transfer aborted.")
754         self.close()
755
756     def handle_error(self):
757         """Called when an exception is raised and not otherwise handled."""
758         try:
759             raise
760         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
761             raise
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, \
765                           errno.ECONNABORTED):
766                 self.handle_close()
767                 return
768             else:
769                 error = str(err[1])
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)
774         except:
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)
780         self.close()
781
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).
788         if self.receive:
789             self.transfer_finished = True
790             action = 'received'
791         else:
792             action = 'sent'
793         if self.transfer_finished:
794             self.cmd_channel.respond("226 Transfer complete.")
795             if self.file_obj:
796                 fname = self.file_obj.name
797                 self.cmd_channel.log('"%s" %s.' %(fname, action))
798         else:
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)
803         self.close()
804
805     def close(self):
806         """Close the data channel, first attempting to close any remaining
807         file handles."""
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()
812
813
814 # --- producers
815
816 class FileProducer:
817     """Producer wrapper for file[-like] objects."""
818
819     buffer_size = 65536
820
821     def __init__(self, file, type):
822         """Initialize the producer with a data_wrapper appropriate to TYPE.
823
824          - (file) file: the file[-like] object.
825          - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
826         """
827         self.done = False
828         self.file = file
829         if type == 'a':
830             self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
831         elif type == 'i':
832             self.data_wrapper = lambda x: x
833         else:
834             raise TypeError, "Unsupported type"
835
836     def more(self):
837         """Attempt a chunk of data of size self.buffer_size."""
838         if self.done:
839             return ''
840         data = self.data_wrapper(self.file.read(self.buffer_size))
841         if not data:
842             self.done = True
843             if not self.file.closed:
844                 self.file.close()
845         return data
846
847
848 class IteratorProducer:
849     """Producer for iterator objects."""
850
851     def __init__(self, iterator):
852         self.iterator = iterator
853
854     def more(self):
855         """Attempt a chunk of data from iterator by calling its next()
856         method.
857         """
858         try:
859             return self.iterator.next()
860         except StopIteration:
861             return ''
862
863
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
868     loops = 20
869
870     def __init__(self, iterator):
871         self.iterator = iterator
872
873     def more(self):
874         """Attempt a chunk of data from iterator by calling
875         its next() method different times.
876         """
877         buffer = []
878         for x in xrange(self.loops):
879             try:
880                 buffer.append(self.iterator.next())
881             except StopIteration:
882                 break
883         return ''.join(buffer)
884
885
886 # --- filesystem
887
888 class AbstractedFS:
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.
892
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.
896
897     Instance attributes:
898      - (str) root: the user home directory.
899      - (str) cwd: the current working directory.
900      - (str) rnfr: source file to be renamed.
901     """
902
903     def __init__(self):
904         self.root = None
905         self.cwd = '/'
906         self.rnfr = None
907
908     # --- Pathname / conversion utilities
909
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.
913
914         Example (having "/foo" as current working directory):
915         'x' -> '/foo/x'
916
917         Note: directory separators are system independent ("/").
918         Pathname returned is always absolutized.
919         """
920         if os.path.isabs(ftppath):
921             p = os.path.normpath(ftppath)
922         else:
923             p = os.path.normpath(os.path.join(self.cwd, ftppath))
924         # normalize string in a standard web-path notation having '/'
925         # as separator.
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
930         while p[:2] == '//':
931             p = p[1:]
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):
936             p = "/"
937         return p
938
939     def ftp2fs(self, ftppath):
940         """Translate a "virtual" ftp pathname (tipically the raw string
941         coming from client) into equivalent absolute "real" filesystem
942         pathname.
943
944         Example (having "/home/user" as root directory):
945         'x' -> '/home/user/x'
946
947         Note: directory separators are system dependent.
948         """
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))
952         else:
953             p = self.ftpnorm(ftppath)[1:]
954             return os.path.normpath(os.path.join(self.root, p))
955
956     def fs2ftp(self, fspath):
957         """Translate a "real" filesystem pathname into equivalent
958         absolute "virtual" ftp pathname depending on the user's
959         root directory.
960
961         Example (having "/home/user" as root directory):
962         '/home/user/x' -> '/x'
963
964         As for ftpnorm, directory separators are system independent
965         ("/") and pathname returned is always absolutized.
966
967         On invalid pathnames escaping from user's root directory
968         (e.g. "/home" when root is "/home/user") always return "/".
969         """
970         if os.path.isabs(fspath):
971             p = os.path.normpath(fspath)
972         else:
973             p = os.path.normpath(os.path.join(self.root, fspath))
974         if not self.validpath(p):
975             return '/'
976         p = p.replace(os.sep, "/")
977         p = p[len(self.root):]
978         if not p.startswith('/'):
979             p = '/' + p
980         return p
981
982     # alias for backward compatibility with 0.2.0
983     normalize = ftpnorm
984     translate = ftp2fs
985
986     def validpath(self, path):
987         """Check whether the path belongs to user's home directory.
988         Expected argument is a "real" filesystem pathname.
989
990         If path is a symbolic link it is resolved to check its real
991         destination.
992
993         Pathnames escaping from user's root directory are considered
994         not valid.
995         """
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:
1003             return True
1004         return False
1005
1006     # --- Wrapper methods around open() and tempfile.mkstemp
1007
1008     def open(self, filename, mode):
1009         """Open a file returning its handler."""
1010         return open(filename, mode)
1011
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
1015         interface.
1016         """
1017         class FileWrapper:
1018             def __init__(self, fd, name):
1019                 self.file = fd
1020                 self.name = name
1021             def __getattr__(self, attr):
1022                 return getattr(self.file, attr)
1023
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)
1030
1031     # --- Wrapper methods around os.*
1032
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()
1038         try:
1039             os.chdir(path)
1040         except os.error:
1041             raise
1042         else:
1043             os.chdir(basedir)
1044             self.cwd = self.fs2ftp(path)
1045
1046     def mkdir(self, path, basename):
1047         """Create the specified directory."""
1048         os.mkdir(os.path.join(path, basename))
1049
1050     def listdir(self, path):
1051         """List the content of a directory."""
1052         return os.listdir(path)
1053
1054     def rmdir(self, path):
1055         """Remove the specified directory."""
1056         os.rmdir(path)
1057
1058     def remove(self, path):
1059         """Remove the specified file."""
1060         os.remove(path)
1061
1062     def rename(self, src, dst):
1063         """Rename the specified src file to the dst filename."""
1064         os.rename(src, dst)
1065
1066     def stat(self, path):
1067         """Perform a stat() system call on the given path."""
1068         return os.stat(path)
1069
1070     def lstat(self, path):
1071         """Like stat but does not follow symbolic links."""
1072         return os.lstat(path)
1073
1074     if not hasattr(os, 'lstat'):
1075         lstat = stat
1076
1077     # --- Wrapper methods around os.path.*
1078
1079     def isfile(self, path):
1080         """Return True if path is a file."""
1081         return os.path.isfile(path)
1082
1083     def islink(self, path):
1084         """Return True if path is a symbolic link."""
1085         return os.path.islink(path)
1086
1087     def isdir(self, path):
1088         """Return True if path is a directory."""
1089         return os.path.isdir(path)
1090
1091     def getsize(self, path):
1092         """Return the size of the specified file in bytes."""
1093         return os.path.getsize(path)
1094
1095     def getmtime(self, path):
1096         """Return the last modified time as a number of seconds since
1097         the epoch."""
1098         return os.path.getmtime(path)
1099
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).
1104         """
1105         return os.path.realpath(path)
1106
1107     def lexists(self, path):
1108         """Return True if path refers to an existing path, including
1109         a broken or circular symbolic link.
1110         """
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'):
1115             try:
1116                 os.lstat(path)
1117             except os.error:
1118                 return False
1119             return True
1120         # fallback
1121         else:
1122             return os.path.exists(path)
1123
1124     exists = lexists  # alias for backward compatibility with 0.2.0
1125
1126     def glob1(self, dirname, pattern):
1127         """Return a list of files matching a dirname pattern
1128         non-recursively.
1129
1130         Unlike glob.glob1 raises exception if os.listdir() fails.
1131         """
1132         names = self.listdir(dirname)
1133         if pattern[0] != '.':
1134             names = filter(lambda x: x[0] != '.', names)
1135         return fnmatch.filter(names, pattern)
1136
1137     # --- Listing utilities
1138
1139     # note: the following operations are no more blocking
1140
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.
1144         """
1145         if self.isdir(path):
1146             listing = self.listdir(path)
1147             listing.sort()
1148             return self.format_list(path, listing)
1149         # if path is a file or a symlink we return information about it
1150         else:
1151             basedir, filename = os.path.split(path)
1152             self.lstat(path)  # raise exc in case of problems
1153             return self.format_list(basedir, [filename])
1154
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.
1159
1160          - (str) rawline: the raw string passed by client as command
1161          argument.
1162         """
1163         ftppath = self.ftpnorm(rawline)
1164         if not glob.has_magic(ftppath):
1165             return self.get_list_dir(self.ftp2fs(rawline))
1166         else:
1167             basedir, basename = os.path.split(ftppath)
1168             if glob.has_magic(basedir):
1169                 return iter(['Directory recursion not supported.\r\n'])
1170             else:
1171                 basedir = self.ftp2fs(basedir)
1172                 listing = self.glob1(basedir, basename)
1173                 if listing:
1174                     listing.sort()
1175                 return self.format_list(basedir, listing)
1176
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.
1180
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()
1184          call fails.
1185
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
1190         printed.
1191
1192         This is how output appears to client:
1193
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
1197         """
1198         for basename in listing:
1199             file = os.path.join(basedir, basename)
1200             try:
1201                 st = self.lstat(file)
1202             except os.error:
1203                 if ignore_err:
1204                     continue
1205                 raise
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
1209                 nlinks = 1
1210             size = st.st_size  # file size
1211             if pwd and grp:
1212                 # get user and group name, else just use the raw uid/gid
1213                 try:
1214                     uname = pwd.getpwuid(st.st_uid).pw_name
1215                 except KeyError:
1216                     uname = st.st_uid
1217                 try:
1218                     gname = grp.getgrgid(st.st_gid).gr_name
1219                 except KeyError:
1220                     gname = st.st_gid
1221             else:
1222                 # on non-posix systems the only chance we use default
1223                 # bogus values for owner and group
1224                 uname = "owner"
1225                 gname = "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
1228             try:
1229                 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1230             except ValueError:
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)
1235
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)
1239
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
1243         MLST commands.
1244
1245         Every entry includes a list of "facts" referring the listed
1246         element.  See RFC-3659, chapter 7, to see what every single
1247         fact stands for.
1248
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()
1254          call fails.
1255
1256         Note that "facts" returned may change depending on the platform
1257         and on what user specified by using the OPTS command.
1258
1259         This is how output could appear to the client issuing
1260         a MLSD request:
1261
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
1265         """
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):
1269             permdir += 'c'
1270         if 'd' in perms:
1271             permdir += 'p'
1272         type = size = perm = modify = create = unique = mode = uid = gid = ""
1273         for basename in listing:
1274             file = os.path.join(basedir, basename)
1275             try:
1276                 st = self.stat(file)
1277             except OSError:
1278                 if ignore_err:
1279                     continue
1280                 raise
1281             # type + perm
1282             if stat.S_ISDIR(st.st_mode):
1283                 if 'type' in facts:
1284                     if basename == '.':
1285                         type = 'type=cdir;'
1286                     elif basename == '..':
1287                         type = 'type=pdir;'
1288                     else:
1289                         type = 'type=dir;'
1290                 if 'perm' in facts:
1291                     perm = 'perm=%s;' %permdir
1292             else:
1293                 if 'type' in facts:
1294                     type = 'type=file;'
1295                 if 'perm' in facts:
1296                     perm = 'perm=%s;' %permfile
1297             if 'size' in facts:
1298                 size = 'size=%s;' %st.st_size  # file size
1299             # last modification time
1300             if 'modify' in facts:
1301                 try:
1302                     modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1303                                            time.localtime(st.st_mtime))
1304                 except ValueError:
1305                     # stat.st_mtime could fail (-1) if last mtime is too old
1306                     modify = ""
1307             if 'create' in facts:
1308                 # on Windows we can provide also the creation time
1309                 try:
1310                     create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1311                                            time.localtime(st.st_ctime))
1312                 except ValueError:
1313                     create = ""
1314             # UNIX only
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)
1331
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)
1334
1335
1336 # --- FTP
1337
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.
1341
1342     All relevant session information is stored in class attributes
1343     reproduced below and can be modified before instantiating this
1344     class.
1345
1346      - (str) banner: the string sent when client connects.
1347
1348      - (int) max_login_attempts:
1349         the maximum number of wrong authentications before disconnecting
1350         the client (default 3).
1351
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).
1360
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).
1364
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).
1370
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).
1376
1377
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.
1381
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).
1390     """
1391     # these are overridable defaults
1392
1393     # default classes
1394     authorizer = DummyAuthorizer()
1395     active_dtp = ActiveDTP
1396     passive_dtp = PassiveDTP
1397     dtp_handler = DTPHandler
1398     abstracted_fs = AbstractedFS
1399
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
1407
1408     def __init__(self, conn, server):
1409         """Initialize the command channel.
1410
1411          - (instance) conn: the socket object instance of the newly
1412             established connection.
1413          - (instance) server: the ftp server class instance.
1414         """
1415         try:
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]
1421         self.in_buffer = []
1422         self.in_buffer_len = 0
1423         self.set_terminator("\r\n")
1424
1425         # session attributes
1426         self.fs = self.abstracted_fs()
1427         self.authenticated = False
1428         self.username = ""
1429         self.password = ""
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
1437
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[:]
1443         if pwd and grp:
1444             self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1445         if os.name == 'nt':
1446             self.available_facts.append('create')
1447
1448         # dtp attributes
1449         self.data_server = None
1450         self.data_channel = None
1451
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]
1458
1459     def handle(self):
1460         """Return a 220 'Ready' response to the client over the command
1461         channel.
1462         """
1463         if len(self.banner) <= 75:
1464             self.respond("220 %s" %str(self.banner))
1465         else:
1466             self.push('220-%s\r\n' %str(self.banner))
1467             self.respond('220 ')
1468
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)
1473         self.log(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.
1481         self.close()
1482
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)
1487         self.log(msg)
1488         self.close_when_done()
1489
1490     # --- asyncore / asynchat overridden methods
1491
1492     def readable(self):
1493         # if there's a quit pending we stop reading data from socket
1494         return not self.quit_pending
1495
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
1502         # such cases
1503         buflimit = 2048
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))
1507             self.in_buffer = []
1508             self.in_buffer_len = 0
1509
1510     # commands accepted before authentication
1511     unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1512
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')
1517
1518     # commands needing no argument
1519     unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1520                   'SYST','XCUP','XPWD')
1521
1522     def found_terminator(self):
1523         r"""Called when the incoming data stream matches the \r\n
1524         terminator.
1525
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).
1529         """
1530         line = ''.join(self.in_buffer)
1531         self.in_buffer = []
1532         self.in_buffer_len = 0
1533
1534         cmd = line.split(' ')[0].upper()
1535         space = line.find(' ')
1536         if space != -1:
1537             arg = line[space + 1:]
1538         else:
1539             arg = ""
1540
1541         if cmd != 'PASS':
1542             self.logline("<== %s" %line)
1543         else:
1544             self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1545
1546         # let's check if user provided an argument for those commands
1547         # needing one
1548         if not arg and cmd in self.arg_cmds:
1549             self.respond("501 Syntax error: command needs an argument.")
1550             return
1551
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.")
1555             return
1556
1557         # provide a limited set of commands if user isn't
1558         # authenticated yet
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.")
1567                 else:
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.")
1572             else:
1573                 self.respond('500 Command "%s" not understood.' %line)
1574
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)):
1578                 return
1579             method = getattr(self, 'ftp_' + cmd)
1580             method(arg)  # call the proper ftp_* method
1581
1582         else:
1583             # recognize those commands having "special semantics"
1584             if 'ABOR' in cmd:
1585                 self.ftp_ABOR("")
1586             elif 'STAT' in cmd:
1587                 self.ftp_STAT("")
1588             # unknown command
1589             else:
1590                 self.respond('500 Command "%s" not understood.' %line)
1591
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
1597         # to do so.
1598         if cmd in ('APPE','CWD','DELE','MDTM','NLST','MLSD','MLST','RETR',
1599                    'RMD','SIZE','STOR','XCWD','XRMD'):
1600             datacr = None
1601             datacr = self.fs.get_cr(line)
1602             try:
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)
1610                     return False
1611             except:
1612                 pass
1613             self.fs.close_cr(datacr)
1614         return True
1615
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',
1620                'RETR':'r',
1621                'APPE':'a',
1622                'DELE':'d', 'RMD':'d', 'XRMD':'d',
1623                'RNFR':'f',
1624                'MKD':'m', 'XMKD':'m',
1625                'STOR':'w'}
1626         if cmd in map:
1627             if cmd == 'STAT' and not line:
1628                 return True
1629             perm = map[cmd]
1630             if not line and (cmd in ('LIST','NLST','MLSD')):
1631                 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1632             else:
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)
1638                 return False
1639         return True
1640
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.
1646         """
1647         if hasattr(socket, 'MSG_OOB'):
1648             try:
1649                 data = self.socket.recv(1024, socket.MSG_OOB)
1650             except socket.error:
1651                 pass
1652             else:
1653                 self.in_buffer.append(data)
1654                 return
1655         self.log("Can't handle OOB data.")
1656         self.close()
1657
1658     def handle_error(self):
1659         try:
1660             raise
1661         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1662             raise
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):
1667                 self.handle_close()
1668                 return
1669             else:
1670                 logerror(traceback.format_exc())
1671         except:
1672             logerror(traceback.format_exc())
1673         self.close()
1674
1675     def handle_close(self):
1676         self.close()
1677
1678     _closed = False
1679     def close(self):
1680         """Close the current channel disconnecting the client."""
1681         if not self._closed:
1682             self._closed = True
1683             if self.data_server:
1684                 self.data_server.close()
1685                 del self.data_server
1686
1687             if self.data_channel:
1688                 self.data_channel.close()
1689                 del self.data_channel
1690
1691             del self.__out_dtp_queue
1692             del self.__in_dtp_queue
1693
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.")
1698
1699     # --- callbacks
1700
1701     def on_dtp_connection(self):
1702         """Called every time data channel connects (either active or
1703         passive).
1704
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
1708         receiving.
1709         """
1710         if self.data_server:
1711             self.data_server.close()
1712         self.data_server = None
1713
1714         # check for data to send
1715         if self.__out_dtp_queue:
1716             data, isproducer, file = self.__out_dtp_queue
1717             if file:
1718                 self.data_channel.file_obj = file
1719             if not isproducer:
1720                 self.data_channel.push(data)
1721             else:
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
1726
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
1732
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()
1738
1739     # --- utility
1740
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)
1745
1746     def push_dtp_data(self, data, isproducer=False, file=None):
1747         """Pushes data into the data channel.
1748
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.
1754
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).
1759         """
1760         if self.data_channel:
1761             self.respond("125 Data connection already open. Transfer starting.")
1762             if file:
1763                 self.data_channel.file_obj = file
1764             if not isproducer:
1765                 self.data_channel.push(data)
1766             else:
1767                 self.data_channel.push_with_producer(data)
1768             if self.data_channel:
1769                 self.data_channel.close_when_done()
1770         else:
1771             self.respond("150 File status okay. About to open data connection.")
1772             self.__out_dtp_queue = (data, isproducer, file)
1773
1774     def log(self, msg):
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))
1778
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))
1782
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.
1786         """
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
1794
1795         self.fs.rnfr = None
1796         self.authenticated = False
1797         self.username = ""
1798         self.password = ""
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
1805
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)
1809         try:
1810             return function(*args, **kwargs)
1811         finally:
1812             self.authorizer.terminate_impersonation()
1813
1814         # --- connection
1815
1816     def _make_eport(self, ip, port):
1817         """Establish an active data channel with remote client which
1818         issued a PORT or EPRT command.
1819         """
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."
1826                          %(ip, port))
1827                 self.respond("501 Can't connect to a foreign address.")
1828                 return
1829
1830         # ...another RFC-2577 recommendation is rejecting connections
1831         # to privileged ports (< 1024) for security reasons.
1832         if not self.permit_privileged_ports:
1833             if port < 1024:
1834                 self.log('PORT against the privileged port "%s" refused.' %port)
1835                 self.respond("501 Can't connect over a privileged port.")
1836                 return
1837
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
1845
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)
1851                 self.log(msg)
1852                 return
1853
1854         # open data channel
1855         self.active_dtp(ip, port, self)
1856
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).
1862         """
1863         # close existing DTP-server instance, if any
1864         if self.data_server:
1865             self.data_server.close()
1866             self.data_server = None
1867
1868         if self.data_channel:
1869             self.data_channel.close()
1870             self.data_channel = None
1871
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)
1877                 self.log(msg)
1878                 return
1879
1880         # open data channel
1881         self.data_server = self.passive_dtp(self, extmode)
1882
1883     def ftp_PORT(self, line):
1884         """Start an active data channel by using IPv4."""
1885         if self._epsvall:
1886             self.respond("501 PORT not allowed after EPSV ALL.")
1887             return
1888         if self.af != socket.AF_INET:
1889             self.respond("425 You cannot use PORT on IPv6 connections. "
1890                          "Use EPRT instead.")
1891             return
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.
1897         try:
1898             addr = map(int, line.split(','))
1899             assert len(addr) == 6
1900             for x in addr[:4]:
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.")
1907             return
1908         self._make_eport(ip, port)
1909
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.
1913         """
1914         if self._epsvall:
1915             self.respond("501 EPRT not allowed after EPSV ALL.")
1916             return
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).
1922         try:
1923             af, ip, port = line.split(line[0])[1:-1]
1924             port = int(port)
1925             assert 0 <= port <= 65535
1926         except (AssertionError, ValueError, IndexError, OverflowError):
1927             self.respond("501 Invalid EPRT format.")
1928             return
1929
1930         if af == "1":
1931             if self.af != socket.AF_INET:
1932                 self.respond('522 Network protocol not supported (use 2).')
1933             else:
1934                 try:
1935                     octs = map(int, ip.split('.'))
1936                     assert len(octs) == 4
1937                     for x in octs:
1938                         assert 0 <= x <= 255
1939                 except (AssertionError, ValueError, OverflowError), err:
1940                     self.respond("501 Invalid EPRT format.")
1941                 else:
1942                     self._make_eport(ip, port)
1943         elif af == "2":
1944             if self.af == socket.AF_INET:
1945                 self.respond('522 Network protocol not supported (use 1).')
1946             else:
1947                 self._make_eport(ip, port)
1948         else:
1949             if self.af == socket.AF_INET:
1950                 self.respond('501 Unknown network protocol (use 1).')
1951             else:
1952                 self.respond('501 Unknown network protocol (use 2).')
1953
1954     def ftp_PASV(self, line):
1955         """Start a passive data channel by using IPv4."""
1956         if self._epsvall:
1957             self.respond("501 PASV not allowed after EPSV ALL.")
1958             return
1959         if self.af != socket.AF_INET:
1960             self.respond("425 You cannot use PASV on IPv6 connections. "
1961                          "Use EPSV instead.")
1962         else:
1963             self._make_epasv(extmode=False)
1964
1965     def ftp_EPSV(self, line):
1966         """Start a passive data channel by using IPv4 or IPv6 as defined
1967         in RFC-2428.
1968         """
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.
1980         if not line:
1981             self._make_epasv(extmode=True)
1982         elif line == "1":
1983             if self.af != socket.AF_INET:
1984                 self.respond('522 Network protocol not supported (use 2).')
1985             else:
1986                 self._make_epasv(extmode=True)
1987         elif line == "2":
1988             if self.af == socket.AF_INET:
1989                 self.respond('522 Network protocol not supported (use 1).')
1990             else:
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.')
1995         else:
1996             if self.af == socket.AF_INET:
1997                 self.respond('501 Unknown network protocol (use 1).')
1998             else:
1999                 self.respond('501 Unknown network protocol (use 2).')
2000
2001     def ftp_QUIT(self, line):
2002         """Quit the current session."""
2003         # From RFC-959:
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)
2010         else:
2011             msg_quit = "Goodbye."
2012         if len(msg_quit) <= 75:
2013             self.respond("221 %s" %msg_quit)
2014         else:
2015             self.push("221-%s\r\n" %msg_quit)
2016             self.respond("221 ")
2017
2018         if not self.data_channel:
2019             self.close_when_done()
2020         else:
2021             # tell the cmd channel to stop responding to commands.
2022             self.quit_pending = True
2023
2024
2025         # --- data transferring
2026
2027     def ftp_LIST(self, line):
2028         """Return a list of files in the specified directory to the
2029         client.
2030         """
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'):
2035             line = self.fs.cwd
2036         try:
2037             data = None
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)
2047         else:
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)
2052
2053     def ftp_NLST(self, line):
2054         """Return a list of files in the specified directory in a
2055         compact form to the client.
2056         """
2057         if not line:
2058             line = self.fs.cwd
2059         try:
2060             data = None
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)
2067             else:
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)
2077         else:
2078             self.fs.close_cr(data)
2079             data = ''
2080             if listing:
2081                 listing.sort()
2082                 data = '\r\n'.join(listing) + '\r\n'
2083             self.log('OK NLST "%s". Transfer starting.' %line)
2084             self.push_dtp_data(data)
2085
2086         # --- MLST and MLSD commands
2087
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.
2092
2093     def ftp_MLST(self, line):
2094         """Return information about a pathname in a machine-processable
2095         form as defined in RFC-3659.
2096         """
2097         # if no argument, fall back on cwd as default
2098         if not line:
2099             line = self.fs.cwd
2100         try:
2101             datacr = None
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)
2115         else:
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.')
2125
2126     def ftp_MLSD(self, line):
2127         """Return contents of a directory in a machine-processable form
2128         as defined in RFC-3659.
2129         """
2130         # if no argument, fall back on cwd as default
2131         if not line:
2132             line = self.fs.cwd
2133         try:
2134             datacr = None
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)
2143                 return
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)
2150         else:
2151             self.fs.close_cr(datacr)
2152             perms = self.authorizer.get_perms(self.username)
2153             iterator = self.fs.format_mlsx(path, listing, perms,
2154                        self.current_facts)
2155             producer = BufferedIteratorProducer(iterator)
2156             self.log('OK MLSD "%s". Transfer starting.' %line)
2157             self.push_dtp_data(producer, isproducer=True)
2158
2159     def ftp_RETR(self, line):
2160         """Retrieve the specified file (transfer from the server to the
2161         client)
2162         """
2163         try:
2164             datacr = None
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)
2174             return
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)
2180             return
2181
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
2187             # the REST.
2188             ok = 0
2189             try:
2190                 assert not self.restart_position > self.fs.getsize(file)
2191                 fd.seek(self.restart_position)
2192                 ok = 1
2193             except AssertionError:
2194                 why = "Invalid REST parameter"
2195             except IOError, err:
2196                 why = _strerror(err)
2197             self.restart_position = 0
2198             if not ok:
2199                 self.respond('554 %s' %why)
2200                 self.log('FAIL RETR "%s". %s.' %(line, why))
2201                 self.fs.close_cr(datacr)
2202                 return
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)
2207
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:
2212         # STOR: mode = 'w'
2213         # APPE: mode = 'a'
2214         # REST: mode = 'r+' (to permit seeking on file object)
2215         if 'a' in mode:
2216             cmd = 'APPE'
2217         else:
2218             cmd = 'STOR'
2219
2220         line = self.fs.ftpnorm(line)
2221         basedir,basename = os.path.split(line)
2222
2223         datacr = None
2224         try:
2225             datacr = self.fs.get_cr(line)
2226             file = self.fs.ftp2fs(basedir, datacr)
2227
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)
2233             return
2234
2235         if self.restart_position:
2236             mode = 'r+'
2237         try:
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)
2244             return
2245
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.
2252             ok = 0
2253             try:
2254                 assert not self.restart_position > self.fs.getsize(self.fs.ftp2fs(line, datacr))
2255                 fd.seek(self.restart_position)
2256                 ok = 1
2257             except AssertionError:
2258                 why = "Invalid REST parameter"
2259             except IOError, err:
2260                 why = _strerror(err)
2261             self.restart_position = 0
2262             if not ok:
2263                 self.fs.close_cr(datacr)
2264                 self.respond('554 %s' %why)
2265                 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2266                 return
2267
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)
2273         else:
2274             self.respond("150 File status okay. About to open data connection.")
2275             self.__in_dtp_queue = fd
2276         self.fs.close_cr(datacr)
2277
2278
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:
2287         # > 125 FILE: pppp
2288         # ...where pppp represents the unique path name of the
2289         # file that will be written.
2290
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.")
2294             return
2295
2296         datacr = None
2297         datacr = self.fs.get_cr(line)
2298
2299         if line:
2300             line = self.fs.ftpnorm(line)
2301             basedir,prefix = os.path.split(line)
2302             basedir = self.fs.ftp2fs(basedir, datacr)
2303             #prefix = prefix + '.'
2304         else:
2305             basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
2306             prefix = 'ftpd.'
2307         try:
2308             fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
2309                                           dir=basedir)
2310         except IOError, err:
2311             # hitted the max number of tries to find out file with
2312             # unique name
2313             if err.errno == errno.EEXIST:
2314                 why = 'No usable unique file name found'
2315             # something else happened
2316             else:
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)
2321             return
2322
2323         filename = line
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)
2329             return
2330
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)
2337         else:
2338             self.respond("150 FILE: %s" %filename)
2339             self.__in_dtp_queue = fd
2340         self.fs.close_cr(datacr)
2341
2342
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.")
2348         else:
2349             self.ftp_STOR(line, mode='a')
2350
2351     def ftp_REST(self, line):
2352         """Restart a file transfer from a previous mark."""
2353         try:
2354             marker = int(line)
2355             if marker < 0:
2356                 raise ValueError
2357         except (ValueError, OverflowError):
2358             self.respond("501 Invalid parameter.")
2359         else:
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
2364
2365     def ftp_ABOR(self, line):
2366         """Abort the current data transfer."""
2367
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."
2371         else:
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."
2377
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."
2392                 else:
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."
2397         self.respond(resp)
2398
2399
2400         # --- authentication
2401
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":
2406             line = "anonymous"
2407
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.')
2417         else:
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
2427
2428     def ftp_PASS(self, line):
2429         """Check username's password against the authorizer."""
2430
2431         if self.authenticated:
2432             self.respond("503 User already authenticated.")
2433             return
2434         if not self.username:
2435             self.respond("503 Login with USER first.")
2436             return
2437
2438         # username ok
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)
2445                 else:
2446                     self.push("230-%s\r\n" %msg_login)
2447                     self.respond("230 ")
2448
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)
2456             else:
2457                 self.attempted_logins += 1
2458                 if self.attempted_logins >= self.max_login_attempts:
2459                     self.respond("530 Maximum login attempts. Disconnecting.")
2460                     self.close()
2461                 else:
2462                     self.respond("530 Authentication failed.")
2463                 self.log('Authentication failed (user: "%s").' %self.username)
2464                 self.username = ""
2465
2466         # wrong username
2467         else:
2468             self.attempted_logins += 1
2469             if self.attempted_logins >= self.max_login_attempts:
2470                 self.log('Authentication failed: unknown username "%s".'
2471                             %self.username)
2472                 self.respond("530 Maximum login attempts. Disconnecting.")
2473                 self.close()
2474             elif self.username.lower() == 'anonymous':
2475                 self.respond("530 Anonymous access not allowed.")
2476                 self.log('Authentication failed: anonymous access not allowed.')
2477             else:
2478                 self.respond("530 Authentication failed.")
2479                 self.log('Authentication failed: unknown username "%s".'
2480                             %self.username)
2481                 self.username = ""
2482
2483     def ftp_REIN(self, line):
2484         """Reinitialize user's current session."""
2485         # From RFC-959:
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.")
2497
2498
2499         # --- filesystem operations
2500
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)
2504
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.
2510         if not line:
2511             line = '/'
2512         datacr = None
2513         try:
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:
2518             if err.errno==2:
2519                 why = 'Authentication Required or Failed'
2520                 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2521                 self.respond('530 %s.' %why)
2522             else:
2523                 why = _strerror(err)
2524                 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2525                 self.respond('550 %s.' %why)
2526         else:
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)
2530
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.
2535         self.ftp_CWD('..')
2536
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.
2540
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.
2550
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.
2554         """
2555         datacr = None
2556         try:
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)
2565                 return
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)
2571         else:
2572             self.respond("213 %s" %size)
2573             self.log('OK SIZE "%s".' %line)
2574         self.fs.close_cr(datacr)
2575
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.
2579         """
2580         datacr = None
2581         try:
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)
2590                 return
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)
2596         else:
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)
2601
2602     def ftp_MKD(self, line):
2603         """Create the specified directory."""
2604         datacr = None
2605         line = self.fs.ftpnorm(line)
2606         basedir,basename = os.path.split(line)
2607         try:
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)
2615         else:
2616             self.log('OK MKD "%s".' %line)
2617             self.respond("257 Directory created.")
2618         self.fs.close_cr(datacr)
2619
2620     def ftp_RMD(self, line):
2621         """Remove the specified directory."""
2622         datacr = None
2623         try:
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)
2632                 return
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)
2638         else:
2639             self.log('OK RMD "%s".' %line)
2640             self.respond("250 Directory removed.")
2641         self.fs.close_cr(datacr)
2642
2643     def ftp_DELE(self, line):
2644         """Delete the specified file."""
2645         datacr = None
2646         try:
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)
2655         else:
2656             self.log('OK DELE "%s".' %line)
2657             self.respond("250 File removed.")
2658         self.fs.close_cr(datacr)
2659
2660     def ftp_RNFR(self, line):
2661         """Rename the specified (only the source name is specified
2662         here, see RNTO command)"""
2663         datacr = None
2664         try:
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.")
2672             else:
2673                 self.fs.rnfr = line
2674                 self.respond("350 Ready for destination name.")
2675         except:
2676             self.respond("550 Can't find the file or directory.")
2677         self.fs.close_cr(datacr)
2678
2679     def ftp_RNTO(self, line):
2680         """Rename file (destination name only, source is specified with
2681         RNFR).
2682         """
2683         if not self.fs.rnfr:
2684             self.respond("503 Bad sequence of commands: use RNFR first.")
2685             return
2686         datacr = None
2687         try:
2688             try:
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)
2700             else:
2701                 self.log('OK RNFR/RNTO "%s ==> %s".' \
2702                          %(self.fs.ftpnorm(self.fs.rnfr), line))
2703                 self.respond("250 Renaming ok.")
2704         finally:
2705             self.fs.rnfr = None
2706             self.fs.close_cr(datacr)
2707
2708
2709         # --- others
2710
2711     def ftp_TYPE(self, line):
2712         """Set current type data type to binary/ascii"""
2713         line = line.upper()
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'
2720         else:
2721             self.respond('504 Unsupported type "%s".' %line)
2722
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.')
2728         else:
2729             self.respond('504 Unimplemented STRU type.')
2730
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')
2736         else:
2737             self.respond('504 Unimplemented MODE type.')
2738
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.
2742
2743         Implementation note:
2744
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.
2749
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.
2756         """
2757         # return STATus information about ftpd
2758         if not line:
2759             s = []
2760             s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2761             if self.authenticated:
2762                 s.append('Logged in as: %s' %self.username)
2763             else:
2764                 if not self.username:
2765                     s.append("Waiting for username.")
2766                 else:
2767                     s.append("Waiting for password.")
2768             if self.current_type == 'a':
2769                 type = 'ASCII'
2770             else:
2771                 type = 'Binary'
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)
2781             else:
2782                 s.append('Data connection closed.')
2783
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
2788         else:
2789             datacr = None
2790             try:
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))
2795             else:
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)
2800
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']
2804         s = ''
2805         for fact in self.available_facts:
2806             if fact in self.current_facts:
2807                 s += fact + '*;'
2808             else:
2809                 s += fact + ';'
2810         features.append('MLST ' + s)
2811         features.sort()
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.')
2815
2816     def ftp_OPTS(self, line):
2817         """Specify options for FTP commands as specified in RFC-2389."""
2818         try:
2819             assert (not line.count(' ') > 1), 'Invalid number of arguments'
2820             if ' ' in line:
2821                 cmd, arg = line.split(' ')
2822                 assert (';' in arg), 'Invalid argument'
2823             else:
2824                 cmd, arg = line, ''
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)
2829         else:
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)
2834
2835     def ftp_NOOP(self, line):
2836         """Do nothing."""
2837         self.respond("200 I successfully done nothin'.")
2838
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")
2847
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.")
2852
2853     def ftp_HELP(self, line):
2854         """Return help text to the client."""
2855         if line:
2856             if line.upper() in proto_cmds:
2857                 self.respond("214 %s" %proto_cmds[line.upper()])
2858             else:
2859                 self.respond("501 Unrecognized command.")
2860         else:
2861             # provide a compact list of recognized commands
2862             def formatted_help():
2863                 cmds = []
2864                 keys = proto_cmds.keys()
2865                 keys.sort()
2866                 while keys:
2867                     elems = tuple((keys[0:8]))
2868                     cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2869                     del keys[0:8]
2870                 return ''.join(cmds)
2871
2872             self.push("214-The following commands are recognized:\r\n")
2873             self.push(formatted_help())
2874             self.respond("214 Help command successful.")
2875
2876
2877         # --- support for deprecated cmds
2878
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.
2883
2884     def ftp_XCUP(self, line):
2885         """Change to the parent directory. Synonym for CDUP. Deprecated."""
2886         self.ftp_CDUP(line)
2887
2888     def ftp_XCWD(self, line):
2889         """Change the current working directory. Synonym for CWD. Deprecated."""
2890         self.ftp_CWD(line)
2891
2892     def ftp_XMKD(self, line):
2893         """Create the specified directory. Synonym for MKD. Deprecated."""
2894         self.ftp_MKD(line)
2895
2896     def ftp_XPWD(self, line):
2897         """Return the current working directory. Synonym for PWD. Deprecated."""
2898         self.ftp_PWD(line)
2899
2900     def ftp_XRMD(self, line):
2901         """Remove the specified directory. Synonym for RMD. Deprecated."""
2902         self.ftp_RMD(line)
2903
2904
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).
2909
2910     Depending on the type of address specified IPv4 or IPv6 connections
2911     (or both, depending from the underlying system) will be accepted.
2912
2913     All relevant session information is stored in class attributes
2914     described below.
2915     Overriding them is strongly recommended to avoid running out of
2916     file descriptors (DoS)!
2917
2918      - (int) max_cons:
2919         number of maximum simultaneous connections accepted (defaults
2920         to 0 == unlimited).
2921
2922      - (int) max_cons_per_ip:
2923         number of maximum connections accepted for the same IP address
2924         (defaults to 0 == unlimited).
2925     """
2926
2927     max_cons = 0
2928     max_cons_per_ip = 0
2929
2930     def __init__(self, address, handler):
2931         """Initiate the FTP server opening listening on address.
2932
2933          - (tuple) address: the host:port pair on which the command
2934            channel will listen.
2935
2936          - (classobj) handler: the handler class to use.
2937         """
2938         asyncore.dispatcher.__init__(self)
2939         self.handler = handler
2940         self.ip_map = []
2941         host, port = address
2942
2943         # AF_INET or AF_INET6 socket
2944         # Get the correct address family for our host (allows IPv6 addresses)
2945         try:
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))
2953         else:
2954             for res in info:
2955                 af, socktype, proto, canonname, sa = res
2956                 try:
2957                     self.create_socket(af, socktype)
2958                     self.set_reuse_addr()
2959                     self.bind(sa)
2960                 except socket.error, msg:
2961                     if self.socket:
2962                         self.socket.close()
2963                     self.socket = None
2964                     continue
2965                 break
2966             if not self.socket:
2967                 raise socket.error, msg
2968         self.listen(5)
2969
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'):
2973             return
2974         asyncore.dispatcher.set_reuse_addr(self)
2975
2976     def serve_forever(self, **kwargs):
2977         """A wrap around asyncore.loop(); starts the asyncore polling
2978         loop.
2979
2980         The keyword arguments in kwargs are the same expected by
2981         asyncore.loop() function: timeout, use_poll, map and count.
2982         """
2983         if not 'count' in kwargs:
2984             log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2985
2986         # backward compatibility for python < 2.4
2987         if not hasattr(self, '_map'):
2988             if not 'map' in kwargs:
2989                 map = asyncore.socket_map
2990             else:
2991                 map = kwargs['map']
2992             self._map = self.handler._map = map
2993
2994         try:
2995             # FIX #16, #26
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)
3002             if kwargs:
3003                 asyncore.loop(**kwargs)
3004             else:
3005                 asyncore.loop(timeout=1.0, use_poll=False)
3006         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3007             log("Shutting down FTPd.")
3008             self.close_all()
3009
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])
3014
3015         handler = self.handler(sock_obj, self)
3016         ip = addr[0]
3017         self.ip_map.append(ip)
3018
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.
3024         if self.max_cons:
3025             if len(self._map) > self.max_cons:
3026                 handler.handle_max_cons()
3027                 return
3028
3029         # accept only a limited number of connections from the same
3030         # source address.
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()
3034                 return
3035
3036         handler.handle()
3037
3038     def writable(self):
3039         return 0
3040
3041     def handle_error(self):
3042         """Called to handle any uncaught exceptions."""
3043         try:
3044             raise
3045         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3046             raise
3047         logerror(traceback.format_exc())
3048         self.close()
3049
3050     def close_all(self, map=None, ignore_all=False):
3051         """Stop serving; close all existent connections disconnecting
3052         clients.
3053
3054          - (dict) map:
3055             A dictionary whose items are the channels to close.
3056             If map is omitted, the default asyncore.socket_map is used.
3057
3058          - (bool) ignore_all:
3059             having it set to False results in raising exception in case
3060             of unexpected errors.
3061
3062         Implementation note:
3063
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
3067         leaks.
3068
3069         This is how asyncore.close_all() function should work in
3070         Python 2.6.
3071         """
3072         if map is None:
3073             map = self._map
3074         for x in map.values():
3075             try:
3076                 x.close()
3077             except OSError, x:
3078                 if x[0] == errno.EBADF:
3079                     pass
3080                 elif not ignore_all:
3081                     raise
3082             except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3083                 raise
3084             except:
3085                 if not ignore_all:
3086                     raise
3087         map.clear()
3088
3089
3090 def test():
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()
3099
3100 if __name__ == '__main__':
3101     test()
3102
3103 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: