~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/stub_sftp.py

  • Committer: Martin Pool
  • Date: 2010-01-29 14:09:05 UTC
  • mto: This revision was merged to the branch mainline in revision 4992.
  • Revision ID: mbp@sourcefrog.net-20100129140905-2uiarb6p8di1ywsr
Correction to url

from review: https://code.edge.launchpad.net/~mbp/bzr/doc/+merge/18250

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2008, 2009, 2010 Robey Pointer <robey@lag.net>, Canonical Ltd
 
1
# Copyright (C) 2005 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
20
20
"""
21
21
 
22
22
import os
23
 
import paramiko
24
 
import select
25
 
import socket
 
23
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
 
24
    SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
26
25
import sys
27
 
import threading
28
 
import time
29
 
 
30
 
from bzrlib import (
31
 
    osutils,
32
 
    trace,
33
 
    urlutils,
34
 
    )
35
 
from bzrlib.transport import (
36
 
    ssh,
37
 
    )
38
 
from bzrlib.tests import test_server
39
 
 
40
 
 
41
 
class StubServer (paramiko.ServerInterface):
 
26
 
 
27
from bzrlib.osutils import pathjoin
 
28
from bzrlib.trace import mutter
 
29
 
 
30
 
 
31
class StubServer (ServerInterface):
42
32
 
43
33
    def __init__(self, test_case):
44
 
        paramiko.ServerInterface.__init__(self)
 
34
        ServerInterface.__init__(self)
45
35
        self._test_case = test_case
46
36
 
47
37
    def check_auth_password(self, username, password):
48
38
        # all are allowed
49
39
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
50
 
        return paramiko.AUTH_SUCCESSFUL
 
40
        return AUTH_SUCCESSFUL
51
41
 
52
42
    def check_channel_request(self, kind, chanid):
53
 
        self._test_case.log(
54
 
            'sftpserver - channel request: %s, %s' % (kind, chanid))
55
 
        return paramiko.OPEN_SUCCEEDED
56
 
 
57
 
 
58
 
class StubSFTPHandle (paramiko.SFTPHandle):
 
43
        self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
 
44
        return OPEN_SUCCEEDED
 
45
 
 
46
 
 
47
class StubSFTPHandle (SFTPHandle):
59
48
    def stat(self):
60
49
        try:
61
 
            return paramiko.SFTPAttributes.from_stat(
62
 
                os.fstat(self.readfile.fileno()))
 
50
            return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
63
51
        except OSError, e:
64
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
52
            return SFTPServer.convert_errno(e.errno)
65
53
 
66
54
    def chattr(self, attr):
67
55
        # python doesn't have equivalents to fchown or fchmod, so we have to
68
56
        # use the stored filename
69
 
        trace.mutter('Changing permissions on %s to %s', self.filename, attr)
 
57
        mutter('Changing permissions on %s to %s', self.filename, attr)
70
58
        try:
71
 
            paramiko.SFTPServer.set_file_attr(self.filename, attr)
 
59
            SFTPServer.set_file_attr(self.filename, attr)
72
60
        except OSError, e:
73
 
            return paramiko.SFTPServer.convert_errno(e.errno)
74
 
 
75
 
 
76
 
class StubSFTPServer (paramiko.SFTPServerInterface):
 
61
            return SFTPServer.convert_errno(e.errno)
 
62
 
 
63
 
 
64
class StubSFTPServer (SFTPServerInterface):
77
65
 
78
66
    def __init__(self, server, root, home=None):
79
 
        paramiko.SFTPServerInterface.__init__(self, server)
 
67
        SFTPServerInterface.__init__(self, server)
80
68
        # All paths are actually relative to 'root'.
81
69
        # this is like implementing chroot().
82
70
        self.root = root
125
113
 
126
114
    def chattr(self, path, attr):
127
115
        try:
128
 
            paramiko.SFTPServer.set_file_attr(path, attr)
 
116
            SFTPServer.set_file_attr(path, attr)
129
117
        except OSError, e:
130
 
            return paramiko.SFTPServer.convert_errno(e.errno)
131
 
        return paramiko.SFTP_OK
 
118
            return SFTPServer.convert_errno(e.errno)
 
119
        return SFTP_OK
132
120
 
133
121
    def list_folder(self, path):
134
122
        path = self._realpath(path)
135
123
        try:
136
124
            out = [ ]
137
125
            # TODO: win32 incorrectly lists paths with non-ascii if path is not
138
 
            # unicode. However on unix the server should only deal with
 
126
            # unicode. However on Linux the server should only deal with
139
127
            # bytestreams and posix.listdir does the right thing
140
128
            if sys.platform == 'win32':
141
129
                flist = [f.encode('utf8') for f in os.listdir(path)]
142
130
            else:
143
131
                flist = os.listdir(path)
144
132
            for fname in flist:
145
 
                attr = paramiko.SFTPAttributes.from_stat(
146
 
                    os.stat(osutils.pathjoin(path, fname)))
 
133
                attr = SFTPAttributes.from_stat(os.stat(pathjoin(path, fname)))
147
134
                attr.filename = fname
148
135
                out.append(attr)
149
136
            return out
150
137
        except OSError, e:
151
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
138
            return SFTPServer.convert_errno(e.errno)
152
139
 
153
140
    def stat(self, path):
154
141
        path = self._realpath(path)
155
142
        try:
156
 
            return paramiko.SFTPAttributes.from_stat(os.stat(path))
 
143
            return SFTPAttributes.from_stat(os.stat(path))
157
144
        except OSError, e:
158
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
145
            return SFTPServer.convert_errno(e.errno)
159
146
 
160
147
    def lstat(self, path):
161
148
        path = self._realpath(path)
162
149
        try:
163
 
            return paramiko.SFTPAttributes.from_stat(os.lstat(path))
 
150
            return SFTPAttributes.from_stat(os.lstat(path))
164
151
        except OSError, e:
165
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
152
            return SFTPServer.convert_errno(e.errno)
166
153
 
167
154
    def open(self, path, flags, attr):
168
155
        path = self._realpath(path)
175
162
                # an odd default mode for files
176
163
                fd = os.open(path, flags, 0666)
177
164
        except OSError, e:
178
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
165
            return SFTPServer.convert_errno(e.errno)
179
166
 
180
167
        if (flags & os.O_CREAT) and (attr is not None):
181
168
            attr._flags &= ~attr.FLAG_PERMISSIONS
182
 
            paramiko.SFTPServer.set_file_attr(path, attr)
 
169
            SFTPServer.set_file_attr(path, attr)
183
170
        if flags & os.O_WRONLY:
184
171
            fstr = 'wb'
185
172
        elif flags & os.O_RDWR:
190
177
        try:
191
178
            f = os.fdopen(fd, fstr)
192
179
        except (IOError, OSError), e:
193
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
180
            return SFTPServer.convert_errno(e.errno)
194
181
        fobj = StubSFTPHandle()
195
182
        fobj.filename = path
196
183
        fobj.readfile = f
202
189
        try:
203
190
            os.remove(path)
204
191
        except OSError, e:
205
 
            return paramiko.SFTPServer.convert_errno(e.errno)
206
 
        return paramiko.SFTP_OK
 
192
            return SFTPServer.convert_errno(e.errno)
 
193
        return SFTP_OK
207
194
 
208
195
    def rename(self, oldpath, newpath):
209
196
        oldpath = self._realpath(oldpath)
211
198
        try:
212
199
            os.rename(oldpath, newpath)
213
200
        except OSError, e:
214
 
            return paramiko.SFTPServer.convert_errno(e.errno)
215
 
        return paramiko.SFTP_OK
 
201
            return SFTPServer.convert_errno(e.errno)
 
202
        return SFTP_OK
216
203
 
217
204
    def mkdir(self, path, attr):
218
205
        path = self._realpath(path)
225
212
                os.mkdir(path)
226
213
            if attr is not None:
227
214
                attr._flags &= ~attr.FLAG_PERMISSIONS
228
 
                paramiko.SFTPServer.set_file_attr(path, attr)
 
215
                SFTPServer.set_file_attr(path, attr)
229
216
        except OSError, e:
230
 
            return paramiko.SFTPServer.convert_errno(e.errno)
231
 
        return paramiko.SFTP_OK
 
217
            return SFTPServer.convert_errno(e.errno)
 
218
        return SFTP_OK
232
219
 
233
220
    def rmdir(self, path):
234
221
        path = self._realpath(path)
235
222
        try:
236
223
            os.rmdir(path)
237
224
        except OSError, e:
238
 
            return paramiko.SFTPServer.convert_errno(e.errno)
239
 
        return paramiko.SFTP_OK
 
225
            return SFTPServer.convert_errno(e.errno)
 
226
        return SFTP_OK
240
227
 
241
228
    # removed: chattr, symlink, readlink
242
229
    # (nothing in bzr's sftp transport uses those)
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 SocketListener(threading.Thread):
266
 
 
267
 
    def __init__(self, callback):
268
 
        threading.Thread.__init__(self)
269
 
        self._callback = callback
270
 
        self._socket = socket.socket()
271
 
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
272
 
        self._socket.bind(('localhost', 0))
273
 
        self._socket.listen(1)
274
 
        self.host, self.port = self._socket.getsockname()[:2]
275
 
        self._stop_event = threading.Event()
276
 
 
277
 
    def stop(self):
278
 
        # called from outside this thread
279
 
        self._stop_event.set()
280
 
        # use a timeout here, because if the test fails, the server thread may
281
 
        # never notice the stop_event.
282
 
        self.join(5.0)
283
 
        self._socket.close()
284
 
 
285
 
    def run(self):
286
 
        trace.mutter('SocketListener %r has started', self)
287
 
        while True:
288
 
            readable, writable_unused, exception_unused = \
289
 
                select.select([self._socket], [], [], 0.1)
290
 
            if self._stop_event.isSet():
291
 
                trace.mutter('SocketListener %r has stopped', self)
292
 
                return
293
 
            if len(readable) == 0:
294
 
                continue
295
 
            try:
296
 
                s, addr_unused = self._socket.accept()
297
 
                trace.mutter('SocketListener %r has accepted connection %r',
298
 
                    self, s)
299
 
                # because the loopback socket is inline, and transports are
300
 
                # never explicitly closed, best to launch a new thread.
301
 
                threading.Thread(target=self._callback, args=(s,)).start()
302
 
            except socket.error, x:
303
 
                sys.excepthook(*sys.exc_info())
304
 
                trace.warning('Socket error during accept() '
305
 
                              'within unit test server thread: %r' % x)
306
 
            except Exception, x:
307
 
                # probably a failed test; unit test thread will log the
308
 
                # failure/error
309
 
                sys.excepthook(*sys.exc_info())
310
 
                trace.warning(
311
 
                    'Exception from within unit test server thread: %r' % x)
312
 
 
313
 
 
314
 
class SocketDelay(object):
315
 
    """A socket decorator to make TCP appear slower.
