~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/stub_sftp.py

(jameinel) Allow 'bzr serve' to interpret SIGHUP as a graceful shutdown.
 (bug #795025) (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>, Canonical Ltd
 
1
# Copyright (C) 2005, 2006, 2008-2011 Robey Pointer <robey@lag.net>, Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""
18
18
A stub SFTP server for loopback SFTP testing.
20
20
"""
21
21
 
22
22
import os
23
 
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
24
 
    SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
 
23
import paramiko
 
24
import socket
 
25
import SocketServer
25
26
import sys
26
 
 
27
 
from bzrlib.osutils import pathjoin
28
 
from bzrlib.trace import mutter
29
 
 
30
 
 
31
 
class StubServer (ServerInterface):
32
 
 
33
 
    def __init__(self, test_case):
34
 
        ServerInterface.__init__(self)
35
 
        self._test_case = test_case
 
27
import time
 
28
 
 
29
from bzrlib import (
 
30
    osutils,
 
31
    trace,
 
32
    urlutils,
 
33
    )
 
34
from bzrlib.transport import (
 
35
    ssh,
 
36
    )
 
37
from bzrlib.tests import test_server
 
38
 
 
39
 
 
40
class StubServer(paramiko.ServerInterface):
 
41
 
 
42
    def __init__(self, test_case_server):
 
43
        paramiko.ServerInterface.__init__(self)
 
44
        self.log = test_case_server.log
36
45
 
37
46
    def check_auth_password(self, username, password):
38
47
        # all are allowed
39
 
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
40
 
        return AUTH_SUCCESSFUL
 
48
        self.log('sftpserver - authorizing: %s' % (username,))
 
49
        return paramiko.AUTH_SUCCESSFUL
41
50
 
42
51
    def check_channel_request(self, kind, chanid):
43
 
        self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
44
 
        return OPEN_SUCCEEDED
45
 
 
46
 
 
47
 
class StubSFTPHandle (SFTPHandle):
 
52
        self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
 
53
        return paramiko.OPEN_SUCCEEDED
 
54
 
 
55
 
 
56
class StubSFTPHandle(paramiko.SFTPHandle):
 
57
 
48
58
    def stat(self):
49
59
        try:
50
 
            return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
 
60
            return paramiko.SFTPAttributes.from_stat(
 
61
                os.fstat(self.readfile.fileno()))
51
62
        except OSError, e:
52
 
            return SFTPServer.convert_errno(e.errno)
 
63
            return paramiko.SFTPServer.convert_errno(e.errno)
53
64
 
54
65
    def chattr(self, attr):
55
66
        # python doesn't have equivalents to fchown or fchmod, so we have to
56
67
        # use the stored filename
57
 
        mutter('Changing permissions on %s to %s', self.filename, attr)
 
68
        trace.mutter('Changing permissions on %s to %s', self.filename, attr)
58
69
        try:
59
 
            SFTPServer.set_file_attr(self.filename, attr)
 
70
            paramiko.SFTPServer.set_file_attr(self.filename, attr)
60
71
        except OSError, e:
61
 
            return SFTPServer.convert_errno(e.errno)
62
 
 
63
 
 
64
 
class StubSFTPServer (SFTPServerInterface):
 
72
            return paramiko.SFTPServer.convert_errno(e.errno)
 
73
 
 
74
 
 
75
class StubSFTPServer(paramiko.SFTPServerInterface):
65
76
 
66
77
    def __init__(self, server, root, home=None):
67
 
        SFTPServerInterface.__init__(self, server)
 
78
        paramiko.SFTPServerInterface.__init__(self, server)
68
79
        # All paths are actually relative to 'root'.
69
80
        # this is like implementing chroot().
70
81
        self.root = root
78
89
            self.home = home[len(self.root):]
79
90
        if self.home.startswith('/'):
80
91
            self.home = self.home[1:]
81
 
        server._test_case.log('sftpserver - new connection')
 
92
        server.log('sftpserver - new connection')
82
93
 
83
94
    def _realpath(self, path):
84
95
        # paths returned from self.canonicalize() always start with
85
96
        # a path separator. So if 'root' is just '/', this would cause
86
 
        # a double slash at the beginning '//home/dir'. 
 
97
        # a double slash at the beginning '//home/dir'.
87
98
        if self.root == '/':
88
99
            return self.canonicalize(path)
89
100
        return self.root + self.canonicalize(path)
96
107
            #     /h:/foo/bar
97
108
            # and relative paths stay the same:
98
109
            #     foo/bar
99
 
            # win32 needs to use the Unicode APIs. so we require the 
 
110
            # win32 needs to use the Unicode APIs. so we require the
100
111
            # paths to be utf8 (Linux just uses bytestreams)
101
112
            thispath = path.decode('utf8')
102
113
            if path.startswith('/'):
113
124
 
114
125
    def chattr(self, path, attr):
115
126
        try:
116
 
            SFTPServer.set_file_attr(path, attr)
 
127
            paramiko.SFTPServer.set_file_attr(path, attr)
117
128
        except OSError, e:
118
 
            return SFTPServer.convert_errno(e.errno)
119
 
        return SFTP_OK
 
129
            return paramiko.SFTPServer.convert_errno(e.errno)
 
130
        return paramiko.SFTP_OK
120
131
 
121
132
    def list_folder(self, path):
122
133
        path = self._realpath(path)
123
134
        try:
124
135
            out = [ ]
125
136
            # TODO: win32 incorrectly lists paths with non-ascii if path is not
126
 
            # unicode. However on Linux the server should only deal with
127
 
            # bytestreams and posix.listdir does the right thing 
 
137
            # unicode. However on unix the server should only deal with
 
138
            # bytestreams and posix.listdir does the right thing
128
139
            if sys.platform == 'win32':
129
140
                flist = [f.encode('utf8') for f in os.listdir(path)]
130
141
            else:
131
142
                flist = os.listdir(path)
132
143
            for fname in flist:
133
 
                attr = SFTPAttributes.from_stat(os.stat(pathjoin(path, fname)))
 
144
                attr = paramiko.SFTPAttributes.from_stat(
 
145
                    os.stat(osutils.pathjoin(path, fname)))
134
146
                attr.filename = fname
135
147
                out.append(attr)
136
148
            return out
137
149
        except OSError, e:
138
 
            return SFTPServer.convert_errno(e.errno)
 
150
            return paramiko.SFTPServer.convert_errno(e.errno)
139
151
 
140
152
    def stat(self, path):
141
153
        path = self._realpath(path)
142
154
        try:
143
 
            return SFTPAttributes.from_stat(os.stat(path))
 
155
            return paramiko.SFTPAttributes.from_stat(os.stat(path))
144
156
        except OSError, e:
145
 
            return SFTPServer.convert_errno(e.errno)
 
157
            return paramiko.SFTPServer.convert_errno(e.errno)
146
158
 
147
159
    def lstat(self, path):
148
160
        path = self._realpath(path)
149
161
        try:
150
 
            return SFTPAttributes.from_stat(os.lstat(path))
 
162
            return paramiko.SFTPAttributes.from_stat(os.lstat(path))
151
163
        except OSError, e:
152
 
            return SFTPServer.convert_errno(e.errno)
 
164
            return paramiko.SFTPServer.convert_errno(e.errno)
153
165
 
154
166
    def open(self, path, flags, attr):
155
167
        path = self._realpath(path)
162
174
                # an odd default mode for files
163
175
                fd = os.open(path, flags, 0666)
164
176
        except OSError, e:
165
 
            return SFTPServer.convert_errno(e.errno)
 
177
            return paramiko.SFTPServer.convert_errno(e.errno)
166
178
 
167
179
        if (flags & os.O_CREAT) and (attr is not None):
168
180
            attr._flags &= ~attr.FLAG_PERMISSIONS
169
 
            SFTPServer.set_file_attr(path, attr)
 
181
            paramiko.SFTPServer.set_file_attr(path, attr)
170
182
        if flags & os.O_WRONLY:
171
183
            fstr = 'wb'
172
184
        elif flags & os.O_RDWR:
177
189
        try:
178
190
            f = os.fdopen(fd, fstr)
179
191
        except (IOError, OSError), e:
180
 
            return SFTPServer.convert_errno(e.errno)
 
192
            return paramiko.SFTPServer.convert_errno(e.errno)
181
193
        fobj = StubSFTPHandle()
182
194
        fobj.filename = path
183
195
        fobj.readfile = f
189
201
        try:
190
202
            os.remove(path)
191
203
        except OSError, e:
192
 
            return SFTPServer.convert_errno(e.errno)
193
 
        return SFTP_OK
 
204
            return paramiko.SFTPServer.convert_errno(e.errno)
 
205
        return paramiko.SFTP_OK
194
206
 
195
207
    def rename(self, oldpath, newpath):
196
208
        oldpath = self._realpath(oldpath)
198
210
        try:
199
211
            os.rename(oldpath, newpath)
200
212
        except OSError, e:
201
 
            return SFTPServer.convert_errno(e.errno)
202
 
        return SFTP_OK
 
213
            return paramiko.SFTPServer.convert_errno(e.errno)
 
214
        return paramiko.SFTP_OK
203
215
 
204
216
    def mkdir(self, path, attr):
205
217
        path = self._realpath(path)
212
224
                os.mkdir(path)
213
225
            if attr is not None:
214
226
                attr._flags &= ~attr.FLAG_PERMISSIONS
215
 
                SFTPServer.set_file_attr(path, attr)
 
227
                paramiko.SFTPServer.set_file_attr(path, attr)
216
228
        except OSError, e:
217
 
            return SFTPServer.convert_errno(e.errno)
218
 
        return SFTP_OK
 
229
            return paramiko.SFTPServer.convert_errno(e.errno)
 
230
        return paramiko.SFTP_OK
219
231
 
220
232
    def rmdir(self, path):
221
233
        path = self._realpath(path)
222
234
        try:
223
235
            os.rmdir(path)
224
236
        except OSError, e:
225
 
            return SFTPServer.convert_errno(e.errno)
226
 
        return SFTP_OK
 
237
            return paramiko.SFTPServer.convert_errno(e.errno)
 
238
        return paramiko.SFTP_OK
227
239
 
228
240
    # removed: chattr, symlink, readlink
229
241
    # (nothing in bzr's sftp transport uses those)
 
242
 
 
243
 
 
244
# ------------- server test implementation --------------
 
245
 
 
246
STUB_SERVER_KEY = """
 
247
-----BEGIN RSA PRIVATE KEY-----
 
248
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
249
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
250
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
251
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
252
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
253
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
254
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
255
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
256
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
257
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
258
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
259
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
260
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
261
-----END RSA PRIVATE KEY-----
 
262
"""
 
263
 
 
264
 
 
265
class SocketDelay(object):
 
266
    """A socket decorator to make TCP appear slower.
 
267
 
 
268
    This changes recv, send, and sendall to add a fixed latency to each python
 
269
    call if a new roundtrip is detected. That is, when a recv is called and the
 
270
    flag new_roundtrip is set, latency is charged. Every send and send_all
 
271
    sets this flag.
 
272
 
 
273
    In addition every send, sendall and recv sleeps a bit per character send to
 
274
    simulate bandwidth.
 
275
 
 
276
    Not all methods are implemented, this is deliberate as this class is not a
 
277
    replacement for the builtin sockets layer. fileno is not implemented to
 
278
    prevent the proxy being bypassed.
 
279
    """
 
280
 
 
281
    simulated_time = 0
 
282
    _proxied_arguments = dict.fromkeys([
 
283
        "close", "getpeername", "getsockname", "getsockopt", "gettimeout",
 
284
        "setblocking", "setsockopt", "settimeout", "shutdown"])
 
285
 
 
286
    def __init__(self, sock, latency, bandwidth=1.0,
 
287
                 really_sleep=True):
 
288
        """
 
289
        :param bandwith: simulated bandwith (MegaBit)
 
290
        :param really_sleep: If set to false, the SocketDelay will just
 
291
        increase a counter, instead of calling time.sleep. This is useful for
 
292
        unittesting the SocketDelay.
 
293
        """
 
294
        self.sock = sock
 
295
        self.latency = latency
 
296
        self.really_sleep = really_sleep
 
297
        self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
 
298
        self.new_roundtrip = False
 
299
 
 
300
    def sleep(self, s):
 
301
        if self.really_sleep:
 
302
            time.sleep(s)
 
303
        else:
 
304
            SocketDelay.simulated_time += s
 
305
 
 
306
    def __getattr__(self, attr):
 
307
        if attr in SocketDelay._proxied_arguments:
 
308
            return getattr(self.sock, attr)
 
309
        raise AttributeError("'SocketDelay' object has no attribute %r" %
 
310
                             attr)
 
311
 
 
312
    def dup(self):
 
313
        return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
 
314
                           self._sleep)
 
315
 
 
316
    def recv(self, *args):
 
317
        data = self.sock.recv(*args)
 
318
        if data and self.new_roundtrip:
 
319
            self.new_roundtrip = False
 
320
            self.sleep(self.latency)
 
321
        self.sleep(len(data) * self.time_per_byte)
 
322
        return data
 
323
 
 
324
    def sendall(self, data, flags=0):
 
325
        if not self.new_roundtrip:
 
326
            self.new_roundtrip = True
 
327
            self.sleep(self.latency)
 
328
        self.sleep(len(data) * self.time_per_byte)
 
329
        return self.sock.sendall(data, flags)
 
330
 
 
331
    def send(self, data, flags=0):
 
332
        if not self.new_roundtrip:
 
333
            self.new_roundtrip = True
 
334
            self.sleep(self.latency)
 
335
        bytes_sent = self.sock.send(data, flags)
 
336
        self.sleep(bytes_sent * self.time_per_byte)
 
337
        return bytes_sent
 
338
 
 
339
 
 
340
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
 
341
 
 
342
    def setup(self):
 
343
        self.wrap_for_latency()
 
344
        tcs = self.server.test_case_server
 
345
        ptrans = paramiko.Transport(self.request)
 
346
        self.paramiko_transport = ptrans
 
347
        # Set it to a channel under 'bzr' so that we get debug info
 
348
        ptrans.set_log_channel('bzr.paramiko.transport')
 
349
        ptrans.add_server_key(tcs.get_host_key())
 
350
        ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
351
                                     StubSFTPServer, root=tcs._root,
 
352
                                     home=tcs._server_homedir)
 
353
        server = tcs._server_interface(tcs)
 
354
        # This blocks until the key exchange has been done
 
355
        ptrans.start_server(None, server)
 
356
 
 
357
    def finish(self):
 
358
        # Wait for the conversation to finish, when the paramiko.Transport
 
359
        # thread finishes
 
360
        # TODO: Consider timing out after XX seconds rather than hanging.
 
361
        #       Also we could check paramiko_transport.active and possibly
 
362
        #       paramiko_transport.getException().
 
363
        self.paramiko_transport.join()
 
364
 
 
365
    def wrap_for_latency(self):
 
366
        tcs = self.server.test_case_server
 
367
        if tcs.add_latency:
 
368
            # Give the socket (which the request really is) a latency adding
 
369
            # decorator.
 
370
            self.request = SocketDelay(self.request, tcs.add_latency)
 
371
 
 
372
 
 
373
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
 
374
 
 
375
    def setup(self):
 
376
        self.wrap_for_latency()
 
377
        # Re-import these as locals, so that they're still accessible during
 
378
        # interpreter shutdown (when all module globals get set to None, leading
 
379
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
380
        class FakeChannel(object):
 
381
            def get_transport(self):
 
382
                return self
 
383
            def get_log_channel(self):
 
384
                return 'bzr.paramiko'
 
385
            def get_name(self):
 
386
                return '1'
 
387
            def get_hexdump(self):
 
388
                return False
 
389
            def close(self):
 
390
                pass
 
391
 
 
392
        tcs = self.server.test_case_server
 
393
        sftp_server = paramiko.SFTPServer(
 
394
            FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
 
395
            root=tcs._root, home=tcs._server_homedir)
 
396
        self.sftp_server = sftp_server
 
397
        sys_stderr = sys.stderr # Used in error reporting during shutdown
 
398
        try:
 
399
            sftp_server.start_subsystem(
 
400
                'sftp', None, ssh.SocketAsChannelAdapter(self.request))
 
401
        except socket.error, e:
 
402
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
403
                # it's okay for the client to disconnect abruptly
 
404
                # (bug in paramiko 1.6: it should absorb this exception)
 
405
                pass
 
406
            else:
 
407
                raise
 
408
        except Exception, e:
 
409
            # This typically seems to happen during interpreter shutdown, so
 
410
            # most of the useful ways to report this error won't work.
 
411
            # Writing the exception type, and then the text of the exception,
 
412
            # seems to be the best we can do.
 
413
            # FIXME: All interpreter shutdown errors should have been related
 
414
            # to daemon threads, cleanup needed -- vila 20100623
 
415
            sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
416
            sys_stderr.write('%s\n\n' % (e,))
 
417
 
 
418
    def finish(self):
 
419
        self.sftp_server.finish_subsystem()
 
420
 
 
421
 
 
422
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
 
423
 
 
424
    def __init__(self, server_address, request_handler_class, test_case_server):
 
425
        test_server.TestingThreadingTCPServer.__init__(
 
426
            self, server_address, request_handler_class)
 
427
        self.test_case_server = test_case_server
 
428
 
 
429
 
 
430
class SFTPServer(test_server.TestingTCPServerInAThread):
 
431
    """Common code for SFTP server facilities."""
 
432
 
 
433
    def __init__(self, server_interface=StubServer):
 
434
        self.host = '127.0.0.1'
 
435
        self.port = 0
 
436
        super(SFTPServer, self).__init__((self.host, self.port),
 
437
                                         TestingSFTPServer,
 
438
                                         TestingSFTPConnectionHandler)
 
439
        self._original_vendor = None
 
440
        self._vendor = ssh.ParamikoVendor()
 
441
        self._server_interface = server_interface
 
442
        self._host_key = None
 
443
        self.logs = []
 
444
        self.add_latency = 0
 
445
        self._homedir = None
 
446
        self._server_homedir = None
 
447
        self._root = None
 
448
 
 
449
    def _get_sftp_url(self, path):
 
450
        """Calculate an sftp url to this server for path."""
 
451
        return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
 
452
 
 
453
    def log(self, message):
 
454
        """StubServer uses this to log when a new server is created."""
 
455
        self.logs.append(message)
 
456
 
 
457
    def create_server(self):
 
458
        server = self.server_class((self.host, self.port),
 
459
                                   self.request_handler_class,
 
460
                                   self)
 
461
        return server
 
462
 
 
463
    def get_host_key(self):
 
464
        if self._host_key is None:
 
465
            key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
 
466
            f = open(key_file, 'w')
 
467
            try:
 
468
                f.write(STUB_SERVER_KEY)
 
469
            finally:
 
470
                f.close()
 
471
            self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
472
        return self._host_key
 
473
 
 
474
    def start_server(self, backing_server=None):
 
475
        # XXX: TODO: make sftpserver back onto backing_server rather than local
 
476
        # disk.
 
477
        if not (backing_server is None or
 
478
                isinstance(backing_server, test_server.LocalURLServer)):
 
479
            raise AssertionError(
 
480
                'backing_server should not be %r, because this can only serve '
 
481
                'the local current working directory.' % (backing_server,))
 
482
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
 
483
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
 
484
        if sys.platform == 'win32':
 
485
            # Win32 needs to use the UNICODE api
 
486
            self._homedir = os.getcwdu()
 
487
            # Normalize the path or it will be wrongly escaped
 
488
            self._homedir = osutils.normpath(self._homedir)
 
489
        else:
 
490
            # But unix SFTP servers should just deal in bytestreams
 
491
            self._homedir = os.getcwd()
 
492
        if self._server_homedir is None:
 
493
            self._server_homedir = self._homedir
 
494
        self._root = '/'
 
495
        if sys.platform == 'win32':
 
496
            self._root = ''
 
497
        super(SFTPServer, self).start_server()
 
498
 
 
499
    def stop_server(self):
 
500
        try:
 
501
            super(SFTPServer, self).stop_server()
 
502
        finally:
 
503
            ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
504
 
 
505
    def get_bogus_url(self):
 
506
        """See bzrlib.transport.Server.get_bogus_url."""
 
507
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
 
508
        # we bind a random socket, so that we get a guaranteed unused port
 
509
        # we just never listen on that port
 
510
        s = socket.socket()
 
511
        s.bind(('localhost', 0))
 
512
        return 'sftp://%s:%s/' % s.getsockname()
 
513
 
 
514
 
 
515
class SFTPFullAbsoluteServer(SFTPServer):
 
516
    """A test server for sftp transports, using absolute urls and ssh."""
 
517
 
 
518
    def get_url(self):
 
519
        """See bzrlib.transport.Server.get_url."""
 
520
        homedir = self._homedir
 
521
        if sys.platform != 'win32':
 
522
            # Remove the initial '/' on all platforms but win32
 
523
            homedir = homedir[1:]
 
524
        return self._get_sftp_url(urlutils.escape(homedir))
 
525
 
 
526
 
 
527
class SFTPServerWithoutSSH(SFTPServer):
 
528
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
 
529
 
 
530
    def __init__(self):
 
531
        super(SFTPServerWithoutSSH, self).__init__()
 
532
        self._vendor = ssh.LoopbackVendor()
 
533
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
 
534
 
 
535
    def get_host_key():
 
536
        return None
 
537
 
 
538
 
 
539
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
 
540
    """A test server for sftp transports, using absolute urls."""
 
541
 
 
542
    def get_url(self):
 
543
        """See bzrlib.transport.Server.get_url."""
 
544
        homedir = self._homedir
 
545
        if sys.platform != 'win32':
 
546
            # Remove the initial '/' on all platforms but win32
 
547
            homedir = homedir[1:]
 
548
        return self._get_sftp_url(urlutils.escape(homedir))
 
549
 
 
550
 
 
551
class SFTPHomeDirServer(SFTPServerWithoutSSH):
 
552
    """A test server for sftp transports, using homedir relative urls."""
 
553
 
 
554
    def get_url(self):
 
555
        """See bzrlib.transport.Server.get_url."""
 
556
        return self._get_sftp_url("%7E/")
 
557
 
 
558
 
 
559
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
560
    """A test server for sftp transports where only absolute paths will work.
 
561
 
 
562
    It does this by serving from a deeply-nested directory that doesn't exist.
 
563
    """
 
564
 
 
565
    def create_server(self):
 
566
        # FIXME: Can't we do that in a cleaner way ? -- vila 20100623
 
567
        server = super(SFTPSiblingAbsoluteServer, self).create_server()
 
568
        server._server_homedir = '/dev/noone/runs/tests/here'
 
569
        return server
 
570