~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/stub_sftp.py

  • Committer: John Arbash Meinel
  • Date: 2010-02-17 17:11:16 UTC
  • mfrom: (4797.2.17 2.1)
  • mto: (4797.2.18 2.1)
  • mto: This revision was merged to the branch mainline in revision 5055.
  • Revision ID: john@arbash-meinel.com-20100217171116-h7t9223ystbnx5h8
merge bzr.2.1 in preparation for NEWS entry.

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
34
33
    urlutils,
35
34
    )
36
35
from bzrlib.transport import (
 
36
    local,
 
37
    Server,
37
38
    ssh,
38
39
    )
39
 
from bzrlib.tests import test_server
40
 
 
41
 
 
42
 
class StubServer(paramiko.ServerInterface):
43
 
 
44
 
    def __init__(self, test_case_server):
 
40
 
 
41
class StubServer (paramiko.ServerInterface):
 
42
 
 
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
                warning('Socket error during accept() within unit test server'
 
301
                        ' 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
                warning('Exception from within unit test server thread: %r' %
 
307
                        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(Server):
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
472
437
        # disk.
473
438
        if not (backing_server is None or
474
 
                isinstance(backing_server, test_server.LocalURLServer)):
 
439
                isinstance(backing_server, local.LocalURLServer)):
475
440
            raise AssertionError(
476
 
                'backing_server should not be %r, because this can only serve '
477
 
                'the local current working directory.' % (backing_server,))
 
441
                "backing_server should not be %r, because this can only serve the "
 
442
                "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
480
445
        if sys.platform == 'win32':
481
446
            # Win32 needs to use the UNICODE api
482
 
            self._homedir = os.getcwdu()
483
 
            # Normalize the path or it will be wrongly escaped
484
 
            self._homedir = osutils.normpath(self._homedir)
 
447
            self._homedir = getcwd()
485
448
        else:
486
 
            # But unix SFTP servers should just deal in bytestreams
 
449
            # But Linux SFTP servers should just deal in bytestreams
487
450
            self._homedir = os.getcwd()
488
451
        if self._server_homedir is None:
489
452
            self._server_homedir = self._homedir
490
453
        self._root = '/'
491
454
        if sys.platform == 'win32':
492
455
            self._root = ''
493
 
        super(SFTPServer, self).start_server()
 
456
        self._listener = SocketListener(self._run_server_entry)
 
457
        self._listener.setDaemon(True)
 
458
        self._listener.start()
494
459
 
495
460
    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
 
461
        self._listener.stop()
 
462
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
500
463
 
501
464
    def get_bogus_url(self):
502
465
        """See bzrlib.transport.Server.get_bogus_url."""
503
 
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
 
466
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
504
467
        # we bind a random socket, so that we get a guaranteed unused port
505
468
        # we just never listen on that port
506
469
        s = socket.socket()
526
489
    def __init__(self):
527
490
        super(SFTPServerWithoutSSH, self).__init__()
528
491
        self._vendor = ssh.LoopbackVendor()
529
 
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
530
 
 
531
 
    def get_host_key():
532
 
        return None
 
492
 
 
493
    def _run_server(self, sock):
 
494
        # Re-import these as locals, so that they're still accessible during
 
495
        # interpreter shutdown (when all module globals get set to None, leading
 
496
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
497
        class FakeChannel(object):
 
498
            def get_transport(self):
 
499
                return self
 
500
            def get_log_channel(self):
 
501
                return 'paramiko'
 
502
            def get_name(self):
 
503
                return '1'
 
504
            def get_hexdump(self):
 
505
                return False
 
506
            def close(self):
 
507
                pass
 
508
 
 
509
        server = paramiko.SFTPServer(
 
510
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
511
            root=self._root, home=self._server_homedir)
 
512
        try:
 
513
            server.start_subsystem(
 
514
                'sftp', None, ssh.SocketAsChannelAdapter(sock))
 
515
        except socket.error, e:
 
516
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
517
                # it's okay for the client to disconnect abruptly
 
518
                # (bug in paramiko 1.6: it should absorb this exception)
 
519
                pass
 
520
            else:
 
521
                raise
 
522
        except Exception, e:
 
523
            # This typically seems to happen during interpreter shutdown, so
 
524
            # most of the useful ways to report this error are won't work.
 
525
            # Writing the exception type, and then the text of the exception,
 
526
            # seems to be the best we can do.
 
527
            import sys
 
528
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
529
            sys.stderr.write('%s\n\n' % (e,))
 
530
        server.finish_subsystem()
533
531
 
534
532
 
535
533
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
558
556
    It does this by serving from a deeply-nested directory that doesn't exist.
559
557
    """
560
558
 
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
 
559
    def start_server(self, backing_server=None):
 
560
        self._server_homedir = '/dev/noone/runs/tests/here'
 
561
        super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)
566
562