316
 
 
317
 
    This changes recv, send, and sendall to add a fixed latency to each python
318
 
    call if a new roundtrip is detected. That is, when a recv is called and the
319
 
    flag new_roundtrip is set, latency is charged. Every send and send_all
320
 
    sets this flag.
321
 
 
322
 
    In addition every send, sendall and recv sleeps a bit per character send to
323
 
    simulate bandwidth.
324
 
 
325
 
    Not all methods are implemented, this is deliberate as this class is not a
326
 
    replacement for the builtin sockets layer. fileno is not implemented to
327
 
    prevent the proxy being bypassed.
328
 
    """
329
 
 
330
 
    simulated_time = 0
331
 
    _proxied_arguments = dict.fromkeys([
332
 
        "close", "getpeername", "getsockname", "getsockopt", "gettimeout",
333
 
        "setblocking", "setsockopt", "settimeout", "shutdown"])
334
 
 
335
 
    def __init__(self, sock, latency, bandwidth=1.0,
336
 
                 really_sleep=True):
337
 
        """
338
 
        :param bandwith: simulated bandwith (MegaBit)
339
 
        :param really_sleep: If set to false, the SocketDelay will just
340
 
        increase a counter, instead of calling time.sleep. This is useful for
341
 
        unittesting the SocketDelay.
