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