Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Side by Side Diff: third_party/pyftpdlib/pyftpdlib/ftpserver.py

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

Powered by Google App Engine
This is Rietveld 408576698