~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/ftp_server.py

merge merge tweaks from aaron, which includes latest .dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2007 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
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
"""
17
 
FTP test server.
18
 
 
19
 
Based on medusa: http://www.amk.ca/python/code/medusa.html
20
 
"""
21
 
 
22
 
import asyncore
23
 
import errno
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
 
    transport,
37
 
    )
38
 
 
39
 
 
40
 
class test_filesystem(medusa.filesys.os_filesystem):
41
 
    """A custom filesystem wrapper to add missing functionalities."""
42
 
 
43
 
    def chmod(self, path, mode):
44
 
        p = self.normalize(self.path_module.join (self.wd, path))
45
 
        return os.chmod(self.translate(p), mode)
46
 
 
47
 
 
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:
75
 
            return 1, 'OK.', test_filesystem(self.root)
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
 
 
129
 
        This is overloaded to help the test suite determine if the 
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:
139
 
            self.respond('213 %d' 
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
 
 
164
 
    def cmd_site(self, line):
165
 
        """Site specific commands."""
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:
172
 
                # We catch both malformed line and malformed mode with the same
173
 
                # ValueError.
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:
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
184
 
                self.command_not_authorized(' '.join(line))
185
 
        else:
186
 
            # Another site specific command was requested. We don't know that
187
 
            # one
188
 
            self.command_not_understood(' '.join(line))
189
 
 
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
 
 
213
 
class FTPServer(transport.Server):
214
 
    """Common code for FTP server facilities."""
215
 
 
216
 
    def __init__(self):
217
 
        self._root = None
218
 
        self._ftp_server = None
219
 
        self._port = None
220
 
        self._async_thread = None
221
 
        # ftp server logs
222
 
        self.logs = []
223
 
 
224
 
    def get_url(self):
225
 
        """Calculate an ftp url to this server."""
226
 
        return 'ftp://foo:bar@localhost:%d/' % (self._port)
227
 
 
228
 
    def get_bogus_url(self):
229
 
        """Return a URL which cannot be connected to."""
230
 
        return 'ftp://127.0.0.1:1'
231
 
 
232
 
    def log(self, message):
233
 
        """This is used by medusa.ftp_server to log connections, etc."""
234
 
        self.logs.append(message)
235
 
 
236
 
    def setUp(self, vfs_server=None):
237
 
        from bzrlib.transport.local import LocalURLServer
238
 
        if not (vfs_server is None or isinstance(vfs_server, LocalURLServer)):
239
 
            raise AssertionError(
240
 
                "FTPServer currently assumes local transport, got %s" % vfs_server)
241
 
        self._root = os.getcwdu()
242
 
        self._ftp_server = ftp_server(
243
 
            authorizer=test_authorizer(root=self._root),
244
 
            ip='localhost',
245
 
            port=0, # bind to a random port
246
 
            resolver=None,
247
 
            logger_object=self # Use FTPServer.log() for messages
248
 
            )
249
 
        self._port = self._ftp_server.getsockname()[1]
250
 
        # Don't let it loop forever, or handle an infinite number of requests.
251
 
        # In this case it will run for 1000s, or 10000 requests
252
 
        self._async_thread = threading.Thread(
253
 
                target=FTPServer._asyncore_loop_ignore_EBADF,
254
 
                kwargs={'timeout':0.1, 'count':10000})
255
 
        self._async_thread.setDaemon(True)
256
 
        self._async_thread.start()
257
 
 
258
 
    def tearDown(self):
259
 
        """See bzrlib.transport.Server.tearDown."""
260
 
        self._ftp_server.close()
261
 
        asyncore.close_all()
262
 
        self._async_thread.join()
263
 
 
264
 
    @staticmethod
265
 
    def _asyncore_loop_ignore_EBADF(*args, **kwargs):
266
 
        """Ignore EBADF during server shutdown.
267
 
 
268
 
        We close the socket to get the server to shutdown, but this causes
269
 
        select.select() to raise EBADF.
270
 
        """
271
 
        try:
272
 
            asyncore.loop(*args, **kwargs)
273
 
            # FIXME: If we reach that point, we should raise an exception
274
 
            # explaining that the 'count' parameter in setUp is too low or
275
 
            # testers may wonder why their test just sits there waiting for a
276
 
            # server that is already dead. Note that if the tester waits too
277
 
            # long under pdb the server will also die.
278
 
        except select.error, e:
279
 
            if e.args[0] != errno.EBADF:
280
 
                raise
281
 
 
282
 
 
283
 
 
284