~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/stub_sftp.py

  • Committer: Andrew Bennetts
  • Date: 2010-09-17 04:35:23 UTC
  • mfrom: (5050.17.20 2.2)
  • mto: This revision was merged to the branch mainline in revision 5431.
  • Revision ID: andrew.bennetts@canonical.com-20100917043523-c5t63gmvxqxmqh5j
Merge lp:bzr/2.2, including fixes for #625574, #636930, #254278.

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
26
27
import sys
27
28
import threading
28
29
import time
38
39
from bzrlib.tests import test_server
39
40
 
40
41
 
41
 
class StubServer (paramiko.ServerInterface):
 
42
class StubServer(paramiko.ServerInterface):
42
43
 
43
 
    def __init__(self, test_case):
 
44
    def __init__(self, test_case_server):
44
45
        paramiko.ServerInterface.__init__(self)
45
 
        self._test_case = test_case
 
46
        self.log = test_case_server.log
46
47
 
47
48
    def check_auth_password(self, username, password):
48
49
        # all are allowed
49
 
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
 
50
        self.log('sftpserver - authorizing: %s' % (username,))
50
51
        return paramiko.AUTH_SUCCESSFUL
51
52
 
52
53
    def check_channel_request(self, kind, chanid):
53
 
        self._test_case.log(
54
 
            'sftpserver - channel request: %s, %s' % (kind, chanid))
 
54
        self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
55
55
        return paramiko.OPEN_SUCCEEDED
56
56
 
57
57
 
58
 
class StubSFTPHandle (paramiko.SFTPHandle):
 
58
class StubSFTPHandle(paramiko.SFTPHandle):
 
59
 
59
60
    def stat(self):
60
61
        try:
