~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
37
class AnonymousWithWriteAccessAuthorizer(ftpserver.DummyAuthorizer):
38
39
    def _check_permissions(self, username, perm):
40
        # Like base implementation but don't warn about write permissions
3508.1.18 by Vincent Ladeuil
Final tweaks.
41
        # assigned to anonymous, since that's exactly our purpose.
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
42
        for p in perm:
43
            if p not in self.read_perms + self.write_perms:
44
                raise ftpserver.AuthorizerError('No such permission "%s"' %p)
45
46
47
class BzrConformingFS(ftpserver.AbstractedFS):
48
3508.1.20 by Vincent Ladeuil
Test passing for python2.5 and 2.6.
49
    def chmod(self, path, mode):
50
        return os.chmod(path, mode)
51
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
52
    def listdir(self, path):
53
        """List the content of a directory."""
3508.1.18 by Vincent Ladeuil
Final tweaks.
54
        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.
55
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
56
    def fs2ftp(self, fspath):
4935.1.3 by Vincent Ladeuil
Better fix for fancy_rename respecting callers file encoding.
57
        p = ftpserver.AbstractedFS.fs2ftp(self, osutils.safe_unicode(fspath))
3508.1.18 by Vincent Ladeuil
Final tweaks.
58
        return osutils.safe_utf8(p)
59
4935.1.1 by Vincent Ladeuil
Support Unicode paths for ftp transport (encoded as utf8).
60
    def ftp2fs(self, ftppath):
61
        p = osutils.safe_unicode(ftppath)
62
        return ftpserver.AbstractedFS.ftp2fs(self, p)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
63
3508.1.23 by Vincent Ladeuil
Fix as per Martin's review.
64
class BzrConformingFTPHandler(ftpserver.FTPHandler):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
65
66
    abstracted_fs = BzrConformingFS
67
68
    def __init__(self, conn, server):
69
        ftpserver.FTPHandler.__init__(self, conn, server)
70
        self.authorizer = server.authorizer
71
72
    def ftp_SIZE(self, path):
73
        # bzr is overly picky here, but we want to make the test suite pass
74
        # first. This may need to be revisited -- vila 20090226
75
        line = self.fs.fs2ftp(path)
76
        if self.fs.isdir(self.fs.realpath(path)):
77
            why = "%s is a directory" % line
78
            self.log('FAIL SIZE "%s". %s.' % (line, why))
79
            self.respond("550 %s."  %why)
80
        else:
81
            ftpserver.FTPHandler.ftp_SIZE(self, path)
82
83
    def ftp_NLST(self, path):
84
        # bzr is overly picky here, but we want to make the test suite pass
85
        # first. This may need to be revisited -- vila 20090226
86
        line = self.fs.fs2ftp(path)
87
        if self.fs.isfile(self.fs.realpath(path)):
88
            why = "Not a directory: %s" % line
4725.3.3 by Vincent Ladeuil
Fix test failure at the root without cleaning up ftp APPE.
89
            self.log('FAIL NLST "%s". %s.' % (line, why))
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
90
            self.respond("550 %s."  %why)
91
        else:
92
            ftpserver.FTPHandler.ftp_NLST(self, path)
93
94
    def ftp_SITE_CHMOD(self, line):
95
        try:
3508.1.14 by Vincent Ladeuil
Tweak SITE_CHMOD server command.
96
            mode, path = line.split(None, 1)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
97
            mode = int(mode, 8)
98
        except ValueError:
99
            # We catch both malformed line and malformed mode with the same
100
            # ValueError.
101
            self.respond("500 'SITE CHMOD %s': command not understood."
102
                         % line)
3508.1.18 by Vincent Ladeuil
Final tweaks.
103
            self.log('FAIL SITE CHMOD ' % line)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
104
            return
105
        ftp_path = self.fs.fs2ftp(path)
106
        try:
107
            self.run_as_current_user(self.fs.chmod, self.fs.ftp2fs(path), mode)
108
        except OSError, err:
109
            why = ftpserver._strerror(err)
110
            self.log('FAIL SITE CHMOD 0%03o "%s". %s.' % (mode, ftp_path, why))
111
            self.respond('550 %s.' % why)
112
        else:
113
            self.log('OK SITE CHMOD 0%03o "%s".' % (mode, ftp_path))
114
            self.respond('200 SITE CHMOD succesful.')
115
116
117
# pyftpdlib says to define SITE commands by declaring ftp_SITE_<CMD> methods,
118
# but fails to recognize them.
119
ftpserver.proto_cmds['SITE CHMOD'] = ftpserver._CommandProperty(
120
    perm='w', # Best fit choice even if not exactly right (can be d, f or m too)
121
    auth_needed=True, arg_needed=True, check_path=False,
122
    help='Syntax: SITE CHMOD <SP>  octal_mode_bits file-name (chmod file)',
123
    )
