~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-04-01 04:41:18 UTC
  • mto: This revision was merged to the branch mainline in revision 5128.
  • Revision ID: mbp@sourcefrog.net-20100401044118-shyctqc02ob08ngz
ignore .testrepository

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
import paramiko
24
24
import select
25
25
import socket
26
 
import SocketServer
27
26
import sys
28
27
import threading
29
28
import time
39
38
from bzrlib.tests import test_server
40
39
 
41
40
 
42
 
class StubServer(paramiko.ServerInterface):
 
41
class StubServer (paramiko.ServerInterface):
43
42
 
44
 
    def __init__(self, test_case_server):
 
43
    def __init__(self, test_case):
45
44
        paramiko.ServerInterface.__init__(self)
46
 
        self.log = test_case_server.log
 
45
        self._test_case = test_case
47
46
 
48
47
    def check_auth_password(self, username, password):
49
48
        # all are allowed
50
 
        self.log('sftpserver - authorizing: %s' % (username,))
 
49
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
51
50
        return paramiko.AUTH_SUCCESSFUL
52
51
 
53
52
    def check_channel_request(self, kind, chanid):
54
 
        self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
 
53
        self._test_case.log(
 
54
            'sftpserver - channel request: %s, %s' % (kind, chanid))
55
55
        return paramiko.OPEN_SUCCEEDED
56
56
 
57
57
 
58
 
class StubSFTPHandle(paramiko.SFTPHandle):
59
 
 
 
58
class StubSFTPHandle (paramiko.SFTPHandle):
60
59
    def stat(self):
61
60
        try:
