28
import SocketServer as socketserver
37
from ..transport import (
40
from . import test_server
43
class StubServer(paramiko.ServerInterface):
45
def __init__(self, test_case_server):
46
paramiko.ServerInterface.__init__(self)
47
self.log = test_case_server.log
23
from paramiko import ServerInterface, SFTPServerInterface, SFTPServer, SFTPAttributes, \
24
SFTPHandle, SFTP_OK, AUTH_SUCCESSFUL, OPEN_SUCCEEDED
26
from bzrlib.osutils import pathjoin
27
from bzrlib.trace import mutter
30
class StubServer (ServerInterface):
32
def __init__(self, test_case):
33
ServerInterface.__init__(self)
34
self._test_case = test_case
49
36
def check_auth_password(self, username, password):
51
self.log('sftpserver - authorizing: %s' % (username,))
52
return paramiko.AUTH_SUCCESSFUL
38
self._test_case.log('sftpserver - authorizing: %s' % (username,))
39
return AUTH_SUCCESSFUL
54
41
def check_channel_request(self, kind, chanid):
55
self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
56
return paramiko.OPEN_SUCCEEDED
59
class StubSFTPHandle(paramiko.SFTPHandle):
42
self._test_case.log('sftpserver - channel request: %s, %s' % (kind, chanid))
46
class StubSFTPHandle (SFTPHandle):
63
return paramiko.SFTPAttributes.from_stat(
64
os.fstat(self.readfile.fileno()))
66
return paramiko.SFTPServer.convert_errno(e.errno)
49
return SFTPAttributes.from_stat(os.fstat(self.readfile.fileno()))
51
return SFTPServer.convert_errno(e.errno)
68
53
def chattr(self, attr):
69
54
# python doesn't have equivalents to fchown or fchmod, so we have to
70
55
# use the stored filename
71
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
56
mutter('Changing permissions on %s to %s', self.filename, attr)
73
paramiko.SFTPServer.set_file_attr(self.filename, attr)
75
return paramiko.SFTPServer.convert_errno(e.errno)
78
class StubSFTPServer(paramiko.SFTPServerInterface):
58
SFTPServer.set_file_attr(self.filename, attr)
60
return SFTPServer.convert_errno(e.errno)
63
class StubSFTPServer (SFTPServerInterface):
80
65
def __init__(self, server, root, home=None):
81
paramiko.SFTPServerInterface.__init__(self, server)
82
# All paths are actually relative to 'root'.
83
# this is like implementing chroot().
66
SFTPServerInterface.__init__(self, server)
88
if not home.startswith(self.root):
90
"home must be a subdirectory of root (%s vs %s)"
92
71
self.home = home[len(self.root):]
93
if self.home.startswith('/'):
72
if (len(self.home) > 0) and (self.home[0] == '/'):
94
73
self.home = self.home[1:]
95
server.log('sftpserver - new connection')
74
server._test_case.log('sftpserver - new connection')
97
76
def _realpath(self, path):
98
# paths returned from self.canonicalize() always start with
99
# a path separator. So if 'root' is just '/', this would cause
100
# a double slash at the beginning '//home/dir'.
102
return self.canonicalize(path)
103
77
return self.root + self.canonicalize(path)
105
if sys.platform == 'win32':
106
def canonicalize(self, path):
107
# Win32 sftp paths end up looking like
108
# sftp://host@foo/h:/foo/bar
109
# which means absolute paths look like:
111
# and relative paths stay the same:
113
# win32 needs to use the Unicode APIs. so we require the
114
# paths to be utf8 (Linux just uses bytestreams)
115
thispath = path.decode('utf8')
116
if path.startswith('/'):
118
return os.path.normpath(thispath[1:])
120
return os.path.normpath(os.path.join(self.home, thispath))
122
def canonicalize(self, path):
123
if os.path.isabs(path):
124
return osutils.normpath(path)
126
return osutils.normpath('/' + os.path.join(self.home, path))
79
def canonicalize(self, path):
80
if os.path.isabs(path):
81
return os.path.normpath(path)
83
return os.path.normpath('/' + os.path.join(self.home, path))
128
85
def chattr(self, path, attr):
130
paramiko.SFTPServer.set_file_attr(path, attr)
132
return paramiko.SFTPServer.convert_errno(e.errno)
133
return paramiko.SFTP_OK
87
SFTPServer.set_file_attr(path, attr)
89
return SFTPServer.convert_errno(e.errno)
135
92
def list_folder(self, path):
136
93
path = self._realpath(path)
139
# TODO: win32 incorrectly lists paths with non-ascii if path is not
140
# unicode. However on unix the server should only deal with
141
# bytestreams and posix.listdir does the right thing
142
if sys.platform == 'win32':
143
flist = [f.encode('utf8') for f in os.listdir(path)]
145
flist = os.listdir(path)
96
flist = os.listdir(path)
146
97
for fname in flist:
147
attr = paramiko.SFTPAttributes.from_stat(
148
os.stat(osutils.pathjoin(path, fname)))
98
attr = SFTPAttributes.from_stat(os.stat(pathjoin(path, fname)))
149
99
attr.filename = fname
153
return paramiko.SFTPServer.convert_errno(e.errno)
103
return SFTPServer.convert_errno(e.errno)
155
105
def stat(self, path):
156
106
path = self._realpath(path)
158
return paramiko.SFTPAttributes.from_stat(os.stat(path))
160
return paramiko.SFTPServer.convert_errno(e.errno)
108
return SFTPAttributes.from_stat(os.stat(path))
110
return SFTPServer.convert_errno(e.errno)
162
112
def lstat(self, path):
163
113
path = self._realpath(path)
165
return paramiko.SFTPAttributes.from_stat(os.lstat(path))
167
return paramiko.SFTPServer.convert_errno(e.errno)
115
return SFTPAttributes.from_stat(os.lstat(path))
117
return SFTPServer.convert_errno(e.errno)
169
119
def open(self, path, flags, attr):
170
120
path = self._realpath(path)
172
flags |= getattr(os, 'O_BINARY', 0)
122
if hasattr(os, 'O_BINARY'):
173
124
if getattr(attr, 'st_mode', None):
174
125
fd = os.open(path, flags, attr.st_mode)
176
# os.open() defaults to 0777 which is
177
# an odd default mode for files
178
fd = os.open(path, flags, 0o666)
180
return paramiko.SFTPServer.convert_errno(e.errno)
127
fd = os.open(path, flags)
129
return SFTPServer.convert_errno(e.errno)
182
130
if (flags & os.O_CREAT) and (attr is not None):
183
131
attr._flags &= ~attr.FLAG_PERMISSIONS
184
paramiko.SFTPServer.set_file_attr(path, attr)
132
SFTPServer.set_file_attr(path, attr)
185
133
if flags & os.O_WRONLY:
187
135
elif flags & os.O_RDWR:
244
176
if attr is not None:
245
177
attr._flags &= ~attr.FLAG_PERMISSIONS
246
paramiko.SFTPServer.set_file_attr(path, attr)
248
return paramiko.SFTPServer.convert_errno(e.errno)
249
return paramiko.SFTP_OK
178
SFTPServer.set_file_attr(path, attr)
180
return SFTPServer.convert_errno(e.errno)
251
183
def rmdir(self, path):
252
184
path = self._realpath(path)
256
return paramiko.SFTPServer.convert_errno(e.errno)
257
return paramiko.SFTP_OK
188
return SFTPServer.convert_errno(e.errno)
191
# removed: chattr, symlink, readlink
260
192
# (nothing in bzr's sftp transport uses those)
263
# ------------- server test implementation --------------
265
STUB_SERVER_KEY = """\
266
-----BEGIN RSA PRIVATE KEY-----
267
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
268
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
269
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
270
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
271
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
272
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
273
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
274
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
275
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
276
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
277
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
278
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
279
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
280
-----END RSA PRIVATE KEY-----
284
class SocketDelay(object):
285
"""A socket decorator to make TCP appear slower.
287
This changes recv, send, and sendall to add a fixed latency to each python
288
call if a new roundtrip is detected. That is, when a recv is called and the
289
flag new_roundtrip is set, latency is charged. Every send and send_all
292
In addition every send, sendall and recv sleeps a bit per character send to
295
Not all methods are implemented, this is deliberate as this class is not a
296
replacement for the builtin sockets layer. fileno is not implemented to
297
prevent the proxy being bypassed.
301
_proxied_arguments = dict.fromkeys([
302
"close", "getpeername", "getsockname", "getsockopt", "gettimeout",
303
"setblocking", "setsockopt", "settimeout", "shutdown"])
305
def __init__(self, sock, latency, bandwidth=1.0,
308
:param bandwith: simulated bandwith (MegaBit)
309
:param really_sleep: If set to false, the SocketDelay will just
310
increase a counter, instead of calling time.sleep. This is useful for
311
unittesting the SocketDelay.
314
self.latency = latency
315
self.really_sleep = really_sleep
316
self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
317
self.new_roundtrip = False
320
if self.really_sleep:
323
SocketDelay.simulated_time += s
325
def __getattr__(self, attr):
326
if attr in SocketDelay._proxied_arguments:
327
return getattr(self.sock, attr)
328
raise AttributeError("'SocketDelay' object has no attribute %r" %
332
return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
335
def recv(self, *args):
336
data = self.sock.recv(*args)
337
if data and self.new_roundtrip:
338
self.new_roundtrip = False
339
self.sleep(self.latency)
340
self.sleep(len(data) * self.time_per_byte)
343
def sendall(self, data, flags=0):
344
if not self.new_roundtrip:
345
self.new_roundtrip = True
346
self.sleep(self.latency)
347
self.sleep(len(data) * self.time_per_byte)
348
return self.sock.sendall(data, flags)
350
def send(self, data, flags=0):
351
if not self.new_roundtrip:
352
self.new_roundtrip = True
353
self.sleep(self.latency)
354
bytes_sent = self.sock.send(data, flags)
355
self.sleep(bytes_sent * self.time_per_byte)
359
class TestingSFTPConnectionHandler(socketserver.BaseRequestHandler):
362
self.wrap_for_latency()
363
tcs = self.server.test_case_server
364
ptrans = paramiko.Transport(self.request)
365
self.paramiko_transport = ptrans
366
# Set it to a channel under 'bzr' so that we get debug info
367
ptrans.set_log_channel('brz.paramiko.transport')
368
ptrans.add_server_key(tcs.get_host_key())
369
ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
370
StubSFTPServer, root=tcs._root,
371
home=tcs._server_homedir)
372
server = tcs._server_interface(tcs)
373
# This blocks until the key exchange has been done
374
ptrans.start_server(None, server)
377
# Wait for the conversation to finish, when the paramiko.Transport
379
# TODO: Consider timing out after XX seconds rather than hanging.
380
# Also we could check paramiko_transport.active and possibly
381
# paramiko_transport.getException().
382
self.paramiko_transport.join()
384
def wrap_for_latency(self):
385
tcs = self.server.test_case_server
387
# Give the socket (which the request really is) a latency adding
389
self.request = SocketDelay(self.request, tcs.add_latency)
392
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
395
self.wrap_for_latency()
396
# Re-import these as locals, so that they're still accessible during
397
# interpreter shutdown (when all module globals get set to None, leading
398
# to confusing errors like "'NoneType' object has no attribute 'error'".
400
class FakeChannel(object):
401
def get_transport(self):
404
def get_log_channel(self):
405
return 'brz.paramiko'
410
def get_hexdump(self):
416
tcs = self.server.test_case_server
417
sftp_server = paramiko.SFTPServer(
418
FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
419
root=tcs._root, home=tcs._server_homedir)
420
self.sftp_server = sftp_server
421
sys_stderr = sys.stderr # Used in error reporting during shutdown
423
sftp_server.start_subsystem(
424
'sftp', None, ssh.SocketAsChannelAdapter(self.request))
425
except socket.error as e:
426
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
427
# it's okay for the client to disconnect abruptly
428
# (bug in paramiko 1.6: it should absorb this exception)
432
except Exception as e:
433
# This typically seems to happen during interpreter shutdown, so
434
# most of the useful ways to report this error won't work.
435
# Writing the exception type, and then the text of the exception,
436
# seems to be the best we can do.
437
# FIXME: All interpreter shutdown errors should have been related
438
# to daemon threads, cleanup needed -- vila 20100623
439
sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
440
sys_stderr.write('%s\n\n' % (e,))
443
self.sftp_server.finish_subsystem()
446
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
448
def __init__(self, server_address, request_handler_class, test_case_server):
449
test_server.TestingThreadingTCPServer.__init__(
450
self, server_address, request_handler_class)
451
self.test_case_server = test_case_server
454
class SFTPServer(test_server.TestingTCPServerInAThread):
455
"""Common code for SFTP server facilities."""
457
def __init__(self, server_interface=StubServer):
458
self.host = '127.0.0.1'
460
super(SFTPServer, self).__init__((self.host, self.port),
462
TestingSFTPConnectionHandler)
463
self._original_vendor = None
464
self._vendor = ssh.ParamikoVendor()
465
self._server_interface = server_interface
466
self._host_key = None
470
self._server_homedir = None
473
def _get_sftp_url(self, path):
474
"""Calculate an sftp url to this server for path."""
475
return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
477
def log(self, message):
478
"""StubServer uses this to log when a new server is created."""
479
self.logs.append(message)
481
def create_server(self):
482
server = self.server_class((self.host, self.port),
483
self.request_handler_class,
487
def get_host_key(self):
488
if self._host_key is None:
489
key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
490
f = open(key_file, 'w')
492
f.write(STUB_SERVER_KEY)
495
self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
496
return self._host_key
498
def start_server(self, backing_server=None):
499
# XXX: TODO: make sftpserver back onto backing_server rather than local
501
if not (backing_server is None
502
or isinstance(backing_server, test_server.LocalURLServer)):
503
raise AssertionError(
504
'backing_server should not be %r, because this can only serve '
505
'the local current working directory.' % (backing_server,))
506
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
507
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
508
self._homedir = osutils.getcwd()
509
if sys.platform == 'win32':
510
# Normalize the path or it will be wrongly escaped
511
self._homedir = osutils.normpath(self._homedir)
513
self._homedir = self._homedir
514
if self._server_homedir is None:
515
self._server_homedir = self._homedir
517
if sys.platform == 'win32':
519
super(SFTPServer, self).start_server()
521
def stop_server(self):
523
super(SFTPServer, self).stop_server()
525
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
527
def get_bogus_url(self):
528
"""See breezy.transport.Server.get_bogus_url."""
529
# this is chosen to try to prevent trouble with proxies, weird dns, etc
530
# we bind a random socket, so that we get a guaranteed unused port
531
# we just never listen on that port
533
s.bind(('localhost', 0))
534
return 'sftp://%s:%s/' % s.getsockname()
537
class SFTPFullAbsoluteServer(SFTPServer):
538
"""A test server for sftp transports, using absolute urls and ssh."""
541
"""See breezy.transport.Server.get_url."""
542
homedir = self._homedir
543
if sys.platform != 'win32':
544
# Remove the initial '/' on all platforms but win32
545
homedir = homedir[1:]
546
return self._get_sftp_url(urlutils.escape(homedir))
549
class SFTPServerWithoutSSH(SFTPServer):
550
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
553
super(SFTPServerWithoutSSH, self).__init__()
554
self._vendor = ssh.LoopbackVendor()
555
self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
561
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
562
"""A test server for sftp transports, using absolute urls."""
565
"""See breezy.transport.Server.get_url."""
566
homedir = self._homedir
567
if sys.platform != 'win32':
568
# Remove the initial '/' on all platforms but win32
569
homedir = homedir[1:]
570
return self._get_sftp_url(urlutils.escape(homedir))
573
class SFTPHomeDirServer(SFTPServerWithoutSSH):
574
"""A test server for sftp transports, using homedir relative urls."""
577
"""See breezy.transport.Server.get_url."""
578
return self._get_sftp_url("%7E/")
581
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
582
"""A test server for sftp transports where only absolute paths will work.
584
It does this by serving from a deeply-nested directory that doesn't exist.
587
def create_server(self):
588
# FIXME: Can't we do that in a cleaner way ? -- vila 20100623
589
server = super(SFTPSiblingAbsoluteServer, self).create_server()
590
server._server_homedir = '/dev/noone/runs/tests/here'