23
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
24
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
34
from bzrlib.transport import (
37
from bzrlib.tests import test_server
40
class StubServer(paramiko.ServerInterface):
42
def __init__(self, test_case_server):
43
paramiko.ServerInterface.__init__(self)
44
self.log = test_case_server.log
27
from bzrlib.osutils import pathjoin
28
from bzrlib.trace import mutter
31
class StubServer (ServerInterface):
33
def __init__(self, test_case):
34
ServerInterface.__init__(self)
35
self._test_case = test_case
46
37
def check_auth_password(self, username, password):
48
self.log('sftpserver - authorizing: %s' % (username,))
49
return paramiko.AUTH_SUCCESSFUL
39
self._test_case.log('sftpserver - authorizing: %s' % (username,))
40
return AUTH_SUCCESSFUL
51
42
def check_channel_request(self, kind, chanid):
52
self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
53
return paramiko.OPEN_SUCCEEDED
56
class StubSFTPHandle(paramiko.SFTPHandle):
43
self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
47
class StubSFTPHandle (SFTPHandle):
60
return paramiko.SFTPAttributes.from_stat(
61
os.fstat(self.readfile.fileno()))
50
return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
63
return paramiko.SFTPServer.convert_errno(e.errno)
52
return SFTPServer.convert_errno(e.errno)
65
54
def chattr(self, attr):
66
55
# python doesn't have equivalents to fchown or fchmod, so we have to
67
56
# use the stored filename
68
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
57
mutter('Changing permissions on %s to %s', self.filename, attr)
70
paramiko.SFTPServer.set_file_attr(self.filename, attr)
59
SFTPServer.set_file_attr(self.filename, attr)
72
return paramiko.SFTPServer.convert_errno(e.errno)
75
class StubSFTPServer(paramiko.SFTPServerInterface):
61
return SFTPServer.convert_errno(e.errno)
64
class StubSFTPServer (SFTPServerInterface):
77
66
def __init__(self, server, root, home=None):
78
paramiko.SFTPServerInterface.__init__(self, server)
67
SFTPServerInterface.__init__(self, server)
79
68
# All paths are actually relative to 'root'.
80
69
# this is like implementing chroot().
225
213
if attr is not None:
226
214
attr._flags &= ~attr.FLAG_PERMISSIONS
227
paramiko.SFTPServer.set_file_attr(path, attr)
215
SFTPServer.set_file_attr(path, attr)
228
216
except OSError, e:
229
return paramiko.SFTPServer.convert_errno(e.errno)
230
return paramiko.SFTP_OK
217
return SFTPServer.convert_errno(e.errno)
232
220
def rmdir(self, path):
233
221
path = self._realpath(path)
236
224
except OSError, e:
237
return paramiko.SFTPServer.convert_errno(e.errno)
238
return paramiko.SFTP_OK
225
return SFTPServer.convert_errno(e.errno)
240
228
# removed: chattr, symlink, readlink
241
229
# (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 SocketDelay(object):
266
"""A socket decorator to make TCP appear slower.
268
This changes recv, send, and sendall to add a fixed latency to each python
269
call if a new roundtrip is detected. That is, when a recv is called and the
270
flag new_roundtrip is set, latency is charged. Every send and send_all
273
In addition every send, sendall and recv sleeps a bit per character send to
276
Not all methods are implemented, this is deliberate as this class is not a
277
replacement for the builtin sockets layer. fileno is not implemented to
278
prevent the proxy being bypassed.
282
_proxied_arguments = dict.fromkeys([
283
"close", "getpeername", "getsockname", "getsockopt", "gettimeout",
284
"setblocking", "setsockopt", "settimeout", "shutdown"])
286
def __init__(self, sock, latency, bandwidth=1.0,
289
:param bandwith: simulated bandwith (MegaBit)
290
:param really_sleep: If set to false, the SocketDelay will just
291
increase a counter, instead of calling time.sleep. This is useful for
292
unittesting the SocketDelay.
295
self.latency = latency
296
self.really_sleep = really_sleep
297
self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
298
self.new_roundtrip = False
301
if self.really_sleep:
304
SocketDelay.simulated_time += s
306
def __getattr__(self, attr):
307
if attr in SocketDelay._proxied_arguments:
308
return getattr(self.sock, attr)
309
raise AttributeError("'SocketDelay' object has no attribute %r" %
313
return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
316
def recv(self, *args):
317
data = self.sock.recv(*args)
318
if data and self.new_roundtrip:
319
self.new_roundtrip = False
320
self.sleep(self.latency)
321
self.sleep(len(data) * self.time_per_byte)
324
def sendall(self, data, flags=0):
325
if not self.new_roundtrip:
326
self.new_roundtrip = True
327
self.sleep(self.latency)
328
self.sleep(len(data) * self.time_per_byte)
329
return self.sock.sendall(data, flags)
331
def send(self, data, flags=0):
332
if not self.new_roundtrip:
333
self.new_roundtrip = True
334
self.sleep(self.latency)
335
bytes_sent = self.sock.send(data, flags)
336
self.sleep(bytes_sent * self.time_per_byte)
340
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
343
self.wrap_for_latency()
344
tcs = self.server.test_case_server
345
ptrans = paramiko.Transport(self.request)
346
self.paramiko_transport = ptrans
347
# Set it to a channel under 'bzr' so that we get debug info
348
ptrans.set_log_channel('bzr.paramiko.transport')
349
ptrans.add_server_key(tcs.get_host_key())
350
ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
351
StubSFTPServer, root=tcs._root,
352
home=tcs._server_homedir)
353
server = tcs._server_interface(tcs)
354
# This blocks until the key exchange has been done
355
ptrans.start_server(None, server)
358
# Wait for the conversation to finish, when the paramiko.Transport
360
# TODO: Consider timing out after XX seconds rather than hanging.
361
# Also we could check paramiko_transport.active and possibly
362
# paramiko_transport.getException().
363
self.paramiko_transport.join()
365
def wrap_for_latency(self):
366
tcs = self.server.test_case_server
368
# Give the socket (which the request really is) a latency adding
370
self.request = SocketDelay(self.request, tcs.add_latency)
373
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
376
self.wrap_for_latency()
377
# Re-import these as locals, so that they're still accessible during
378
# interpreter shutdown (when all module globals get set to None, leading
379
# to confusing errors like "'NoneType' object has no attribute 'error'".
380
class FakeChannel(object):
381
def get_transport(self):
383
def get_log_channel(self):
384
return 'bzr.paramiko'
387
def get_hexdump(self):
392
tcs = self.server.test_case_server
393
sftp_server = paramiko.SFTPServer(
394
FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
395
root=tcs._root, home=tcs._server_homedir)
396
self.sftp_server = sftp_server
397
sys_stderr = sys.stderr # Used in error reporting during shutdown
399
sftp_server.start_subsystem(
400
'sftp', None, ssh.SocketAsChannelAdapter(self.request))
401
except socket.error, e:
402
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
403
# it's okay for the client to disconnect abruptly
404
# (bug in paramiko 1.6: it should absorb this exception)
409
# This typically seems to happen during interpreter shutdown, so
410
# most of the useful ways to report this error won't work.
411
# Writing the exception type, and then the text of the exception,
412
# seems to be the best we can do.
413
# FIXME: All interpreter shutdown errors should have been related
414
# to daemon threads, cleanup needed -- vila 20100623
415
sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
416
sys_stderr.write('%s\n\n' % (e,))
419
self.sftp_server.finish_subsystem()
422
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
424
def __init__(self, server_address, request_handler_class, test_case_server):
425
test_server.TestingThreadingTCPServer.__init__(
426
self, server_address, request_handler_class)
427
self.test_case_server = test_case_server
430
class SFTPServer(test_server.TestingTCPServerInAThread):
431
"""Common code for SFTP server facilities."""
433
def __init__(self, server_interface=StubServer):
434
self.host = '127.0.0.1'
436
super(SFTPServer, self).__init__((self.host, self.port),
438
TestingSFTPConnectionHandler)
439
self._original_vendor = None
440
self._vendor = ssh.ParamikoVendor()
441
self._server_interface = server_interface
442
self._host_key = None
446
self._server_homedir = None
449
def _get_sftp_url(self, path):
450
"""Calculate an sftp url to this server for path."""
451
return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
453
def log(self, message):
454
"""StubServer uses this to log when a new server is created."""
455
self.logs.append(message)
457
def create_server(self):
458
server = self.server_class((self.host, self.port),
459
self.request_handler_class,
463
def get_host_key(self):
464
if self._host_key is None:
465
key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
466
f = open(key_file, 'w')
468
f.write(STUB_SERVER_KEY)
471
self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
472
return self._host_key
474
def start_server(self, backing_server=None):
475
# XXX: TODO: make sftpserver back onto backing_server rather than local
477
if not (backing_server is None or
478
isinstance(backing_server, test_server.LocalURLServer)):
479
raise AssertionError(
480
'backing_server should not be %r, because this can only serve '
481
'the local current working directory.' % (backing_server,))
482
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
483
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
484
if sys.platform == 'win32':
485
# Win32 needs to use the UNICODE api
486
self._homedir = os.getcwdu()
487
# Normalize the path or it will be wrongly escaped
488
self._homedir = osutils.normpath(self._homedir)
490
# But unix SFTP servers should just deal in bytestreams
491
self._homedir = os.getcwd()
492
if self._server_homedir is None:
493
self._server_homedir = self._homedir
495
if sys.platform == 'win32':
497
super(SFTPServer, self).start_server()
499
def stop_server(self):
501
super(SFTPServer, self).stop_server()
503
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
505
def get_bogus_url(self):
506
"""See bzrlib.transport.Server.get_bogus_url."""
507
# this is chosen to try to prevent trouble with proxies, weird dns, etc
508
# we bind a random socket, so that we get a guaranteed unused port
509
# we just never listen on that port
511
s.bind(('localhost', 0))
512
return 'sftp://%s:%s/' % s.getsockname()
515
class SFTPFullAbsoluteServer(SFTPServer):
516
"""A test server for sftp transports, using absolute urls and ssh."""
519
"""See bzrlib.transport.Server.get_url."""
520
homedir = self._homedir
521
if sys.platform != 'win32':
522
# Remove the initial '/' on all platforms but win32
523
homedir = homedir[1:]
524
return self._get_sftp_url(urlutils.escape(homedir))
527
class SFTPServerWithoutSSH(SFTPServer):
528
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
531
super(SFTPServerWithoutSSH, self).__init__()
532
self._vendor = ssh.LoopbackVendor()
533
self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
539
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
540
"""A test server for sftp transports, using absolute urls."""
543
"""See bzrlib.transport.Server.get_url."""
544
homedir = self._homedir
545
if sys.platform != 'win32':
546
# Remove the initial '/' on all platforms but win32
547
homedir = homedir[1:]
548
return self._get_sftp_url(urlutils.escape(homedir))
551
class SFTPHomeDirServer(SFTPServerWithoutSSH):
552
"""A test server for sftp transports, using homedir relative urls."""
555
"""See bzrlib.transport.Server.get_url."""
556
return self._get_sftp_url("%7E/")
559
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
560
"""A test server for sftp transports where only absolute paths will work.
562
It does this by serving from a deeply-nested directory that doesn't exist.
565
def create_server(self):
566
# FIXME: Can't we do that in a cleaner way ? -- vila 20100623
567
server = super(SFTPSiblingAbsoluteServer, self).create_server()
568
server._server_homedir = '/dev/noone/runs/tests/here'