342
 
        """
343
 
        self.sock = sock
344
 
        self.latency = latency
345
 
        self.really_sleep = really_sleep
346
 
        self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
347
 
        self.new_roundtrip = False
348
 
 
349
 
    def sleep(self, s):
350
 
        if self.really_sleep:
351
 
            time.sleep(s)
352
 
        else:
353
 
            SocketDelay.simulated_time += s
354
 
 
355
 
    def __getattr__(self, attr):
356
 
        if attr in SocketDelay._proxied_arguments:
357
 
            return getattr(self.sock, attr)
358
 
        raise AttributeError("'SocketDelay' object has no attribute %r" %
359
 
                             attr)
360
 
 
361
 
    def dup(self):
362
 
        return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
363
 
                           self._sleep)
364
 
 
365
 
    def recv(self, *args):
366
 
        data = self.sock.recv(*args)
367
 
        if data and self.new_roundtrip:
368
 
            self.new_roundtrip = False
369
 
            self.sleep(self.latency)
370
 
        self.sleep(len(data) * self.time_per_byte)
371
 
        return data
372
 
 
373
 
    def sendall(self, data, flags=0):
374
 
        if not self.new_roundtrip:
375
 
            self.new_roundtrip = True
376
 
            self.sleep(self.latency)
377
 
        self.sleep(len(data) * self.time_per_byte)
378
 
        return self.sock.sendall(data, flags)
379
 
 
380
 
    def send(self, data, flags=0):
381
 
        if not self.new_roundtrip:
382
 
            self.new_roundtrip = True
383
 
            self.sleep(self.latency)
384
 
        bytes_sent = self.sock.send(data, flags)
385
 
        self.sleep(bytes_sent * self.time_per_byte)
386
 
        return bytes_sent
387
 
 
388
 
 
389
 
class SFTPServer(test_server.TestServer):
390
 
    """Common code for SFTP server facilities."""
391
 
 
392
 
    def __init__(self, server_interface=StubServer):
393
 
        self._original_vendor = None
394
 
        self._homedir = None
395
 
        self._server_homedir = None
396
 
        self._listener = None
397
 
        self._root = None
398
 
        self._vendor = ssh.ParamikoVendor()
399
 
        self._server_interface = server_interface
400
 
        # sftp server logs
401
 
        self.logs = []
402
 
        self.add_latency = 0
403
 
 
404
 
    def _get_sftp_url(self, path):
405
 
        """Calculate an sftp url to this server for path."""
406
 
        return 'sftp://foo:bar@%s:%d/%s' % (self._listener.host,
407
 
                                            self._listener.port, path)
408
 
 
409
 
    def log(self, message):
410
 
        """StubServer uses this to log when a new server is created."""
411
 
        self.logs.append(message)
412
 
 
413
 
    def _run_server_entry(self, sock):
414
 
        """Entry point for all implementations of _run_server.
