/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/stub_sftp.py

  • Committer: Jelmer Vernooij
  • Date: 2017-07-23 22:06:41 UTC
  • mfrom: (6738 trunk)
  • mto: This revision was merged to the branch mainline in revision 6739.
  • Revision ID: jelmer@jelmer.uk-20170723220641-69eczax9bmv8d6kk
Merge trunk, address review comments.

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, 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
21
21
 
22
22
import os
23
23
import paramiko
24
 
import select
25
24
import socket
 
25
try:
 
26
    import socketserver
 
27
except ImportError:
 
28
    import SocketServer as socketserver
26
29
import sys
27
 
import threading
28
30
import time
29
31
 
30
 
from bzrlib import (
 
32
from .. import (
31
33
    osutils,
32
34
    trace,
33
35
    urlutils,
34
36
    )
35
 
from bzrlib.transport import (
 
37
from ..transport import (
36
38
    ssh,
37
39
    )
38
 
from bzrlib.tests import test_server
39
 
 
40
 
 
41
 
class StubServer (paramiko.ServerInterface):
42
 
 
43
 
    def __init__(self, test_case):
 
40
from . import test_server
 
41
 
 
42
 
 
43
class StubServer(paramiko.ServerInterface):
 
44
 
 
45
    def __init__(self, test_case_server):
44
46
        paramiko.ServerInterface.__init__(self)
45
 
        self._test_case = test_case
 
47
        self.log = test_case_server.log
46
48
 
47
49
    def check_auth_password(self, username, password):
48
50
        # all are allowed
49
 
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
 
51
        self.log('sftpserver - authorizing: %s' % (username,))
50
52
        return paramiko.AUTH_SUCCESSFUL
51
53
 
52
54
    def check_channel_request(self, kind, chanid):
53
 
        self._test_case.log(
54
 
            'sftpserver - channel request: %s, %s' % (kind, chanid))
 
55
        self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
55
56
        return paramiko.OPEN_SUCCEEDED
56
57
 
57
58
 
58
 
class StubSFTPHandle (paramiko.SFTPHandle):
 
59
class StubSFTPHandle(paramiko.SFTPHandle):
 
60
 
59
61
    def stat(self):
60
62
        try:
61
63
            return paramiko.SFTPAttributes.from_stat(
62
64
                os.fstat(self.readfile.fileno()))
63
 
        except OSError, e:
 
65
        except OSError as e:
64
66
            return paramiko.SFTPServer.convert_errno(e.errno)
65
67
 
66
68
    def chattr(self, attr):
69
71
        trace.mutter('Changing permissions on %s to %s', self.filename, attr)
70
72
        try:
71
73
            paramiko.SFTPServer.set_file_attr(self.filename, attr)
72
 
        except OSError, e:
 
74
        except OSError as e:
73
75
            return paramiko.SFTPServer.convert_errno(e.errno)
74
76
 
75
77
 
76
 
class StubSFTPServer (paramiko.SFTPServerInterface):
 
78
class StubSFTPServer(paramiko.SFTPServerInterface):
77
79
 
78
80
    def __init__(self, server, root, home=None):
79
81
        paramiko.SFTPServerInterface.__init__(self, server)
90
92
            self.home = home[len(self.root):]
91
93
        if self.home.startswith('/'):
92
94
            self.home = self.home[1:]
93
 
        server._test_case.log('sftpserver - new connection')
 
95
        server.log('sftpserver - new connection')
94
96
 
95
97
    def _realpath(self, path):
96
98
        # paths returned from self.canonicalize() always start with
119
121
    else:
120
122
        def canonicalize(self, path):
121
123
            if os.path.isabs(path):
122
 
                return os.path.normpath(path)
 
124
                return osutils.normpath(path)
123
125
            else:
124
 
                return os.path.normpath('/' + os.path.join(self.home, path))
 
126
                return osutils.normpath('/' + os.path.join(self.home, path))
125
127
 
126
128
    def chattr(self, path, attr):
127
129
        try:
128
130
            paramiko.SFTPServer.set_file_attr(path, attr)
129
 
        except OSError, e:
 
131
        except OSError as e:
130
132
            return paramiko.SFTPServer.convert_errno(e.errno)
131
133
        return paramiko.SFTP_OK
132
134
 
135
137
        try:
136
138
            out = [ ]
137
139
            # TODO: win32 incorrectly lists paths with non-ascii if path is not
138
 
            # unicode. However on Linux the server should only deal with
 
140
            # unicode. However on unix the server should only deal with
139
141
            # bytestreams and posix.listdir does the right thing
140
142
            if sys.platform == 'win32':
141
143
                flist = [f.encode('utf8') for f in os.listdir(path)]
147
149
                attr.filename = fname
148
150
                out.append(attr)
149
151
            return out
150
 
        except OSError, e:
 
152
        except OSError as e:
151
153
            return paramiko.SFTPServer.convert_errno(e.errno)
152
154
 
153
155
    def stat(self, path):
154
156
        path = self._realpath(path)
155
157
        try:
156
158
            return paramiko.SFTPAttributes.from_stat(os.stat(path))
157
 
        except OSError, e:
 
159
        except OSError as e:
158
160
            return paramiko.SFTPServer.convert_errno(e.errno)
159
161
 
160
162
    def lstat(self, path):
161
163
        path = self._realpath(path)
162
164
        try:
163
165
            return paramiko.SFTPAttributes.from_stat(os.lstat(path))
164
 
        except OSError, e:
 
166
        except OSError as e:
165
167
            return paramiko.SFTPServer.convert_errno(e.errno)
166
168
 
167
169
    def open(self, path, flags, attr):
173
175
            else:
174
176
                # os.open() defaults to 0777 which is
175
177
                # an odd default mode for files
176
 
                fd = os.open(path, flags, 0666)
177
 
        except OSError, e:
 
178
                fd = os.open(path, flags, 0o666)
 
179
        except OSError as e:
178
180
            return paramiko.SFTPServer.convert_errno(e.errno)
179
181
 
180
182
        if (flags & os.O_CREAT) and (attr is not None):
189
191
            fstr = 'rb'
190
192
        try:
191
193
            f = os.fdopen(fd, fstr)
192
 
        except (IOError, OSError), e:
 
194
        except (IOError, OSError) as e:
193
195
            return paramiko.SFTPServer.convert_errno(e.errno)
194
196
        fobj = StubSFTPHandle()
195
197
        fobj.filename = path
201
203
        path = self._realpath(path)
202
204
        try:
203
205
            os.remove(path)
204
 
        except OSError, e:
 
206
        except OSError as e:
205
207
            return paramiko.SFTPServer.convert_errno(e.errno)
206
208
        return paramiko.SFTP_OK
207
209
 
210
212
        newpath = self._realpath(newpath)
211
213
        try:
212
214
            os.rename(oldpath, newpath)
213
 
        except OSError, e:
 
215
        except OSError as e:
214
216
            return paramiko.SFTPServer.convert_errno(e.errno)
215
217
        return paramiko.SFTP_OK
216
218
 
226
228
            if attr is not None:
227
229
                attr._flags &= ~attr.FLAG_PERMISSIONS
228
230
                paramiko.SFTPServer.set_file_attr(path, attr)
229
 
        except OSError, e:
 
231
        except OSError as e:
230
232
            return paramiko.SFTPServer.convert_errno(e.errno)
231
233
        return paramiko.SFTP_OK
232
234
 
234
236
        path = self._realpath(path)
235
237
        try:
236
238
            os.rmdir(path)
237
 
        except OSError, e:
 
239
        except OSError as e:
238
240
            return paramiko.SFTPServer.convert_errno(e.errno)
239
241
        return paramiko.SFTP_OK
240
242
 
241
243
    # removed: chattr, symlink, readlink
242
244
    # (nothing in bzr's sftp transport uses those)
243
245
 
 
246
 
244
247
# ------------- server test implementation --------------
245
248
 
246
249
STUB_SERVER_KEY = """
262
265
"""
263
266
 
264
267
 
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
268
class SocketDelay(object):
315
269
    """A socket decorator to make TCP appear slower.
316
270
 
386
340
        return bytes_sent
387
341
 
388
342
 
389
 
class SFTPServer(test_server.TestServer):
 
343
class TestingSFTPConnectionHandler(socketserver.BaseRequestHandler):
 
344
 
 
345
    def setup(self):
 
346
        self.wrap_for_latency()
 
347
        tcs = self.server.test_case_server
 
348
        ptrans = paramiko.Transport(self.request)
 
349
        self.paramiko_transport = ptrans
 
350
        # Set it to a channel under 'bzr' so that we get debug info
 
351
        ptrans.set_log_channel('brz.paramiko.transport')
 
352
        ptrans.add_server_key(tcs.get_host_key())
 
353
        ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
 
354
                                     StubSFTPServer, root=tcs._root,
 
355
                                     home=tcs._server_homedir)
 
356
        server = tcs._server_interface(tcs)
 
357
        # This blocks until the key exchange has been done
 
358
        ptrans.start_server(None, server)
 
359
 
 
360
    def finish(self):
 
361
        # Wait for the conversation to finish, when the paramiko.Transport
 
362
        # thread finishes
 
363
        # TODO: Consider timing out after XX seconds rather than hanging.
 
364
        #       Also we could check paramiko_transport.active and possibly
 
365
        #       paramiko_transport.getException().
 
366
        self.paramiko_transport.join()
 
367
 
 
368
    def wrap_for_latency(self):
 
369
        tcs = self.server.test_case_server
 
370
        if tcs.add_latency:
 
371
            # Give the socket (which the request really is) a latency adding
 
372
            # decorator.
 
373
            self.request = SocketDelay(self.request, tcs.add_latency)
 
374
 
 
375
 
 
376
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
 
377
 
 
378
    def setup(self):
 
379
        self.wrap_for_latency()
 
380
        # Re-import these as locals, so that they're still accessible during
 
381
        # interpreter shutdown (when all module globals get set to None, leading
 
382
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
383
        class FakeChannel(object):
 
384
            def get_transport(self):
 
385
                return self
 
386
            def get_log_channel(self):
 
387
                return 'brz.paramiko'
 
388
            def get_name(self):
 
389
                return '1'
 
390
            def get_hexdump(self):
 
391
                return False
 
392
            def close(self):
 
393
                pass
 
394
 
 
395
        tcs = self.server.test_case_server
 
396
        sftp_server = paramiko.SFTPServer(
 
397
            FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
 
398
            root=tcs._root, home=tcs._server_homedir)
 
399
        self.sftp_server = sftp_server
 
400
        sys_stderr = sys.stderr # Used in error reporting during shutdown
 
401
        try:
 
402
            sftp_server.start_subsystem(
 
403
                'sftp', None, ssh.SocketAsChannelAdapter(self.request))
 
404
        except socket.error as e:
 
405
            if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
 
406
                # it's okay for the client to disconnect abruptly
 
407
                # (bug in paramiko 1.6: it should absorb this exception)
 
408
                pass
 
409
            else:
 
410
                raise
 
411
        except Exception as e:
 
412
            # This typically seems to happen during interpreter shutdown, so
 
413
            # most of the useful ways to report this error won't work.
 
414
            # Writing the exception type, and then the text of the exception,
 
415
            # seems to be the best we can do.
 
416
            # FIXME: All interpreter shutdown errors should have been related
 
417
            # to daemon threads, cleanup needed -- vila 20100623
 
418
            sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
419
            sys_stderr.write('%s\n\n' % (e,))
 
420
 
 
421
    def finish(self):
 
422
        self.sftp_server.finish_subsystem()
 
423
 
 
424
 
 
425
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
 
426
 
 
427
    def __init__(self, server_address, request_handler_class, test_case_server):
 
428
        test_server.TestingThreadingTCPServer.__init__(
 
429
            self, server_address, request_handler_class)
 
430
        self.test_case_server = test_case_server
 
431
 
 
432
 
 
433
class SFTPServer(test_server.TestingTCPServerInAThread):
390
434
    """Common code for SFTP server facilities."""
391
435
 
392
436
    def __init__(self, server_interface=StubServer):
 
437
        self.host = '127.0.0.1'
 
438
        self.port = 0
 
439
        super(SFTPServer, self).__init__((self.host, self.port),
 
440
                                         TestingSFTPServer,
 
441
                                         TestingSFTPConnectionHandler)
393
442
        self._original_vendor = None
394
 
        self._homedir = None
395
 
        self._server_homedir = None
396
 
        self._listener = None
397
 
        self._root = None
398
443
        self._vendor = ssh.ParamikoVendor()
399
444
        self._server_interface = server_interface
400
 
        # sftp server logs
 
445
        self._host_key = None
401
446
        self.logs = []
402
447
        self.add_latency = 0
 
448
        self._homedir = None
 
449
        self._server_homedir = None
 
450
        self._root = None
403
451
 
404
452
    def _get_sftp_url(self, path):
405
453
        """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)
 
454
        return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
408
455
 
409
456
    def log(self, message):
410
457
        """StubServer uses this to log when a new server is created."""
411
458
        self.logs.append(message)
412
459
 
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)
 