61
62
            return paramiko.SFTPAttributes.from_stat(
73
74
            return paramiko.SFTPServer.convert_errno(e.errno)
74
75
 
75
76
 
76
 
class StubSFTPServer (paramiko.SFTPServerInterface):
 
77
class StubSFTPServer(paramiko.SFTPServerInterface):
77
78
 
78
79
    def __init__(self, server, root, home=None):
79
80
        paramiko.SFTPServerInterface.__init__(self, server)
90
91
            self.home = home[len(self.root):]
91
92
        if self.home.startswith('/'):
92
93
            self.home = self.home[1:]
93
 
        server._test_case.log('sftpserver - new connection')
 
94
        server.log('sftpserver - new connection')
94
95
 
95
96
    def _realpath(self, path):
96
97
        # paths returned from self.canonicalize() always start with
135
136
        try:
136
137
            out = [ ]
137
138
            # TODO: win32 incorrectly lists paths with non-ascii if path is not
138
 
            # unicode. However on Linux the server should only deal with
 
139
            # unicode. However on unix the server should only deal with
139
140
            # bytestreams and posix.listdir does the right thing
140
141
            if sys.platform == 'win32':
141
142
                flist = [f.encode('utf8') for f in os.listdir(path)]
241
242
    # removed: chattr, symlink, readlink
242
243
    # (nothing in bzr's sftp transport uses those)
243
244
 
 
245
 
244
246
# ------------- server test implementation --------------
245
247
 
246
248
STUB_SERVER_KEY = """
262
264
"""
263
265
 
264
266
 
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
 
 
310
267
class SocketDelay(object):
311
268
    """A socket decorator to make TCP appear slower.
312
269
 
382
339
        return bytes_sent
383
340
 
384
341
 
385
 
class SFTPServer(test_server.TestServer):
 
342
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
 
343
 
 
344
    def setup(self):
 
345
        self.wrap_for_latency()
 
346
        tcs = self.server.test_case_server
 
347
        ptrans = paramiko.Transport(self.request)
 
348
        self.paramiko_transport = ptrans
 
349
        # Set it to a channel under 'bzr' so that we get debug info
 
350
        ptrans.set_log_channel('bzr.paramiko.transport')
 
351
        ptrans.add_server_key(tcs.get_host_key())
 
352
        ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
353
                                     StubSFTPServer, root=tcs._root,
 
354
                                     home=tcs._server_homedir)
 
355
        server = tcs._server_interface(tcs)
 
356
        # This blocks until the key exchange has been done
 
357
        ptrans.start_server(None, server)
 
358
 
 
359
    def finish(self):
 
360
        # Wait for the conversation to finish, when the paramiko.Transport
 
361
        # thread finishes
 
362
        # TODO: Consider timing out after XX seconds rather than hanging.
 
363
        #       Also we could check paramiko_transport.active and possibly
 
364
        #       paramiko_transport.getException().
 
365
        self.paramiko_transport.join()
 
366
 
 
367
    def wrap_for_latency(self):
 
368
        tcs = self.server.test_case_server
 
369
        if tcs.add_latency:
 
370
            # Give the socket (which the request really is) a latency adding
 
371
            # decorator.
 
372
            self.request = SocketDelay(self.request, tcs.add_latency)
 
373
 
 
374
 
 
375
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
 
376
 
 
377
    def setup(self):
 
378
        self.wrap_for_latency()
 
379
        # Re-import these as locals, so that they're still accessible during
 
380
        # interpreter shutdown (when all module globals get set to None, leading
 
381
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
382
        class FakeChannel(object):
 
383
            def get_transport(self):
 
384
                return self
 
385
            def get_log_channel(self):
 
386
                return 'bzr.paramiko'
 
387
            def get_name(self):
 
388
                return '1'
 
389
            def get_hexdump(self):
 
390
                return False
 
391
            def close(self):
 
392
                pass
 
393
 
 
394
        tcs = self.server.test_case_server
 
395
        sftp_server = paramiko.SFTPServer(
 
396
            FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
 
397
            root=tcs._root, home=tcs._server_homedir)
 
398
        self.sftp_server = sftp_server
 
399
        sys_stderr = sys.stderr # Used in error reporting during shutdown
 
400
        try:
 
401
            sftp_server.start_subsystem(
 
402
                'sftp', None, ssh.SocketAsChannelAdapter(self.request))
 
403
        except socket.error, e:
 
404
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
405
                # it's okay for the client to disconnect abruptly
 
406
                # (bug in paramiko 1.6: it should absorb this exception)
 
407
                pass
 
408
            else:
 
409
                raise
 
410
        except Exception, e:
 
411
            # This typically seems to happen during interpreter shutdown, so
 
412
            # most of the useful ways to report this error won't work.
 
413
            # Writing the exception type, and then the text of the exception,
 
414
            # seems to be the best we can do.
 
415
            # FIXME: All interpreter shutdown errors should have been related
 
416
            # to daemon threads, cleanup needed -- vila 20100623
 
417
            sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
418
            sys_stderr.write('%s\n\n' % (e,))
 
419
 
 
420
    def finish(self):
 
421
        self.sftp_server.finish_subsystem()
 
422
 
 
423
 
 
424
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
 
425
 
 
426
    def __init__(self, server_address, request_handler_class, test_case_server):
 
427
        test_server.TestingThreadingTCPServer.__init__(
 
428
            self, server_address, request_handler_class)
 
429
        self.test_case_server = test_case_server
 
430
 
 
431
 
 
432
class SFTPServer(test_server.TestingTCPServerInAThread):
386
433
    """Common code for SFTP server facilities."""
387
434
 
388
435
    def __init__(self, server_interface=StubServer):
 
436
        self.host = '127.0.0.1'
 
437
        self.port = 0
 
438
        super(SFTPServer, self).__init__((self.host, self.port),
 
439
                                         TestingSFTPServer,
 
440
                                         TestingSFTPConnectionHandler)
389
441
        self._original_vendor = None
390
 
        self._homedir = None
391
 
        self._server_homedir = None
392
 
        self._listener = None
393
 
        self._root = None
394
442
        self._vendor = ssh.ParamikoVendor()
395
443
        self._server_interface = server_interface
396
 
        # sftp server logs
 
444
        self._host_key = None
397
445
        self.logs = []
398
446
        self.add_latency = 0
 
447
        self._homedir = None
 
448
        self._server_homedir = None
 
449
        self._root = None
399
450
 
400
451
    def _get_sftp_url(self, path):
401
452
        """Calculate an sftp url to this server for path."""
402
 
        return 'sftp://foo:bar@%s:%d/%s' % (self._listener.host,
403
 
                                            self._listener.port, path)
 
453
        return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
404
454
 
405
455
    def log(self, message):
406
456
        """StubServer uses this to log when a new server is created."""
407
457
        self.logs.append(message)
408
458
 
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)
 
459
    def create_server(self):
 
460
        server = self.server_class((self.host, self.port),
 
461
                                   self.request_handler_class,
 
462
                                   self)
 
463
        return server
 
464
 
 
465
    def get_host_key(self):
 
466
        if self._host_key is None:
 
467
            key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
 
468
            f = open(key_file, 'w')
 
469
            try:
 
470
                f.write(STUB_SERVER_KEY)
 
471
            finally:
 
472
                f.close()
 
473
            self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
474
        return self._host_key
434
475
 
435
476
    def start_server(self, backing_server=None):
436
477
        # XXX: TODO: make sftpserver back onto backing_server rather than local
442
483
                'the local current working directory.' % (backing_server,))
443
484
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
444
485
        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
447
486
        if sys.platform == 'win32':
448
487
            # Win32 needs to use the UNICODE api
449
488
            self._homedir = os.getcwdu()
 
489
            # Normalize the path or it will be wrongly escaped
 
490
            self._homedir = osutils.normpath(self._homedir)
450
491
        else:
451
 
            # But Linux SFTP servers should just deal in bytestreams
 
492
            # But unix SFTP servers should just deal in bytestreams
452
493
            self._homedir = os.getcwd()
453
494
        if self._server_homedir is None:
454
495
            self._server_homedir = self._homedir
455
496
        self._root = '/'
456
497
        if sys.platform == 'win32':
457
498
            self._root = ''
458
 
        self._listener = SocketListener(self._run_server_entry)
459
 
        self._listener.setDaemon(True)
460
 
        self._listener.start()
 
499
        super(SFTPServer, self).start_server()
461
500
 
462
501
    def stop_server(self):
463
 
        self._listener.stop()
464
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
502
        try:
 
503
            super(SFTPServer, self).stop_server()
 
504
        finally:
 
505
            ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
465
506
 
466
507
    def get_bogus_url(self):
467
508
        """See bzrlib.transport.Server.get_bogus_url."""
468
 
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
 
509
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
469
510
        # we bind a random socket, so that we get a guaranteed unused port
470
511
        # we just never listen on that port
471
512
        s = socket.socket()
491
532
    def __init__(self):
492
533
        super(SFTPServerWithoutSSH, self).__init__()
493
534
        self._vendor = ssh.LoopbackVendor()
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()
 
535
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
 
536
 
 
537
    def get_host_key():
 
538
        return None
533
539
 
534
540
 
535
541
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
558
564
    It does this by serving from a deeply-nested directory that doesn't exist.
559
565
    """
560
566
 
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)
 
567
    def create_server(self):
 
568
        # FIXME: Can't we do that in a cleaner way ? -- vila 20100623
 
569
        server = super(SFTPSiblingAbsoluteServer, self).create_server()
 
570
        server._server_homedir = '/dev/noone/runs/tests/here'
 
571
        return server
564
572