~bzr-pqm/bzr/bzr.dev

4763.2.4 by John Arbash Meinel
merge bzr.2.1 in preparation for NEWS entry.
1
# Copyright (C) 2009, 2010 Canonical Ltd
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
2
#
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
16
"""
17
FTP test server.
18
19
Based on pyftpdlib: http://code.google.com/p/pyftpdlib/
20
"""
21
22
import errno
23
import os
24
from pyftpdlib import ftpserver
25
import select
26
import threading
27
28
29
from bzrlib import (
3508.1.18 by Vincent Ladeuil
Final tweaks.
30
    osutils,
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
31
    tests,
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
32
    trace,
33
    )
5017.3.16 by Vincent Ladeuil
Fix imports for ftp_server/pyftpdlib_based.py
34
from bzrlib.tests import test_server
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
35
36
5947.1.1 by Vincent Ladeuil
Support pyftplib-0.6.0 as an ftp test server
37
# Convert the pyftplib string version into a tuple to avoid traps in string
38
# comparison.
39
pyftplib_version = tuple(map(int, ftpserver.__ver__.split('.')))
40
41
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
42
class AnonymousWithWriteAccessAuthorizer(ftpserver.DummyAuthorizer):
43
44
    def _check_permissions(self, username, perm):
45
        # Like base implementation but don't warn about write permissions
3508.1.18 by Vincent Ladeuil
Final tweaks.
46
        # assigned to anonymous, since that's exactly our purpose.
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
47
        for p in perm:
48
            if p not in self.read_perms + self.write_perms:
49
                raise ftpserver.AuthorizerError('No such permission "%s"' %p)
50
51
52
class BzrConformingFS(ftpserver.AbstractedFS):
53
3508.1.20 by Vincent Ladeuil
Test passing for python2.5 and 2.6.
54
    def chmod(self, path, mode):
55
        return os.chmod(path, mode)
56
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
57
    def listdir(self, path):
58
        """List the content of a directory."""
3508.1.18 by Vincent Ladeuil
Final tweaks.
59
        return [osutils.safe_utf8(s) for s in os.listdir(path)]
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
60
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
61
    def fs2ftp(self, fspath):
4935.1.3 by Vincent Ladeuil
Better fix for fancy_rename respecting callers file encoding.
62
        p = ftpserver.AbstractedFS.fs2ftp(self, osutils.safe_unicode(fspath))
3508.1.18 by Vincent Ladeuil
Final tweaks.
63
        return osutils.safe_utf8(p)
64
4935.1.1 by Vincent Ladeuil
Support Unicode paths for ftp transport (encoded as utf8).
65
    def ftp2fs(self, ftppath):
66
        p = osutils.safe_unicode(ftppath)
67
        return ftpserver.AbstractedFS.ftp2fs(self, p)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
68
3508.1.23 by Vincent Ladeuil
Fix as per Martin's review.
69
class BzrConformingFTPHandler(ftpserver.FTPHandler):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
70
71
    abstracted_fs = BzrConformingFS
72
73
    def __init__(self, conn, server):
74
        ftpserver.FTPHandler.__init__(self, conn, server)
75
        self.authorizer = server.authorizer
76
77
    def ftp_SIZE(self, path):
78
        # bzr is overly picky here, but we want to make the test suite pass
79
        # first. This may need to be revisited -- vila 20090226
80
        line = self.fs.fs2ftp(path)
81
        if self.fs.isdir(self.fs.realpath(path)):
82
            why = "%s is a directory" % line
83
            self.log('FAIL SIZE "%s". %s.' % (line, why))
84
            self.respond("550 %s."  %why)
85
        else:
86
            ftpserver.FTPHandler.ftp_SIZE(self, path)
87
88
    def ftp_NLST(self, path):
89
        # bzr is overly picky here, but we want to make the test suite pass
90
        # first. This may need to be revisited -- vila 20090226
91
        line = self.fs.fs2ftp(path)