460
    def create_server(self):
 
461
        server = self.server_class((self.host, self.port),
 
462
                                   self.request_handler_class,
 
463
                                   self)
 
464
        return server
 
465
 
 
466
    def get_host_key(self):
 
467
        if self._host_key is None:
 
468
            key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
 
469
            f = open(key_file, 'w')
 
470
            try:
 
471
                f.write(STUB_SERVER_KEY)
 
472
            finally:
 
473
                f.close()
 
474
            self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
475
        return self._host_key
438
476
 
439
477
    def start_server(self, backing_server=None):
440
478
        # XXX: TODO: make sftpserver back onto backing_server rather than local
446
484
                'the local current working directory.' % (backing_server,))
447
485
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
448
486
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
449
 
        # FIXME: the following block should certainly just be self._homedir =
450
 
        # osutils.getcwd() but that fails badly on Unix -- vila 20100224
 
487
        self._homedir = osutils.getcwd()
451
488
        if sys.platform == 'win32':
452
 
            # Win32 needs to use the UNICODE api
453
 
            self._homedir = os.getcwdu()
 
489
            # Normalize the path or it will be wrongly escaped
 
490
            self._homedir = osutils.normpath(self._homedir)
454
491
        else:
455
 
            # But Linux SFTP servers should just deal in bytestreams
