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.
35
from bzrlib.transport import (
38
from bzrlib.tests import test_server
41
class StubServer (paramiko.ServerInterface):
43
def __init__(self, test_case):
44
paramiko.ServerInterface.__init__(self)
45
self._test_case = test_case
47
def check_auth_password(self, username, password):
49
self._test_case.log('sftpserver - authorizing: %s' % (username,))
50
return paramiko.AUTH_SUCCESSFUL
52
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):
61
return paramiko.SFTPAttributes.from_stat(
62
os.fstat(self.readfile.fileno()))
64
return paramiko.SFTPServer.convert_errno(e.errno)
66
def chattr(self, attr):
67
# python doesn't have equivalents to fchown or fchmod, so we have to
68
# use the stored filename
69
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
71
paramiko.SFTPServer.set_file_attr(self.filename, attr)
73
return paramiko.SFTPServer.convert_errno(e.errno)
76
class StubSFTPServer (paramiko.SFTPServerInterface):
78
def __init__(self, server, root, home=None):
79
paramiko.SFTPServerInterface.__init__(self, server)
80
# All paths are actually relative to 'root'.
81
# this is like implementing chroot().
86
if not home.startswith(self.root):
88
"home must be a subdirectory of root (%s vs %s)"
90
self.home = home[len(self.root):]
91
if self.home.startswith('/'):
92
self.home = self.home[1:]
93
server._test_case.log('sftpserver - new connection')
95
def _realpath(self, path):
96
# paths returned from self.canonicalize() always start with
97
# a path separator. So if 'root' is just '/', this would cause
98
# a double slash at the beginning '//home/dir'.
100
return self.canonicalize(path)
101
return self.root + self.canonicalize(path)
103
if sys.platform == 'win32':
104
def canonicalize(self, path):
105
# Win32 sftp paths end up looking like
106
# sftp://host@foo/h:/foo/bar
107
# which means absolute paths look like:
109
# and relative paths stay the same:
111
# win32 needs to use the Unicode APIs. so we require the
112
# paths to be utf8 (Linux just uses bytestreams)
113
thispath = path.decode('utf8')
114
if path.startswith('/'):
116
return os.path.normpath(thispath[1:])
118
return os.path.normpath(os.path.join(self.home, thispath))
120
def canonicalize(self, path):
121
if os.path.isabs(path):
122
return os.path.normpath(path)
124
return os.path.normpath('/' + os.path.join(self.home, path))
126
def chattr(self, path, attr):
128
paramiko.SFTPServer.set_file_attr(path, attr)
130
return paramiko.SFTPServer.convert_errno(e.errno)
131
return paramiko.SFTP_OK
133
def list_folder(self, path):
134
path = self._realpath(path)
137
# TODO: win32 incorrectly lists paths with non-ascii if path is not
138
# unicode. However on Linux the server should only deal with
139
# bytestreams and posix.listdir does the right thing
140
if sys.platform == 'win32':
141
flist = [f.encode('utf8') for f in os.listdir(path)]
143
flist = os.listdir(path)
145
attr = paramiko.SFTPAttributes.from_stat(
146
os.stat(osutils.pathjoin(path, fname)))
147
attr.filename = fname
151
return paramiko.SFTPServer.convert_errno(e.errno)
153
def stat(self, path):
154
path = self._realpath(path)
156
return paramiko.SFTPAttributes.from_stat(os.stat(path))
158
return paramiko.SFTPServer.convert_errno(e.errno)
160
def lstat(self, path):
161
path = self._realpath(path)
163
return paramiko.SFTPAttributes.from_stat(os.lstat(path))
165
return paramiko.SFTPServer.convert_errno(e.errno)
167
def open(self, path, flags, attr):
168
path = self._realpath(path)
170
flags |= getattr(os, 'O_BINARY', 0)
171
if getattr(attr, 'st_mode', None):
172
fd = os.open(path, flags, attr.st_mode)
174
# os.open() defaults to 0777 which is
175
# an odd default mode for files
176
fd = os.open(path, flags, 0666)
178
return paramiko.SFTPServer.convert_errno(e.errno)
180
if (flags & os.O_CREAT) and (attr is not None):
181
attr._flags &= ~attr.FLAG_PERMISSIONS
182
paramiko.SFTPServer.set_file_attr(path, attr)
183
if flags & os.O_WRONLY:
185
elif flags & os.O_RDWR:
191
f = os.fdopen(fd, fstr)
192
except (IOError, OSError), e:
193
return paramiko.SFTPServer.convert_errno(e.errno)
194
fobj = StubSFTPHandle()
200
def remove(self, path):
201
path = self._realpath(path)
205
return paramiko.SFTPServer.convert_errno(e.errno)
206
return paramiko.SFTP_OK
208
def rename(self, oldpath, newpath):
209
oldpath = self._realpath(oldpath)
210
newpath = self._realpath(newpath)
212
os.rename(oldpath, newpath)
214
return paramiko.SFTPServer.convert_errno(e.errno)
215
return paramiko.SFTP_OK
217
def mkdir(self, path, attr):
218
path = self._realpath(path)
220
# Using getattr() in case st_mode is None or 0
221
# both evaluate to False
222
if getattr(attr, 'st_mode', None):
223
os.mkdir(path, attr.st_mode)
227
attr._flags &= ~attr.FLAG_PERMISSIONS
228
paramiko.SFTPServer.set_file_attr(path, attr)
230
return paramiko.SFTPServer.convert_errno(e.errno)
231
return paramiko.SFTP_OK
233
def rmdir(self, path):
234
path = self._realpath(path)
238
return paramiko.SFTPServer.convert_errno(e.errno)
239
return paramiko.SFTP_OK
241
# removed: chattr, symlink, readlink
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.
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
warning('Socket error during accept() within unit test server'
303
# probably a failed test; unit test thread will log the
305
sys.excepthook(*sys.exc_info())
306
warning('Exception from within unit test server thread: %r' %
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 the "
442
"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
if sys.platform == 'win32':
446
# Win32 needs to use the UNICODE api
447
self._homedir = getcwd()
449
# But Linux SFTP servers should just deal in bytestreams
450
self._homedir = os.getcwd()
451
if self._server_homedir is None:
452
self._server_homedir = self._homedir
454
if sys.platform == 'win32':
456
self._listener = SocketListener(self._run_server_entry)
457
self._listener.setDaemon(True)
458
self._listener.start()
460
def stop_server(self):
461
self._listener.stop()
462
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
464
def get_bogus_url(self):
465
"""See bzrlib.transport.Server.get_bogus_url."""
466
# this is chosen to try to prevent trouble with proxies, wierd dns, etc
467
# we bind a random socket, so that we get a guaranteed unused port
468
# we just never listen on that port
470
s.bind(('localhost', 0))
471
return 'sftp://%s:%s/' % s.getsockname()
474
class SFTPFullAbsoluteServer(SFTPServer):
475
"""A test server for sftp transports, using absolute urls and ssh."""
478
"""See bzrlib.transport.Server.get_url."""
479
homedir = self._homedir
480
if sys.platform != 'win32':
481
# Remove the initial '/' on all platforms but win32
482
homedir = homedir[1:]
483
return self._get_sftp_url(urlutils.escape(homedir))
486
class SFTPServerWithoutSSH(SFTPServer):
487
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
490
super(SFTPServerWithoutSSH, self).__init__()
491
self._vendor = ssh.LoopbackVendor()
493
def _run_server(self, sock):
494
# Re-import these as locals, so that they're still accessible during
495
# interpreter shutdown (when all module globals get set to None, leading
496
# to confusing errors like "'NoneType' object has no attribute 'error'".
497
class FakeChannel(object):
498
def get_transport(self):
500
def get_log_channel(self):
504
def get_hexdump(self):
509
server = paramiko.SFTPServer(
510
FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
511
root=self._root, home=self._server_homedir)
513
server.start_subsystem(
514
'sftp', None, ssh.SocketAsChannelAdapter(sock))
515
except socket.error, e:
516
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
517
# it's okay for the client to disconnect abruptly
518
# (bug in paramiko 1.6: it should absorb this exception)
523
# This typically seems to happen during interpreter shutdown, so
524
# most of the useful ways to report this error are won't work.
525
# Writing the exception type, and then the text of the exception,
526
# seems to be the best we can do.
528
sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
529
sys.stderr.write('%s\n\n' % (e,))
530
server.finish_subsystem()
533
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
534
"""A test server for sftp transports, using absolute urls."""
537
"""See bzrlib.transport.Server.get_url."""
538
homedir = self._homedir
539
if sys.platform != 'win32':
540
# Remove the initial '/' on all platforms but win32
541
homedir = homedir[1:]
542
return self._get_sftp_url(urlutils.escape(homedir))
545
class SFTPHomeDirServer(SFTPServerWithoutSSH):
546
"""A test server for sftp transports, using homedir relative urls."""
549
"""See bzrlib.transport.Server.get_url."""
550
return self._get_sftp_url("~/")
553
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
554
"""A test server for sftp transports where only absolute paths will work.
556
It does this by serving from a deeply-nested directory that doesn't exist.
559
def start_server(self, backing_server=None):
560
self._server_homedir = '/dev/noone/runs/tests/here'
561
super(SFTPSiblingAbsoluteServer, self).start_server(backing_server)