62
61
            return paramiko.SFTPAttributes.from_stat(
74
73
            return paramiko.SFTPServer.convert_errno(e.errno)
75
74
 
76
75
 
77
 
class StubSFTPServer(paramiko.SFTPServerInterface):
 
76
class StubSFTPServer (paramiko.SFTPServerInterface):
78
77
 
79
78
    def __init__(self, server, root, home=None):
80
79
        paramiko.SFTPServerInterface.__init__(self, server)
91
90
            self.home = home[len(self.root):]
92
91
        if self.home.startswith('/'):
93
92
            self.home = self.home[1:]
94
 
        server.log('sftpserver - new connection')
 
93
        server._test_case.log('sftpserver - new connection')
95
94
 
96
95
    def _realpath(self, path):
97
96
        # paths returned from self.canonicalize() always start with
136
135
        try:
137
136
            out = [ ]
138
137
            # TODO: win32 incorrectly lists paths with non-ascii if path is not
139
 
            # unicode. However on unix the server should only deal with
 
138
            # unicode. However on Linux the server should only deal with
140
139
            # bytestreams and posix.listdir does the right thing
141
140
            if sys.platform == 'win32':
142
141
                flist = [f.encode('utf8') for f in os.listdir(path)]
242
241
    # removed: chattr, symlink, readlink
243
242
    # (nothing in bzr's sftp transport uses those)
244
243
 
245
 
 
246
244
# ------------- server test implementation --------------
247
245
 
248
246
STUB_SERVER_KEY = """
264
262
"""
265
263
 
266
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
        while True:
 
287
            readable, writable_unused, exception_unused = \
 
288
                select.select([self._socket], [], [], 0.1)
 
289
            if self._stop_event.isSet():
 
290
                return
 
291
            if len(readable) == 0:
 
292
                continue
 
293
            try:
 
294
                s, addr_unused = self._socket.accept()
 
295
                # because the loopback socket is inline, and transports are
 
296
                # never explicitly closed, best to launch a new thread.
 
297
                threading.Thread(target=self._callback, args=(s,)).start()
 
298
            except socket.error, x:
 
299
                sys.excepthook(*sys.exc_info())
 
300
                trace.warning('Socket error during accept() '
 
301
                              'within unit test server thread: %r' % x)
 
302
            except Exception, x:
 
303
                # probably a failed test; unit test thread will log the
 
304
                # failure/error
 
305
                sys.excepthook(*sys.exc_info())
 
306
                trace.warning(
 
307
                    'Exception from within unit test server thread: %r' % x)
 
308
 
 
309
 
267
310
class SocketDelay(object):
268
311
    """A socket decorator to make TCP appear slower.
269
312
 
339
382
        return bytes_sent
340
383
 
341
384
 
342
 
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
343
 
 
344
 
    def setup(self):
345
 
        self.wrap_for_latency()
346
 
        tcs = self.server.test_case_server
347
 
        ssh_server = paramiko.Transport(self.request)
348
 
        ssh_server.add_server_key(tcs.get_host_key())
349
 
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
350
 
                                         StubSFTPServer, root=tcs._root,
351
 
                                         home=tcs._server_homedir)
352
 
        server = tcs._server_interface(tcs)
353
 
        ssh_server.start_server(None, server)
354
 
        # FIXME: Long story short:
355
 
        # bt.test_transport.TestSSHConnections.test_bzr_connect_to_bzr_ssh
356
 
        # fails if we wait less than 0.2 seconds... paramiko uses a lot of
357
 
        # timeouts internally which probably mask a synchronisation
358
 
        # problem. Note that this is the only test that requires this hack and
359
 
        # the test may need to be fixed instead, but it's late and the test is
360
 
        # horrible as mentioned in its comments :) -- vila 20100623
361
 
        import time
362
 
        time.sleep(0.2)
363
 
 
364
 
    def wrap_for_latency(self):
365
 
        tcs = self.server.test_case_server
366
 
        if tcs.add_latency:
367
 
            # Give the socket (which the request really is) a latency adding
368
 
            # decorator.
369
 
            self.request = SocketDelay(self.request, tcs.add_latency)
370
 
 
371
 
 
372
 
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
373
 
 
374
 
    def setup(self):
375
 
        self.wrap_for_latency()
376
 
        # Re-import these as locals, so that they're still accessible during
377
 
        # interpreter shutdown (when all module globals get set to None, leading
378
 
        # to confusing errors like "'NoneType' object has no attribute 'error'".
379
 
        class FakeChannel(object):
380
 
            def get_transport(self):
381
 
                return self
382
 
            def get_log_channel(self):
383
 
                return 'paramiko'
384
 
            def get_name(self):
385
 
                return '1'
386
 
            def get_hexdump(self):
387
 
                return False
388
 
            def close(self):
389
 
                pass
390
 
 
391
 
        tcs = self.server.test_case_server
392
 
        server = paramiko.SFTPServer(
393
 
            FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
394
 
            root=tcs._root, home=tcs._server_homedir)
395
 
        try:
396
 
            server.start_subsystem(
397
 
                'sftp', None, ssh.SocketAsChannelAdapter(self.request))
398
 
        except socket.error, e:
399
 
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
400
 
                # it's okay for the client to disconnect abruptly
401
 
                # (bug in paramiko 1.6: it should absorb this exception)
402
 
                pass
403
 
            else:
404
 
                raise
405
 
        except Exception, e:
406
 
            # This typically seems to happen during interpreter shutdown, so
407
 
            # most of the useful ways to report this error won't work.
408
 
            # Writing the exception type, and then the text of the exception,
409
 
            # seems to be the best we can do.
410
 
            # FIXME: All interpreter shutdown errors should have been related
411
 
            # to daemon threads, cleanup needed -- vila 20100623
412
 
            import sys
413
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
414
 
            sys.stderr.write('%s\n\n' % (e,))
415
 
        server.finish_subsystem()
416
 
 
417
 
 
418
 
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
419
 
 
420
 
    def __init__(self, server_address, request_handler_class, test_case_server):
421
 
        test_server.TestingThreadingTCPServer.__init__(
422
 
            self, server_address, request_handler_class)
423
 
        self.test_case_server = test_case_server
424
 
 
425
 
 
426
 
class SFTPServer(test_server.TestingTCPServerInAThread):
 
385
class SFTPServer(test_server.TestServer):
427
386
    """Common code for SFTP server facilities."""
428
387
 
429
388
    def __init__(self, server_interface=StubServer):
430
 
        self.host = '127.0.0.1'
431
 
        self.port = 0
432
 
        super(SFTPServer, self).__init__((self.host, self.port),
433
 
                                         TestingSFTPServer,
434
 
                                         TestingSFTPConnectionHandler)
435
389
        self._original_vendor = None
 
390
        self._homedir = None
 
391
        self._server_homedir = None
 
392
        self._listener = None
 
393
        self._root = None
436
394
        self._vendor = ssh.ParamikoVendor()
437
395
        self._server_interface = server_interface
438
 
        self._host_key = None
 
396
        # sftp server logs
439
397
        self.logs = []
440
398
        self.add_latency = 0
441
 
        self._homedir = None
442
 
        self._server_homedir = None
443
 
        self._root = None
444
399
 
445
400
    def _get_sftp_url(self, path):
446
401
        """Calculate an sftp url to this server for path."""
447
 
        return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
 
402
        return 'sftp://foo:bar@%s:%d/%s' % (self._listener.host,
 
403
                                            self._listener.port, path)
448
404
 
449
405
    def log(self, message):
450
406
        """StubServer uses this to log when a new server is created."""
451
407
        self.logs.append(message)
452
408
 
453
 
    def create_server(self):
454
 
        server = self.server_class((self.host, self.port),
455
 
                                   self.request_handler_class,
456
 
                                   self)
457
 
        return server
458
 
 
459
 
    def get_host_key(self):
460
 
        if self._host_key is None:
461
 
            key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
462
 
            f = open(key_file, 'w')
463
 
            try:
464
 
                f.write(STUB_SERVER_KEY)
465
 
            finally:
466
 
                f.close()
467
 
            self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
468
 
        return self._host_key
 
409
    def _run_server_entry(self, sock):
 
410
        """Entry point for all implementations of _run_server.
 
411
 
 
412
        If self.add_latency is > 0.000001 then sock is given a latency adding
 
413
        decorator.
 
414
        """
 
415
        if self.add_latency > 0.000001:
 
416
            sock = SocketDelay(sock, self.add_latency)
 
417
        return self._run_server(sock)
 
418
 
 
419
    def _run_server(self, s):
 
420
        ssh_server = paramiko.Transport(s)
 
421
        key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
 
422
        f = open(key_file, 'w')
 
423
        f.write(STUB_SERVER_KEY)
 
424
        f.close()
 
425
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
426
        ssh_server.add_server_key(host_key)
 
427
        server = self._server_interface(self)
 
428
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
429
                                         StubSFTPServer, root=self._root,
 
430
                                         home=self._server_homedir)
 
431
        event = threading.Event()
 
432
        ssh_server.start_server(event, server)
 
433
        event.wait(5.0)
469
434
 
470
435
    def start_server(self, backing_server=None):
471
436
        # XXX: TODO: make sftpserver back onto backing_server rather than local
477
442
                'the local current working directory.' % (backing_server,))
478
443
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
479
444
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
 
445
        # FIXME: the following block should certainly just be self._homedir =
 
446
        # osutils.getcwd() but that fails badly on Unix -- vila 20100224
480
447
        if sys.platform == 'win32':
481
448
            # Win32 needs to use the UNICODE api
482
449
            self._homedir = os.getcwdu()
483
 
            # Normalize the path or it will be wrongly escaped
484
 
            self._homedir = osutils.normpath(self._homedir)
485
450
        else:
486
 
            # But unix SFTP servers should just deal in bytestreams
 
451
            # But Linux SFTP servers should just deal in bytestreams
487
452
            self._homedir = os.getcwd()
488
453
        if self._server_homedir is None:
489
454
            self._server_homedir = self._homedir
490
455
        self._root = '/'
491
456
        if sys.platform == 'win32':
492
457
            self._root = ''
493
 
        super(SFTPServer, self).start_server()
 
458
        self._listener = SocketListener(self._run_server_entry)
 
459
        self._listener.setDaemon(True)
 
460
        self._listener.start()
494
461
 
495
462
    def stop_server(self):
496
 
        try:
497
 
            super(SFTPServer, self).stop_server()
498
 
        finally:
499
 
            ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
463
        self._listener.stop()
 
464
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
500
465
 
501
466
    def get_bogus_url(self):
502
467
        """See bzrlib.transport.Server.get_bogus_url."""
503
 
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
 
468
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
504
469
        # we bind a random socket, so that we get a guaranteed unused port
505
470
        # we just never listen on that port
506
471
        s = socket.socket()
526
491
    def __init__(self):
527
492
        super(SFTPServerWithoutSSH, self).__init__()
528
493
        self._vendor = ssh.LoopbackVendor()
529
 
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
530
 
 
531
 
    def get_host_key():
532
 
        return None
 
494
 
 
495
    def _run_server(self, sock):
 
496
        # Re-import these as locals, so that they're still accessible during
 
497
        # interpreter shutdown (when all module globals get set to None, leading
 
498
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
499
        class FakeChannel(object):
 
500
            def get_transport(self):
 
501
                return self
 
502
            def get_log_channel(self):
 
503
                return 'paramiko'
 
504
            def get_name(self):
 
505
                return '1'
 
506
            def get_hexdump(self):
 
507
                return False
 
508
            def close(self):
 
509
                pass
 
510
 
 
511
        server = paramiko.SFTPServer(
 
512
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
513
            root=self._root, home=self._server_homedir)
 
514
        try:
 
515
            server.start_subsystem(
 
516
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
 
517
        except socket.error, e:
 
518
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
519
                # it's okay for the client to disconnect abruptly
 
520
                # (bug in paramiko 1.6: it should absorb this exception)
 
521
                pass
 
522
            else:
 
523
                raise
 
524
        except Exception, e:
 
525
            # This typically seems to happen during interpreter shutdown, so
 
526
            # most of the useful ways to report this error are won't work.
 
527
            # Writing the exception type, and then the text of the exception,
 
528
            # seems to be the best we can do.
 
529
            import sys
 
530
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
531
            sys.stderr.write('%s\n\n' % (e,))
 
532
        server.finish_subsystem()
533
533
 
534
534
 
535
535
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
558
558
    It does this by serving from a deeply-nested directory that doesn't exist.
559
559
    """
560
560
 
561
 
    def create_server(self):
562
 
        # FIXME: Can't we do that in a cleaner way ? -- vila 20100623
563
 
        server = super(SFTPSiblingAbsoluteServer, self).create_server()
564
 
        server._server_homedir = '/dev/noone/runs/tests/here'
565
 
        return server
 
561
    def start_server(self, backing_server=None):
 
562
        self._server_homedir = '/dev/noone/runs/tests/here'
 
563
        super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)
566
564