[Zope3-checkins] CVS: Zope3/src/zope/server/ftp - __init__.py:1.2 commonftpactivitylogger.py:1.2 ftpserver.py:1.2 ftpserverchannel.py:1.2 ftpstatusmessages.py:1.2 osemulators.py:1.2 passiveacceptor.py:1.2 publisherfilesystemaccess.py:1.2 publisherftpserver.py:1.2 publisherftpserverchannel.py:1.2 recvchannel.py:1.2 testfilesystemaccess.py:1.2 xmitchannel.py:1.2

Jim Fulton jim@zope.com
Wed, 25 Dec 2002 09:15:54 -0500


Update of /cvs-repository/Zope3/src/zope/server/ftp
In directory cvs.zope.org:/tmp/cvs-serv20790/src/zope/server/ftp

Added Files:
	__init__.py commonftpactivitylogger.py ftpserver.py 
	ftpserverchannel.py ftpstatusmessages.py osemulators.py 
	passiveacceptor.py publisherfilesystemaccess.py 
	publisherftpserver.py publisherftpserverchannel.py 
	recvchannel.py testfilesystemaccess.py xmitchannel.py 
Log Message:
Grand renaming:

- Renamed most files (especially python modules) to lower case.

- Moved views and interfaces into separate hierarchies within each
  project, where each top-level directory under the zope package
  is a separate project.

- Moved everything to src from lib/python.

  lib/python will eventually go away. I need access to the cvs
  repository to make this happen, however.

There are probably some bits that are broken. All tests pass
and zope runs, but I haven't tried everything. There are a number
of cleanups I'll work on tomorrow.



=== Zope3/src/zope/server/ftp/__init__.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/__init__.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,2 @@
+#
+# This file is necessary to make this directory a package.


=== Zope3/src/zope/server/ftp/commonftpactivitylogger.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/commonftpactivitylogger.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,54 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+import time
+import sys
+
+from zope.server.logger.filelogger import FileLogger
+from zope.server.logger.resolvinglogger import ResolvingLogger
+from zope.server.logger.unresolvinglogger import UnresolvingLogger
+
+class CommonFTPActivityLogger:
+    """Outputs hits in common HTTP log format.
+    """
+
+    def __init__(self, logger_object=None, resolver=None):
+        if logger_object is None:
+            logger_object = FileLogger(sys.stdout)
+
+        if resolver is not None:
+            self.output = ResolvingLogger(resolver, logger_object)
+        else:
+            self.output = UnresolvingLogger(logger_object)
+
+
+    def log(self, task):
+        """
+        Receives a completed task and logs it in the
+        common log format.
+        """
+
+        now = time.localtime(time.time())
+
+        message = '%s [%s] "%s %s"' % (task.channel.username,
+                                       time.strftime('%Y/%m/%d %H:%M', now),
+                                       task.m_name[4:].upper(),
+                                       task.channel.cwd,
+                                       )
+
+        self.output.logRequest('127.0.0.1', message)


=== Zope3/src/zope/server/ftp/ftpserver.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/ftpserver.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,55 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+import asyncore
+from zope.server.ftp.ftpserverchannel import FTPServerChannel
+from zope.server.serverbase import ServerBase
+from zope.server.interfaces.vfs import IFilesystemAccess
+
+
+
+class FTPServer(ServerBase):
+    """Generic FTP Server"""
+
+    channel_class = FTPServerChannel
+    SERVER_IDENT = 'zope.server.ftp'
+
+
+    def __init__(self, ip, port, fs_access, *args, **kw):
+
+        assert IFilesystemAccess.isImplementedBy(fs_access)
+        self.fs_access = fs_access
+
+        super(FTPServer, self).__init__(ip, port, *args, **kw)
+
+
+if __name__ == '__main__':
+    from zope.server.taskthreads import ThreadedTaskDispatcher
+    from zope.server.vfs.osfilesystem import OSFileSystem
+    from zope.server.vfs.testfilesystemaccess import TestFilesystemAccess
+    td = ThreadedTaskDispatcher()
+    td.setThreadCount(4)
+    fs = OSFileSystem('/')
+    fs_access = TestFilesystemAccess(fs)
+    FTPServer('', 8021, fs_access, task_dispatcher=td)
+    try:
+        while 1:
+            asyncore.poll(5)
+            print 'active channels:', FTPServerChannel.active_channels
+    except KeyboardInterrupt:
+        print 'shutting down...'
+        td.shutdown()


