~bzr-pqm/bzr/bzr.dev

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