3508.1.18 by Vincent Ladeuil
Final tweaks.
124
# An empty password is valid, hence the arg is neither mandatory not forbidden
125
ftpserver.proto_cmds['PASS'].arg_needed = None
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
126
127
128
class ftp_server(ftpserver.FTPServer):
129
130
    def __init__(self, address, handler, authorizer):
131
        ftpserver.FTPServer.__init__(self, address, handler)
132
        self.authorizer = authorizer
3508.1.17 by Vincent Ladeuil
Allows empty passwords with pyftpdlib ftp test server.
133
        # Worth backporting upstream ?
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
134
        self.addr = self.socket.getsockname()
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
135
136
5017.3.16 by Vincent Ladeuil
Fix imports for ftp_server/pyftpdlib_based.py
137
class FTPTestServer(test_server.TestServer):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
138
    """Common code for FTP server facilities."""
139
140
    def __init__(self):
141
        self._root = None
142
        self._ftp_server = None
143
        self._port = None
144
        self._async_thread = None
145
        # ftp server logs
146
        self.logs = []
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
147
        self._ftpd_running = False
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
148
149
    def get_url(self):
150
        """Calculate an ftp url to this server."""
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
151
        return 'ftp://anonymous@localhost:%d/' % (self._port)
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
152
153
    def get_bogus_url(self):
154
        """Return a URL which cannot be connected to."""
155
        return 'ftp://127.0.0.1:1/'
156
157
    def log(self, message):
3508.1.18 by Vincent Ladeuil
Final tweaks.
158
        """This is used by ftp_server to log connections, etc."""
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
159
        self.logs.append(message)
160
4934.3.3 by Martin Pool
Rename Server.setUp to Server.start_server
161
    def start_server(self, vfs_server=None):
5017.3.16 by Vincent Ladeuil
Fix imports for ftp_server/pyftpdlib_based.py
162
        if not (vfs_server is None or isinstance(vfs_server,
163
                                                 test_server.LocalURLServer)):
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
164
            raise AssertionError(
165
                "FTPServer currently assumes local transport, got %s"
166
                % vfs_server)
167
        self._root = os.getcwdu()
168
169
        address = ('localhost', 0) # bind to a random port
170
        authorizer = AnonymousWithWriteAccessAuthorizer()
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
171
        authorizer.add_anonymous(self._root, perm='elradfmw')
3508.1.23 by Vincent Ladeuil
Fix as per Martin's review.
172
        self._ftp_server = ftp_server(address, BzrConformingFTPHandler,
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
173
                                      authorizer)
174
        # This is hacky as hell, will not work if we need two servers working
175
        # at the same time, but that's the best we can do so far...
176
        # FIXME: At least log and logline could be overriden in the handler ?
177
        # -- vila 20090227
178
        ftpserver.log = self.log
179
        ftpserver.logline = self.log
180
        ftpserver.logerror = self.log
181
182
        self._port = self._ftp_server.socket.getsockname()[1]
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
183
        self._ftpd_starting = threading.Lock()
184
        self._ftpd_starting.acquire() # So it can be released by the server
4725.3.2 by Vincent Ladeuil
Cosmetic change.
185
        self._ftpd_thread = threading.Thread(target=self._run_server,)
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
186
        self._ftpd_thread.start()
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
187
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
188
            sys.stderr.write('Thread started: %s\n'
189
                             % (self._ftpd_thread.ident,))
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
190
        # Wait for the server thread to start (i.e release the lock)
191
        self._ftpd_starting.acquire()
192
        self._ftpd_starting.release()
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
193
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
194
    def stop_server(self):
195
        """See bzrlib.transport.Server.stop_server."""
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
196
        # Tell the server to stop, but also close the server socket for tests
197
        # that start the server but never initiate a connection. Closing the
198
        # 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 :).
199
        self._ftp_server.close()
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
200
        self._ftpd_running = False
201
        self._ftpd_thread.join()
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
202
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
203
            sys.stderr.write('Thread  joined: %s\n'
204
                             % (self._ftpd_thread.ident,))
3508.1.10 by Vincent Ladeuil
Start supporting pyftpdlib as an ftp test server.
205
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
206
    def _run_server(self):
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
207
        """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.
208
        """
3508.1.11 by Vincent Ladeuil
Tests passing for python2.5 and python2.6 (and python2.4 :).
209
        self._ftpd_running = True
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
210
        self._ftpd_starting.release()
211
        while self._ftpd_running:
4725.3.1 by Vincent Ladeuil
Fix test failure and clean up ftp APPE.
212
            try:
213
                self._ftp_server.serve_forever(timeout=0.1, count=1)
214
            except select.error, e:
215
                if e.args[0] != errno.EBADF:
216
                    raise
3508.1.12 by Vincent Ladeuil
Don't require patched version for pyftpdlib.
217
        self._ftp_server.close_all(ignore_all=True)
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
218
219
    def add_user(self, user, password):
220
        """Add a user with write access."""
221
        self._ftp_server.authorizer.add_user(user, password, self._root,
222
                                             perm='elradfmw')