=== Zope3/src/zope/server/ftp/ftpserverchannel.py 1.1 => 1.2 === (443/543 lines abridged)
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/ftpserverchannel.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,540 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+import posixpath
+import stat
+import socket
+import time
+
+from zope.server.linereceiver.lineserverchannel import LineServerChannel
+from zope.server.ftp.ftpstatusmessages import status_msgs
+from zope.server.ftp.osemulators import ls_longify
+
+from zope.server.interfaces.ftp import IFTPCommandHandler
+from zope.server.ftp.passiveacceptor import PassiveAcceptor
+from zope.server.ftp.recvchannel import RecvChannel
+from zope.server.ftp.xmitchannel import XmitChannel, ApplicationXmitStream
+from zope.server.vfs.usernamepassword import UsernamePassword
+from zope.exceptions import Unauthorized
+
+
+class FTPServerChannel(LineServerChannel):
+    """The FTP Server Channel represents a connection to a particular
+       client. We can therefore store information here."""
+
+    __implements__ = LineServerChannel.__implements__, IFTPCommandHandler
+
+
+    # List of commands that are always available
+    special_commands = ('cmd_quit', 'cmd_type', 'cmd_noop', 'cmd_user',
+                        'cmd_pass')
+
+    # These are the commands that are accessing the filesystem.

[-=- -=- -=- 443 lines omitted -=- -=- -=-]

+        if fs.isdir(path):
+            file_list = fs.listdir(path, long)
+        else:
+            file_list = [ (posixpath.split(path)[1], fs.stat(path)) ]
+        # Make a pretty unix-like FTP output
+        if long:
+            file_list = map(ls_longify, file_list)
+        return ''.join(map(lambda line: line + '\r\n', file_list))
+
+
+
+    def connectDataChannel(self, cdc):
+        pa = self.passive_acceptor
+        if pa:
+            # PASV mode.
+            if pa.ready:
+                # a connection has already been made.
+                conn, addr = pa.ready
+                cdc.set_socket (conn)
+                cdc.connected = 1
+                self.passive_acceptor.close()
+                self.passive_acceptor = None
+            # else we're still waiting for a connect to the PASV port.
+            # FTP Explorer is known to do this.
+        else:
+            # not in PASV mode.
+            ip, port = self.client_addr
+            cdc.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+            if self.bind_local_minus_one:
+                cdc.bind(('', self.server.port - 1))
+            try:
+                cdc.connect((ip, port))
+            except socket.error, err:
+                cdc.close('NO_DATA_CONN')
+
+
+    def notifyClientDCClosing(self, *reply_args):
+        if self.client_dc is not None:
+            self.client_dc = None
+            if reply_args:
+                self.reply(*reply_args)
+
+
+    def close(self):
+        LineServerChannel.close(self)
+        # Make sure the client DC gets closed too.
+        cdc = self.client_dc
+        if cdc is not None:
+            self.client_dc = None
+            cdc.close()