415
 
 
416
 
        If self.add_latency is > 0.000001 then sock is given a latency adding
417
 
        decorator.
418
 
        """
419
 
        if self.add_latency > 0.000001:
420
 
            sock = SocketDelay(sock, self.add_latency)
421
 
        return self._run_server(sock)
422
 
 
423
 
    def _run_server(self, s):
424
 
        ssh_server = paramiko.Transport(s)
425
 
        key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
426
 
        f = open(key_file, 'w')
427
 
        f.write(STUB_SERVER_KEY)
428
 
        f.close()
429
 
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
430
 
        ssh_server.add_server_key(host_key)
431
 
        server = self._server_interface(self)
432
 
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
433
 
                                         StubSFTPServer, root=self._root,
434
 
                                         home=self._server_homedir)
435
 
        event = threading.Event()
436
 
        ssh_server.start_server(event, server)
437
 
        event.wait(5.0)
438
 
 
439
 
    def start_server(self, backing_server=None):
440
 
        # XXX: TODO: make sftpserver back onto backing_server rather than local
441
 
        # disk.
442
 
        if not (backing_server is None or
443
 
                isinstance(backing_server, test_server.LocalURLServer)):
444
 
            raise AssertionError(
445
 
                'backing_server should not be %r, because this can only serve '
446
 
                'the local current working directory.' % (backing_server,))
447
 
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
448
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
449
 
        if sys.platform == 'win32':
450
 
            # Win32 needs to use the UNICODE api
451
 
            self._homedir = os.getcwdu()
452
 
            # Normalize the path or it will be wrongly escaped
453
 
            self._homedir = osutils.normpath(self._homedir)
454
 
        else:
455
 
            # But unix SFTP servers should just deal in bytestreams
456
 
            self._homedir = os.getcwd()
457
 
        if self._server_homedir is None:
458
 
            self._server_homedir = self._homedir
459
 
        self._root = '/'
460
 
        if sys.platform == 'win32':
461
 
            self._root = ''
462
 
        self._listener = SocketListener(self._run_server_entry)
463
 
        self._listener.setDaemon(True)
464
 
        self._listener.start()
465
 
 
466
 
    def stop_server(self):
467
 
        self._listener.stop()
468
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
469
 
 
470
 
    def get_bogus_url(self):
471
 
        """See bzrlib.transport.Server.get_bogus_url."""
472
 
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
473
 
        # we bind a random socket, so that we get a guaranteed unused port
474
 
        # we just never listen on that port
475
 
        s = socket.socket()
476
 
        s.bind(('localhost', 0))
477
 
        return 'sftp://%s:%s/' % s.getsockname()
478
 
 
479
 
 
480
 
class SFTPFullAbsoluteServer(SFTPServer):
481
 
    """A test server for sftp transports, using absolute urls and ssh."""
482
 
 
483
 
    def get_url(self):
484
 
        """See bzrlib.transport.Server.get_url."""
485
 
        homedir = self._homedir
486
 
        if sys.platform != 'win32':
487
 
            # Remove the initial '/' on all platforms but win32
488
 
            homedir = homedir[1:]
489
 
        return self._get_sftp_url(urlutils.escape(homedir))
490
 
 
491
 
 
492
 
class SFTPServerWithoutSSH(SFTPServer):
493
 
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
494
 
 
495
 
    def __init__(self):
496
 
        super(SFTPServerWithoutSSH, self).__init__()
497
 
        self._vendor = ssh.LoopbackVendor()
498
 
 
499
 
    def _run_server(self, sock):
500
 
        # Re-import these as locals, so that they're still accessible during
501
 
        # interpreter shutdown (when all module globals get set to None, leading
502
 
        # to confusing errors like "'NoneType' object has no attribute 'error'".
503
 
        class FakeChannel(object):
504
 
            def get_transport(self):
505
 
                return self
506
 
            def get_log_channel(self):
507
 
                return 'paramiko'
508
 
            def get_name(self):
509
 
                return '1'
510
 
            def get_hexdump(self):
511
 
                return False
512
 
            def close(self):
513
 
                pass
514
 
 
515
 
        server = paramiko.SFTPServer(
516
 
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
517
 
            root=self._root, home=self._server_homedir)
518
 
        try:
519
 
            server.start_subsystem(
520
 
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
521
 
        except socket.error, e:
522
 
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
523
 
                # it's okay for the client to disconnect abruptly
524
 
                # (bug in paramiko 1.6: it should absorb this exception)
525
 
                pass
526
 
            else:
527
 
                raise
528
 
        except Exception, e:
529
 
            # This typically seems to happen during interpreter shutdown, so
530
 
            # most of the useful ways to report this error are won't work.
531
 
            # Writing the exception type, and then the text of the exception,
532
 
            # seems to be the best we can do.
533
 
            import sys
534
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
535
 
            sys.stderr.write('%s\n\n' % (e,))
536
 
        server.finish_subsystem()
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("~/")
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 start_server(self, backing_server=None):
566
 
        self._server_homedir = '/dev/noone/runs/tests/here'
567
 
        super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)
568