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