23
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
 
 
24
    SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
 
35
 
from bzrlib.transport import (
 
38
 
from bzrlib.tests import test_server
 
41
 
class StubServer (paramiko.ServerInterface):
 
 
27
from bzrlib.osutils import pathjoin
 
 
28
from bzrlib.trace import mutter
 
 
31
class StubServer (ServerInterface):
 
43
33
    def __init__(self, test_case):
 
44
 
        paramiko.ServerInterface.__init__(self)
 
 
34
        ServerInterface.__init__(self)
 
45
35
        self._test_case = test_case
 
47
37
    def check_auth_password(self, username, password):
 
49
39
        self._test_case.log('sftpserver - authorizing: %s' % (username,))
 
50
 
        return paramiko.AUTH_SUCCESSFUL
 
 
40
        return AUTH_SUCCESSFUL
 
52
42
    def check_channel_request(self, kind, chanid):
 
54
 
            'sftpserver - channel request: %s, %s' % (kind, chanid))
 
55
 
        return paramiko.OPEN_SUCCEEDED
 
58
 
class StubSFTPHandle (paramiko.SFTPHandle):
 
 
43
        self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
 
 
47
class StubSFTPHandle (SFTPHandle):
 
61
 
            return paramiko.SFTPAttributes.from_stat(
 
62
 
                os.fstat(self.readfile.fileno()))
 
 
50
            return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
 
64
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
 
52
            return SFTPServer.convert_errno(e.errno)
 
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)
 
71
 
            paramiko.SFTPServer.set_file_attr(self.filename, attr)
 
 
59
            SFTPServer.set_file_attr(self.filename, attr)
 
73
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
76
 
class StubSFTPServer (paramiko.SFTPServerInterface):
 
 
61
            return SFTPServer.convert_errno(e.errno)
 
 
64
class StubSFTPServer (SFTPServerInterface):
 
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().
 
86
 
            if not home.startswith(self.root):
 
88
 
                    "home must be a subdirectory of root (%s vs %s)"
 
 
74
            assert home.startswith(self.root), \
 
 
75
                    "home must be a subdirectory of root (%s vs %s)" \
 
90
77
            self.home = home[len(self.root):]
 
91
78
        if self.home.startswith('/'):
 
92
79
            self.home = self.home[1:]
 
 
226
212
            if attr is not None:
 
227
213
                attr._flags &= ~attr.FLAG_PERMISSIONS
 
228
 
                paramiko.SFTPServer.set_file_attr(path, attr)
 
 
214
                SFTPServer.set_file_attr(path, attr)
 
229
215
        except OSError, e:
 
230
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
231
 
        return paramiko.SFTP_OK
 
 
216
            return SFTPServer.convert_errno(e.errno)
 
233
219
    def rmdir(self, path):
 
234
220
        path = self._realpath(path)
 
237
223
        except OSError, e:
 
238
 
            return paramiko.SFTPServer.convert_errno(e.errno)
 
239
 
        return paramiko.SFTP_OK
 
 
224
            return SFTPServer.convert_errno(e.errno)
 
241
227
    # removed: chattr, symlink, readlink
 
242
228
    # (nothing in bzr's sftp transport uses those)
 
244
 
# ------------- server test implementation --------------
 
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-----
 
265
 
class SocketListener(threading.Thread):
 
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()
 
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.
 
287
 
            readable, writable_unused, exception_unused = \
 
288
 
                select.select([self._socket], [], [], 0.1)
 
289
 
            if self._stop_event.isSet():
 
291
 
            if len(readable) == 0:
 
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)
 
303
 
                # probably a failed test; unit test thread will log the
 
305
 
                sys.excepthook(*sys.exc_info())
 
307
 
                    'Exception from within unit test server thread: %r' % x)
 
310
 
class SocketDelay(object):
 
311
 
    """A socket decorator to make TCP appear slower.
 
313
 
    This changes recv, send, and sendall to add a fixed latency to each python
 
314
 
    call if a new roundtrip is detected. That is, when a recv is called and the
 
315
 
    flag new_roundtrip is set, latency is charged. Every send and send_all
 
318
 
    In addition every send, sendall and recv sleeps a bit per character send to
 
321
 
    Not all methods are implemented, this is deliberate as this class is not a
 
322
 
    replacement for the builtin sockets layer. fileno is not implemented to
 
323
 
    prevent the proxy being bypassed.
 
327
 
    _proxied_arguments = dict.fromkeys([
 
328
 
        "close", "getpeername", "getsockname", "getsockopt", "gettimeout",
 
329
 
        "setblocking", "setsockopt", "settimeout", "shutdown"])
 
331
 
    def __init__(self, sock, latency, bandwidth=1.0,
 
334
 
        :param bandwith: simulated bandwith (MegaBit)
 
335
 
        :param really_sleep: If set to false, the SocketDelay will just
 
336
 
        increase a counter, instead of calling time.sleep. This is useful for
 
337
 
        unittesting the SocketDelay.
 
340
 
        self.latency = latency
 
341
 
        self.really_sleep = really_sleep
 
342
 
        self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
 
343
 
        self.new_roundtrip = False
 
346
 
        if self.really_sleep:
 
349
 
            SocketDelay.simulated_time += s
 
351
 
    def __getattr__(self, attr):
 
352
 
        if attr in SocketDelay._proxied_arguments:
 
353
 
            return getattr(self.sock, attr)
 
354
 
        raise AttributeError("'SocketDelay' object has no attribute %r" %
 
358
 
        return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
 
361
 
    def recv(self, *args):
 
362
 
        data = self.sock.recv(*args)
 
363
 
        if data and self.new_roundtrip:
 
364
 
            self.new_roundtrip = False
 
365
 
            self.sleep(self.latency)
 
366
 
        self.sleep(len(data) * self.time_per_byte)
 
369
 
    def sendall(self, data, flags=0):
 
370
 
        if not self.new_roundtrip:
 
371
 
            self.new_roundtrip = True
 
372
 
            self.sleep(self.latency)
 
373
 
        self.sleep(len(data) * self.time_per_byte)
 
374
 
        return self.sock.sendall(data, flags)
 
376
 
    def send(self, data, flags=0):
 
377
 
        if not self.new_roundtrip:
 
378
 
            self.new_roundtrip = True
 
379
 
            self.sleep(self.latency)
 
380
 
        bytes_sent = self.sock.send(data, flags)
 
381
 
        self.sleep(bytes_sent * self.time_per_byte)
 
385
 
class SFTPServer(test_server.TestServer):
 
386
 
    """Common code for SFTP server facilities."""
 
388
 
    def __init__(self, server_interface=StubServer):
 
389
 
        self._original_vendor = None
 
391
 
        self._server_homedir = None
 
392
 
        self._listener = None
 
394
 
        self._vendor = ssh.ParamikoVendor()
 
395
 
        self._server_interface = server_interface
 
400
 
    def _get_sftp_url(self, path):
 
401
 
        """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)
 