92
        if self.fs.isfile(self.fs.realpath(path)):
93
            why = "Not a directory: %s" % line
4725.3.3 by Vincent Ladeuil
Fix test failure at the root without cleaning up ftp APPE.
94
            self.log('FAIL NLST "%s". %s.' % (line, why))
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
95
            self.respond("550 %s."  %why)
96
        else:
97
            ftpserver.FTPHandler.ftp_NLST(self, path)
98
99
    def ftp_SITE_CHMOD(self, line):
100
        try:
3508.1.14 by Vincent Ladeuil
Tweak SITE_CHMOD server command.
101
            mode, path = line.split(None, 1)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
102
            mode = int(mode, 8)
103
        except ValueError:
104
            # We catch both malformed line and malformed mode with the same
105
            # ValueError.
106
            self.respond("500 'SITE CHMOD %s': command not understood."
107
                         % line)
3508.1.18 by Vincent Ladeuil
Final tweaks.
108
            self.log('FAIL SITE CHMOD ' % line)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
109
            return
110
        ftp_path = self.fs.fs2ftp(path)
111
        try:
112
            self.run_as_current_user(self.fs.chmod, self.fs.ftp2fs(path), mode)
113
        except OSError, err:
114
            why = ftpserver._strerror(err)
115
            self.log('FAIL SITE CHMOD 0%03o "%s". %s.' % (mode, ftp_path, why))
116
            self.respond('550 %s.' % why)
117
        else:
118
            self.log('OK SITE CHMOD 0%03o "%s".' % (mode, ftp_path))
119
            self.respond('200 SITE CHMOD succesful.')
120
5947.1.1 by Vincent Ladeuil
Support pyftplib-0.6.0 as an ftp test server
121
    if pyftplib_version >= (0, 6, 0):
122
        def log_cmd(self, cmd, arg, respcode, respstr):
123
            # base class version choke on unicode, the alternative is to just
124
            # provide an empty implementation and relies on the client to do
125
            # the logging for debugging purposes. Not worth the trouble so far
126
            # -- vila 20110607
127
            if cmd in ("DELE", "RMD", "RNFR", "RNTO", "MKD"):
128
                line = '"%s" %s' % (' '.join([cmd, unicode(arg)]).strip(),
129
                                    respcode)
130
                self.log(line)
131
132
133
if pyftplib_version < (0, 6,0):
134
    # pyftpdlib says to define SITE commands by declaring ftp_SITE_<CMD>
135
    # methods, but fails to recognize them.
136
    ftpserver.proto_cmds['SITE CHMOD'] = ftpserver._CommandProperty(
137
        # Best fit choice even if not exactly right (can be d, f or m too)
138
        perm='w',
139
        auth_needed=True, arg_needed=True, check_path=False,
140
        help='Syntax: SITE CHMOD <SP>  octal_mode_bits file-name (chmod file)',
141
        )
142
    # An empty password is valid, hence the arg is neither mandatory not
143
    # forbidden
144
    ftpserver.proto_cmds['PASS'].arg_needed = None
145
else:
146
    # Same rationale as above (the password should be optional), but the hole
147
    # in pyftplib-0.6.0 is narrower
148
    ftpserver.proto_cmds['PASS']['arg'] = None
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
149
150
151
class ftp_server(ftpserver.FTPServer):
152
153
    def __init__(self, address, handler, authorizer):
154
        ftpserver.FTPServer.__init__(self, address, handler)
155
        self.authorizer = authorizer
3508.1.17 by Vincent Ladeuil
Allows empty passwords with pyftpdlib ftp test server.
156
        # Worth backporting upstream ?
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
157
        self.addr = self.socket.getsockname()
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
158
159
5017.3.16 by Vincent Ladeuil
Fix imports for ftp_server/pyftpdlib_based.py
160
class FTPTestServer(test_server.TestServer):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
161
    """Common code for FTP server facilities."""
162
163
    def __init__(self):
164
        self._root = None
