23
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
24
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
27
from bzrlib.osutils import pathjoin
28
from bzrlib.trace import mutter
31
class StubServer (ServerInterface):
35
from bzrlib.transport import (
38
from bzrlib.tests import test_server
41
class StubServer (paramiko.ServerInterface):
33
43
def __init__(self, test_case):
34
ServerInterface.__init__(self)
44
paramiko.ServerInterface.__init__(self)
35
45
self._test_case = test_case
37
47
def check_auth_password(self, username, password):
39
49
self._test_case.log('sftpserver - authorizing: %s' % (username,))
40
return AUTH_SUCCESSFUL
50
return paramiko.AUTH_SUCCESSFUL
42
52
def check_channel_request(self, kind, chanid):
43
self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
47
class StubSFTPHandle (SFTPHandle):
54
'sftpserver - channel request: %s, %s' % (kind, chanid))
55
return paramiko.OPEN_SUCCEEDED
58
class StubSFTPHandle (paramiko.SFTPHandle):
50
return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
61
return paramiko.SFTPAttributes.from_stat(
62
os.fstat(self.readfile.fileno()))
52
return SFTPServer.convert_errno(e.errno)
64
return paramiko.SFTPServer.convert_errno(e.errno)
54
66
def chattr(self, attr):
55
67
# python doesn't have equivalents to fchown or fchmod, so we have to
56
68
# use the stored filename
57
mutter('Changing permissions on %s to %s', self.filename, attr)
69
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
59
SFTPServer.set_file_attr(self.filename, attr)
71
paramiko.SFTPServer.set_file_attr(self.filename, attr)
61
return SFTPServer.convert_errno(e.errno)
64
class StubSFTPServer (SFTPServerInterface):
73
return paramiko.SFTPServer.convert_errno(e.errno)
76
class StubSFTPServer (paramiko.SFTPServerInterface):
66
78
def __init__(self, server, root, home=None):
67
SFTPServerInterface.__init__(self, server)
79
paramiko.SFTPServerInterface.__init__(self, server)
68
80
# All paths are actually relative to 'root'.
69
81
# this is like implementing chroot().
74
assert home.startswith(self.root), \
75
"home must be a subdirectory of root (%s vs %s)" \
86
if not home.startswith(self.root):
88
"home must be a subdirectory of root (%s vs %s)"
77
90
self.home = home[len(self.root):]
78
91
if self.home.startswith('/'):
79
92
self.home = self.home[1:]
212
226
if attr is not None:
213
227
attr._flags &= ~attr.FLAG_PERMISSIONS
214
SFTPServer.set_file_attr(path, attr)
228
paramiko.SFTPServer.set_file_attr(path, attr)
215
229
except OSError, e:
216
return SFTPServer.convert_errno(e.errno)
230
return paramiko.SFTPServer.convert_errno(e.errno)
231
return paramiko.SFTP_OK
219
233
def rmdir(self, path):
220
234
path = self._realpath(path)
223
237
except OSError, e:
224
return SFTPServer.convert_errno(e.errno)
238
return paramiko.SFTPServer.convert_errno(e.errno)
239
return paramiko.SFTP_OK
227
241
# removed: chattr, symlink, readlink
228
242
# (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.
286
trace.mutter('SocketListener %r has started', self)
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)
293
if len(readable) == 0:
296
s, addr_unused = self._socket.accept()
297
trace.mutter('SocketListener %r has accepted connection %r',
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)
307
# probably a failed test; unit test thread will log the
309
sys.excepthook(*sys.exc_info())
311
'Exception from within unit test server thread: %r' % x)
314
class SocketDelay(object):
315
"""A socket decorator to make TCP appear slower.
317
This changes recv, send, and sendall to add a fixed latency to each python
318
call if a new roundtrip is detected. That is, when a recv is called and the
319
flag new_roundtrip is set, latency is charged. Every send and send_all
322
In addition every send, sendall and recv sleeps a bit per character send to
325
Not all methods are implemented, this is deliberate as this class is not a
326
replacement for the builtin sockets layer. fileno is not implemented to
327
prevent the proxy being bypassed.
331
_proxied_arguments = dict.fromkeys([
332
"close", "getpeername", "getsockname", "getsockopt", "gettimeout",
333
"setblocking", "setsockopt", "settimeout", "shutdown"])
335
def __init__(self, sock, latency, bandwidth=1.0,
338
:param bandwith: simulated bandwith (MegaBit)
339
:param really_sleep: If set to false, the SocketDelay will just
340
increase a counter, instead of calling time.sleep. This is useful for
341
unittesting the SocketDelay.
344
self.latency = latency
345
self.really_sleep = really_sleep
346
self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
347
self.new_roundtrip = False
350
if self.really_sleep:
353
SocketDelay.simulated_time += s
355
def __getattr__(self, attr):
356
if attr in SocketDelay._proxied_arguments:
357
return getattr(self.sock, attr)
358
raise AttributeError("'SocketDelay' object has no attribute %r" %
362
return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
365
def recv(self, *args):
366
data = self.sock.recv(*args)
367
if data and self.new_roundtrip:
368
self.new_roundtrip = False
369
self.sleep(self.latency)
370
self.sleep(len(data) * self.time_per_byte)
373
def sendall(self, data, flags=0):
374
if not self.new_roundtrip:
375
self.new_roundtrip = True
376
self.sleep(self.latency)
377
self.sleep(len(data) * self.time_per_byte)
378
return self.sock.sendall(data, flags)
380
def send(self, data, flags=0):
381
if not self.new_roundtrip:
382
self.new_roundtrip = True
383
self.sleep(self.latency)
384
bytes_sent = self.sock.send(data, flags)
385
self.sleep(bytes_sent * self.time_per_byte)
389
class SFTPServer(test_server.TestServer):
390
"""Common code for SFTP server facilities."""
392
def __init__(self, server_interface=StubServer):
393
self._original_vendor = None
395
self._server_homedir = None
396
self._listener = None
398
self._vendor = ssh.ParamikoVendor()
399
self._server_interface = server_interface
404
def _get_sftp_url(self, path):
405
"""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)
409
def log(self, message):
410
"""StubServer uses this to log when a new server is created."""
411
self.logs.append(message)
413
def _run_server_entry(self, sock):
414
"""Entry point for all implementations of _run_server.
416
If self.add_latency is > 0.000001 then sock is given a latency adding
419
if self.add_latency > 0.000001:
420
sock = SocketDelay(sock, self.add_latency)
421
return self._run_server(sock)
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)
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)
439
def start_server(self, backing_server=None):
440
# XXX: TODO: make sftpserver back onto backing_server rather than local
442
if not (backing_server is None or
443
isinstance(backing_server, test_server.LocalURLServer)):
444
raise AssertionError(
445
'backing_server should not be %r, because this can only serve '
446
'the local current working directory.' % (backing_server,))
447
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
448
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
449
if sys.platform == 'win32':
450
# Win32 needs to use the UNICODE api
451
self._homedir = os.getcwdu()
452
# Normalize the path or it will be wrongly escaped
453
self._homedir = osutils.normpath(self._homedir)
455
# But unix SFTP servers should just deal in bytestreams
456
self._homedir = os.getcwd()
457
if self._server_homedir is None:
458
self._server_homedir = self._homedir
460
if sys.platform == 'win32':
462
self._listener = SocketListener(self._run_server_entry)
463
self._listener.setDaemon(True)
464
self._listener.start()
466
def stop_server(self):
467
self._listener.stop()
468
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
470
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
473
# we bind a random socket, so that we get a guaranteed unused port
474
# we just never listen on that port
476
s.bind(('localhost', 0))
477
return 'sftp://%s:%s/' % s.getsockname()
480
class SFTPFullAbsoluteServer(SFTPServer):
481
"""A test server for sftp transports, using absolute urls and ssh."""
484
"""See bzrlib.transport.Server.get_url."""
485
homedir = self._homedir
486
if sys.platform != 'win32':
487
# Remove the initial '/' on all platforms but win32
488
homedir = homedir[1:]
489
return self._get_sftp_url(urlutils.escape(homedir))
492
class SFTPServerWithoutSSH(SFTPServer):
493
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
496
super(SFTPServerWithoutSSH, self).__init__()
497
self._vendor = ssh.LoopbackVendor()
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):
506
def get_log_channel(self):
510
def get_hexdump(self):
515
server = paramiko.SFTPServer(
516
FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
517
root=self._root, home=self._server_homedir)
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)
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.
534
sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
535
sys.stderr.write('%s\n\n' % (e,))
536
server.finish_subsystem()
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("~/")
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 start_server(self, backing_server=None):
566
self._server_homedir = '/dev/noone/runs/tests/here'
567
super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)