405
 
    def log(self, message):
 
406
 
        """StubServer uses this to log when a new server is created."""
 
407
 
        self.logs.append(message)
 
409
 
    def _run_server_entry(self, sock):
 
410
 
        """Entry point for all implementations of _run_server.
 
412
 
        If self.add_latency is > 0.000001 then sock is given a latency adding
 
415
 
        if self.add_latency > 0.000001:
 
416
 
            sock = SocketDelay(sock, self.add_latency)
 
417
 
        return self._run_server(sock)
 
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)
 
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)
 
435
 
    def start_server(self, backing_server=None):
 
436
 
        # XXX: TODO: make sftpserver back onto backing_server rather than local
 
438
 
        if not (backing_server is None or
 
439
 
                isinstance(backing_server, test_server.LocalURLServer)):
 
440
 
            raise AssertionError(
 
441
 
                'backing_server should not be %r, because this can only serve '
 
442
 
                'the local current working directory.' % (backing_server,))
 
443
 
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
 
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
 
447
 
        if sys.platform == 'win32':
 
448
 
            # Win32 needs to use the UNICODE api
 
449
 
            self._homedir = os.getcwdu()
 
451
 
            # But Linux SFTP servers should just deal in bytestreams
 
452
 
            self._homedir = os.getcwd()
 
453
 
        if self._server_homedir is None:
 
454
 
            self._server_homedir = self._homedir
 
456
 
        if sys.platform == 'win32':
 
458
 
        self._listener = SocketListener(self._run_server_entry)
 
459
 
        self._listener.setDaemon(True)
 
460
 
        self._listener.start()
 
462
 
    def stop_server(self):
 
463
 
        self._listener.stop()
 
464
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
466
 
    def get_bogus_url(self):
 
467
 
        """See bzrlib.transport.Server.get_bogus_url."""
 
468
 
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
 
469
 
        # we bind a random socket, so that we get a guaranteed unused port
 
470
 
        # we just never listen on that port
 
472
 
        s.bind(('localhost', 0))
 
473
 
        return 'sftp://%s:%s/' % s.getsockname()
 
476
 
class SFTPFullAbsoluteServer(SFTPServer):
 
477
 
    """A test server for sftp transports, using absolute urls and ssh."""
 
480
 
        """See bzrlib.transport.Server.get_url."""
 
481
 
        homedir = self._homedir
 
482
 
        if sys.platform != 'win32':
 
483
 
            # Remove the initial '/' on all platforms but win32
 
484
 
            homedir = homedir[1:]
 
485
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
488
 
class SFTPServerWithoutSSH(SFTPServer):
 
489
 
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
 
492
 
        super(SFTPServerWithoutSSH, self).__init__()
 
493
 
        self._vendor = ssh.LoopbackVendor()
 
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):
 
502
 
            def get_log_channel(self):
 
506
 
            def get_hexdump(self):
 
511
 
        server = paramiko.SFTPServer(
 
512
 
            FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
 
513
 
            root=self._root, home=self._server_homedir)
 
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)
 
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.
 
530
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
 
531
 
            sys.stderr.write('%s\n\n' % (e,))
 
532
 
        server.finish_subsystem()
 
535
 
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
 
536
 
    """A test server for sftp transports, using absolute urls."""
 
539
 
        """See bzrlib.transport.Server.get_url."""
 
540
 
        homedir = self._homedir
 
541
 
        if sys.platform != 'win32':
 
542
 
            # Remove the initial '/' on all platforms but win32
 
543
 
            homedir = homedir[1:]
 
544
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
547
 
class SFTPHomeDirServer(SFTPServerWithoutSSH):
 
548
 
    """A test server for sftp transports, using homedir relative urls."""
 
551
 
        """See bzrlib.transport.Server.get_url."""
 
552
 
        return self._get_sftp_url("~/")
 
555
 
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
 
556
 
    """A test server for sftp transports where only absolute paths will work.
 
558
 
    It does this by serving from a deeply-nested directory that doesn't exist.
 
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)