/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/ftp_server/medusa_based.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2017-06-11 17:59:24 UTC
  • mfrom: (6686.1.4 medusa)
  • Revision ID: breezy.the.bot@gmail.com-20170611175924-a87niklxj8d8l36a
Drop support for medusa, support newer versions of pyftpdlib.

Merged from https://code.launchpad.net/~jelmer/brz/medusa/+merge/325457

Show diffs side-by-side

added added

removed removed

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