Document ftp: improve exception handling, size may return 404
[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 def _to_unicode(s):
225     try:
226         return s.decode('utf-8')
227     except UnicodeError:
228         pass
229     try:
230         return s.decode('latin')
231     except UnicodeError:
232         pass
233     try:
234         return s.encode('ascii')
235     except UnicodeError:
236         return s
237
238 def _to_decode(s):
239     try:
240         return s.encode('utf-8')
241     except UnicodeError:
242         pass
243     try:
244         return s.encode('latin')
245     except UnicodeError:
246         pass
247     try:
248         return s.decode('ascii')
249     except UnicodeError:
250         return s
251
252 # --- library defined exceptions
253
254 class Error(Exception):
255     """Base class for module exceptions."""
256
257 class AuthorizerError(Error):
258     """Base class for authorizer exceptions."""
259
260
261 # --- loggers
262
263 def log(msg):
264     """Log messages intended for the end user."""
265     if LOG_ACTIVE:
266         print msg
267
268 def logline(msg):
269     """Log commands and responses passing through the command channel."""
270     if LOG_ACTIVE:
271         print msg
272
273 def logerror(msg):
274     """Log traceback outputs occurring in case of errors."""
275     sys.stderr.write(str(msg) + '\n')
276     sys.stderr.flush()
277
278
279 # --- authorizers
280
281 class DummyAuthorizer:
282     """Basic "dummy" authorizer class, suitable for subclassing to
283     create your own custom authorizers.
284
285     An "authorizer" is a class handling authentications and permissions
286     of the FTP server.  It is used inside FTPHandler class for verifying
287     user's password, getting users home directory, checking user
288     permissions when a file read/write event occurs and changing user
289     before accessing the filesystem.
290
291     DummyAuthorizer is the base authorizer, providing a platform
292     independent interface for managing "virtual" FTP users. System
293     dependent authorizers can by written by subclassing this base
294     class and overriding appropriate methods as necessary.
295     """
296
297     read_perms = "elr"
298     write_perms = "adfmw"
299
300     def __init__(self):
301         self.user_table = {}
302
303     def add_user(self, username, password, homedir, perm='elr',
304                     msg_login="Login successful.", msg_quit="Goodbye."):
305         """Add a user to the virtual users table.
306
307         AuthorizerError exceptions raised on error conditions such as
308         invalid permissions, missing home directory or duplicate usernames.
309
310         Optional perm argument is a string referencing the user's
311         permissions explained below:
312
313         Read permissions:
314          - "e" = change directory (CWD command)
315          - "l" = list files (LIST, NLST, MLSD commands)
316          - "r" = retrieve file from the server (RETR command)
317
318         Write permissions:
319          - "a" = append data to an existing file (APPE command)
320          - "d" = delete file or directory (DELE, RMD commands)
321          - "f" = rename file or directory (RNFR, RNTO commands)
322          - "m" = create directory (MKD command)
323          - "w" = store a file to the server (STOR, STOU commands)
324
325         Optional msg_login and msg_quit arguments can be specified to
326         provide customized response strings when user log-in and quit.
327         """
328         if self.has_user(username):
329             raise AuthorizerError('User "%s" already exists' %username)
330         homedir = os.path.realpath(homedir)
331         if not os.path.isdir(homedir):
332             raise AuthorizerError('No such directory: "%s"' %homedir)
333         for p in perm:
334             if p not in 'elradfmw':
335                 raise AuthorizerError('No such permission "%s"' %p)
336         for p in perm:
337             if (p in self.write_perms) and (username == 'anonymous'):
338                 warnings.warn("write permissions assigned to anonymous user.",
339                               RuntimeWarning)
340                 break
341         dic = {'pwd': str(password),
342                'home': homedir,
343                'perm': perm,
344                'msg_login': str(msg_login),
345                'msg_quit': str(msg_quit)
346                }
347         self.user_table[username] = dic
348
349     def add_anonymous(self, homedir, **kwargs):
350         """Add an anonymous user to the virtual users table.
351
352         AuthorizerError exception raised on error conditions such as
353         invalid permissions, missing home directory, or duplicate
354         anonymous users.
355
356         The keyword arguments in kwargs are the same expected by
357         add_user method: "perm", "msg_login" and "msg_quit".
358
359         The optional "perm" keyword argument is a string defaulting to
360         "elr" referencing "read-only" anonymous user's permissions.
361
362         Using write permission values ("adfmw") results in a
363         RuntimeWarning.
364         """
365         DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs)
366
367     def remove_user(self, username):
368         """Remove a user from the virtual users table."""
369         del self.user_table[username]
370
371     def validate_authentication(self, username, password):
372         """Return True if the supplied username and password match the
373         stored credentials."""
374         return self.user_table[username]['pwd'] == password
375
376     def impersonate_user(self, username, password):
377         """Impersonate another user (noop).
378
379         It is always called before accessing the filesystem.
380         By default it does nothing.  The subclass overriding this
381         method is expected to provide a mechanism to change the
382         current user.
383         """
384
385     def terminate_impersonation(self):
386         """Terminate impersonation (noop).
387
388         It is always called after having accessed the filesystem.
389         By default it does nothing.  The subclass overriding this
390         method is expected to provide a mechanism to switch back
391         to the original user.
392         """
393
394     def has_user(self, username):
395         """Whether the username exists in the virtual users table."""
396         return username in self.user_table
397
398     def has_perm(self, username, perm, path=None):
399         """Whether the user has permission over path (an absolute
400         pathname of a file or a directory).
401
402         Expected perm argument is one of the following letters:
403         "elradfmw".
404         """
405         return perm in self.user_table[username]['perm']
406
407     def get_perms(self, username):
408         """Return current user permissions."""
409         return self.user_table[username]['perm']
410
411     def get_home_dir(self, username):
412         """Return the user's home directory."""
413         return self.user_table[username]['home']
414
415     def get_msg_login(self, username):
416         """Return the user's login message."""
417         return self.user_table[username]['msg_login']
418
419     def get_msg_quit(self, username):
420         """Return the user's quitting message."""
421         return self.user_table[username]['msg_quit']
422
423
424 # --- DTP classes
425
426 class PassiveDTP(asyncore.dispatcher):
427     """This class is an asyncore.disptacher subclass.  It creates a
428     socket listening on a local port, dispatching the resultant
429     connection to DTPHandler.
430     """
431
432     def __init__(self, cmd_channel, extmode=False):
433         """Initialize the passive data server.
434
435          - (instance) cmd_channel: the command channel class instance.
436          - (bool) extmode: wheter use extended passive mode response type.
437         """
438         asyncore.dispatcher.__init__(self)
439         self.cmd_channel = cmd_channel
440
441         ip = self.cmd_channel.getsockname()[0]
442         self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
443
444         if not self.cmd_channel.passive_ports:
445         # By using 0 as port number value we let kernel choose a free
446         # unprivileged random port.
447             self.bind((ip, 0))
448         else:
449             ports = list(self.cmd_channel.passive_ports)
450             while ports:
451                 port = ports.pop(random.randint(0, len(ports) -1))
452                 try:
453                     self.bind((ip, port))
454                 except socket.error, why:
455                     if why[0] == errno.EADDRINUSE:  # port already in use
456                         if ports:
457                             continue
458                         # If cannot use one of the ports in the configured
459                         # range we'll use a kernel-assigned port, and log
460                         # a message reporting the issue.
461                         # By using 0 as port number value we let kernel
462                         # choose a free unprivileged random port.
463                         else:
464                             self.bind((ip, 0))
465                             self.cmd_channel.log(
466                                 "Can't find a valid passive port in the "
467                                 "configured range. A random kernel-assigned "
468                                 "port will be used."
469                                 )
470                     else:
471                         raise
472                 else:
473                     break
474         self.listen(5)
475         port = self.socket.getsockname()[1]
476         if not extmode:
477             if self.cmd_channel.masquerade_address:
478                 ip = self.cmd_channel.masquerade_address
479             # The format of 227 response in not standardized.
480             # This is the most expected:
481             self.cmd_channel.respond('227 Entering passive mode (%s,%d,%d).' %(
482                     ip.replace('.', ','), port / 256, port % 256))
483         else:
484             self.cmd_channel.respond('229 Entering extended passive mode '
485                                      '(|||%d|).' %port)
486
487     # --- connection / overridden
488
489     def handle_accept(self):
490         """Called when remote client initiates a connection."""
491         sock, addr = self.accept()
492
493         # Check the origin of data connection.  If not expressively
494         # configured we drop the incoming data connection if remote
495         # IP address does not match the client's IP address.
496         if (self.cmd_channel.remote_ip != addr[0]):
497             if not self.cmd_channel.permit_foreign_addresses:
498                 try:
499                     sock.close()
500                 except socket.error:
501                     pass
502                 msg = 'Rejected data connection from foreign address %s:%s.' \
503                         %(addr[0], addr[1])
504                 self.cmd_channel.respond("425 %s" %msg)
505                 self.cmd_channel.log(msg)
506                 # do not close listening socket: it couldn't be client's blame
507                 return
508             else:
509                 # site-to-site FTP allowed
510                 msg = 'Established data connection with foreign address %s:%s.'\
511                         %(addr[0], addr[1])
512                 self.cmd_channel.log(msg)
513         # Immediately close the current channel (we accept only one
514         # connection at time) and avoid running out of max connections
515         # limit.
516         self.close()
517         # delegate such connection to DTP handler
518         handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel)
519         self.cmd_channel.data_channel = handler
520         self.cmd_channel.on_dtp_connection()
521
522     def writable(self):
523         return 0
524
525     def handle_error(self):
526         """Called to handle any uncaught exceptions."""
527         try:
528             raise
529         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
530             raise
531         logerror(traceback.format_exc())
532         self.close()
533
534     def handle_close(self):
535         """Called on closing the data connection."""
536         self.close()
537
538
539 class ActiveDTP(asyncore.dispatcher):
540     """This class is an asyncore.disptacher subclass. It creates a
541     socket resulting from the connection to a remote user-port,
542     dispatching it to DTPHandler.
543     """
544
545     def __init__(self, ip, port, cmd_channel):
546         """Initialize the active data channel attemping to connect
547         to remote data socket.
548
549          - (str) ip: the remote IP address.
550          - (int) port: the remote port.
551          - (instance) cmd_channel: the command channel class instance.
552         """
553         asyncore.dispatcher.__init__(self)
554         self.cmd_channel = cmd_channel
555         self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
556         try:
557             self.connect((ip, port))
558         except socket.gaierror:
559             self.cmd_channel.respond("425 Can't connect to specified address.")
560             self.close()
561
562     # --- connection / overridden
563
564     def handle_write(self):
565         """NOOP, must be overridden to prevent unhandled write event."""
566
567     def handle_connect(self):
568         """Called when connection is established."""
569         self.cmd_channel.respond('200 Active data connection established.')
570         # delegate such connection to DTP handler
571         handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
572         self.cmd_channel.data_channel = handler
573         self.cmd_channel.on_dtp_connection()
574         #self.close()  # <-- (done automatically)
575
576     def handle_expt(self):
577         self.cmd_channel.respond("425 Can't connect to specified address.")
578         self.close()
579
580     def handle_error(self):
581         """Called to handle any uncaught exceptions."""
582         try:
583             raise
584         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
585             raise
586         except socket.error:
587             pass
588         except:
589             logerror(traceback.format_exc())
590         self.cmd_channel.respond("425 Can't connect to specified address.")
591         self.close()
592
593
594 try:
595     from collections import deque
596 except ImportError:
597     # backward compatibility with Python < 2.4 by replacing deque with a list
598     class deque(list):
599         def appendleft(self, obj):
600             list.insert(self, 0, obj)
601
602
603 class DTPHandler(asyncore.dispatcher):
604     """Class handling server-data-transfer-process (server-DTP, see
605     RFC-959) managing data-transfer operations involving sending
606     and receiving data.
607
608     Instance attributes defined in this class, initialized when
609     channel is opened:
610
611      - (instance) cmd_channel: the command channel class instance.
612      - (file) file_obj: the file transferred (if any).
613      - (bool) receive: True if channel is used for receiving data.
614      - (bool) transfer_finished: True if transfer completed successfully.
615      - (int) tot_bytes_sent: the total bytes sent.
616      - (int) tot_bytes_received: the total bytes received.
617
618     DTPHandler implementation note:
619
620     When a producer is consumed and close_when_done() has been called
621     previously, refill_buffer() erroneously calls close() instead of
622     handle_close() - (see: http://bugs.python.org/issue1740572)
623
624     To avoid this problem DTPHandler is implemented as a subclass of
625     asyncore.dispatcher instead of asynchat.async_chat.
626     This implementation follows the same approach that asynchat module
627     should use in Python 2.6.
628
629     The most important change in the implementation is related to
630     producer_fifo, which is a pure deque object instead of a
631     producer_fifo instance.
632
633     Since we don't want to break backward compatibily with older python
634     versions (deque has been introduced in Python 2.4), if deque is not
635     available we use a list instead.
636     """
637
638     ac_in_buffer_size = 8192
639     ac_out_buffer_size  = 8192
640
641     def __init__(self, sock_obj, cmd_channel):
642         """Initialize the command channel.
643
644          - (instance) sock_obj: the socket object instance of the newly
645             established connection.
646          - (instance) cmd_channel: the command channel class instance.
647         """
648         asyncore.dispatcher.__init__(self, sock_obj)
649         # we toss the use of the asynchat's "simple producer" and
650         # replace it  with a pure deque, which the original fifo
651         # was a wrapping of
652         self.producer_fifo = deque()
653
654         self.cmd_channel = cmd_channel
655         self.file_obj = None
656         self.receive = False
657         self.transfer_finished = False
658         self.tot_bytes_sent = 0
659         self.tot_bytes_received = 0
660         self.data_wrapper = lambda x: x
661
662     # --- utility methods
663
664     def enable_receiving(self, type):
665         """Enable receiving of data over the channel. Depending on the
666         TYPE currently in use it creates an appropriate wrapper for the
667         incoming data.
668
669          - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
670         """
671         if type == 'a':
672             self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
673         elif type == 'i':
674             self.data_wrapper = lambda x: x
675         else:
676             raise TypeError, "Unsupported type"
677         self.receive = True
678
679     def get_transmitted_bytes(self):
680         "Return the number of transmitted bytes."
681         return self.tot_bytes_sent + self.tot_bytes_received
682
683     def transfer_in_progress(self):
684         "Return True if a transfer is in progress, else False."
685         return self.get_transmitted_bytes() != 0
686
687     # --- connection
688
689     def handle_read(self):
690         """Called when there is data waiting to be read."""
691         try:
692             chunk = self.recv(self.ac_in_buffer_size)
693         except socket.error:
694             self.handle_error()
695         else:
696             self.tot_bytes_received += len(chunk)
697             if not chunk:
698                 self.transfer_finished = True
699                 #self.close()  # <-- asyncore.recv() already do that...
700                 return
701             # while we're writing on the file an exception could occur
702             # in case  that filesystem gets full;  if this happens we
703             # let handle_error() method handle this exception, providing
704             # a detailed error message.
705             self.file_obj.write(self.data_wrapper(chunk))
706
707     def handle_write(self):
708         """Called when data is ready to be written, initiates send."""
709         self.initiate_send()
710
711     def push(self, data):
712         """Push data onto the deque and initiate send."""
713         sabs = self.ac_out_buffer_size
714         if len(data) > sabs:
715             for i in xrange(0, len(data), sabs):
716                 self.producer_fifo.append(data[i:i+sabs])
717         else:
718             self.producer_fifo.append(data)
719         self.initiate_send()
720
721     def push_with_producer(self, producer):
722         """Push data using a producer and initiate send."""
723         self.producer_fifo.append(producer)
724         self.initiate_send()
725
726     def readable(self):
727         """Predicate for inclusion in the readable for select()."""
728         return self.receive
729
730     def writable(self):
731         """Predicate for inclusion in the writable for select()."""
732         return self.producer_fifo or (not self.connected)
733
734     def close_when_done(self):
735         """Automatically close this channel once the outgoing queue is empty."""
736         self.producer_fifo.append(None)
737
738     def initiate_send(self):
739         """Attempt to send data in fifo order."""
740         while self.producer_fifo and self.connected:
741             first = self.producer_fifo[0]
742             # handle empty string/buffer or None entry
743             if not first:
744                 del self.producer_fifo[0]
745                 if first is None:
746                     self.transfer_finished = True
747                     self.handle_close()
748                     return
749
750             # handle classic producer behavior
751             obs = self.ac_out_buffer_size
752             try:
753                 data = buffer(first, 0, obs)
754             except TypeError:
755                 data = first.more()
756                 if data:
757                     self.producer_fifo.appendleft(data)
758                 else:
759                     del self.producer_fifo[0]
760                 continue
761
762             # send the data
763             try:
764                 num_sent = self.send(data)
765             except socket.error:
766                 self.handle_error()
767                 return
768
769             if num_sent:
770                 self.tot_bytes_sent += num_sent
771                 if num_sent < len(data) or obs < len(first):
772                     self.producer_fifo[0] = first[num_sent:]
773                 else:
774                     del self.producer_fifo[0]
775             # we tried to send some actual data
776             return
777
778     def handle_expt(self):
779         """Called on "exceptional" data events."""
780         self.cmd_channel.respond("426 Connection error; transfer aborted.")
781         self.close()
782
783     def handle_error(self):
784         """Called when an exception is raised and not otherwise handled."""
785         try:
786             raise
787         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
788             raise
789         except socket.error, err:
790             # fix around asyncore bug (http://bugs.python.org/issue1736101)
791             if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
792                           errno.ECONNABORTED):
793                 self.handle_close()
794                 return
795             else:
796                 error = str(err[1])
797         # an error could occur in case we fail reading / writing
798         # from / to file (e.g. file system gets full)
799         except EnvironmentError, err:
800             error = _strerror(err)
801         except:
802             # some other exception occurred;  we don't want to provide
803             # confidential error messages
804             logerror(traceback.format_exc())
805             error = "Internal error"
806         self.cmd_channel.respond("426 %s; transfer aborted." %error)
807         self.close()
808
809     def handle_close(self):
810         """Called when the socket is closed."""
811         # If we used channel for receiving we assume that transfer is
812         # finished when client close connection , if we used channel
813         # for sending we have to check that all data has been sent
814         # (responding with 226) or not (responding with 426).
815         if self.receive:
816             self.transfer_finished = True
817             action = 'received'
818         else:
819             action = 'sent'
820         if self.transfer_finished:
821             self.cmd_channel.respond("226 Transfer complete.")
822             if self.file_obj:
823                 fname = self.file_obj.name
824                 self.cmd_channel.log('"%s" %s.' %(fname, action))
825         else:
826             tot_bytes = self.get_transmitted_bytes()
827             msg = "Transfer aborted; %d bytes transmitted." %tot_bytes
828             self.cmd_channel.respond("426 " + msg)
829             self.cmd_channel.log(msg)
830         self.close()
831
832     def close(self):
833         """Close the data channel, first attempting to close any remaining
834         file handles."""
835         if self.file_obj and not self.file_obj.closed:
836             self.file_obj.close()
837         asyncore.dispatcher.close(self)
838         self.cmd_channel.on_dtp_close()
839
840
841 # --- producers
842
843 class FileProducer:
844     """Producer wrapper for file[-like] objects."""
845
846     buffer_size = 65536
847
848     def __init__(self, file, type):
849         """Initialize the producer with a data_wrapper appropriate to TYPE.
850
851          - (file) file: the file[-like] object.
852          - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary).
853         """
854         self.done = False
855         self.file = file
856         if type == 'a':
857             self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
858         elif type == 'i':
859             self.data_wrapper = lambda x: x
860         else:
861             raise TypeError, "Unsupported type"
862
863     def more(self):
864         """Attempt a chunk of data of size self.buffer_size."""
865         if self.done:
866             return ''
867         data = self.data_wrapper(self.file.read(self.buffer_size))
868         if not data:
869             self.done = True
870             if not self.file.closed:
871                 self.file.close()
872         return data
873
874
875 class IteratorProducer:
876     """Producer for iterator objects."""
877
878     def __init__(self, iterator):
879         self.iterator = iterator
880
881     def more(self):
882         """Attempt a chunk of data from iterator by calling its next()
883         method.
884         """
885         try:
886             return self.iterator.next()
887         except StopIteration:
888             return ''
889
890
891 class BufferedIteratorProducer:
892     """Producer for iterator objects with buffer capabilities."""
893     # how many times iterator.next() will be called before
894     # returning some data
895     loops = 20
896
897     def __init__(self, iterator):
898         self.iterator = iterator
899
900     def more(self):
901         """Attempt a chunk of data from iterator by calling
902         its next() method different times.
903         """
904         buffer = []
905         for x in xrange(self.loops):
906             try:
907                 buffer.append(self.iterator.next())
908             except StopIteration:
909                 break
910         return ''.join(buffer)
911
912
913 # --- filesystem
914
915 class AbstractedFS:
916     """A class used to interact with the file system, providing a high
917     level, cross-platform interface compatible with both Windows and
918     UNIX style filesystems.
919
920     It provides some utility methods and some wraps around operations
921     involved in file creation and file system operations like moving
922     files or removing directories.
923
924     Instance attributes:
925      - (str) root: the user home directory.
926      - (str) cwd: the current working directory.
927      - (str) rnfr: source file to be renamed.
928     """
929
930     def __init__(self):
931         self.root = None
932         self.cwd = '/'
933         self.rnfr = None
934
935     # --- Pathname / conversion utilities
936
937     def ftpnorm(self, ftppath):
938         """Normalize a "virtual" ftp pathname (tipically the raw string
939         coming from client) depending on the current working directory.
940
941         Example (having "/foo" as current working directory):
942         'x' -> '/foo/x'
943
944         Note: directory separators are system independent ("/").
945         Pathname returned is always absolutized.
946         """
947         if os.path.isabs(ftppath):
948             p = os.path.normpath(ftppath)
949         else:
950             p = os.path.normpath(os.path.join(self.cwd, ftppath))
951         # normalize string in a standard web-path notation having '/'
952         # as separator.
953         p = p.replace("\\", "/")
954         # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
955         # don't need them.  In case we get an UNC path we collapse
956         # redundant separators appearing at the beginning of the string
957         while p[:2] == '//':
958             p = p[1:]
959         # Anti path traversal: don't trust user input, in the event
960         # that self.cwd is not absolute, return "/" as a safety measure.
961         # This is for extra protection, maybe not really necessary.
962         if not os.path.isabs(p):
963             p = "/"
964         return p
965
966     def ftp2fs(self, ftppath):
967         """Translate a "virtual" ftp pathname (tipically the raw string
968         coming from client) into equivalent absolute "real" filesystem
969         pathname.
970
971         Example (having "/home/user" as root directory):
972         'x' -> '/home/user/x'
973
974         Note: directory separators are system dependent.
975         """
976         # as far as I know, it should always be path traversal safe...
977         if os.path.normpath(self.root) == os.sep:
978             return os.path.normpath(self.ftpnorm(ftppath))
979         else:
980             p = self.ftpnorm(ftppath)[1:]
981             return os.path.normpath(os.path.join(self.root, p))
982
983     def fs2ftp(self, fspath):
984         """Translate a "real" filesystem pathname into equivalent
985         absolute "virtual" ftp pathname depending on the user's
986         root directory.
987
988         Example (having "/home/user" as root directory):
989         '/home/user/x' -> '/x'
990
991         As for ftpnorm, directory separators are system independent
992         ("/") and pathname returned is always absolutized.
993
994         On invalid pathnames escaping from user's root directory
995         (e.g. "/home" when root is "/home/user") always return "/".
996         """
997         if os.path.isabs(fspath):
998             p = os.path.normpath(fspath)
999         else:
1000             p = os.path.normpath(os.path.join(self.root, fspath))
1001         if not self.validpath(p):
1002             return '/'
1003         p = p.replace(os.sep, "/")
1004         p = p[len(self.root):]
1005         if not p.startswith('/'):
1006             p = '/' + p
1007         return p
1008
1009     # alias for backward compatibility with 0.2.0
1010     normalize = ftpnorm
1011     translate = ftp2fs
1012
1013     def validpath(self, path):
1014         """Check whether the path belongs to user's home directory.
1015         Expected argument is a "real" filesystem pathname.
1016
1017         If path is a symbolic link it is resolved to check its real
1018         destination.
1019
1020         Pathnames escaping from user's root directory are considered
1021         not valid.
1022         """
1023         root = self.realpath(self.root)
1024         path = self.realpath(path)
1025         if not self.root.endswith(os.sep):
1026             root = self.root + os.sep
1027         if not path.endswith(os.sep):
1028             path = path + os.sep
1029         if path[0:len(root)] == root:
1030             return True
1031         return False
1032
1033     # --- Wrapper methods around open() and tempfile.mkstemp
1034
1035     def open(self, filename, mode):
1036         """Open a file returning its handler."""
1037         return open(filename, mode)
1038
1039     def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
1040         """A wrap around tempfile.mkstemp creating a file with a unique
1041         name.  Unlike mkstemp it returns an object with a file-like
1042         interface.
1043         """
1044         class FileWrapper:
1045             def __init__(self, fd, name):
1046                 self.file = fd
1047                 self.name = name
1048             def __getattr__(self, attr):
1049                 return getattr(self.file, attr)
1050
1051         text = not 'b' in mode
1052         # max number of tries to find out a unique file name
1053         tempfile.TMP_MAX = 50
1054         fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
1055         file = os.fdopen(fd, mode)
1056         return FileWrapper(file, name)
1057
1058     # --- Wrapper methods around os.*
1059
1060     def chdir(self, path):
1061         """Change the current directory."""
1062         # temporarily join the specified directory to see if we have
1063         # permissions to do so
1064         basedir = os.getcwd()
1065         try:
1066             os.chdir(path)
1067         except os.error:
1068             raise
1069         else:
1070             os.chdir(basedir)
1071             self.cwd = self.fs2ftp(path)
1072
1073     def mkdir(self, path, basename):
1074         """Create the specified directory."""
1075         os.mkdir(os.path.join(path, basename))
1076
1077     def listdir(self, path):
1078         """List the content of a directory."""
1079         return os.listdir(path)
1080
1081     def rmdir(self, path):
1082         """Remove the specified directory."""
1083         os.rmdir(path)
1084
1085     def remove(self, path):
1086         """Remove the specified file."""
1087         os.remove(path)
1088
1089     def rename(self, src, dst):
1090         """Rename the specified src file to the dst filename."""
1091         os.rename(src, dst)
1092
1093     def stat(self, path):
1094         """Perform a stat() system call on the given path."""
1095         return os.stat(path)
1096
1097     def lstat(self, path):
1098         """Like stat but does not follow symbolic links."""
1099         return os.lstat(path)
1100
1101     if not hasattr(os, 'lstat'):
1102         lstat = stat
1103
1104     # --- Wrapper methods around os.path.*
1105
1106     def isfile(self, path):
1107         """Return True if path is a file."""
1108         return os.path.isfile(path)
1109
1110     def islink(self, path):
1111         """Return True if path is a symbolic link."""
1112         return os.path.islink(path)
1113
1114     def isdir(self, path):
1115         """Return True if path is a directory."""
1116         return os.path.isdir(path)
1117
1118     def getsize(self, path):
1119         """Return the size of the specified file in bytes."""
1120         return os.path.getsize(path)
1121
1122     def getmtime(self, path):
1123         """Return the last modified time as a number of seconds since
1124         the epoch."""
1125         return os.path.getmtime(path)
1126
1127     def realpath(self, path):
1128         """Return the canonical version of path eliminating any
1129         symbolic links encountered in the path (if they are
1130         supported by the operating system).
1131         """
1132         return os.path.realpath(path)
1133
1134     def lexists(self, path):
1135         """Return True if path refers to an existing path, including
1136         a broken or circular symbolic link.
1137         """
1138         if hasattr(os.path, 'lexists'):
1139             return os.path.lexists(path)
1140         # grant backward compatibility with python 2.3
1141         elif hasattr(os, 'lstat'):
1142             try:
1143                 os.lstat(path)
1144             except os.error:
1145                 return False
1146             return True
1147         # fallback
1148         else:
1149             return os.path.exists(path)
1150
1151     exists = lexists  # alias for backward compatibility with 0.2.0
1152
1153     def glob1(self, dirname, pattern):
1154         """Return a list of files matching a dirname pattern
1155         non-recursively.
1156
1157         Unlike glob.glob1 raises exception if os.listdir() fails.
1158         """
1159         names = self.listdir(dirname)
1160         if pattern[0] != '.':
1161             names = filter(lambda x: x[0] != '.', names)
1162         return fnmatch.filter(names, pattern)
1163
1164     # --- Listing utilities
1165
1166     # note: the following operations are no more blocking
1167
1168     def get_list_dir(self, datacr):
1169         """"Return an iterator object that yields a directory listing
1170         in a form suitable for LIST command.
1171         """
1172         raise DeprecationWarning()
1173
1174     def get_stat_dir(self, rawline):
1175         """Return an iterator object that yields a list of files
1176         matching a dirname pattern non-recursively in a form
1177         suitable for STAT command.
1178
1179          - (str) rawline: the raw string passed by client as command
1180          argument.
1181         """
1182         ftppath = self.ftpnorm(rawline)
1183         if not glob.has_magic(ftppath):
1184             return self.get_list_dir(self.ftp2fs(rawline))
1185         else:
1186             basedir, basename = os.path.split(ftppath)
1187             if glob.has_magic(basedir):
1188                 return iter(['Directory recursion not supported.\r\n'])
1189             else:
1190                 basedir = self.ftp2fs(basedir)
1191                 listing = self.glob1(basedir, basename)
1192                 if listing:
1193                     listing.sort()
1194                 return self.format_list(basedir, listing)
1195
1196     def format_list(self, basedir, listing, ignore_err=True):
1197         """Return an iterator object that yields the entries of given
1198         directory emulating the "/bin/ls -lA" UNIX command output.
1199
1200          - (str) basedir: the absolute dirname.
1201          - (list) listing: the names of the entries in basedir
1202          - (bool) ignore_err: when False raise exception if os.lstat()
1203          call fails.
1204
1205         On platforms which do not support the pwd and grp modules (such
1206         as Windows), ownership is printed as "owner" and "group" as a
1207         default, and number of hard links is always "1". On UNIX
1208         systems, the actual owner, group, and number of links are
1209         printed.
1210
1211         This is how output appears to client:
1212
1213         -rw-rw-rw-   1 owner   group    7045120 Sep 02  3:47 music.mp3
1214         drwxrwxrwx   1 owner   group          0 Aug 31 18:50 e-books
1215         -rw-rw-rw-   1 owner   group        380 Sep 02  3:40 module.py
1216         """
1217         for basename in listing:
1218             file = os.path.join(basedir, basename)
1219             try:
1220                 st = self.lstat(file)
1221             except os.error:
1222                 if ignore_err:
1223                     continue
1224                 raise
1225             perms = filemode(st.st_mode)  # permissions
1226             nlinks = st.st_nlink  # number of links to inode
1227             if not nlinks:  # non-posix system, let's use a bogus value
1228                 nlinks = 1
1229             size = st.st_size  # file size
1230             if pwd and grp:
1231                 # get user and group name, else just use the raw uid/gid
1232                 try:
1233                     uname = pwd.getpwuid(st.st_uid).pw_name
1234                 except KeyError:
1235                     uname = st.st_uid
1236                 try:
1237                     gname = grp.getgrgid(st.st_gid).gr_name
1238                 except KeyError:
1239                     gname = st.st_gid
1240             else:
1241                 # on non-posix systems the only chance we use default
1242                 # bogus values for owner and group
1243                 uname = "owner"
1244                 gname = "group"
1245             # stat.st_mtime could fail (-1) if last mtime is too old
1246             # in which case we return the local time as last mtime
1247             try:
1248                 mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
1249             except ValueError:
1250                 mtime = time.strftime("%b %d %H:%M")
1251             # if the file is a symlink, resolve it, e.g. "symlink -> realfile"
1252             if stat.S_ISLNK(st.st_mode):
1253                 basename = basename + " -> " + os.readlink(file)
1254
1255             # formatting is matched with proftpd ls output
1256             yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
1257                                                      size, mtime, basename)
1258
1259     def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
1260         """Return an iterator object that yields the entries of a given
1261         directory or of a single file in a form suitable with MLSD and
1262         MLST commands.
1263
1264         Every entry includes a list of "facts" referring the listed
1265         element.  See RFC-3659, chapter 7, to see what every single
1266         fact stands for.
1267
1268          - (str) basedir: the absolute dirname.
1269          - (list) listing: the names of the entries in basedir
1270          - (str) perms: the string referencing the user permissions.
1271          - (str) facts: the list of "facts" to be returned.
1272          - (bool) ignore_err: when False raise exception if os.stat()
1273          call fails.
1274
1275         Note that "facts" returned may change depending on the platform
1276         and on what user specified by using the OPTS command.
1277
1278         This is how output could appear to the client issuing
1279         a MLSD request:
1280
1281         type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
1282         type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
1283         type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
1284         """
1285         permdir = ''.join([x for x in perms if x not in 'arw'])
1286         permfile = ''.join([x for x in perms if x not in 'celmp'])
1287         if ('w' in perms) or ('a' in perms) or ('f' in perms):
1288             permdir += 'c'
1289         if 'd' in perms:
1290             permdir += 'p'
1291         type = size = perm = modify = create = unique = mode = uid = gid = ""
1292         for basename in listing:
1293             file = os.path.join(basedir, basename)
1294             try:
1295                 st = self.stat(file)
1296             except OSError:
1297                 if ignore_err:
1298                     continue
1299                 raise
1300             # type + perm
1301             if stat.S_ISDIR(st.st_mode):
1302                 if 'type' in facts:
1303                     if basename == '.':
1304                         type = 'type=cdir;'
1305                     elif basename == '..':
1306                         type = 'type=pdir;'
1307                     else:
1308                         type = 'type=dir;'
1309                 if 'perm' in facts:
1310                     perm = 'perm=%s;' %permdir
1311             else:
1312                 if 'type' in facts:
1313                     type = 'type=file;'
1314                 if 'perm' in facts:
1315                     perm = 'perm=%s;' %permfile
1316             if 'size' in facts:
1317                 size = 'size=%s;' %st.st_size  # file size
1318             # last modification time
1319             if 'modify' in facts:
1320                 try:
1321                     modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
1322                                            time.localtime(st.st_mtime))
1323                 except ValueError:
1324                     # stat.st_mtime could fail (-1) if last mtime is too old
1325                     modify = ""
1326             if 'create' in facts:
1327                 # on Windows we can provide also the creation time
1328                 try:
1329                     create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
1330                                            time.localtime(st.st_ctime))
1331                 except ValueError:
1332                     create = ""
1333             # UNIX only
1334             if 'unix.mode' in facts:
1335                 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
1336             if 'unix.uid' in facts:
1337                 uid = 'unix.uid=%s;' %st.st_uid
1338             if 'unix.gid' in facts:
1339                 gid = 'unix.gid=%s;' %st.st_gid
1340             # We provide unique fact (see RFC-3659, chapter 7.5.2) on
1341             # posix platforms only; we get it by mixing st_dev and
1342             # st_ino values which should be enough for granting an
1343             # uniqueness for the file listed.
1344             # The same approach is used by pure-ftpd.
1345             # Implementors who want to provide unique fact on other
1346             # platforms should use some platform-specific method (e.g.
1347             # on Windows NTFS filesystems MTF records could be used).
1348             if 'unique' in facts:
1349                 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
1350
1351             yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
1352                                                 mode, uid, gid, unique, basename)
1353
1354
1355 # --- FTP
1356
1357 class FTPHandler(asynchat.async_chat):
1358     """Implements the FTP server Protocol Interpreter (see RFC-959),
1359     handling commands received from the client on the control channel.
1360
1361     All relevant session information is stored in class attributes
1362     reproduced below and can be modified before instantiating this
1363     class.
1364
1365      - (str) banner: the string sent when client connects.
1366
1367      - (int) max_login_attempts:
1368         the maximum number of wrong authentications before disconnecting
1369         the client (default 3).
1370
1371      - (bool)permit_foreign_addresses:
1372         FTP site-to-site transfer feature: also referenced as "FXP" it
1373         permits for transferring a file between two remote FTP servers
1374         without the transfer going through the client's host (not
1375         recommended for security reasons as described in RFC-2577).
1376         Having this attribute set to False means that all data
1377         connections from/to remote IP addresses which do not match the
1378         client's IP address will be dropped (defualt False).
1379
1380      - (bool) permit_privileged_ports:
1381         set to True if you want to permit active data connections (PORT)
1382         over privileged ports (not recommended, defaulting to False).
1383
1384      - (str) masquerade_address:
1385         the "masqueraded" IP address to provide along PASV reply when
1386         pyftpdlib is running behind a NAT or other types of gateways.
1387         When configured pyftpdlib will hide its local address and
1388         instead use the public address of your NAT (default None).
1389
1390      - (list) passive_ports:
1391         what ports ftpd will use for its passive data transfers.
1392         Value expected is a list of integers (e.g. range(60000, 65535)).
1393         When configured pyftpdlib will no longer use kernel-assigned
1394         random ports (default None).
1395
1396
1397     All relevant instance attributes initialized when client connects
1398     are reproduced below.  You may be interested in them in case you
1399     want to subclass the original FTPHandler.
1400
1401      - (bool) authenticated: True if client authenticated himself.
1402      - (str) username: the name of the connected user (if any).
1403      - (int) attempted_logins: number of currently attempted logins.
1404      - (str) current_type: the current transfer type (default "a")
1405      - (int) af: the address family (IPv4/IPv6)
1406      - (instance) server: the FTPServer class instance.
1407      - (instance) data_server: the data server instance (if any).
1408      - (instance) data_channel: the data channel instance (if any).
1409     """
1410     # these are overridable defaults
1411
1412     # default classes
1413     authorizer = DummyAuthorizer()
1414     active_dtp = ActiveDTP
1415     passive_dtp = PassiveDTP
1416     dtp_handler = DTPHandler
1417     abstracted_fs = AbstractedFS
1418
1419     # session attributes (explained in the docstring)
1420     banner = "pyftpdlib %s ready." %__ver__
1421     max_login_attempts = 3
1422     permit_foreign_addresses = False
1423     permit_privileged_ports = False
1424     masquerade_address = None
1425     passive_ports = None
1426
1427     def __init__(self, conn, server):
1428         """Initialize the command channel.
1429
1430          - (instance) conn: the socket object instance of the newly
1431             established connection.
1432          - (instance) server: the ftp server class instance.
1433         """
1434         try:
1435             asynchat.async_chat.__init__(self, conn=conn) # python2.5
1436         except TypeError, e:
1437             asynchat.async_chat.__init__(self, sock=conn) # python2.6
1438         self.server = server
1439         self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
1440         self.in_buffer = []
1441         self.in_buffer_len = 0
1442         self.set_terminator("\r\n")
1443
1444         # session attributes
1445         self.fs = self.abstracted_fs()
1446         self.authenticated = False
1447         self.username = ""
1448         self.password = ""
1449         self.attempted_logins = 0
1450         self.current_type = 'a'
1451         self.restart_position = 0
1452         self.quit_pending = False
1453         self._epsvall = False
1454         self.__in_dtp_queue = None
1455         self.__out_dtp_queue = None
1456
1457         # mlsx facts attributes
1458         self.current_facts = ['type', 'perm', 'size', 'modify']
1459         if os.name == 'posix':
1460             self.current_facts.append('unique')
1461         self.available_facts = self.current_facts[:]
1462         if pwd and grp:
1463             self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
1464         if os.name == 'nt':
1465             self.available_facts.append('create')
1466
1467         # dtp attributes
1468         self.data_server = None
1469         self.data_channel = None
1470
1471         if hasattr(self.socket, 'family'):
1472             self.af = self.socket.family
1473         else:  # python < 2.5
1474             ip, port = self.socket.getsockname()[:2]
1475             self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
1476                                          socket.SOCK_STREAM)[0][0]
1477
1478     def handle(self):
1479         """Return a 220 'Ready' response to the client over the command
1480         channel.
1481         """
1482         if len(self.banner) <= 75:
1483             self.respond("220 %s" %str(self.banner))
1484         else:
1485             self.push('220-%s\r\n' %str(self.banner))
1486             self.respond('220 ')
1487
1488     def handle_max_cons(self):
1489         """Called when limit for maximum number of connections is reached."""
1490         msg = "Too many connections. Service temporary unavailable."
1491         self.respond("421 %s" %msg)
1492         self.log(msg)
1493         # If self.push is used, data could not be sent immediately in
1494         # which case a new "loop" will occur exposing us to the risk of
1495         # accepting new connections.  Since this could cause asyncore to
1496         # run out of fds (...and exposes the server to DoS attacks), we
1497         # immediately close the channel by using close() instead of
1498         # close_when_done(). If data has not been sent yet client will
1499         # be silently disconnected.
1500         self.close()
1501
1502     def handle_max_cons_per_ip(self):
1503         """Called when too many clients are connected from the same IP."""
1504         msg = "Too many connections from the same IP address."
1505         self.respond("421 %s" %msg)
1506         self.log(msg)
1507         self.close_when_done()
1508
1509     # --- asyncore / asynchat overridden methods
1510
1511     def readable(self):
1512         # if there's a quit pending we stop reading data from socket
1513         return not self.quit_pending
1514
1515     def collect_incoming_data(self, data):
1516         """Read incoming data and append to the input buffer."""
1517         self.in_buffer.append(data)
1518         self.in_buffer_len += len(data)
1519         # Flush buffer if it gets too long (possible DoS attacks).
1520         # RFC-959 specifies that a 500 response could be given in
1521         # such cases
1522         buflimit = 2048
1523         if self.in_buffer_len > buflimit:
1524             self.respond('500 Command too long.')
1525             self.log('Command received exceeded buffer limit of %s.' %(buflimit))
1526             self.in_buffer = []
1527             self.in_buffer_len = 0
1528
1529     # commands accepted before authentication
1530     unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
1531
1532     # commands needing an argument
1533     arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
1534                 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
1535                 'TYPE','USER','XMKD','XRMD')
1536
1537     # commands needing no argument
1538     unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
1539                   'SYST','XCUP','XPWD')
1540
1541     def found_terminator(self):
1542         r"""Called when the incoming data stream matches the \r\n
1543         terminator.
1544
1545         Depending on the command received it calls the command's
1546         corresponding method (e.g. for received command "MKD pathname",
1547         ftp_MKD() method is called with "pathname" as the argument).
1548         """
1549         line = ''.join(self.in_buffer)
1550         self.in_buffer = []
1551         self.in_buffer_len = 0
1552
1553         cmd = line.split(' ')[0].upper()
1554         space = line.find(' ')
1555         if space != -1:
1556             arg = line[space + 1:]
1557         else:
1558             arg = ""
1559
1560         if cmd != 'PASS':
1561             self.logline("<== %s" %line)
1562         else:
1563             self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
1564
1565         # let's check if user provided an argument for those commands
1566         # needing one
1567         if not arg and cmd in self.arg_cmds:
1568             self.respond("501 Syntax error: command needs an argument.")
1569             return
1570
1571         # let's do the same for those commands requiring no argument.
1572         elif arg and cmd in self.unarg_cmds:
1573             self.respond("501 Syntax error: command does not accept arguments.")
1574             return
1575
1576         # provide a limited set of commands if user isn't
1577         # authenticated yet
1578         if (not self.authenticated):
1579             if cmd in self.unauth_cmds:
1580                 # we permit STAT during this phase but we don't want
1581                 # STAT to return a directory LISTing if the user is
1582                 # not authenticated yet (this could happen if STAT
1583                 # is used with an argument)
1584                 if (cmd == 'STAT') and arg:
1585                     self.respond("530 Log in with USER and PASS first.")
1586                 else:
1587                     method = getattr(self, 'ftp_' + cmd)
1588                     method(arg)  # call the proper ftp_* method
1589             elif cmd in proto_cmds:
1590                 self.respond("530 Log in with USER and PASS first.")
1591             else:
1592                 self.respond('500 Command "%s" not understood.' %line)
1593
1594         # provide full command set
1595         elif (self.authenticated) and (cmd in proto_cmds):
1596             if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
1597                 return
1598             method = getattr(self, 'ftp_' + cmd)
1599             method(arg)  # call the proper ftp_* method
1600
1601         else:
1602             # recognize those commands having "special semantics"
1603             if 'ABOR' in cmd:
1604                 self.ftp_ABOR("")
1605             elif 'STAT' in cmd:
1606                 self.ftp_STAT("")
1607             # unknown command
1608             else:
1609                 self.respond('500 Command "%s" not understood.' %line)
1610
1611     def __check_path(self, cmd, line):
1612         """Check whether a path is valid."""
1613         
1614         # Always true, we will only check later, once we have a cursor
1615         return True
1616
1617     def __check_perm(self, cmd, line, datacr):
1618         """Check permissions depending on issued command."""
1619         map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
1620                'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
1621                'RETR':'r',
1622                'APPE':'a',
1623                'DELE':'d', 'RMD':'d', 'XRMD':'d',
1624                'RNFR':'f',
1625                'MKD':'m', 'XMKD':'m',
1626                'STOR':'w'}
1627         raise NotImplementedError
1628         if cmd in map:
1629             if cmd == 'STAT' and not line:
1630                 return True
1631             perm = map[cmd]
1632             if not line and (cmd in ('LIST','NLST','MLSD')):
1633                 path = self.fs.ftp2fs(self.fs.cwd, datacr)
1634             else:
1635                 path = self.fs.ftp2fs(line, datacr)
1636             if not self.authorizer.has_perm(self.username, perm, path):
1637                 self.log('FAIL %s "%s". Not enough privileges.' \
1638                          %(cmd, self.fs.ftpnorm(line)))
1639                 self.respond("550 Can't %s. Not enough privileges." %cmd)
1640                 return False
1641         return True
1642
1643     def handle_expt(self):
1644         """Called when there is out of band (OOB) data for the socket
1645         connection.  This could happen in case of such commands needing
1646         "special action" (typically STAT and ABOR) in which case we
1647         append OOB data to incoming buffer.
1648         """
1649         if hasattr(socket, 'MSG_OOB'):
1650             try:
1651                 data = self.socket.recv(1024, socket.MSG_OOB)
1652             except socket.error:
1653                 pass
1654             else:
1655                 self.in_buffer.append(data)
1656                 return
1657         self.log("Can't handle OOB data.")
1658         self.close()
1659
1660     def handle_error(self):
1661         try:
1662             raise
1663         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
1664             raise
1665         except socket.error, err:
1666             # fix around asyncore bug (http://bugs.python.org/issue1736101)
1667             if err[0] in (errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, \
1668                           errno.ECONNABORTED):
1669                 self.handle_close()
1670                 return
1671             else:
1672                 logerror(traceback.format_exc())
1673         except:
1674             logerror(traceback.format_exc())
1675         self.close()
1676
1677     def handle_close(self):
1678         self.close()
1679
1680     _closed = False
1681     def close(self):
1682         """Close the current channel disconnecting the client."""
1683         if not self._closed:
1684             self._closed = True
1685             if self.data_server:
1686                 self.data_server.close()
1687                 del self.data_server
1688
1689             if self.data_channel:
1690                 self.data_channel.close()
1691                 del self.data_channel
1692
1693             del self.__out_dtp_queue
1694             del self.__in_dtp_queue
1695
1696             # remove client IP address from ip map
1697             self.server.ip_map.remove(self.remote_ip)
1698             asynchat.async_chat.close(self)
1699             self.log("Disconnected.")
1700
1701     # --- callbacks
1702
1703     def on_dtp_connection(self):
1704         """Called every time data channel connects (either active or
1705         passive).
1706
1707         Incoming and outgoing queues are checked for pending data.
1708         If outbound data is pending, it is pushed into the data channel.
1709         If awaiting inbound data, the data channel is enabled for
1710         receiving.
1711         """
1712         if self.data_server:
1713             self.data_server.close()
1714         self.data_server = None
1715
1716         # check for data to send
1717         if self.__out_dtp_queue:
1718             data, isproducer, file = self.__out_dtp_queue
1719             if file:
1720                 self.data_channel.file_obj = file
1721             if not isproducer:
1722                 self.data_channel.push(data)
1723             else:
1724                 self.data_channel.push_with_producer(data)
1725             if self.data_channel:
1726                 self.data_channel.close_when_done()
1727             self.__out_dtp_queue = None
1728
1729         # check for data to receive
1730         elif self.__in_dtp_queue:
1731             self.data_channel.file_obj = self.__in_dtp_queue
1732             self.data_channel.enable_receiving(self.current_type)
1733             self.__in_dtp_queue = None
1734
1735     def on_dtp_close(self):
1736         """Called every time the data channel is closed."""
1737         self.data_channel = None
1738         if self.quit_pending:
1739             self.close_when_done()
1740
1741     # --- utility
1742
1743     def respond(self, resp):
1744         """Send a response to the client using the command channel."""
1745         self.push(resp + '\r\n')
1746         self.logline('==> %s' % resp)
1747
1748     def push_dtp_data(self, data, isproducer=False, file=None):
1749         """Pushes data into the data channel.
1750
1751         It is usually called for those commands requiring some data to
1752         be sent over the data channel (e.g. RETR).
1753         If data channel does not exist yet, it queues the data to send
1754         later; data will then be pushed into data channel when
1755         on_dtp_connection() will be called.
1756
1757          - (str/classobj) data: the data to send which may be a string
1758             or a producer object).
1759          - (bool) isproducer: whether treat data as a producer.
1760          - (file) file: the file[-like] object to send (if any).
1761         """
1762         if self.data_channel:
1763             self.respond("125 Data connection already open. Transfer starting.")
1764             if file:
1765                 self.data_channel.file_obj = file
1766             if not isproducer:
1767                 self.data_channel.push(data)
1768             else:
1769                 self.data_channel.push_with_producer(data)
1770             if self.data_channel:
1771                 self.data_channel.close_when_done()
1772         else:
1773             self.respond("150 File status okay. About to open data connection.")
1774             self.__out_dtp_queue = (data, isproducer, file)
1775
1776     def log(self, msg):
1777         """Log a message, including additional identifying session data."""        
1778         log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
1779                               self.remote_port, msg))
1780
1781     def logline(self, msg):
1782         """Log a line including additional indentifying session data."""
1783         logline("%s:%s %s" %(self.remote_ip, self.remote_port, msg))
1784
1785     def flush_account(self):
1786         """Flush account information by clearing attributes that need
1787         to be reset on a REIN or new USER command.
1788         """
1789         if self.data_channel:
1790             if not self.data_channel.transfer_in_progress():
1791                 self.data_channel.close()
1792                 self.data_channel = None
1793         if self.data_server:
1794             self.data_server.close()
1795             self.data_server = None
1796
1797         self.fs.rnfr = None
1798         self.authenticated = False
1799         self.username = ""
1800         self.password = ""
1801         self.attempted_logins = 0
1802         self.current_type = 'a'
1803         self.restart_position = 0
1804         self.quit_pending = False
1805         self.__in_dtp_queue = None
1806         self.__out_dtp_queue = None
1807
1808     def run_as_current_user(self, function, *args, **kwargs):
1809         """Execute a function impersonating the current logged-in user."""
1810         self.authorizer.impersonate_user(self.username, self.password)
1811         try:
1812             return function(*args, **kwargs)
1813         finally:
1814             self.authorizer.terminate_impersonation()
1815
1816         # --- connection
1817
1818     def _make_eport(self, ip, port):
1819         """Establish an active data channel with remote client which
1820         issued a PORT or EPRT command.
1821         """
1822         # FTP bounce attacks protection: according to RFC-2577 it's
1823         # recommended to reject PORT if IP address specified in it
1824         # does not match client IP address.
1825         if not self.permit_foreign_addresses:
1826             if ip != self.remote_ip:
1827                 self.log("Rejected data connection to foreign address %s:%s."
1828                          %(ip, port))
1829                 self.respond("501 Can't connect to a foreign address.")
1830                 return
1831
1832         # ...another RFC-2577 recommendation is rejecting connections
1833         # to privileged ports (< 1024) for security reasons.
1834         if not self.permit_privileged_ports:
1835             if port < 1024:
1836                 self.log('PORT against the privileged port "%s" refused.' %port)
1837                 self.respond("501 Can't connect over a privileged port.")
1838                 return
1839
1840         # close existent DTP-server instance, if any.
1841         if self.data_server:
1842             self.data_server.close()
1843             self.data_server = None
1844         if self.data_channel:
1845             self.data_channel.close()
1846             self.data_channel = None
1847
1848         # make sure we are not hitting the max connections limit
1849         if self.server.max_cons:
1850             if len(self._map) >= self.server.max_cons:
1851                 msg = "Too many connections. Can't open data channel."
1852                 self.respond("425 %s" %msg)
1853                 self.log(msg)
1854                 return
1855
1856         # open data channel
1857         self.active_dtp(ip, port, self)
1858
1859     def _make_epasv(self, extmode=False):
1860         """Initialize a passive data channel with remote client which
1861         issued a PASV or EPSV command.
1862         If extmode argument is False we assume that client issued EPSV in
1863         which case extended passive mode will be used (see RFC-2428).
1864         """
1865         # close existing DTP-server instance, if any
1866         if self.data_server:
1867             self.data_server.close()
1868             self.data_server = None
1869
1870         if self.data_channel:
1871             self.data_channel.close()
1872             self.data_channel = None
1873
1874         # make sure we are not hitting the max connections limit
1875         if self.server.max_cons:
1876             if len(self._map) >= self.server.max_cons:
1877                 msg = "Too many connections. Can't open data channel."
1878                 self.respond("425 %s" %msg)
1879                 self.log(msg)
1880                 return
1881
1882         # open data channel
1883         self.data_server = self.passive_dtp(self, extmode)
1884
1885     def ftp_PORT(self, line):
1886         """Start an active data channel by using IPv4."""
1887         if self._epsvall:
1888             self.respond("501 PORT not allowed after EPSV ALL.")
1889             return
1890         if self.af != socket.AF_INET:
1891             self.respond("425 You cannot use PORT on IPv6 connections. "
1892                          "Use EPRT instead.")
1893             return
1894         # Parse PORT request for getting IP and PORT.
1895         # Request comes in as:
1896         # > h1,h2,h3,h4,p1,p2
1897         # ...where the client's IP address is h1.h2.h3.h4 and the TCP
1898         # port number is (p1 * 256) + p2.
1899         try:
1900             addr = map(int, line.split(','))
1901             assert len(addr) == 6
1902             for x in addr[:4]:
1903                 assert 0 <= x <= 255
1904             ip = '%d.%d.%d.%d' %tuple(addr[:4])
1905             port = (addr[4] * 256) + addr[5]
1906             assert 0 <= port <= 65535
1907         except (AssertionError, ValueError, OverflowError):
1908             self.respond("501 Invalid PORT format.")
1909             return
1910         self._make_eport(ip, port)
1911
1912     def ftp_EPRT(self, line):
1913         """Start an active data channel by choosing the network protocol
1914         to use (IPv4/IPv6) as defined in RFC-2428.
1915         """
1916         if self._epsvall:
1917             self.respond("501 EPRT not allowed after EPSV ALL.")
1918             return
1919         # Parse EPRT request for getting protocol, IP and PORT.
1920         # Request comes in as:
1921         # # <d>proto<d>ip<d>port<d>
1922         # ...where <d> is an arbitrary delimiter character (usually "|") and
1923         # <proto> is the network protocol to use (1 for IPv4, 2 for IPv6).
1924         try:
1925             af, ip, port = line.split(line[0])[1:-1]
1926             port = int(port)
1927             assert 0 <= port <= 65535
1928         except (AssertionError, ValueError, IndexError, OverflowError):
1929             self.respond("501 Invalid EPRT format.")
1930             return
1931
1932         if af == "1":
1933             if self.af != socket.AF_INET:
1934                 self.respond('522 Network protocol not supported (use 2).')
1935             else:
1936                 try:
1937                     octs = map(int, ip.split('.'))
1938                     assert len(octs) == 4
1939                     for x in octs:
1940                         assert 0 <= x <= 255
1941                 except (AssertionError, ValueError, OverflowError), err:
1942                     self.respond("501 Invalid EPRT format.")
1943                 else:
1944                     self._make_eport(ip, port)
1945         elif af == "2":
1946             if self.af == socket.AF_INET:
1947                 self.respond('522 Network protocol not supported (use 1).')
1948             else:
1949                 self._make_eport(ip, port)
1950         else:
1951             if self.af == socket.AF_INET:
1952                 self.respond('501 Unknown network protocol (use 1).')
1953             else:
1954                 self.respond('501 Unknown network protocol (use 2).')
1955
1956     def ftp_PASV(self, line):
1957         """Start a passive data channel by using IPv4."""
1958         if self._epsvall:
1959             self.respond("501 PASV not allowed after EPSV ALL.")
1960             return
1961         if self.af != socket.AF_INET:
1962             self.respond("425 You cannot use PASV on IPv6 connections. "
1963                          "Use EPSV instead.")
1964         else:
1965             self._make_epasv(extmode=False)
1966
1967     def ftp_EPSV(self, line):
1968         """Start a passive data channel by using IPv4 or IPv6 as defined
1969         in RFC-2428.
1970         """
1971         # RFC-2428 specifies that if an optional parameter is given,
1972         # we have to determine the address family from that otherwise
1973         # use the same address family used on the control connection.
1974         # In such a scenario a client may use IPv4 on the control channel
1975         # and choose to use IPv6 for the data channel.
1976         # But how could we use IPv6 on the data channel without knowing
1977         # which IPv6 address to use for binding the socket?
1978         # Unfortunately RFC-2428 does not provide satisfing information
1979         # on how to do that.  The assumption is that we don't have any way
1980         # to know which address to use, hence we just use the same address
1981         # family used on the control connection.
1982         if not line:
1983             self._make_epasv(extmode=True)
1984         elif line == "1":
1985             if self.af != socket.AF_INET:
1986                 self.respond('522 Network protocol not supported (use 2).')
1987             else:
1988                 self._make_epasv(extmode=True)
1989         elif line == "2":
1990             if self.af == socket.AF_INET:
1991                 self.respond('522 Network protocol not supported (use 1).')
1992             else:
1993                 self._make_epasv(extmode=True)
1994         elif line.lower() == 'all':
1995             self._epsvall = True
1996             self.respond('220 Other commands other than EPSV are now disabled.')
1997         else:
1998             if self.af == socket.AF_INET:
1999                 self.respond('501 Unknown network protocol (use 1).')
2000             else:
2001                 self.respond('501 Unknown network protocol (use 2).')
2002
2003     def ftp_QUIT(self, line):
2004         """Quit the current session."""
2005         # From RFC-959:
2006         # This command terminates a USER and if file transfer is not
2007         # in progress, the server closes the control connection.
2008         # If file transfer is in progress, the connection will remain
2009         # open for result response and the server will then close it.
2010         if self.authenticated:
2011             msg_quit = self.authorizer.get_msg_quit(self.username)
2012         else:
2013             msg_quit = "Goodbye."
2014         if len(msg_quit) <= 75:
2015             self.respond("221 %s" %msg_quit)
2016         else:
2017             self.push("221-%s\r\n" %msg_quit)
2018             self.respond("221 ")
2019
2020         if not self.data_channel:
2021             self.close_when_done()
2022         else:
2023             # tell the cmd channel to stop responding to commands.
2024             self.quit_pending = True
2025
2026
2027         # --- data transferring
2028
2029     def ftp_LIST(self, line):
2030         """Return a list of files in the specified directory to the
2031         client.
2032         """
2033         # - If no argument, fall back on cwd as default.
2034         # - Some older FTP clients erroneously issue /bin/ls-like LIST
2035         #   formats in which case we fall back on cwd as default.
2036         if not line or line.lower() in ('-a', '-l', '-al', '-la'):
2037             line = ''
2038         datacr = None
2039         try:
2040             datacr = self.fs.get_crdata(line, mode='list')
2041             iterator = self.run_as_current_user(self.fs.get_list_dir, datacr)
2042         except IOError, err:
2043             self.fs.close_cr(datacr)
2044             self.respond('550 %s.'% err.strerror)
2045             return
2046         except OSError, err:
2047             self.fs.close_cr(datacr)
2048             why = _strerror(err)
2049             self.log('FAIL LIST "%s". %s.' %(line, why))
2050             self.respond('550 %s.' %why)
2051             return
2052         try:
2053             self.log('OK LIST "%s". Transfer starting.' % line)
2054             producer = BufferedIteratorProducer(iterator)
2055             self.push_dtp_data(producer, isproducer=True)
2056         finally:
2057             self.fs.close_cr(datacr)
2058
2059
2060     def ftp_NLST(self, line):
2061         """Return a list of files in the specified directory in a
2062         compact form to the client.
2063         """
2064         if not line:
2065             line = ''
2066
2067         datacr = None
2068         try:
2069             datacr = self.fs.get_crdata(line, mode='list')
2070             if not datacr:
2071                 datacr = ( None, None, None )
2072             if self.fs.isdir(datacr[1]):
2073                 nodelist = self.run_as_current_user(self.fs.listdir, datacr)
2074             else:
2075                 # if path is a file we just list its name
2076                 nodelist = [datacr[1],]
2077             
2078             listing = []
2079             for nl in nodelist:
2080                 if isinstance(nl.path, (list, tuple)):
2081                     listing.append(nl.path[-1])
2082                 else:
2083                     listing.append(nl.path)    # assume string
2084         except IOError, err:
2085             self.fs.close_cr(datacr)
2086             self.respond('550 %s.'% err.strerror)
2087             return
2088         except OSError, err:
2089             self.fs.close_cr(datacr)
2090             why = _strerror(err)
2091             self.log('FAIL NLST "%s". %s.' %(line, why))
2092             self.respond('550 %s.' %why)
2093             return
2094         
2095         self.fs.close_cr(datacr)
2096         data = ''
2097         if listing:
2098             listing.sort()
2099             data =  ''.join([ _to_decode(x) + '\r\n' for x in listing ])
2100         self.log('OK NLST "%s". Transfer starting.' %line)
2101         self.push_dtp_data(data)
2102
2103         # --- MLST and MLSD commands
2104
2105     # The MLST and MLSD commands are intended to standardize the file and
2106     # directory information returned by the server-FTP process.  These
2107     # commands differ from the LIST command in that the format of the
2108     # replies is strictly defined although extensible.
2109
2110     def ftp_MLST(self, line):
2111         """Return information about a pathname in a machine-processable
2112         form as defined in RFC-3659.
2113         """
2114         # if no argument, fall back on cwd as default
2115         if not line:
2116             line = ''
2117         datacr = None
2118         try:
2119             datacr = self.fs.get_crdata(line, mode='list')
2120             perms = self.authorizer.get_perms(self.username)
2121             iterator = self.run_as_current_user(self.fs.format_mlsx, datacr[0], datacr[1].parent,
2122                        [datacr[1],], perms, self.current_facts, ignore_err=False)
2123             data = ''.join(iterator)
2124         except EnvironmentError, err:
2125             self.fs.close_cr(datacr)
2126             why = _strerror(err)
2127             self.log('FAIL MLST "%s". %s.' %(line, why))
2128             self.respond('550 %s.' %why)
2129         else:
2130             self.fs.close_cr(datacr)
2131             # since TVFS is supported (see RFC-3659 chapter 6), a fully
2132             # qualified pathname should be returned
2133             data = data.split(' ')[0] + ' %s\r\n' %line
2134             # response is expected on the command channel
2135             self.push('250-Listing "%s":\r\n' %line)
2136             # the fact set must be preceded by a space
2137             self.push(' ' + data)
2138             self.respond('250 End MLST.')
2139
2140     def ftp_MLSD(self, line):
2141         """Return contents of a directory in a machine-processable form
2142         as defined in RFC-3659.
2143         """
2144         # if no argument, fall back on cwd as default
2145         if not line:
2146             line = ''
2147         
2148         datacr = None
2149         try:
2150             datacr = self.fs.get_crdata(line, mode='list')
2151             # RFC-3659 requires 501 response code if path is not a directory
2152             if not self.fs.isdir(datacr[1]):
2153                 err = 'No such directory'
2154                 self.log('FAIL MLSD "%s". %s.' %(line, err))
2155                 self.respond("501 %s." %err)
2156                 return
2157             listing = self.run_as_current_user(self.fs.listdir, datacr)
2158         except OSError, err:
2159             self.fs.close_cr(datacr)
2160             why = _strerror(err)
2161             self.log('FAIL MLSD "%s". %s.' %(line, why))
2162             self.respond('550 %s.' %why)
2163         else:
2164             self.fs.close_cr(datacr)
2165             perms = self.authorizer.get_perms(self.username)
2166             iterator = self.fs.format_mlsx(datacr[0], datacr[1], listing, perms,
2167                        self.current_facts)
2168             producer = BufferedIteratorProducer(iterator)
2169             self.log('OK MLSD "%s". Transfer starting.' %line)
2170             self.push_dtp_data(producer, isproducer=True)
2171
2172     def ftp_RETR(self, line):
2173         """Retrieve the specified file (transfer from the server to the
2174         client)
2175         """
2176         datacr = None
2177         try:
2178             datacr = self.fs.get_crdata(line, mode='file')
2179             fd = self.run_as_current_user(self.fs.open, datacr, 'rb')
2180         except OSError, err:
2181             self.fs.close_cr(datacr)
2182             why = _strerror(err)
2183             self.log('FAIL RETR "%s". %s.' %(line, why))
2184             self.respond('550 %s.' %why)
2185             return
2186         except IOError, err:
2187             self.fs.close_cr(datacr)
2188             why = err.strerror
2189             self.log('FAIL RETR "%s". %s.' %(line, why))
2190             self.respond('550 %s.' %why)
2191             return
2192
2193         if self.restart_position:
2194             # Make sure that the requested offset is valid (within the
2195             # size of the file being resumed).
2196             # According to RFC-1123 a 554 reply may result in case that
2197             # the existing file cannot be repositioned as specified in
2198             # the REST.
2199             ok = 0
2200             try:
2201                 assert not self.restart_position > self.fs.getsize(datacr)
2202                 fd.seek(self.restart_position)
2203                 ok = 1
2204             except AssertionError:
2205                 why = "Invalid REST parameter"
2206             except IOError, err:
2207                 why = _strerror(err)
2208             self.restart_position = 0
2209             if not ok:
2210                 self.respond('554 %s' %why)
2211                 self.log('FAIL RETR "%s". %s.' %(line, why))
2212                 self.fs.close_cr(datacr)
2213                 return
2214         self.log('OK RETR "%s". Download starting.' %line)
2215         producer = FileProducer(fd, self.current_type)
2216         self.push_dtp_data(producer, isproducer=True, file=fd)
2217         self.fs.close_cr(datacr)
2218
2219     def ftp_STOR(self, line, mode='w'):
2220         """Store a file (transfer from the client to the server)."""
2221         # A resume could occur in case of APPE or REST commands.
2222         # In that case we have to open file object in different ways:
2223         # STOR: mode = 'w'
2224         # APPE: mode = 'a'
2225         # REST: mode = 'r+' (to permit seeking on file object)
2226         if 'a' in mode:
2227             cmd = 'APPE'
2228         else:
2229             cmd = 'STOR'
2230
2231         datacr = None
2232         try:
2233             datacr = self.fs.get_crdata(line,mode='create')
2234         except OSError, err:
2235             self.fs.close_cr(datacr)
2236             why = _strerror(err)
2237             self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2238             self.respond('550 %s.' %why)
2239             return
2240
2241         if self.restart_position:
2242             mode = 'r+'
2243         try:
2244             fd = self.run_as_current_user(self.fs.create, datacr, datacr[2], mode + 'b')
2245         except IOError, err:
2246             self.fs.close_cr(datacr)
2247             why = _strerror(err)
2248             self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2249             self.respond('550 %s.' %why)
2250             return
2251
2252         if self.restart_position:
2253             # Make sure that the requested offset is valid (within the
2254             # size of the file being resumed).
2255             # According to RFC-1123 a 554 reply may result in case
2256             # that the existing file cannot be repositioned as
2257             # specified in the REST.
2258             ok = 0
2259             try:
2260                 assert not self.restart_position > self.fs.getsize(datacr)
2261                 fd.seek(self.restart_position)
2262                 ok = 1
2263             except AssertionError:
2264                 why = "Invalid REST parameter"
2265             except IOError, err:
2266                 why = _strerror(err)
2267             self.restart_position = 0
2268             if not ok:
2269                 self.fs.close_cr(datacr)
2270                 self.respond('554 %s' %why)
2271                 self.log('FAIL %s "%s". %s.' %(cmd, line, why))
2272                 return
2273
2274         self.log('OK %s "%s". Upload starting.' %(cmd, line))
2275         if self.data_channel:
2276             self.respond("125 Data connection already open. Transfer starting.")
2277             self.data_channel.file_obj = fd
2278             self.data_channel.enable_receiving(self.current_type)
2279         else:
2280             self.respond("150 File status okay. About to open data connection.")
2281             self.__in_dtp_queue = fd
2282         self.fs.close_cr(datacr)
2283
2284
2285     def ftp_STOU(self, line):
2286         """Store a file on the server with a unique name."""
2287         # Note 1: RFC-959 prohibited STOU parameters, but this
2288         # prohibition is obsolete.
2289         # Note 2: 250 response wanted by RFC-959 has been declared
2290         # incorrect in RFC-1123 that wants 125/150 instead.
2291         # Note 3: RFC-1123 also provided an exact output format
2292         # defined to be as follow:
2293         # > 125 FILE: pppp
2294         # ...where pppp represents the unique path name of the
2295         # file that will be written.
2296
2297         # watch for STOU preceded by REST, which makes no sense.
2298         if self.restart_position:
2299             self.respond("450 Can't STOU while REST request is pending.")
2300             return
2301
2302
2303         if line:
2304             datacr = self.fs.get_crdata(line, mode='create')
2305             # TODO
2306         else:
2307             # TODO
2308             basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
2309             prefix = 'ftpd.'
2310         try:
2311             fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
2312                                           dir=basedir)
2313         except IOError, err:
2314             # hitted the max number of tries to find out file with
2315             # unique name
2316             if err.errno == errno.EEXIST:
2317                 why = 'No usable unique file name found'
2318             # something else happened
2319             else:
2320                 why = _strerror(err)
2321             self.respond("450 %s." %why)
2322             self.log('FAIL STOU "%s". %s.' %(self.fs.ftpnorm(line), why))
2323             self.fs.close_cr(datacr)
2324             return
2325
2326         filename = line
2327         if not self.authorizer.has_perm(self.username, 'w', filename):
2328             self.log('FAIL STOU "%s". Not enough privileges'
2329                      %self.fs.ftpnorm(line))
2330             self.respond("550 Can't STOU: not enough privileges.")
2331             self.fs.close_cr(datacr)
2332             return
2333
2334         # now just acts like STOR except that restarting isn't allowed
2335         self.log('OK STOU "%s". Upload starting.' %filename)
2336         if self.data_channel:
2337             self.respond("125 FILE: %s" %filename)
2338             self.data_channel.file_obj = fd
2339             self.data_channel.enable_receiving(self.current_type)
2340         else:
2341             self.respond("150 FILE: %s" %filename)
2342             self.__in_dtp_queue = fd
2343         self.fs.close_cr(datacr)
2344
2345
2346     def ftp_APPE(self, line):
2347         """Append data to an existing file on the server."""
2348         # watch for APPE preceded by REST, which makes no sense.
2349         if self.restart_position:
2350             self.respond("550 Can't APPE while REST request is pending.")
2351         else:
2352             self.ftp_STOR(line, mode='a')
2353
2354     def ftp_REST(self, line):
2355         """Restart a file transfer from a previous mark."""
2356         try:
2357             marker = int(line)
2358             if marker < 0:
2359                 raise ValueError
2360         except (ValueError, OverflowError):
2361             self.respond("501 Invalid parameter.")
2362         else:
2363             self.respond("350 Restarting at position %s. " \
2364                         "Now use RETR/STOR for resuming." %marker)
2365             self.log("OK REST %s." %marker)
2366             self.restart_position = marker
2367
2368     def ftp_ABOR(self, line):
2369         """Abort the current data transfer."""
2370
2371         # ABOR received while no data channel exists
2372         if (self.data_server is None) and (self.data_channel is None):
2373             resp = "225 No transfer to abort."
2374         else:
2375             # a PASV was received but connection wasn't made yet
2376             if self.data_server:
2377                 self.data_server.close()
2378                 self.data_server = None
2379                 resp = "225 ABOR command successful; data channel closed."
2380
2381             # If a data transfer is in progress the server must first
2382             # close the data connection, returning a 426 reply to
2383             # indicate that the transfer terminated abnormally, then it
2384             # must send a 226 reply, indicating that the abort command
2385             # was successfully processed.
2386             # If no data has been transmitted we just respond with 225
2387             # indicating that no transfer was in progress.
2388             if self.data_channel:
2389                 if self.data_channel.transfer_in_progress():
2390                     self.data_channel.close()
2391                     self.data_channel = None
2392                     self.respond("426 Connection closed; transfer aborted.")
2393                     self.log("OK ABOR. Transfer aborted, data channel closed.")
2394                     resp = "226 ABOR command successful."
2395                 else:
2396                     self.data_channel.close()
2397                     self.data_channel = None
2398                     self.log("OK ABOR. Data channel closed.")
2399                     resp = "225 ABOR command successful; data channel closed."
2400         self.respond(resp)
2401
2402
2403         # --- authentication
2404
2405     def ftp_USER(self, line):
2406         """Set the username for the current session."""
2407         # we always treat anonymous user as lower-case string.
2408         if line.lower() == "anonymous":
2409             line = "anonymous"
2410
2411         # RFC-959 specifies a 530 response to the USER command if the
2412         # username is not valid.  If the username is valid is required
2413         # ftpd returns a 331 response instead.  In order to prevent a
2414         # malicious client from determining valid usernames on a server,
2415         # it is suggested by RFC-2577 that a server always return 331 to
2416         # the USER command and then reject the combination of username
2417         # and password for an invalid username when PASS is provided later.
2418         if not self.authenticated:
2419             self.respond('331 Username ok, send password.')
2420         else:
2421             # a new USER command could be entered at any point in order
2422             # to change the access control flushing any user, password,
2423             # and account information already supplied and beginning the
2424             # login sequence again.
2425             self.flush_account()
2426             msg = 'Previous account information was flushed'
2427             self.log('OK USER "%s". %s.' %(line, msg))
2428             self.respond('331 %s, send password.' %msg)
2429         self.username = line
2430
2431     def ftp_PASS(self, line):
2432         """Check username's password against the authorizer."""
2433
2434         if self.authenticated:
2435             self.respond("503 User already authenticated.")
2436             return
2437         if not self.username:
2438             self.respond("503 Login with USER first.")
2439             return
2440
2441         # username ok
2442         if self.authorizer.has_user(self.username):
2443             if self.username == 'anonymous' \
2444             or self.authorizer.validate_authentication(self.username, line):
2445                 msg_login = self.authorizer.get_msg_login(self.username)
2446                 if len(msg_login) <= 75:
2447                     self.respond('230 %s' %msg_login)
2448                 else:
2449                     self.push("230-%s\r\n" %msg_login)
2450                     self.respond("230 ")
2451
2452                 self.authenticated = True
2453                 self.password = line
2454                 self.attempted_logins = 0
2455                 self.fs.root = self.authorizer.get_home_dir(self.username)
2456                 self.fs.username=self.username
2457                 self.fs.password=line
2458                 self.log("User %s logged in." %self.username)
2459             else:
2460                 self.attempted_logins += 1
2461                 if self.attempted_logins >= self.max_login_attempts:
2462                     self.respond("530 Maximum login attempts. Disconnecting.")
2463                     self.close()
2464                 else:
2465                     self.respond("530 Authentication failed.")
2466                 self.log('Authentication failed (user: "%s").' %self.username)
2467                 self.username = ""
2468
2469         # wrong username
2470         else:
2471             self.attempted_logins += 1
2472             if self.attempted_logins >= self.max_login_attempts:
2473                 self.log('Authentication failed: unknown username "%s".'
2474                             %self.username)
2475                 self.respond("530 Maximum login attempts. Disconnecting.")
2476                 self.close()
2477             elif self.username.lower() == 'anonymous':
2478                 self.respond("530 Anonymous access not allowed.")
2479                 self.log('Authentication failed: anonymous access not allowed.')
2480             else:
2481                 self.respond("530 Authentication failed.")
2482                 self.log('Authentication failed: unknown username "%s".'
2483                             %self.username)
2484                 self.username = ""
2485
2486     def ftp_REIN(self, line):
2487         """Reinitialize user's current session."""
2488         # From RFC-959:
2489         # REIN command terminates a USER, flushing all I/O and account
2490         # information, except to allow any transfer in progress to be
2491         # completed.  All parameters are reset to the default settings
2492         # and the control connection is left open.  This is identical
2493         # to the state in which a user finds himself immediately after
2494         # the control connection is opened.
2495         self.log("OK REIN. Flushing account information.")
2496         self.flush_account()
2497         # Note: RFC-959 erroneously mention "220" as the correct response
2498         # code to be given in this case, but this is wrong...
2499         self.respond("230 Ready for new user.")
2500
2501
2502         # --- filesystem operations
2503
2504     def ftp_PWD(self, line):
2505         """Return the name of the current working directory to the client."""
2506         cwd = self.fs.get_cwd()
2507         self.respond('257 "%s" is the current directory.' % cwd)
2508
2509     def ftp_CWD(self, line):
2510         """Change the current working directory."""
2511         # check: a lot of FTP servers go back to root directory if no
2512         # arg is provided but this is not specified in RFC-959.
2513         # Search for official references about this behaviour.
2514         datacr = None
2515         try:
2516             datacr = self.fs.get_crdata(line,'cwd')
2517             self.run_as_current_user(self.fs.chdir, datacr)
2518             cwd = self.fs.get_cwd()
2519             self.log('OK CWD "%s".' % cwd)
2520             self.respond('250 "%s" is the current directory.' % cwd)
2521         except EnvironmentError, err:
2522             self.log("Could not cwd: %s" % err)
2523             if err.errno==2:
2524                 why = 'Authentication Required or Failed'
2525                 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2526                 self.respond('530 %s.' %why)
2527             else:
2528                 why = _strerror(err)
2529                 self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
2530                 self.respond('550 %s.' %why)
2531         self.fs.close_cr(datacr)
2532
2533     def ftp_CDUP(self, line):
2534         """Change into the parent directory."""
2535         # Note: RFC-959 says that code 200 is required but it also says
2536         # that CDUP uses the same codes as CWD.
2537         self.ftp_CWD('..')
2538
2539     def ftp_SIZE(self, line):
2540         """Return size of file in a format suitable for using with
2541         RESTart as defined in RFC-3659.
2542
2543         Implementation note:
2544         properly handling the SIZE command when TYPE ASCII is used would
2545         require to scan the entire file to perform the ASCII translation
2546         logic (file.read().replace(os.linesep, '\r\n')) and then
2547         calculating the len of such data which may be different than
2548         the actual size of the file on the server.  Considering that
2549         calculating such result could be very resource-intensive it
2550         could be easy for a malicious client to try a DoS attack, thus
2551         we do not perform the ASCII translation.
2552
2553         However, clients in general should not be resuming downloads in
2554         ASCII mode.  Resuming downloads in binary mode is the recommended
2555         way as specified in RFC-3659.
2556         """
2557         datacr = None
2558         try:
2559             datacr = self.fs.get_crdata(line, mode='file')
2560             #if self.fs.isdir(datacr[1]):
2561             #    why = "%s is not retrievable" %line
2562             #    self.log('FAIL SIZE "%s". %s.' %(line, why))
2563             #    self.respond("550 %s." %why)
2564             #    self.fs.close_cr(datacr)
2565             #    return
2566             size = self.run_as_current_user(self.fs.getsize, datacr)
2567         except EnvironmentError, err:
2568             why = err.strerror
2569             self.log('FAIL SIZE "%s". %s.' %(line, why))
2570             if err.errno == errno.ENOENT:
2571                 self.respond("404 %s." % why)
2572             else:
2573                 self.respond('550 %s.' % why)
2574         else:
2575             self.respond("213 %s" %size)
2576             self.log('OK SIZE "%s".' %line)
2577         self.fs.close_cr(datacr)
2578
2579     def ftp_MDTM(self, line):
2580         """Return last modification time of file to the client as an ISO
2581         3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
2582         """
2583         datacr = None
2584         try:
2585             datacr = self.fs.get_crdata(line)
2586             if not self.fs.isfile(datacr):
2587                 why = "%s is not retrievable" %line
2588                 self.log('FAIL MDTM "%s". %s.' %(line, why))
2589                 self.respond("550 %s." %why)
2590                 self.fs.close_cr(datacr)
2591                 return
2592             lmt = self.run_as_current_user(self.fs.getmtime, datacr)
2593             lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
2594             self.respond("213 %s" %lmt)
2595             self.log('OK MDTM "%s".' %line)
2596         except OSError, err:
2597             why = _strerror(err)
2598             self.log('FAIL MDTM "%s". %s.' %(line, why))
2599             self.respond('550 %s.' %why)
2600         self.fs.close_cr(datacr)
2601
2602     def ftp_MKD(self, line):
2603         """Create the specified directory."""
2604         try:
2605             datacr = self.fs.get_crdata(line, mode='create')
2606             self.run_as_current_user(self.fs.mkdir, datacr, datacr[2])
2607         except IOError, err:
2608             self.log('FAIL MKD "%s". %s.' %(line, err.strerror))
2609             self.respond('550 %s.' % err.strerror)
2610         except OSError, err:
2611             why = _strerror(err)
2612             self.log('FAIL MKD "%s". %s.' %(line, why))
2613             self.respond('550 %s.' %why)
2614         else:
2615             self.log('OK MKD "%s".' %line)
2616             self.respond("257 Directory created.")
2617         self.fs.close_cr(datacr)
2618
2619     def ftp_RMD(self, line):
2620         """Remove the specified directory."""
2621         datacr = None
2622         try:
2623             datacr = self.fs.get_crdata(line, mode='delete')
2624             if not datacr[1]:
2625                 msg = "Can't remove root directory."
2626                 self.respond("550 %s" %msg)
2627                 self.log('FAIL MKD "/". %s' %msg)
2628                 self.fs.close_cr(datacr)
2629                 return
2630             self.run_as_current_user(self.fs.rmdir, datacr)
2631         except OSError, err:
2632             why = _strerror(err)
2633             self.log('FAIL RMD "%s". %s.' %(line, why))
2634             self.respond('550 %s.' %why)
2635         else:
2636             self.log('OK RMD "%s".' %line)
2637             self.respond("250 Directory removed.")
2638         self.fs.close_cr(datacr)
2639
2640     def ftp_DELE(self, line):
2641         """Delete the specified file."""
2642         datacr = None
2643         try:
2644             datacr = self.fs.get_crdata(line, mode='delete')
2645             self.run_as_current_user(self.fs.remove, datacr)
2646         except EnvironmentError, err:
2647             why = err.strerror
2648             self.log('FAIL DELE "%s". %s.' %(line, why))
2649             if err.errno == errno.ENOENT:
2650                 self.respond('404 %s.' % why)
2651             else:
2652                 self.respond('550 %s.' %why)
2653         else:
2654             self.log('OK DELE "%s".' %line)
2655             self.respond("250 File removed.")
2656         self.fs.close_cr(datacr)
2657
2658     def ftp_RNFR(self, line):
2659         """Rename the specified (only the source name is specified
2660         here, see RNTO command)"""
2661         datacr = None
2662         try:
2663             datacr = self.fs.get_crdata(line, mode='rfnr')
2664             if not datacr[1]:
2665                 self.respond("550 No such file or directory.")
2666             elif not datacr[1]:
2667                 self.respond("550 Can't rename the home directory.")
2668             else:
2669                 self.fs.rnfr = datacr[1]
2670                 self.respond("350 Ready for destination name.")
2671         except:
2672             self.respond("550 Can't find the file or directory.")
2673         self.fs.close_cr(datacr)
2674
2675     def ftp_RNTO(self, line):
2676         """Rename file (destination name only, source is specified with
2677         RNFR).
2678         """
2679         if not self.fs.rnfr:
2680             self.respond("503 Bad sequence of commands: use RNFR first.")
2681             return
2682         datacr = None
2683         try:
2684             try:
2685                 datacr = self.fs.get_crdata(line,'create')
2686                 oldname = self.fs.rnfr.path
2687                 if isinstance(oldname, (list, tuple)):
2688                     oldname = '/'.join(oldname)
2689                 self.run_as_current_user(self.fs.rename, self.fs.rnfr, datacr)
2690                 self.fs.rnfr = None
2691                 self.log('OK RNFR/RNTO "%s ==> %s".' % \
2692                         (_to_unicode(oldname), _to_unicode(line)))
2693                 self.respond("250 Renaming ok.")
2694             except EnvironmentError, err:
2695                 why = _strerror(err)
2696                 self.log('FAIL RNFR/RNTO "%s ==> %s". %s.' \
2697                          % (_to_unicode(oldname), _to_unicode(line), why))
2698                 self.respond('550 %s.' %why)
2699         finally:
2700             self.fs.rnfr = None
2701             self.fs.close_cr(datacr)
2702
2703
2704         # --- others
2705
2706     def ftp_TYPE(self, line):
2707         """Set current type data type to binary/ascii"""
2708         line = line.upper()
2709         if line in ("A", "AN", "A N"):
2710             self.respond("200 Type set to: ASCII.")
2711             self.current_type = 'a'
2712         elif line in ("I", "L8", "L 8"):
2713             self.respond("200 Type set to: Binary.")
2714             self.current_type = 'i'
2715         else:
2716             self.respond('504 Unsupported type "%s".' %line)
2717
2718     def ftp_STRU(self, line):
2719         """Set file structure (obsolete)."""
2720         # obsolete (backward compatibility with older ftp clients)
2721         if line in ('f','F'):
2722             self.respond('200 File transfer structure set to: F.')
2723         else:
2724             self.respond('504 Unimplemented STRU type.')
2725
2726     def ftp_MODE(self, line):
2727         """Set data transfer mode (obsolete)"""
2728         # obsolete (backward compatibility with older ftp clients)
2729         if line in ('s', 'S'):
2730             self.respond('200 Transfer mode set to: S')
2731         else:
2732             self.respond('504 Unimplemented MODE type.')
2733
2734     def ftp_STAT(self, line):
2735         """Return statistics about current ftp session. If an argument
2736         is provided return directory listing over command channel.
2737
2738         Implementation note:
2739
2740         RFC-959 do not explicitly mention globbing; this means that FTP
2741         servers are not required to support globbing in order to be
2742         compliant.  However, many FTP servers do support globbing as a
2743         measure of convenience for FTP clients and users.
2744
2745         In order to search for and match the given globbing expression,
2746         the code has to search (possibly) many directories, examine
2747         each contained filename, and build a list of matching files in
2748         memory.  Since this operation can be quite intensive, both CPU-
2749         and memory-wise, we limit the search to only one directory
2750         non-recursively, as LIST does.
2751         """
2752         # return STATus information about ftpd
2753         if not line:
2754             s = []
2755             s.append('Connected to: %s:%s' %self.socket.getsockname()[:2])
2756             if self.authenticated:
2757                 s.append('Logged in as: %s' %self.username)
2758             else:
2759                 if not self.username:
2760                     s.append("Waiting for username.")
2761                 else:
2762                     s.append("Waiting for password.")
2763             if self.current_type == 'a':
2764                 type = 'ASCII'
2765             else:
2766                 type = 'Binary'
2767             s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
2768             if self.data_server:
2769                 s.append('Passive data channel waiting for connection.')
2770             elif self.data_channel:
2771                 bytes_sent = self.data_channel.tot_bytes_sent
2772                 bytes_recv = self.data_channel.tot_bytes_received
2773                 s.append('Data connection open:')
2774                 s.append('Total bytes sent: %s' %bytes_sent)
2775                 s.append('Total bytes received: %s' %bytes_recv)
2776             else:
2777                 s.append('Data connection closed.')
2778
2779             self.push('211-FTP server status:\r\n')
2780             self.push(''.join([' %s\r\n' %item for item in s]))
2781             self.respond('211 End of status.')
2782         # return directory LISTing over the command channel
2783         else:
2784             datacr = None
2785             try:
2786                 datacr = self.fs.get_cr(line)
2787                 iterator = self.run_as_current_user(self.fs.get_stat_dir, line, datacr)
2788             except OSError, err:
2789                 self.respond('550 %s.' %_strerror(err))
2790             else:
2791                 self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
2792                 self.push_with_producer(BufferedIteratorProducer(iterator))
2793                 self.respond('213 End of status.')
2794             self.fs.close_cr(datacr)
2795
2796     def ftp_FEAT(self, line):
2797         """List all new features supported as defined in RFC-2398."""
2798         features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
2799         s = ''
2800         for fact in self.available_facts:
2801             if fact in self.current_facts:
2802                 s += fact + '*;'
2803             else:
2804                 s += fact + ';'
2805         features.append('MLST ' + s)
2806         features.sort()
2807         self.push("211-Features supported:\r\n")
2808         self.push("".join([" %s\r\n" %x for x in features]))
2809         self.respond('211 End FEAT.')
2810
2811     def ftp_OPTS(self, line):
2812         """Specify options for FTP commands as specified in RFC-2389."""
2813         try:
2814             assert (not line.count(' ') > 1), 'Invalid number of arguments'
2815             if ' ' in line:
2816                 cmd, arg = line.split(' ')
2817                 assert (';' in arg), 'Invalid argument'
2818             else:
2819                 cmd, arg = line, ''
2820             # actually the only command able to accept options is MLST
2821             assert (cmd.upper() == 'MLST'), 'Unsupported command "%s"' %cmd
2822         except AssertionError, err:
2823             self.respond('501 %s.' %err)
2824         else:
2825             facts = [x.lower() for x in arg.split(';')]
2826             self.current_facts = [x for x in facts if x in self.available_facts]
2827             f = ''.join([x + ';' for x in self.current_facts])
2828             self.respond('200 MLST OPTS ' + f)
2829
2830     def ftp_NOOP(self, line):
2831         """Do nothing."""
2832         self.respond("200 I successfully done nothin'.")
2833
2834     def ftp_SYST(self, line):
2835         """Return system type (always returns UNIX type: L8)."""
2836         # This command is used to find out the type of operating system
2837         # at the server.  The reply shall have as its first word one of
2838         # the system names listed in RFC-943.
2839         # Since that we always return a "/bin/ls -lA"-like output on
2840         # LIST we  prefer to respond as if we would on Unix in any case.
2841         self.respond("215 UNIX Type: L8")
2842
2843     def ftp_ALLO(self, line):
2844         """Allocate bytes for storage (obsolete)."""
2845         # obsolete (always respond with 202)
2846         self.respond("202 No storage allocation necessary.")
2847
2848     def ftp_HELP(self, line):
2849         """Return help text to the client."""
2850         if line:
2851             if line.upper() in proto_cmds:
2852                 self.respond("214 %s" %proto_cmds[line.upper()])
2853             else:
2854                 self.respond("501 Unrecognized command.")
2855         else:
2856             # provide a compact list of recognized commands
2857             def formatted_help():
2858                 cmds = []
2859                 keys = proto_cmds.keys()
2860                 keys.sort()
2861                 while keys:
2862                     elems = tuple((keys[0:8]))
2863                     cmds.append(' %-6s' * len(elems) %elems + '\r\n')
2864                     del keys[0:8]
2865                 return ''.join(cmds)
2866
2867             self.push("214-The following commands are recognized:\r\n")
2868             self.push(formatted_help())
2869             self.respond("214 Help command successful.")
2870
2871
2872         # --- support for deprecated cmds
2873
2874     # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD
2875     # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD.
2876     # Such commands are obsoleted but some ftp clients (e.g. Windows
2877     # ftp.exe) still use them.
2878
2879     def ftp_XCUP(self, line):
2880         """Change to the parent directory. Synonym for CDUP. Deprecated."""
2881         self.ftp_CDUP(line)
2882
2883     def ftp_XCWD(self, line):
2884         """Change the current working directory. Synonym for CWD. Deprecated."""
2885         self.ftp_CWD(line)
2886
2887     def ftp_XMKD(self, line):
2888         """Create the specified directory. Synonym for MKD. Deprecated."""
2889         self.ftp_MKD(line)
2890
2891     def ftp_XPWD(self, line):
2892         """Return the current working directory. Synonym for PWD. Deprecated."""
2893         self.ftp_PWD(line)
2894
2895     def ftp_XRMD(self, line):
2896         """Remove the specified directory. Synonym for RMD. Deprecated."""
2897         self.ftp_RMD(line)
2898
2899
2900 class FTPServer(asyncore.dispatcher):
2901     """This class is an asyncore.disptacher subclass.  It creates a FTP
2902     socket listening on <address>, dispatching the requests to a <handler>
2903     (typically FTPHandler class).
2904
2905     Depending on the type of address specified IPv4 or IPv6 connections
2906     (or both, depending from the underlying system) will be accepted.
2907
2908     All relevant session information is stored in class attributes
2909     described below.
2910     Overriding them is strongly recommended to avoid running out of
2911     file descriptors (DoS)!
2912
2913      - (int) max_cons:
2914         number of maximum simultaneous connections accepted (defaults
2915         to 0 == unlimited).
2916
2917      - (int) max_cons_per_ip:
2918         number of maximum connections accepted for the same IP address
2919         (defaults to 0 == unlimited).
2920     """
2921
2922     max_cons = 0
2923     max_cons_per_ip = 0
2924
2925     def __init__(self, address, handler):
2926         """Initiate the FTP server opening listening on address.
2927
2928          - (tuple) address: the host:port pair on which the command
2929            channel will listen.
2930
2931          - (classobj) handler: the handler class to use.
2932         """
2933         asyncore.dispatcher.__init__(self)
2934         self.handler = handler
2935         self.ip_map = []
2936         host, port = address
2937
2938         # AF_INET or AF_INET6 socket
2939         # Get the correct address family for our host (allows IPv6 addresses)
2940         try:
2941             info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
2942                                       socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
2943         except socket.gaierror:
2944             # Probably a DNS issue. Assume IPv4.
2945             self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
2946             self.set_reuse_addr()
2947             self.bind((host, port))
2948         else:
2949             for res in info:
2950                 af, socktype, proto, canonname, sa = res
2951                 try:
2952                     self.create_socket(af, socktype)
2953                     self.set_reuse_addr()
2954                     self.bind(sa)
2955                 except socket.error, msg:
2956                     if self.socket:
2957                         self.socket.close()
2958                     self.socket = None
2959                     continue
2960                 break
2961             if not self.socket:
2962                 raise socket.error, msg
2963         self.listen(5)
2964
2965     def set_reuse_addr(self):
2966         # Overridden for convenience. Avoid to reuse address on Windows.
2967         if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'):
2968             return
2969         asyncore.dispatcher.set_reuse_addr(self)
2970
2971     def serve_forever(self, **kwargs):
2972         """A wrap around asyncore.loop(); starts the asyncore polling
2973         loop.
2974
2975         The keyword arguments in kwargs are the same expected by
2976         asyncore.loop() function: timeout, use_poll, map and count.
2977         """
2978         if not 'count' in kwargs:
2979             log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
2980
2981         # backward compatibility for python < 2.4
2982         if not hasattr(self, '_map'):
2983             if not 'map' in kwargs:
2984                 map = asyncore.socket_map
2985             else:
2986                 map = kwargs['map']
2987             self._map = self.handler._map = map
2988
2989         try:
2990             # FIX #16, #26
2991             # use_poll specifies whether to use select module's poll()
2992             # with asyncore or whether to use asyncore's own poll()
2993             # method Python versions < 2.4 need use_poll set to False
2994             # This breaks on OS X systems if use_poll is set to True.
2995             # All systems seem to work fine with it set to False
2996             # (tested on Linux, Windows, and OS X platforms)
2997             if kwargs:
2998                 asyncore.loop(**kwargs)
2999             else:
3000                 asyncore.loop(timeout=1.0, use_poll=False)
3001         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3002             log("Shutting down FTPd.")
3003             self.close_all()
3004
3005     def handle_accept(self):
3006         """Called when remote client initiates a connection."""
3007         sock_obj, addr = self.accept()
3008         log("[]%s:%s Connected." %addr[:2])
3009
3010         handler = self.handler(sock_obj, self)
3011         ip = addr[0]
3012         self.ip_map.append(ip)
3013
3014         # For performance and security reasons we should always set a
3015         # limit for the number of file descriptors that socket_map
3016         # should contain.  When we're running out of such limit we'll
3017         # use the last available channel for sending a 421 response
3018         # to the client before disconnecting it.
3019         if self.max_cons:
3020             if len(self._map) > self.max_cons:
3021                 handler.handle_max_cons()
3022                 return
3023
3024         # accept only a limited number of connections from the same
3025         # source address.
3026         if self.max_cons_per_ip:
3027             if self.ip_map.count(ip) > self.max_cons_per_ip:
3028                 handler.handle_max_cons_per_ip()
3029                 return
3030
3031         handler.handle()
3032
3033     def writable(self):
3034         return 0
3035
3036     def handle_error(self):
3037         """Called to handle any uncaught exceptions."""
3038         try:
3039             raise
3040         except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
3041             raise
3042         logerror(traceback.format_exc())
3043         self.close()
3044
3045     def close_all(self, map=None, ignore_all=False):
3046         """Stop serving; close all existent connections disconnecting
3047         clients.
3048
3049          - (dict) map:
3050             A dictionary whose items are the channels to close.
3051             If map is omitted, the default asyncore.socket_map is used.
3052
3053          - (bool) ignore_all:
3054             having it set to False results in raising exception in case
3055             of unexpected errors.
3056
3057         Implementation note:
3058
3059         Instead of using the current asyncore.close_all() function
3060         which only close sockets, we iterate over all existent channels
3061         calling close() method for each one of them, avoiding memory
3062         leaks.
3063
3064         This is how asyncore.close_all() function should work in
3065         Python 2.6.
3066         """
3067         if map is None:
3068             map = self._map
3069         for x in map.values():
3070             try:
3071                 x.close()
3072             except OSError, x:
3073                 if x[0] == errno.EBADF:
3074                     pass
3075                 elif not ignore_all:
3076                     raise
3077             except (asyncore.ExitNow, KeyboardInterrupt, SystemExit):
3078                 raise
3079             except:
3080                 if not ignore_all:
3081                     raise
3082         map.clear()
3083
3084
3085 def test():
3086     # cmd line usage (provide a read-only anonymous ftp server):
3087     # python -m pyftpdlib.FTPServer
3088     authorizer = DummyAuthorizer()
3089     authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
3090     FTPHandler.authorizer = authorizer
3091     address = ('', 8021)
3092     ftpd = FTPServer(address, FTPHandler)
3093     ftpd.serve_forever()
3094
3095 if __name__ == '__main__':
3096     test()
3097
3098 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: