~bzr-pqm/bzr/bzr.dev

4763.2.4 by John Arbash Meinel
merge bzr.2.1 in preparation for NEWS entry.
1
# Copyright (C) 2007-2010 Canonical Ltd
2917.3.1 by Vincent Ladeuil
Separate transport from 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
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
16
"""
17
FTP test server.
18
19
Based on medusa: http://www.amk.ca/python/code/medusa.html
20
"""
21
22
import asyncore
2975.2.1 by Robert Collins
* FTP server errors don't error in the error handling code.
23
import errno
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
24
import os
25
import select
26
import stat
27
import threading
28
29
import medusa
30
import medusa.filesys
31
import medusa.ftp_server
32
33
from bzrlib import (
34
    tests,
35
    trace,
36
    )
5017.3.17 by Vincent Ladeuil
Fix imports for ftp_server/medusa_based.py
37
from bzrlib.tests import test_server
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
38
39
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
40
class test_filesystem(medusa.filesys.os_filesystem):
41
    """A custom filesystem wrapper to add missing functionalities."""
42
43
    def chmod(self, path, mode):
3508.1.2 by Vincent Ladeuil
Fixed as per John's review.
44
        p = self.normalize(self.path_module.join (self.wd, path))
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
45
        return os.chmod(self.translate(p), mode)
46
47
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
48
class test_authorizer(object):
49
    """A custom Authorizer object for running the test suite.
50
51
    The reason we cannot use dummy_authorizer, is because it sets the
52
    channel to readonly, which we don't always want to do.
53
    """
54
55
    def __init__(self, root):
56
        self.root = root
57
        # If secured_user is set secured_password will be checked
58
        self.secured_user = None
59
        self.secured_password = None
60
61
    def authorize(self, channel, username, password):
62
        """Return (success, reply_string, filesystem)"""
63
        channel.persona = -1, -1
64
        if username == 'anonymous':
65
            channel.read_only = 1
66
        else:
67
            channel.read_only = 0
68
69
        # Check secured_user if set
70
        if (self.secured_user is not None
71
            and username == self.secured_user
72
            and password != self.secured_password):
73
            return 0, 'Password invalid.', None
74
        else:
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
75
            return 1, 'OK.', test_filesystem(self.root)
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
76
77
78
class ftp_channel(medusa.ftp_server.ftp_channel):
79
    """Customized ftp channel"""
80
81
    def log(self, message):
82
        """Redirect logging requests."""
83
        trace.mutter('ftp_channel: %s', message)
84
85
    def log_info(self, message, type='info'):
86
        """Redirect logging requests."""
87
        trace.mutter('ftp_channel %s: %s', type, message)
88
89
    def cmd_rnfr(self, line):
90
        """Prepare for renaming a file."""
91
        self._renaming = line[1]
92
        self.respond('350 Ready for RNTO')
93
        # TODO: jam 20060516 in testing, the ftp server seems to
94
        #       check that the file already exists, or it sends
95
        #       550 RNFR command failed
96
97
    def cmd_rnto(self, line):
98
        """Rename a file based on the target given.
99
100
        rnto must be called after calling rnfr.
101
        """
102
        if not self._renaming:
103
            self.respond('503 RNFR required first.')
104
        pfrom = self.filesystem.translate(self._renaming)
105
        self._renaming = None
106
        pto = self.filesystem.translate(line[1])
107
        if os.path.exists(pto):
108
            self.respond('550 RNTO failed: file exists')
109
            return
110
        try:
111
            os.rename(pfrom, pto)
112
        except (IOError, OSError), e:
113
            # TODO: jam 20060516 return custom responses based on
114
            #       why the command failed
115
            # (bialix 20070418) str(e) on Python 2.5 @ Windows
116
            # sometimes don't provide expected error message;
117
            # so we obtain such message via os.strerror()
118
            self.respond('550 RNTO failed: %s' % os.strerror(e.errno))
119
        except:
120
            self.respond('550 RNTO failed')
121
            # For a test server, we will go ahead and just die
122
            raise
123
        else:
124
            self.respond('250 Rename successful.')
125
126
    def cmd_size(self, line):
127
        """Return the size of a file
128
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
129
        This is overloaded to help the test suite determine if the
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
130
        target is a directory.
131
        """
132
        filename = line[1]
133
        if not self.filesystem.isfile(filename):
134
            if self.filesystem.isdir(filename):
135
                self.respond('550 "%s" is a directory' % (filename,))
136
            else:
137
                self.respond('550 "%s" is not a file' % (filename,))
138
        else:
3943.8.1 by Marius Kruger
remove all trailing whitespace from bzr source
139
            self.respond('213 %d'
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
140
                % (self.filesystem.stat(filename)[stat.ST_SIZE]),)
141
142
    def cmd_mkd(self, line):
143
        """Create a directory.
144
145
        Overloaded because default implementation does not distinguish
146
        *why* it cannot make a directory.
147
        """
148
        if len (line) != 2:
149
            self.command_not_understood(''.join(line))
150
        else:
151
            path = line[1]
152
            try:
153
                self.filesystem.mkdir (path)
154
                self.respond ('257 MKD command successful.')
155
            except (IOError, OSError), e:
156
                # (bialix 20070418) str(e) on Python 2.5 @ Windows
157
                # sometimes don't provide expected error message;
158
                # so we obtain such message via os.strerror()
159
                self.respond ('550 error creating directory: %s' %
160
                              os.strerror(e.errno))
161
            except:
162
                self.respond ('550 error creating directory.')
163
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
164
    def cmd_site(self, line):
3508.1.2 by Vincent Ladeuil
Fixed as per John's review.
165
        """Site specific commands."""
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
166
        command, args = line[1].split(' ', 1)
167
        if command.lower() == 'chmod':
168
            try:
169
                mode, path = args.split()
170
                mode = int(mode, 8)
171
            except ValueError:
3508.1.2 by Vincent Ladeuil
Fixed as per John's review.
172
                # We catch both malformed line and malformed mode with the same
173
                # ValueError.
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
174
                self.command_not_understood(' '.join(line))
175
                return
176
            try:
177
                # Yes path and mode are reversed
178
                self.filesystem.chmod(path, mode)
179
                self.respond('200 SITE CHMOD command successful')
180
            except AttributeError:
3508.1.2 by Vincent Ladeuil
Fixed as per John's review.
181
                # The chmod method is not available in read-only and will raise
182
                # AttributeError since a different filesystem is used in that
183
                # case
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
184
                self.command_not_authorized(' '.join(line))
185
        else:
3508.1.2 by Vincent Ladeuil
Fixed as per John's review.
186
            # Another site specific command was requested. We don't know that
187
            # one
3508.1.1 by Vincent Ladeuil
Fix ftp transport so that it handles the 'mode' parameter when provided.
188
            self.command_not_understood(' '.join(line))
189
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
190
191
class ftp_server(medusa.ftp_server.ftp_server):
192
    """Customize the behavior of the Medusa ftp_server.
193
194
    There are a few warts on the ftp_server, based on how it expects
195
    to be used.
196
    """
197
    _renaming = None
198
    ftp_channel_class = ftp_channel
199
200
    def __init__(self, *args, **kwargs):
201
        trace.mutter('Initializing ftp_server: %r, %r', args, kwargs)
202
        medusa.ftp_server.ftp_server.__init__(self, *args, **kwargs)
203
204
    def log(self, message):
205
        """Redirect logging requests."""
206
        trace.mutter('ftp_server: %s', message)
207
208
    def log_info(self, message, type='info'):
209
        """Override the asyncore.log_info so we don't stipple the screen."""
210
        trace.mutter('ftp_server %s: %s', type, message)
211
212
5017.3.17 by Vincent Ladeuil
Fix imports for ftp_server/medusa_based.py
213
class FTPTestServer(test_server.TestServer):
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
214
    """Common code for FTP server facilities."""