165
        self._ftp_server = None
166
        self._port = None
167
        self._async_thread = None
168
        # ftp server logs
169
        self.logs = []
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
170
        self._ftpd_running = False
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
171
172
    def get_url(self):
173
        """Calculate an ftp url to this server."""
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
174
        return 'ftp://anonymous@localhost:%d/' % (self._port)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
175
176
    def get_bogus_url(self):
177
        """Return a URL which cannot be connected to."""
178
        return 'ftp://127.0.0.1:1/'
179
180
    def log(self, message):
3508.1.18 by Vincent Ladeuil
Final tweaks.
181
        """This is used by ftp_server to log connections, etc."""
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
182
        self.logs.append(message)
183
4934.3.3 by Martin Pool
Rename Server.setUp to Server.start_server
184
    def start_server(self, vfs_server=None):
5017.3.16 by Vincent Ladeuil
Fix imports for ftp_server/pyftpdlib_based.py
185
        if not (vfs_server is None or isinstance(vfs_server,
186
                                                 test_server.LocalURLServer)):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
187
            raise AssertionError(
188
                "FTPServer currently assumes local transport, got %s"
189
                % vfs_server)
190
        self._root = os.getcwdu()
191
192
        address = ('localhost', 0) # bind to a random port
193
        authorizer = AnonymousWithWriteAccessAuthorizer()
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
194
        authorizer.add_anonymous(self._root, perm='elradfmw')
3508.1.23 by Vincent Ladeuil
Fix as per Martin's review.
195
        self._ftp_server = ftp_server(address, BzrConformingFTPHandler,
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
196
                                      authorizer)
197
        # This is hacky as hell, will not work if we need two servers working
198
        # at the same time, but that's the best we can do so far...
199
        # FIXME: At least log and logline could be overriden in the handler ?
200
        # -- vila 20090227
201
        ftpserver.log = self.log
202
        ftpserver.logline = self.log
203
        ftpserver.logerror = self.log
204
205
        self._port = self._ftp_server.socket.getsockname()[1]
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
206
        self._ftpd_starting = threading.Lock()
207
        self._ftpd_starting.acquire() # So it can be released by the server
4725.3.2 by Vincent Ladeuil
Cosmetic change.
208
        self._ftpd_thread = threading.Thread(target=self._run_server,)
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
209
        self._ftpd_thread.start()
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
210
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
211
            sys.stderr.write('Thread started: %s\n'
212
                             % (self._ftpd_thread.ident,))
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
213
        # Wait for the server thread to start (i.e release the lock)
214
        self._ftpd_starting.acquire()
215
        self._ftpd_starting.release()
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
216
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
217
    def stop_server(self):
218
        """See bzrlib.transport.Server.stop_server."""
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
219
        # Tell the server to stop, but also close the server socket for tests
220
        # that start the server but never initiate a connection. Closing the
221
        # socket should be done first though, to avoid further connections.
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
222
        self._ftp_server.close()
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
223
        self._ftpd_running = False
224
        self._ftpd_thread.join()
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
225
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
226
            sys.stderr.write('Thread  joined: %s\n'
227
                             % (self._ftpd_thread.ident,))
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
228
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
229
    def _run_server(self):
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
230
        """Run the server until stop_server is called, shut it down properly then.
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
231
        """
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
232
        self._ftpd_running = True
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
233
        self._ftpd_starting.release()
234
        while self._ftpd_running:
4725.3.1 by Vincent Ladeuil
Fix test failure and clean up ftp APPE.
235
            try:
236
                self._ftp_server.serve_forever(timeout=0.1, count=1)
237
            except select.error, e:
238
                if e.args[0] != errno.EBADF:
239
                    raise
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
240
        self._ftp_server.close_all(ignore_all=True)
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
241
242
    def add_user(self, user, password):
243
        """Add a user with write access."""
244
        self._ftp_server.authorizer.add_user(user, password, self._root,
245
                                             perm='elradfmw')