456
 
            self._homedir = os.getcwd()
 
492
            # But unix SFTP servers should just deal in bytestreams
 
493
            self._homedir = self._homedir.encode('utf-8')
457
494
        if self._server_homedir is None:
458
495
            self._server_homedir = self._homedir
459
496
        self._root = '/'
460
497
        if sys.platform == 'win32':
461
498
            self._root = ''
462
 
        self._listener = SocketListener(self._run_server_entry)
463
 
        self._listener.setDaemon(True)
464
 
        self._listener.start()
 
499
        super(SFTPServer, self).start_server()
465
500
 
466
501
    def stop_server(self):
467
 
        self._listener.stop()
468
 
        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
469
506
 
470
507
    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
 
508
        """See breezy.transport.Server.get_bogus_url."""
 
509
        # this is chosen to try to prevent trouble with proxies, weird dns, etc
473
510
        # we bind a random socket, so that we get a guaranteed unused port
474
511
        # we just never listen on that port
475
512
        s = socket.socket()
481
518
    """A test server for sftp transports, using absolute urls and ssh."""
482
519
 
483
520
    def get_url(self):
484
 
        """See bzrlib.transport.Server.get_url."""
 
521
        """See breezy.transport.Server.get_url."""
485
522
        homedir = self._homedir
486
523
        if sys.platform != 'win32':
487
524
            # Remove the initial '/' on all platforms but win32
495
532
    def __init__(self):
496
533
        super(SFTPServerWithoutSSH, self).__init__()
497
534
        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()
 
535
        self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
 
536
 
 
537
    def get_host_key():
 
538
        return None
537
539
 
538
540
 
539
541
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
540
542
    """A test server for sftp transports, using absolute urls."""
541
543
 
542
544
    def get_url(self):
543
 
        """See bzrlib.transport.Server.get_url."""
 
545
        """See breezy.transport.Server.get_url."""
544
546
        homedir = self._homedir
545
547
        if sys.platform != 'win32':
546
548
            # Remove the initial '/' on all platforms but win32
552
554
    """A test server for sftp transports, using homedir relative urls."""
553
555
 
554
556
    def get_url(self):
555
 
        """See bzrlib.transport.Server.get_url."""
556
 
        return self._get_sftp_url("~/")
 
557
        """See breezy.transport.Server.get_url."""
 
558
        return self._get_sftp_url("%7E/")
557
559
 
558
560
 
559
561
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
562
564
    It does this by serving from a deeply-nested directory that doesn't exist.
563
565
    """
564
566
 
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)
 
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
568
572