=== Zope3/src/zope/server/ftp/ftpstatusmessages.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/ftpstatusmessages.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,71 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+
+status_msgs = {
+    'OPEN_DATA_CONN'   : '150 Opening %s mode data connection for file list',
+    'OPEN_CONN'        : '150 Opening %s connection for %s',
+    'SUCCESS_200'      : '200 %s command successful.',
+    'TYPE_SET_OK'      : '200 Type set to %s.',
+    'STRU_OK'          : '200 STRU F Ok.',
+    'MODE_OK'          : '200 MODE S Ok.',
+    'FILE_DATE'        : '213 %4d%02d%02d%02d%02d%02d',
+    'FILE_SIZE'        : '213 %d Bytes',
+    'HELP_START'       : '214-The following commands are recognized',
+    'HELP_END'         : '214 Help done.',
+    'SERVER_TYPE'      : '215 %s Type: %s',
+    'SERVER_READY'     : '220 %s FTP server (Zope Async/Thread V0.1) ready.',
+    'GOODBYE'          : '221 Goodbye.',
+    'SUCCESS_226'      : '226 %s command successful.',
+    'TRANS_SUCCESS'    : '226 Transfer successful.',
+    'PASV_MODE_MSG'    : '227 Entering Passive Mode (%s,%d,%d)',
+    'LOGIN_SUCCESS'    : '230 Login Successful.',
+    'SUCCESS_250'      : '250 %s command successful.',
+    'SUCCESS_257'      : '257 %s command successful.',
+    'ALREADY_CURRENT'  : '257 "%s" is the current directory.',
+    'PASS_REQUIRED'    : '331 Password required',
+    'RESTART_TRANSFER' : '350 Restarting at %d. Send STORE or '
+                         'RETRIEVE to initiate transfer.',
+    'READY_FOR_DEST'   : '350 File exists, ready for destination.',
+    'NO_DATA_CONN'     : "425 Can't build data connection",
+    'TRANSFER_ABORTED' : '426 Connection closed; transfer aborted.',
+    'CMD_UNKNOWN'      : "500 '%s': command not understood.",
+    'INTERNAL_ERROR'   : "500 Internal error: %s",
+    'ERR_ARGS'         : '500 Bad command arguments',
+    'MODE_UNKOWN'      : '502 Unimplemented MODE type',
+    'WRONG_BYTE_SIZE'  : '504 Byte size must be 8',
+    'STRU_UNKNOWN'     : '504 Unimplemented STRU type',
+    'NOT_AUTH'         : "530 You are not authorized to perform the "
+                         "'%s' command",
+    'LOGIN_REQUIRED'   : '530 Please log in with USER and PASS',
+    'LOGIN_MISMATCH'   : '530 The username and password do not match.',
+    'ERR_NO_LIST'      : '550 Could not list directory or file: %s',
+    'ERR_NO_DIR'       : '550 "%s": No such directory.',
+    'ERR_NO_FILE'      : '550 "%s": No such file.',
+    'ERR_NO_DIR_FILE'  : '550 "%s": No such file or directory.',
+    'ERR_IS_NOT_FILE'  : '550 "%s": Is not a file',
+    'ERR_CREATE_FILE'  : '550 Error creating file.',
+    'ERR_CREATE_DIR'   : '550 Error creating directory: %s',
+    'ERR_DELETE_FILE'  : '550 Error deleting file: %s',
+    'ERR_DELETE_DIR'   : '550 Error removing directory: %s',
+    'ERR_OPEN_READ'    : '553 Could not open file for reading: %s',
+    'ERR_OPEN_WRITE'   : '553 Could not open file for writing: %s',
+    'ERR_IO'           : '553 I/O Error: %s',
+    'ERR_RENAME'       : '560 Could not rename "%s" to "%s": %s',
+    'ERR_RNFR_SOURCE'  : '560 No source filename specify. Call RNFR first.',
+    }


