1
# Copyright (C) 2005, 2006, 2008-2011 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.
34
from ..transport import (
37
from . 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
46
def check_auth_password(self, username, password):
48
self.log('sftpserver - authorizing: %s' % (username,))
49
return paramiko.AUTH_SUCCESSFUL
51
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):
60
return paramiko.SFTPAttributes.from_stat(
61
os.fstat(self.readfile.fileno()))
63
return paramiko.SFTPServer.convert_errno(e.errno)
65
def chattr(self, attr):
66
# python doesn't have equivalents to fchown or fchmod, so we have to
67
# use the stored filename
68
trace.mutter('Changing permissions on %s to %s', self.filename, attr)
70
paramiko.SFTPServer.set_file_attr(self.filename, attr)
72
return paramiko.SFTPServer.convert_errno(e.errno)
75
class StubSFTPServer(paramiko.SFTPServerInterface):
77
def __init__(self, server, root, home=None):
78
paramiko.SFTPServerInterface.__init__(self, server)
79
# All paths are actually relative to 'root'.
80
# this is like implementing chroot().
85
if not home.startswith(self.root):
87
"home must be a subdirectory of root (%s vs %s)"
89
self.home = home[len(self.root):]
90
if self.home.startswith('/'):
91
self.home = self.home[1:]
92
server.log('sftpserver - new connection')
94
def _realpath(self, path):
95
# paths returned from self.canonicalize() always start with
96
# a path separator. So if 'root' is just '/', this would cause
97
# a double slash at the beginning '//home/dir'.
99
return self.canonicalize(path)
100
return self.root + self.canonicalize(path)
102
if sys.platform == 'win32':
103
def canonicalize(self, path):
104
# Win32 sftp paths end up looking like
105
# sftp://host@foo/h:/foo/bar
106
# which means absolute paths look like:
108
# and relative paths stay the same:
110
# win32 needs to use the Unicode APIs. so we require the
111
# paths to be utf8 (Linux just uses bytestreams)
112
thispath = path.decode('utf8')
113
if path.startswith('/'):
115
return os.path.normpath(thispath[1:])
117
return os.path.normpath(os.path.join(self.home, thispath))
119
def canonicalize(self, path):
120
if os.path.isabs(path):
121
return osutils.normpath(path)
123
return osutils.normpath('/' + os.path.join(self.home, path))
125
def chattr(self, path, attr):
127
paramiko.SFTPServer.set_file_attr(path, attr)
129
return paramiko.SFTPServer.convert_errno(e.errno)
130
return paramiko.SFTP_OK
132
def list_folder(self, path):
133
path = self._realpath(path)
136
# TODO: win32 incorrectly lists paths with non-ascii if path is not
137
# unicode. However on unix the server should only deal with
138
# bytestreams and posix.listdir does the right thing
139
if sys.platform == 'win32':
140
flist = [f.encode('utf8') for f in os.listdir(path)]
142
flist = os.listdir(path)
144
attr = paramiko.SFTPAttributes.from_stat(
145
os.stat(osutils.pathjoin(path, fname)))
146
attr.filename = fname
150
return paramiko.SFTPServer.convert_errno(e.errno)
152
def stat(self, path):
153
path = self._realpath(path)
155
return paramiko.SFTPAttributes.from_stat(os.stat(path))
157
return paramiko.SFTPServer.convert_errno(e.errno)
159
def lstat(self, path):
160
path = self._realpath(path)
162
return paramiko.SFTPAttributes.from_stat(os.lstat(path))
164
return paramiko.SFTPServer.convert_errno(e.errno)
166
def open(self, path, flags, attr):
167
path = self._realpath(path)
169
flags |= getattr(os, 'O_BINARY', 0)
170
if getattr(attr, 'st_mode', None):
171
fd = os.open(path, flags, attr.st_mode)
173
# os.open() defaults to 0777 which is
174
# an odd default mode for files
175
fd = os.open(path, flags, 0o666)
177
return paramiko.SFTPServer.convert_errno(e.errno)
179
if (flags & os.O_CREAT) and (attr is not None):
180
attr._flags &= ~attr.FLAG_PERMISSIONS
181
paramiko.SFTPServer.set_file_attr(path, attr)
182
if flags & os.O_WRONLY:
184
elif flags & os.O_RDWR:
190
f = os.fdopen(fd, fstr)
191
except (IOError, OSError) as e:
192
return paramiko.SFTPServer.convert_errno(e.errno)
193
fobj = StubSFTPHandle()
199
def remove(self, path):
200
path = self._realpath(path)
204
return paramiko.SFTPServer.convert_errno(e.errno)
205
return paramiko.SFTP_OK
207
def rename(self, oldpath, newpath):
208
oldpath = self._realpath(oldpath)
209
newpath = self._realpath(newpath)
211
os.rename(oldpath, newpath)
213
return paramiko.SFTPServer.convert_errno(e.errno)
214
return paramiko.SFTP_OK
216
def symlink(self, target_path, path):
217
path = self._realpath(path)
219
os.symlink(target_path, path)
221
return paramiko.SFTPServer.convert_errno(e.errno)
222
return paramiko.SFTP_OK
224
def readlink(self, path):
225
path = self._realpath(path)
227
target_path = os.readlink(path)
229
return paramiko.SFTPServer.convert_errno(e.errno)
232
def mkdir(self, path, attr):
233
path = self._realpath(path)
235
# Using getattr() in case st_mode is None or 0
236
# both evaluate to False
237
if getattr(attr, 'st_mode', None):
238
os.mkdir(path, attr.st_mode)
242
attr._flags &= ~attr.FLAG_PERMISSIONS
243
paramiko.SFTPServer.set_file_attr(path, attr)
245
return paramiko.SFTPServer.convert_errno(e.errno)
246
return paramiko.SFTP_OK
248
def rmdir(self, path):
249
path = self._realpath(path)
253
return paramiko.SFTPServer.convert_errno(e.errno)
254
return paramiko.SFTP_OK
257
# (nothing in bzr's sftp transport uses those)
260
# ------------- server test implementation --------------
262
STUB_SERVER_KEY = """\
263
-----BEGIN RSA PRIVATE KEY-----
264
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
265
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
266
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
267
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
268
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
269
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
270
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
271
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
272
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
273
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
274
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
275
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
276
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
277
-----END RSA PRIVATE KEY-----
281
class SocketDelay(object):
282
"""A socket decorator to make TCP appear slower.
284
This changes recv, send, and sendall to add a fixed latency to each python
285
call if a new roundtrip is detected. That is, when a recv is called and the
286
flag new_roundtrip is set, latency is charged. Every send and send_all
289
In addition every send, sendall and recv sleeps a bit per character send to
292
Not all methods are implemented, this is deliberate as this class is not a
293
replacement for the builtin sockets layer. fileno is not implemented to
294
prevent the proxy being bypassed.
298
_proxied_arguments = dict.fromkeys([
299
"close", "getpeername", "getsockname", "getsockopt", "gettimeout",
300
"setblocking", "setsockopt", "settimeout", "shutdown"])
302
def __init__(self, sock, latency, bandwidth=1.0,
305
:param bandwith: simulated bandwith (MegaBit)
306
:param really_sleep: If set to false, the SocketDelay will just
307
increase a counter, instead of calling time.sleep. This is useful for
308
unittesting the SocketDelay.
311
self.latency = latency
312
self.really_sleep = really_sleep
313
self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024)
314
self.new_roundtrip = False
317
if self.really_sleep:
320
SocketDelay.simulated_time += s
322
def __getattr__(self, attr):
323
if attr in SocketDelay._proxied_arguments:
324
return getattr(self.sock, attr)
325
raise AttributeError("'SocketDelay' object has no attribute %r" %
329
return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
332
def recv(self, *args):
333
data = self.sock.recv(*args)
334
if data and self.new_roundtrip:
335
self.new_roundtrip = False
336
self.sleep(self.latency)
337
self.sleep(len(data) * self.time_per_byte)
340
def sendall(self, data, flags=0):
341
if not self.new_roundtrip:
342
self.new_roundtrip = True
343
self.sleep(self.latency)
344
self.sleep(len(data) * self.time_per_byte)
345
return self.sock.sendall(data, flags)
347
def send(self, data, flags=0):
348
if not self.new_roundtrip:
349
self.new_roundtrip = True
350
self.sleep(self.latency)
351
bytes_sent = self.sock.send(data, flags)
352
self.sleep(bytes_sent * self.time_per_byte)
356
class TestingSFTPConnectionHandler(socketserver.BaseRequestHandler):
359
self.wrap_for_latency()
360
tcs = self.server.test_case_server
361
ptrans = paramiko.Transport(self.request)
362
self.paramiko_transport = ptrans
363
# Set it to a channel under 'bzr' so that we get debug info
364
ptrans.set_log_channel('brz.paramiko.transport')
365
ptrans.add_server_key(tcs.get_host_key())
366
ptrans.set_subsystem_handler('sftp', paramiko.SFTPServer,
367
StubSFTPServer, root=tcs._root,
368
home=tcs._server_homedir)
369
server = tcs._server_interface(tcs)
370
# This blocks until the key exchange has been done
371
ptrans.start_server(None, server)
374
# Wait for the conversation to finish, when the paramiko.Transport
376
# TODO: Consider timing out after XX seconds rather than hanging.
377
# Also we could check paramiko_transport.active and possibly
378
# paramiko_transport.getException().
379
self.paramiko_transport.join()
381
def wrap_for_latency(self):
382
tcs = self.server.test_case_server
384
# Give the socket (which the request really is) a latency adding
386
self.request = SocketDelay(self.request, tcs.add_latency)
389
class TestingSFTPWithoutSSHConnectionHandler(TestingSFTPConnectionHandler):
392
self.wrap_for_latency()
393
# Re-import these as locals, so that they're still accessible during
394
# interpreter shutdown (when all module globals get set to None, leading
395
# to confusing errors like "'NoneType' object has no attribute 'error'".
397
class FakeChannel(object):
398
def get_transport(self):
401
def get_log_channel(self):
402
return 'brz.paramiko'
407
def get_hexdump(self):
413
tcs = self.server.test_case_server
414
sftp_server = paramiko.SFTPServer(
415
FakeChannel(), 'sftp', StubServer(tcs), StubSFTPServer,
416
root=tcs._root, home=tcs._server_homedir)
417
self.sftp_server = sftp_server
418
sys_stderr = sys.stderr # Used in error reporting during shutdown
420
sftp_server.start_subsystem(
421
'sftp', None, ssh.SocketAsChannelAdapter(self.request))
422
except socket.error as e:
423
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
424
# it's okay for the client to disconnect abruptly
425
# (bug in paramiko 1.6: it should absorb this exception)
429
except Exception as e:
430
# This typically seems to happen during interpreter shutdown, so
431
# most of the useful ways to report this error won't work.
432
# Writing the exception type, and then the text of the exception,
433
# seems to be the best we can do.
434
# FIXME: All interpreter shutdown errors should have been related
435
# to daemon threads, cleanup needed -- vila 20100623
436
sys_stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
437
sys_stderr.write('%s\n\n' % (e,))
440
self.sftp_server.finish_subsystem()
443
class TestingSFTPServer(test_server.TestingThreadingTCPServer):
445
def __init__(self, server_address, request_handler_class, test_case_server):
446
test_server.TestingThreadingTCPServer.__init__(
447
self, server_address, request_handler_class)
448
self.test_case_server = test_case_server
451
class SFTPServer(test_server.TestingTCPServerInAThread):
452
"""Common code for SFTP server facilities."""
454
def __init__(self, server_interface=StubServer):
455
self.host = '127.0.0.1'
457
super(SFTPServer, self).__init__((self.host, self.port),
459
TestingSFTPConnectionHandler)
460
self._original_vendor = None
461
self._vendor = ssh.ParamikoVendor()
462
self._server_interface = server_interface
463
self._host_key = None
467
self._server_homedir = None
470
def _get_sftp_url(self, path):
471
"""Calculate an sftp url to this server for path."""
472
return "sftp://foo:bar@%s:%s/%s" % (self.host, self.port, path)
474
def log(self, message):
475
"""StubServer uses this to log when a new server is created."""
476
self.logs.append(message)
478
def create_server(self):
479
server = self.server_class((self.host, self.port),
480
self.request_handler_class,
484
def get_host_key(self):
485
if self._host_key is None:
486
key_file = osutils.pathjoin(self._homedir, 'test_rsa.key')
487
f = open(key_file, 'w')
489
f.write(STUB_SERVER_KEY)
492
self._host_key = paramiko.RSAKey.from_private_key_file(key_file)
493
return self._host_key
495
def start_server(self, backing_server=None):
496
# XXX: TODO: make sftpserver back onto backing_server rather than local
498
if not (backing_server is None
499
or isinstance(backing_server, test_server.LocalURLServer)):
500
raise AssertionError(
501
'backing_server should not be %r, because this can only serve '
502
'the local current working directory.' % (backing_server,))
503
self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
504
ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
505
self._homedir = osutils.getcwd()
506
if sys.platform == 'win32':
507
# Normalize the path or it will be wrongly escaped
508
self._homedir = osutils.normpath(self._homedir)
510
self._homedir = self._homedir
511
if self._server_homedir is None:
512
self._server_homedir = self._homedir
514
if sys.platform == 'win32':
516
super(SFTPServer, self).start_server()
518
def stop_server(self):
520
super(SFTPServer, self).stop_server()
522
ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
524
def get_bogus_url(self):
525
"""See breezy.transport.Server.get_bogus_url."""
526
# this is chosen to try to prevent trouble with proxies, weird dns, etc
527
# we bind a random socket, so that we get a guaranteed unused port
528
# we just never listen on that port
530
s.bind(('localhost', 0))
531
return 'sftp://%s:%s/' % s.getsockname()
534
class SFTPFullAbsoluteServer(SFTPServer):
535
"""A test server for sftp transports, using absolute urls and ssh."""
538
"""See breezy.transport.Server.get_url."""
539
homedir = self._homedir
540
if sys.platform != 'win32':
541
# Remove the initial '/' on all platforms but win32
542
homedir = homedir[1:]
543
return self._get_sftp_url(urlutils.escape(homedir))
546
class SFTPServerWithoutSSH(SFTPServer):
547
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
550
super(SFTPServerWithoutSSH, self).__init__()
551
self._vendor = ssh.LoopbackVendor()
552
self.request_handler_class = TestingSFTPWithoutSSHConnectionHandler
558
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
559
"""A test server for sftp transports, using absolute urls."""
562
"""See breezy.transport.Server.get_url."""
563
homedir = self._homedir
564
if sys.platform != 'win32':
565
# Remove the initial '/' on all platforms but win32
566
homedir = homedir[1:]
567
return self._get_sftp_url(urlutils.escape(homedir))
570
class SFTPHomeDirServer(SFTPServerWithoutSSH):
571
"""A test server for sftp transports, using homedir relative urls."""
574
"""See breezy.transport.Server.get_url."""
575
return self._get_sftp_url("%7E/")
578
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
579
"""A test server for sftp transports where only absolute paths will work.
581
It does this by serving from a deeply-nested directory that doesn't exist.
584
def create_server(self):
585
# FIXME: Can't we do that in a cleaner way ? -- vila 20100623
586
server = super(SFTPSiblingAbsoluteServer, self).create_server()
587
server._server_homedir = '/dev/noone/runs/tests/here'