215
4935.1.2 by Vincent Ladeuil
Fixed as per jam's review.
216
    no_unicode_support = True
4935.1.1 by Vincent Ladeuil
Support Unicode paths for ftp transport (encoded as utf8).
217
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
218
    def __init__(self):
219
        self._root = None
220
        self._ftp_server = None
221
        self._port = None
222
        self._async_thread = None
223
        # ftp server logs
224
        self.logs = []
225
226
    def get_url(self):
227
        """Calculate an ftp url to this server."""
228
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
229
3021.1.1 by Vincent Ladeuil
Reproduce bug 164567.
230
    def get_bogus_url(self):
231
        """Return a URL which cannot be connected to."""
232
        return 'ftp://127.0.0.1:1'
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
233
234
    def log(self, message):
235
        """This is used by medusa.ftp_server to log connections, etc."""
236
        self.logs.append(message)
237
4934.3.3 by Martin Pool
Rename Server.setUp to Server.start_server
238
    def start_server(self, vfs_server=None):
5017.3.17 by Vincent Ladeuil
Fix imports for ftp_server/medusa_based.py
239
        if not (vfs_server is None or isinstance(vfs_server,
240
                                                 test_server.LocalURLServer)):
3376.2.4 by Martin Pool
Remove every assert statement from bzrlib!
241
            raise AssertionError(
242
                "FTPServer currently assumes local transport, got %s" % vfs_server)
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
243
        self._root = os.getcwdu()
244
        self._ftp_server = ftp_server(
245
            authorizer=test_authorizer(root=self._root),
246
            ip='localhost',
247
            port=0, # bind to a random port
248
            resolver=None,
249
            logger_object=self # Use FTPServer.log() for messages
250
            )
251
        self._port = self._ftp_server.getsockname()[1]
252
        # Don't let it loop forever, or handle an infinite number of requests.
253
        # In this case it will run for 1000s, or 10000 requests
254
        self._async_thread = threading.Thread(
3508.1.23 by Vincent Ladeuil
Fix as per Martin's review.
255
                target=FTPTestServer._asyncore_loop_ignore_EBADF,
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
256
                kwargs={'timeout':0.1, 'count':10000})
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
257
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
258
            sys.stderr.write('Thread started: %s\n'
259
                             % (self._async_thread.ident,))
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
260
        self._async_thread.setDaemon(True)
261
        self._async_thread.start()
262
4934.3.1 by Martin Pool
Rename Server.tearDown to .stop_server
263
    def stop_server(self):
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
264
        self._ftp_server.close()
265
        asyncore.close_all()
266
        self._async_thread.join()
4731.2.9 by Vincent Ladeuil
Implement a new -Ethreads to better track the leaks.
267
        if 'threads' in tests.selftest_debug_flags:
5247.5.29 by Vincent Ladeuil
Fixed as per jam's review.
268
            sys.stderr.write('Thread  joined: %s\n'
269
                             % (self._async_thread.ident,))
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
270
271
    @staticmethod
272
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
273
        """Ignore EBADF during server shutdown.
274
275
        We close the socket to get the server to shutdown, but this causes
276
        select.select() to raise EBADF.
277
        """
278
        try:
279
            asyncore.loop(*args, **kwargs)
280
            # FIXME: If we reach that point, we should raise an exception
281
            # explaining that the 'count' parameter in setUp is too low or
282
            # testers may wonder why their test just sits there waiting for a
283
            # server that is already dead. Note that if the tester waits too
284
            # long under pdb the server will also die.
285
        except select.error, e:
286
            if e.args[0] != errno.EBADF:
287
                raise
288
3508.1.13 by Vincent Ladeuil
Fix last failing tests under python2.6.
289
    def add_user(self, user, password):
290
        """Add a user with write access."""
291
        authorizer = server = self._ftp_server.authorizer
292
        authorizer.secured_user = user
293
        authorizer.secured_password = password
2917.3.1 by Vincent Ladeuil
Separate transport from test server.
294