=== Zope3/src/zope/server/ftp/osemulators.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/osemulators.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,96 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""OS-Emulator Package
+
+Simulates OS-level directory listing output for *nix and MS-DOS (including
+Windows NT).
+
+$Id$
+"""
+
+import stat
+import datetime
+
+mode_table = {
+        '0':'---',
+        '1':'--x',
+        '2':'-w-',
+        '3':'-wx',
+        '4':'r--',
+        '5':'r-x',
+        '6':'rw-',
+        '7':'rwx'
+        }
+
+
+def ls_longify((filename, stat_info)):
+    """Formats a directory entry similarly to the 'ls' command.
+    """
+
+    # Note that we expect a little deviance from the result of os.stat():
+    # we expect the ST_UID and ST_GID fields to contain user IDs.
+    username = str(stat_info[stat.ST_UID])[:8]
+    grpname = str(stat_info[stat.ST_GID])[:8]
+
+    mode_octal = ('%o' % stat_info[stat.ST_MODE])[-3:]
+    mode = ''.join(map(mode_table.get, mode_octal))
+    if stat.S_ISDIR (stat_info[stat.ST_MODE]):
+        dirchar = 'd'
+    else:
+        dirchar = '-'
+    date = ls_date(datetime.datetime.now(), stat_info[stat.ST_MTIME])
+
+    return '%s%s %3d %-8s %-8s %8d %s %s' % (
+            dirchar,
+            mode,
+            stat_info[stat.ST_NLINK],
+            username,
+            grpname,
+            stat_info[stat.ST_SIZE],
+            date,
+            filename
+            )
+
+
+def ls_date(now, t):
+    """Emulate the 'ls' command's date field.  It has two formats.
+       If the date is more than 180 days in the past or future, then
+       it's like this:
+         Oct 19  1995
+       otherwise, it looks like this:
+         Oct 19 17:33
+    """
+    if abs((now - t).days) > 180:
+        return t.strftime('%b %d, %Y')
+    else:
+        return t.strftime('%b %d %H:%M')
+
+
+def msdos_longify((file, stat_info)):
+    """This matches the output of NT's ftp server (when in MSDOS mode)
+       exactly.
+    """
+    if stat.S_ISDIR(stat_info[stat.ST_MODE]):
+        dir = '<DIR>'
+    else:
+        dir = '     '
+    date = msdos_date(stat_info[stat.ST_MTIME])
+    return '%s       %s %8d %s' % (date, dir, stat_info[stat.ST_SIZE], file)
+
+
+def msdos_date(t):
+    """Emulate MS-DOS 'dir' command. Example:
+         09-19-95 05:33PM
+    """
+    return t.strftime('%m-%d-%y %H:%M%p')


=== Zope3/src/zope/server/ftp/passiveacceptor.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/passiveacceptor.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,78 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+import asyncore
+import socket
+
+
+class PassiveAcceptor(asyncore.dispatcher):
+    """This socket accepts a data connection, used when the server has
+       been placed in passive mode.  Although the RFC implies that we
+       ought to be able to use the same acceptor over and over again,
+       this presents a problem: how do we shut it off, so that we are
+       accepting connections only when we expect them?  [we can't]
+
+       wuftpd, and probably all the other servers, solve this by
+       allowing only one connection to hit this acceptor.  They then
+       close it.  Any subsequent data-connection command will then try
+       for the default port on the client side [which is of course
+       never there].  So the 'always-send-PORT/PASV' behavior seems
+       required.
+
+       Another note: wuftpd will also be listening on the channel as
+       soon as the PASV command is sent.  It does not wait for a data
+       command first.
+
+       --- we need to queue up a particular behavior:
+       1) xmit : queue up producer[s]
+       2) recv : the file object
+
+       It would be nice if we could make both channels the same.
+       Hmmm.."""
+
+    __implements__ = asyncore.dispatcher.__implements__
+
+    ready = None
+
+    def __init__ (self, control_channel):
+        asyncore.dispatcher.__init__ (self)
+        self.control_channel = control_channel
+        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+        # bind to an address on the interface that the
+        # control connection is coming from.
+        self.bind ( (self.control_channel.getsockname()[0], 0) )
+        self.addr = self.getsockname()
+        self.listen(1)
+
+
+    def log (self, *ignore):
+        pass
+
+
+    def handle_accept (self):
+        conn, addr = self.accept()
+        conn.setblocking(0)
+        dc = self.control_channel.client_dc
+        if dc is not None:
+            dc.set_socket(conn)
+            dc.addr = addr
+            dc.connected = 1
+            self.control_channel.passive_acceptor = None
+        else:
+            self.ready = conn, addr
+        self.close()


=== Zope3/src/zope/server/ftp/publisherfilesystemaccess.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/publisherfilesystemaccess.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,46 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Implementation of IFilesystemAccess intended only for testing.
+
+$Id$
+"""
+
+from cStringIO import StringIO
+from zope.exceptions import Unauthorized
+from zope.app.security.registries.principalregistry import principalRegistry
+
+from zope.server.vfs.publisherfilesystem import PublisherFileSystem
+from zope.server.interfaces.vfs import IFilesystemAccess
+from zope.server.interfaces.vfs import IUsernamePassword
+
+
+class PublisherFilesystemAccess:
+
+    __implements__ = IFilesystemAccess
+
+    def __init__(self, request_factory):
+        self.request_factory = request_factory
+
+
+    def authenticate(self, credentials):
+        assert IUsernamePassword.isImplementedBy(credentials)
+        env = {'credentials' : credentials}
+        request = self.request_factory(StringIO(''), StringIO(), env)
+        id = principalRegistry.authenticate(request)
+        if id is None:
+            raise Unauthorized
+
+
+    def open(self, credentials):
+        return PublisherFileSystem(credentials, self.request_factory)


