1
# Copyright (C) 2005, 2006, 2008, 2009, 2010 Robey Pointer <robey@lag.net>, Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
A stub SFTP server for loopback SFTP testing.
19
Adapted from the one in paramiko's unit tests.
36
from bzrlib.transport import (
39
from bzrlib.tests import test_server
42
class StubServer(paramiko.ServerInterface):
44
def __init__(self, test_case_server):
45
paramiko.ServerInterface.__init__(self)
46
self.log = test_case_server.log
48
def check_auth_password(self, username, password):
50
self.log('sftpserver - authorizing: %s' % (username,))
51
return paramiko.AUTH_SUCCESSFUL
53
def check_channel_request(self, kind, chanid):
54
self.log('sftpserver - channel request: %s, %s' % (kind, chanid))
55
return paramiko.OPEN_SUCCEEDED
58
class StubSFTPHandle(paramiko.SFTPHandle):
62
return paramiko.SFTPAttributes.from_stat(
63
os.fstat(self.readfile.fileno()))
65
return paramiko.SFTPServer.convert_errno(e.errno)
67
def chattr(self, attr):
68
# python doesn't have equivalents to fchown or fchmod, so we have to
69
# use the stored filename
70
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
72
paramiko.SFTPServer.set_file_attr(self.filename, attr)
74
return paramiko.SFTPServer.convert_errno(e.errno)
77
class StubSFTPServer(paramiko.SFTPServerInterface):
79
def __init__(self, server, root, home=None):
80
paramiko.SFTPServerInterface.__init__(self, server)
81
# All paths are actually relative to 'root'.
82
# this is like implementing chroot().
87
if not home.startswith(self.root):
89
"home must be a subdirectory of root (%s vs %s)"
91
self.home = home[len(self.root):]
92
if self.home.startswith('/'):
93
self.home = self.home[1:]
94
server.log('sftpserver - new connection')
96
def _realpath(self, path):
97
# paths returned from self.canonicalize() always start with
98
# a path separator. So if 'root' is just '/', this would cause
99
# a double slash at the beginning '//home/dir'.
101
return self.canonicalize(path)
102
return self.root + self.canonicalize(path)
104
if sys.platform == 'win32':
105
def canonicalize(self, path):
106
# Win32 sftp paths end up looking like
107
# sftp://host@foo/h:/foo/bar
108
# which means absolute paths look like:
110
# and relative paths stay the same:
112
# win32 needs to use the Unicode APIs. so we require the
113
# paths to be utf8 (Linux just uses bytestreams)
114
thispath = path.decode('utf8')
115
if path.startswith('/'):
117
return os.path.normpath(thispath[1:])
119
return os.path.normpath(os.path.join(self.home, thispath))
121
def canonicalize(self, path):
122
if os.path.isabs(path):
123
return os.path.normpath(path)
125
return os.path.normpath('/' + os.path.join(self.home, path))
127
def chattr(self, path, attr):
129
paramiko.SFTPServer.set_file_attr(path, attr)
131
return paramiko.SFTPServer.convert_errno(e.errno)
132
return paramiko.SFTP_OK
134
def list_folder(self, path):
135
path = self._realpath(path)
138
# TODO: win32 incorrectly lists paths with non-ascii if path is not
139
# unicode. However on unix the server should only deal with
140
# bytestreams and posix.listdir does the right thing
141
if sys.platform == 'win32':
142
flist = [f.encode('utf8') for f in os.listdir(path)]
144
flist = os.listdir(path)
146
attr = paramiko.SFTPAttributes.from_stat(
147
os.stat(osutils.pathjoin(path, fname)))
148
attr.filename = fname
152
return paramiko.SFTPServer.convert_errno(e.errno)
154
def stat(self, path):
155
path = self._realpath(path)
157
return paramiko.SFTPAttributes.from_stat(os.stat(path))
159
return paramiko.SFTPServer.convert_errno(e.errno)
161
def lstat(self, path):
162
path = self._realpath(path)
164
return paramiko.SFTPAttributes.from_stat(os.lstat(path))
166
return paramiko.SFTPServer.convert_errno(e.errno)
168
def open(self, path, flags, attr):
169
path = self._realpath(path)
171
flags |= getattr(os, 'O_BINARY', 0)
172
if getattr(attr, 'st_mode', None):
173
fd = os.open(path, flags, attr.st_mode)
175
# os.open() defaults to 0777 which is
176
# an odd default mode for files
177
fd = os.open(path, flags, 0666)
179
return paramiko.SFTPServer.convert_errno(e.errno)
181
if (flags & os.O_CREAT) and (attr is not None):
182
attr._flags &= ~attr.FLAG_PERMISSIONS
183
paramiko.SFTPServer.set_file_attr(path, attr)
184
if flags & os.O_WRONLY:
186
elif flags & os.O_RDWR:
192
f = os.fdopen(fd, fstr)
193
except (IOError, OSError), e:
194
return paramiko.SFTPServer.convert_errno(e.errno)
195
fobj = StubSFTPHandle()
201
def remove(self, path):
202
path = self._realpath(path)
206
return paramiko.SFTPServer.convert_errno(e.errno)
207
return paramiko.SFTP_OK
209
def rename(self, oldpath, newpath):
210
oldpath = self._realpath(oldpath)
211
newpath = self._realpath(newpath)
213
os.rename(oldpath, newpath)
215
return paramiko.SFTPServer.convert_errno(e.errno)
216
return paramiko.SFTP_OK
218
def mkdir(self, path, attr):
219
path = self._realpath(path)
221
# Using getattr() in case st_mode is None or 0
222
# both evaluate to False
223
if getattr(attr, 'st_mode', None):
224
os.mkdir(path, attr.st_mode)
228
attr._flags &= ~attr.FLAG_PERMISSIONS
229
paramiko.SFTPServer.set_file_attr(path, attr)
231
return paramiko.SFTPServer.convert_errno(e.errno)
232
return paramiko.SFTP_OK
234
def rmdir(self, path):
235
path = self._realpath(path)
239
return paramiko.SFTPServer.convert_errno(e.errno)
240
return paramiko.SFTP_OK
242
# removed: chattr, symlink, readlink
243
# (nothing in bzr's sftp transport uses those)
246
# ------------- server test implementation --------------
248
STUB_SERVER_KEY = """
249
-----BEGIN RSA PRIVATE KEY-----
250
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
251
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
252
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
253
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
254
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
255
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
256
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
257
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
258
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
259
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
260
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
261
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
262
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
263
-----END RSA PRIVATE KEY-----
267
class SocketDelay(object):
268
"""A socket decorator to make TCP appear slower.
270
This changes recv, send, and sendall to add a fixed latency to each python
271
call if a new roundtrip is detected. That is, when a recv is called and the
272
flag new_roundtrip is set, latency is charged. Every send and send_all
275
In addition every send, sendall and recv sleeps a bit per character send to
278
Not all methods are implemented, this is deliberate as this class is not a
279
replacement for the builtin sockets layer. fileno is not implemented to
280
prevent the proxy being bypassed.
284
_proxied_arguments = dict.fromkeys([
285
"close", "getpeername", "getsockname", "getsockopt", "gettimeout",
286
"setblocking", "setsockopt", "settimeout", "shutdown"])
288
def __init__(self, sock, latency, bandwidth=1.0,
291
:param bandwith: simulated bandwith (MegaBit)
292
:param really_sleep: If set to false, the SocketDelay will just
293
increase a counter, instead of calling time.sleep. This is useful for
294
unittesting the SocketDelay.
297
self.latency = latency
298
self.really_sleep = really_sleep
299
self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
300
self.new_roundtrip = False
303
if self.really_sleep:
306
SocketDelay.simulated_time += s
308
def __getattr__(self, attr):
309
if attr in SocketDelay._proxied_arguments:
310
return getattr(self.sock, attr)
311
raise AttributeError("'SocketDelay' object has no attribute %r" %
315
return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
318
def recv(self, *args):
319
data = self.sock.recv(*args)
320
if data and self.new_roundtrip:
321
self.new_roundtrip = False
322
self.sleep(self.latency)
323
self.sleep(len(data) * self.time_per_byte)
326
def sendall(self, data, flags=0):
327
if not self.new_roundtrip:
328
self.new_roundtrip = True
329
self.sleep(self.latency)
330
self.sleep(len(data) * self.time_per_byte)
331
return self.sock.sendall(data, flags)
333
def send(self, data, flags=0):
334
if not self.new_roundtrip:
335
self.new_roundtrip = True
336
self.sleep(self.latency)
337
bytes_sent = self.sock.send(data, flags)
338
self.sleep(bytes_sent * self.time_per_byte)
342
class TestingSFTPConnectionHandler(SocketServer.BaseRequestHandler):
345
self.wrap_for_latency()
346
tcs = self.server.test_case_server
347
ssh_server = paramiko.Transport(self.request)
348
ssh_server.add_server_key(tcs.get_host_key())
349
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
350
StubSFTPServer, root=tcs._root,
351
home=tcs._server_homedir)
352
server = tcs._server_interface(tcs)
353
ssh_server.start_server(None, server)
354
# FIXME: Long story short:
355
# bt.test_transport.TestSSHConnections.test_bzr_connect_to_bzr_ssh
356
# fails if we wait less than 0.2 seconds... paramiko uses a lot of
357
# timeouts internally which probably mask a synchronisation
358
# problem. Note that this is the only test that requires this hack and
359
# the test may need to be fixed instead, but it's late and the test is
360
# horrible as mentioned in its comments :) -- vila 20100623
364
def wrap_for_latency(self):
365
tcs = self.server.test_case_server
367
# Give the socket (which the request really is) a latency adding
369
self.request = SocketDelay(self.request, tcs.add_latency)
372
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
375
self.wrap_for_latency()
376
# Re-import these as locals, so that they're still accessible during
377
# interpreter shutdown (when all module globals get set to None, leading
378
# to confusing errors like "'NoneType' object has no attribute 'error'".
379
class FakeChannel(object):
380
def get_transport(self):
382
def get_log_channel(self):
386
def get_hexdump(self):
391
tcs = self.server.test_case_server
392
server = paramiko.SFTPServer(
393
FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
394
root=tcs._root, home=tcs._server_homedir)
396
server.start_subsystem(
397
'sftp', None, ssh.SocketAsChannelAdapter(self.request))
398
except socket.error, e:
399
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
400
# it's okay for the client to disconnect abruptly
401
# (bug in paramiko 1.6: it should absorb this exception)
406
# This typically seems to happen during interpreter shutdown, so
407
# most of the useful ways to report this error won't work.
408
# Writing the exception type, and then the text of the exception,
409
# seems to be the best we can do.
410
# FIXME: All interpreter shutdown errors should have been related
411
# to daemon threads, cleanup needed -- vila 20100623
413
sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
414
sys.stderr.write('%s\n\n' % (e,))
415
server.finish_subsystem()
418
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
420
def __init__(self, server_address, request_handler_class, test_case_server):
421
test_server.TestingThreadingTCPServer.__init__(
422
self, server_address, request_handler_class)
423
self.test_case_server = test_case_server
426
class SFTPServer(test_server.TestingTCPServerInAThread):
427
"""Common code for SFTP server facilities."""
429
def __init__(self, server_interface=StubServer):
430
self.host = '127.0.0.1'
432
super(SFTPServer, self).__init__((self.host, self.port),
434
TestingSFTPConnectionHandler)
435
self._original_vendor = None
436
self._vendor = ssh.ParamikoVendor()
437
self._server_interface = server_interface
438
self._host_key = None
442
self._server_homedir = None
445
def _get_sftp_url(self, path):
446
"""Calculate an sftp url to this server for path."""
447
return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
449
def log(self, message):
450
"""StubServer uses this to log when a new server is created."""
451
self.logs.append(message)
453
def create_server(self):
454
server = self.server_class((self.host, self.port),
455
self.request_handler_class,
459
def get_host_key(self):
460
if self._host_key is None:
461
key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
462
f = open(key_file, 'w')
464
f.write(STUB_SERVER_KEY)
467
self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
468
return self._host_key
470
def start_server(self, backing_server=None):
471
# XXX: TODO: make sftpserver back onto backing_server rather than local
473
if not (backing_server is None or
474
isinstance(backing_server, test_server.LocalURLServer)):
475
raise AssertionError(
476
'backing_server should not be %r, because this can only serve '
477
'the local current working directory.' % (backing_server,))
478
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
479
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
480
if sys.platform == 'win32':
481
# Win32 needs to use the UNICODE api
482
self._homedir = os.getcwdu()
483
# Normalize the path or it will be wrongly escaped
484
self._homedir = osutils.normpath(self._homedir)
486
# But unix SFTP servers should just deal in bytestreams
487
self._homedir = os.getcwd()
488
if self._server_homedir is None:
489
self._server_homedir = self._homedir
491
if sys.platform == 'win32':
493
super(SFTPServer, self).start_server()
495
def stop_server(self):
497
super(SFTPServer, self).stop_server()
499
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
501
def get_bogus_url(self):
502
"""See bzrlib.transport.Server.get_bogus_url."""
503
# this is chosen to try to prevent trouble with proxies, weird dns, etc
504
# we bind a random socket, so that we get a guaranteed unused port
505
# we just never listen on that port
507
s.bind(('localhost', 0))
508
return 'sftp://%s:%s/' % s.getsockname()
511
class SFTPFullAbsoluteServer(SFTPServer):
512
"""A test server for sftp transports, using absolute urls and ssh."""
515
"""See bzrlib.transport.Server.get_url."""
516
homedir = self._homedir
517
if sys.platform != 'win32':
518
# Remove the initial '/' on all platforms but win32
519
homedir = homedir[1:]
520
return self._get_sftp_url(urlutils.escape(homedir))
523
class SFTPServerWithoutSSH(SFTPServer):
524
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
527
super(SFTPServerWithoutSSH, self).__init__()
528
self._vendor = ssh.LoopbackVendor()
529
self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
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 create_server(self):
562
# FIXME: Can't we do that in a cleaner way ? -- vila 20100623
563
server = super(SFTPSiblingAbsoluteServer, self).create_server()
564
server._server_homedir = '/dev/noone/runs/tests/here'