=== Zope3/src/zope/server/ftp/publisherftpserver.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/publisherftpserver.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,30 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+from zope.server.ftp.ftpserver import FTPServer
+
+from zope.server.ftp.publisherfilesystemaccess import PublisherFilesystemAccess
+
+class PublisherFTPServer(FTPServer):
+    """Generic FTP Server"""
+
+
+    def __init__(self, request_factory, name, ip, port, *args, **kw):
+        self.request_factory = request_factory
+        fs_access = PublisherFilesystemAccess(request_factory)
+        super(PublisherFTPServer, self).__init__(ip, port, fs_access,
+                                                 *args, **kw)


=== Zope3/src/zope/server/ftp/publisherftpserverchannel.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/publisherftpserverchannel.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+from zope.server.ftp.ftpserverchannel import FTPServerChannel
+
+class PublisherFTPServerChannel(FTPServerChannel):
+    """The FTP Server Channel represents a connection to a particular
+       client. We can therefore store information here."""
+
+    __implements__ = FTPServerChannel.__implements__
+
+
+    def authenticate(self):
+        if self._getFilesystem()._authenticate():
+            return 1, 'User successfully authenticated.'
+        else:
+            return 0, 'User could not be authenticated.'


=== Zope3/src/zope/server/ftp/recvchannel.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/recvchannel.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,102 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+from zope.server.serverchannelbase import ChannelBaseClass
+from zope.server.buffers import OverflowableBuffer
+from zope.server.interfaces import ITask
+
+
+class RecvChannel(ChannelBaseClass):
+    """ """
+
+    complete_transfer = 0
+    _fileno = None  # provide a default for asyncore.dispatcher._fileno
+
+    def __init__ (self, control_channel, finish_args):
+        self.control_channel = control_channel
+        self.finish_args = finish_args
+        self.inbuf = OverflowableBuffer(control_channel.adj.inbuf_overflow)
+        ChannelBaseClass.__init__(self, None, None, control_channel.adj)
+        # Note that this channel starts out in async mode.
+
+    def writable (self):
+        return 0
+
+    def handle_connect (self):
+        pass
+
+    def received (self, data):
+        if data:
+            self.inbuf.append(data)
+
+    def handle_close (self):
+        """Client closed, indicating EOF."""
+        c = self.control_channel
+        task = FinishedRecvTask(c, self.inbuf, self.finish_args)
+        self.complete_transfer = 1
+        self.close()
+        c.start_task(task)
+
+    def close(self, *reply_args):
+        try:
+            c = self.control_channel
+            if c is not None:
+                self.control_channel = None
+                if not self.complete_transfer and not reply_args:
+                    # Not all data transferred
+                    reply_args = ('TRANSFER_ABORTED',)
+                c.notifyClientDCClosing(*reply_args)
+        finally:
+            if self.socket is not None:
+                # XXX asyncore.dispatcher.close() doesn't like socket == None
+                ChannelBaseClass.close(self)
+
+
+
+class FinishedRecvTask:
+
+    __implements__ = ITask
+
+    def __init__(self, control_channel, inbuf, finish_args):
+        self.control_channel = control_channel
+        self.inbuf = inbuf
+        self.finish_args = finish_args
+
+    def service(self):
+        """Called to execute the task.
+        """
+        close_on_finish = 0
+        c = self.control_channel
+        try:
+            try:
+                c.finishedRecv(self.inbuf, self.finish_args)
+            except socket.error:
+                close_on_finish = 1
+                if c.adj.log_socket_errors:
+                    raise
+        finally:
+            c.end_task(close_on_finish)
+
+
+    def cancel(self):
+        'See ITask'
+        self.control_channel.close_when_done()
+
+
+    def defer(self):
+        'See ITask'
+        pass


=== Zope3/src/zope/server/ftp/testfilesystemaccess.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/testfilesystemaccess.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,44 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Implementation of IFilesystemAccess intended only for testing.
+
+$Id$
+"""
+
+from zope.server.interfaces.vfs import IFilesystemAccess
+from zope.server.interfaces.vfs import IUsernamePassword
+from zope.exceptions import Unauthorized
+
+
+class TestFilesystemAccess:
+
+    __implements__ = IFilesystemAccess
+
+    passwords = {'foo': 'bar'}
+
+    def __init__(self, fs):
+        self.fs = fs
+
+    def authenticate(self, credentials):
+        if not IUsernamePassword.isImplementedBy(credentials):
+            raise Unauthorized
+        name = credentials.getUserName()
+        if not (name in self.passwords):
+            raise Unauthorized
+        if credentials.getPassword() != self.passwords[name]:
+            raise Unauthorized
+
+    def open(self, credentials):
+        self.authenticate(credentials)
+        return self.fs


=== Zope3/src/zope/server/ftp/xmitchannel.py 1.1 => 1.2 ===
--- /dev/null	Wed Dec 25 09:15:54 2002
+++ Zope3/src/zope/server/ftp/xmitchannel.py	Wed Dec 25 09:15:23 2002
@@ -0,0 +1,92 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+
+$Id$
+"""
+
+from zope.server.serverchannelbase import ChannelBaseClass
+
+
+class XmitChannel(ChannelBaseClass):
+
+    opened = 0
+    _fileno = None  # provide a default for asyncore.dispatcher._fileno
+
+    def __init__ (self, control_channel, ok_reply_args):
+        self.control_channel = control_channel
+        self.ok_reply_args = ok_reply_args
+        self.set_sync()
+        ChannelBaseClass.__init__(self, None, None, control_channel.adj)
+
+    def _open(self):
+        """Signal the client to open the connection."""
+        self.opened = 1
+        self.control_channel.reply(*self.ok_reply_args)
+        self.control_channel.connectDataChannel(self)
+
+    def write(self, data):
+        if self.control_channel is None:
+            raise IOError, 'Client FTP connection closed'
+        if not self.opened:
+            self._open()
+        ChannelBaseClass.write(self, data)
+
+    def readable(self):
+        return not self.connected
+
+    def handle_read(self):
+        # This is only called when making the connection.
+        try:
+            self.recv(1)
+        except:
+            # The connection failed.
+            self.close('NO_DATA_CONN')
+
+    def handle_connect(self):
+        pass
+
+    def handle_comm_error(self):
+        self.close('TRANSFER_ABORTED')
+
+    def close(self, *reply_args):
+        try:
+            c = self.control_channel
+            if c is not None:
+                self.control_channel = None
+                if not reply_args:
+                    if not len(self.outbuf):
+                        # All data transferred
+                        if not self.opened:
+                            # Zero-length file
+                            self._open()
+                        reply_args = ('TRANS_SUCCESS',)
+                    else:
+                        # Not all data transferred
+                        reply_args = ('TRANSFER_ABORTED',)
+                c.notifyClientDCClosing(*reply_args)
+        finally:
+            if self.socket is not None:
+                # XXX asyncore.dispatcher.close() doesn't like socket == None
+                ChannelBaseClass.close(self)
+
+
+class ApplicationXmitStream:
+    """Provide stream output, remapping close() to close_when_done().
+    """
+
+    def __init__(self, xmit_channel):
+        self.write = xmit_channel.write
+        self.flush = xmit_channel.flush
+        self.close = xmit_channel.close_when_done