27
from bzrlib.errors import (FileExists,
28
TransportNotPossible, NoSuchFile, NonRelativePath,
30
from bzrlib.config import config_dir
32
from bzrlib.config import config_dir, ensure_config_dir_exists
33
from bzrlib.errors import (ConnectionError,
35
TransportNotPossible, NoSuchFile, PathNotChild,
37
LockError, ParamikoNotPresent
39
from bzrlib.osutils import pathjoin, fancy_rename
31
40
from bzrlib.trace import mutter, warning, error
32
from bzrlib.transport import Transport, register_transport
41
from bzrlib.transport import Transport, Server, urlescape
37
error('The SFTP transport requires paramiko.')
46
except ImportError, e:
47
raise ParamikoNotPresent(e)
49
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
50
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
52
from paramiko.sftp_attr import SFTPAttributes
53
from paramiko.sftp_file import SFTPFile
54
from paramiko.sftp_client import SFTPClient
56
if 'sftp' not in urlparse.uses_netloc:
57
urlparse.uses_netloc.append('sftp')
59
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
60
_default_do_prefetch = False
61
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 2):
62
_default_do_prefetch = True
66
if sys.platform == 'win32':
67
# close_fds not supported on win32
72
def _get_ssh_vendor():
73
"""Find out what version of SSH is on the system."""
75
if _ssh_vendor is not None:
80
if 'BZR_SSH' in os.environ:
81
_ssh_vendor = os.environ['BZR_SSH']
82
if _ssh_vendor == 'paramiko':
87
p = subprocess.Popen(['ssh', '-V'],
89
stdin=subprocess.PIPE,
90
stdout=subprocess.PIPE,
91
stderr=subprocess.PIPE)
92
returncode = p.returncode
93
stdout, stderr = p.communicate()
97
if 'OpenSSH' in stderr:
98
mutter('ssh implementation is OpenSSH')
99
_ssh_vendor = 'openssh'
100
elif 'SSH Secure Shell' in stderr:
101
mutter('ssh implementation is SSH Corp.')
104
if _ssh_vendor != 'none':
107
# XXX: 20051123 jamesh
108
# A check for putty's plink or lsh would go here.
110
mutter('falling back to paramiko implementation')
114
class SFTPSubprocess:
115
"""A socket-like object that talks to an ssh subprocess via pipes."""
116
def __init__(self, hostname, vendor, port=None, user=None):
117
assert vendor in ['openssh', 'ssh']
118
if vendor == 'openssh':
120
'-oForwardX11=no', '-oForwardAgent=no',
121
'-oClearAllForwardings=yes', '-oProtocol=2',
122
'-oNoHostAuthenticationForLocalhost=yes']
124
args.extend(['-p', str(port)])
126
args.extend(['-l', user])
127
args.extend(['-s', hostname, 'sftp'])
128
elif vendor == 'ssh':
131
args.extend(['-p', str(port)])
133
args.extend(['-l', user])
134
args.extend(['-s', 'sftp', hostname])
136
self.proc = subprocess.Popen(args, close_fds=_close_fds,
137
stdin=subprocess.PIPE,
138
stdout=subprocess.PIPE)
140
def send(self, data):
141
return os.write(self.proc.stdin.fileno(), data)
143
def recv_ready(self):
144
# TODO: jam 20051215 this function is necessary to support the
145
# pipelined() function. In reality, it probably should use
146
# poll() or select() to actually return if there is data
147
# available, otherwise we probably don't get any benefit
150
def recv(self, count):
151
return os.read(self.proc.stdout.fileno(), count)
154
self.proc.stdin.close()
155
self.proc.stdout.close()
159
class LoopbackSFTP(object):
160
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
162
def __init__(self, sock):
165
def send(self, data):
166
return self.__socket.send(data)
169
return self.__socket.recv(n)
171
def recv_ready(self):
175
self.__socket.close()
41
178
SYSTEM_HOSTKEYS = {}
181
# This is a weakref dictionary, so that we can reuse connections
182
# that are still active. Long term, it might be nice to have some
183
# sort of expiration policy, such as disconnect if inactive for
184
# X seconds. But that requires a lot more fanciness.
185
_connected_hosts = weakref.WeakValueDictionary()
44
188
def load_host_keys():
46
190
Load system host keys (probably doesn't work on windows) and any
77
222
mutter('failed to save bzr host keys: ' + str(e))
81
class SFTPTransportError (TransportError):
225
class SFTPLock(object):
226
"""This fakes a lock in a remote location."""
227
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
228
def __init__(self, path, transport):
229
assert isinstance(transport, SFTPTransport)
231
self.lock_file = None
233
self.lock_path = path + '.write-lock'
234
self.transport = transport
236
# RBC 20060103 FIXME should we be using private methods here ?
237
abspath = transport._remote_path(self.lock_path)
238
self.lock_file = transport._sftp_open_exclusive(abspath)
240
raise LockError('File %r already locked' % (self.path,))
243
"""Should this warn, or actually try to cleanup?"""
245
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
249
if not self.lock_file:
251
self.lock_file.close()
252
self.lock_file = None
254
self.transport.delete(self.lock_path)
255
except (NoSuchFile,):
256
# What specific errors should we catch here?
85
259
class SFTPTransport (Transport):
87
261
Transport implementation for SFTP access.
263
_do_prefetch = _default_do_prefetch
90
_url_matcher = re.compile(r'^sftp://([^:@]*(:[^@]*)?@)?(.*?)(:\d+)?(/.*)?$')
92
265
def __init__(self, base, clone_from=None):
93
266
assert base.startswith('sftp://')
267
self._parse_url(base)
268
base = self._unparse_url()
94
271
super(SFTPTransport, self).__init__(base)
96
272
if clone_from is None:
97
273
self._sftp_connect()
194
383
Some implementations may return objects which can be read
195
384
past this length, but this is not guaranteed.
386
# TODO: implement get_partial_multi to help with knit support
197
387
f = self.get(relpath)
389
if self._do_prefetch and hasattr(f, 'prefetch'):
201
def put(self, relpath, f):
393
def put(self, relpath, f, mode=None):
203
395
Copy the file-like or string object into the location.
205
397
:param relpath: Location to put the contents, relative to base.
206
398
:param f: File-like or string object.
399
:param mode: The final mode for the file
208
# FIXME: should do something atomic or locking here, this is unsafe
210
path = self._abspath(relpath)
211
fout = self._sftp.file(path, 'wb')
213
self._translate_io_exception(e, relpath)
214
except (IOError, paramiko.SSHException), x:
215
raise SFTPTransportError('Unable to write file %r' % (path,), x)
401
final_path = self._remote_path(relpath)
402
self._put(final_path, f, mode=mode)
404
def _put(self, abspath, f, mode=None):
405
"""Helper function so both put() and copy_abspaths can reuse the code"""
406
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
407
os.getpid(), random.randint(0,0x7FFFFFFF))
408
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
412
fout.set_pipelined(True)
414
except (IOError, paramiko.SSHException), e:
415
self._translate_io_exception(e, tmp_abspath)
417
self._sftp.chmod(tmp_abspath, mode)
420
self._rename(tmp_abspath, abspath)
422
# If we fail, try to clean up the temporary file
423
# before we throw the exception
424
# but don't let another exception mess things up
425
# Write out the traceback, because otherwise
426
# the catch and throw destroys it
428
mutter(traceback.format_exc())
432
self._sftp.remove(tmp_abspath)
434
# raise the saved except
436
# raise the original with its traceback if we can.
221
439
def iter_files_recursive(self):
222
440
"""Walk the relative paths of all files in this transport."""
233
def mkdir(self, relpath):
451
def mkdir(self, relpath, mode=None):
234
452
"""Create a directory at the given path."""
236
path = self._abspath(relpath)
454
path = self._remote_path(relpath)
455
# In the paramiko documentation, it says that passing a mode flag
456
# will filtered against the server umask.
457
# StubSFTPServer does not do this, which would be nice, because it is
458
# what we really want :)
459
# However, real servers do use umask, so we really should do it that way
237
460
self._sftp.mkdir(path)
239
self._translate_io_exception(e, relpath)
240
except (IOError, paramiko.SSHException), x:
241
raise SFTPTransportError('Unable to mkdir %r' % (path,), x)
243
def _translate_io_exception(self, e, relpath):
462
self._sftp.chmod(path, mode=mode)
463
except (paramiko.SSHException, IOError), e:
464
self._translate_io_exception(e, path, ': unable to mkdir',
465
failure_exc=FileExists)
467
def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
468
"""Translate a paramiko or IOError into a friendlier exception.
470
:param e: The original exception
471
:param path: The path in question when the error is raised
472
:param more_info: Extra information that can be included,
473
such as what was going on
474
:param failure_exc: Paramiko has the super fun ability to raise completely
475
opaque errors that just set "e.args = ('Failure',)" with
477
This sometimes means FileExists, but it also sometimes
244
480
# paramiko seems to generate detailless errors.
245
if (e.errno == errno.ENOENT or
246
e.args == ('No such file or directory',) or
247
e.args == ('No such file',)):
248
raise NoSuchFile(relpath)
249
if (e.args == ('mkdir failed',)):
250
raise FileExists(relpath)
251
# strange but true, for the paramiko server.
252
if (e.args == ('Failure',)):
253
raise FileExists(relpath)
481
self._translate_error(e, path, raise_generic=False)
482
if hasattr(e, 'args'):
483
if (e.args == ('No such file or directory',) or
484
e.args == ('No such file',)):
485
raise NoSuchFile(path, str(e) + more_info)
486
if (e.args == ('mkdir failed',)):
487
raise FileExists(path, str(e) + more_info)
488
# strange but true, for the paramiko server.
489
if (e.args == ('Failure',)):
490
raise failure_exc(path, str(e) + more_info)
491
mutter('Raising exception with args %s', e.args)
492
if hasattr(e, 'errno'):
493
mutter('Raising exception with errno %s', e.errno)
256
496
def append(self, relpath, f):
262
path = self._abspath(relpath)
502
path = self._remote_path(relpath)
263
503
fout = self._sftp.file(path, 'ab')
264
504
self._pump(f, fout)
265
except (IOError, paramiko.SSHException), x:
266
raise SFTPTransportError('Unable to append file %r' % (path,), x)
505
except (IOError, paramiko.SSHException), e:
506
self._translate_io_exception(e, relpath, ': unable to append')
268
def copy(self, rel_from, rel_to):
269
"""Copy the item at rel_from to the location at rel_to"""
270
path_from = self._abspath(rel_from)
271
path_to = self._abspath(rel_to)
508
def _rename(self, abs_from, abs_to):
509
"""Do a fancy rename on the remote server.
511
Using the implementation provided by osutils.
273
fin = self._sftp.file(path_from, 'rb')
275
fout = self._sftp.file(path_to, 'wb')
277
fout.set_pipelined(True)
278
self._pump(fin, fout)
283
except (IOError, paramiko.SSHException), x:
284
raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
514
fancy_rename(abs_from, abs_to,
515
rename_func=self._sftp.rename,
516
unlink_func=self._sftp.remove)
517
except (IOError, paramiko.SSHException), e:
518
self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
286
520
def move(self, rel_from, rel_to):
287
521
"""Move the item at rel_from to the location at rel_to"""
288
path_from = self._abspath(rel_from)
289
path_to = self._abspath(rel_to)
291
self._sftp.rename(path_from, path_to)
292
except (IOError, paramiko.SSHException), x:
293
raise SFTPTransportError('Unable to move %r to %r' % (path_from, path_to), x)
522
path_from = self._remote_path(rel_from)
523
path_to = self._remote_path(rel_to)
524
self._rename(path_from, path_to)
295
526
def delete(self, relpath):
296
527
"""Delete the item at relpath"""
297
path = self._abspath(relpath)
528
path = self._remote_path(relpath)
299
530
self._sftp.remove(path)
300
except (IOError, paramiko.SSHException), x:
301
raise SFTPTransportError('Unable to delete %r' % (path,), x)
531
except (IOError, paramiko.SSHException), e:
532
self._translate_io_exception(e, path, ': unable to delete')
303
534
def listable(self):
304
535
"""Return True if this store supports listing."""
309
540
Return a list of all files at the given location.
311
542
# does anything actually use this?
312
path = self._abspath(relpath)
543
path = self._remote_path(relpath)
314
545
return self._sftp.listdir(path)
315
except (IOError, paramiko.SSHException), x:
316
raise SFTPTransportError('Unable to list folder %r' % (path,), x)
546
except (IOError, paramiko.SSHException), e:
547
self._translate_io_exception(e, path, ': failed to list_dir')
549
def rmdir(self, relpath):
550
"""See Transport.rmdir."""
551
path = self._remote_path(relpath)
553
return self._sftp.rmdir(path)
554
except (IOError, paramiko.SSHException), e:
555
self._translate_io_exception(e, path, ': failed to rmdir')
318
557
def stat(self, relpath):
319
558
"""Return the stat information for a file."""
320
path = self._abspath(relpath)
559
path = self._remote_path(relpath)
322
561
return self._sftp.stat(path)
323
except (IOError, paramiko.SSHException), x:
324
raise SFTPTransportError('Unable to stat %r' % (path,), x)
562
except (IOError, paramiko.SSHException), e:
563
self._translate_io_exception(e, path, ': unable to stat')
326
565
def lock_read(self, relpath):
328
567
Lock the given file for shared (read) access.
329
:return: A lock object, which should be passed to Transport.unlock()
568
:return: A lock object, which has an unlock() member function
331
570
# FIXME: there should be something clever i can do here...
332
571
class BogusLock(object):
341
580
Lock the given file for exclusive (write) access.
342
581
WARNING: many transports do not support this, so trying avoid using it
344
:return: A lock object, which should be passed to Transport.unlock()
583
:return: A lock object, which has an unlock() member function
346
# FIXME: there should be something clever i can do here...
347
class BogusLock(object):
348
def __init__(self, path):
352
return BogusLock(relpath)
585
# This is a little bit bogus, but basically, we create a file
586
# which should not already exist, and if it does, we assume
587
# that there is a lock, and if it doesn't, the we assume
588
# that we have taken the lock.
589
return SFTPLock(relpath, self)
355
591
def _unparse_url(self, path=None):
357
593
path = self._path
359
return 'sftp://%s@%s%s' % (self._username, self._host, path)
360
return 'sftp://%s@%s:%d%s' % (self._username, self._host, self._port, path)
594
path = urllib.quote(path)
595
# handle homedir paths
596
if not path.startswith('/'):
598
netloc = urllib.quote(self._host)
599
if self._username is not None:
600
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
601
if self._port is not None:
602
netloc = '%s:%d' % (netloc, self._port)
604
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
606
def _split_url(self, url):
607
if isinstance(url, unicode):
608
url = url.encode('utf-8')
609
(scheme, netloc, path, params,
610
query, fragment) = urlparse.urlparse(url, allow_fragments=False)
611
assert scheme == 'sftp'
612
username = password = host = port = None
614
username, host = netloc.split('@', 1)
616
username, password = username.split(':', 1)
617
password = urllib.unquote(password)
618
username = urllib.unquote(username)
623
host, port = host.rsplit(':', 1)
627
# TODO: Should this be ConnectionError?
628
raise TransportError('%s: invalid port number' % port)
629
host = urllib.unquote(host)
631
path = urllib.unquote(path)
633
# the initial slash should be removed from the path, and treated
634
# as a homedir relative path (the path begins with a double slash
635
# if it is absolute).
636
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
637
# RBC 20060118 we are not using this as its too user hostile. instead
638
# we are following lftp and using /~/foo to mean '~/foo'.
639
# handle homedir paths
640
if path.startswith('/~/'):
644
return (username, password, host, port, path)
362
646
def _parse_url(self, url):
363
assert url[:7] == 'sftp://'
364
m = self._url_matcher.match(url)
366
raise SFTPTransportError('Unable to parse SFTP URL %r' % (url,))
367
self._username, self._password, self._host, self._port, self._path = m.groups()
368
if self._username is None:
369
self._username = getpass.getuser()
371
self._username = self._username[:-1]
373
self._password = self._password[1:]
374
self._username = self._username[len(self._password)+1:]
375
if self._port is None:
378
self._port = int(self._port[1:])
379
if (self._path is None) or (self._path == ''):
647
(self._username, self._password,
648
self._host, self._port, self._path) = self._split_url(url)
382
650
def _sftp_connect(self):
651
"""Connect to the remote sftp server.
652
After this, self._sftp should have a valid connection (or
653
we raise an TransportError 'could not connect').
655
TODO: Raise a more reasonable ConnectionFailed exception
657
global _connected_hosts
659
idx = (self._host, self._port, self._username)
661
self._sftp = _connected_hosts[idx]
666
vendor = _get_ssh_vendor()
667
if vendor == 'loopback':
668
sock = socket.socket()
669
sock.connect((self._host, self._port))
670
self._sftp = SFTPClient(LoopbackSFTP(sock))
671
elif vendor != 'none':
672
sock = SFTPSubprocess(self._host, vendor, self._port,
674
self._sftp = SFTPClient(sock)
676
self._paramiko_connect()
678
_connected_hosts[idx] = self._sftp
680
def _paramiko_connect(self):
383
681
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
388
t = paramiko.Transport((self._host, self._port))
686
t = paramiko.Transport((self._host, self._port or 22))
687
t.set_log_channel('bzr.paramiko')
390
except paramiko.SSHException:
391
raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
689
except paramiko.SSHException, e:
690
raise ConnectionError('Unable to reach SSH host %s:%d' %
691
(self._host, self._port), e)
393
693
server_key = t.get_remote_server_key()
394
694
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
410
710
if server_key != our_server_key:
411
711
filename1 = os.path.expanduser('~/.ssh/known_hosts')
412
filename2 = os.path.join(config_dir(), 'ssh_host_keys')
413
raise SFTPTransportError('Host keys for %s do not match! %s != %s' % \
712
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
713
raise TransportError('Host keys for %s do not match! %s != %s' % \
414
714
(self._host, our_server_key_hex, server_key_hex),
415
715
['Try editing %s or %s' % (filename1, filename2)])
417
self._sftp_auth(t, self._username, self._host)
420
720
self._sftp = t.open_sftp_client()
421
except paramiko.SSHException:
422
raise BzrError('Unable to find path %s on SFTP server %s' % \
423
(self._path, self._host))
425
def _sftp_auth(self, transport, username, host):
426
agent = paramiko.Agent()
427
for key in agent.get_keys():
428
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
430
transport.auth_publickey(self._username, key)
432
except paramiko.SSHException, e:
721
except paramiko.SSHException, e:
722
raise ConnectionError('Unable to start sftp client %s:%d' %
723
(self._host, self._port), e)
725
def _sftp_auth(self, transport):
726
# paramiko requires a username, but it might be none if nothing was supplied
727
# use the local username, just in case.
728
# We don't override self._username, because if we aren't using paramiko,
729
# the username might be specified in ~/.ssh/config and we don't want to
730
# force it to something else
731
# Also, it would mess up the self.relpath() functionality
732
username = self._username or getpass.getuser()
734
# Paramiko tries to open a socket.AF_UNIX in order to connect
735
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
736
# so we get an AttributeError exception. For now, just don't try to
737
# connect to an agent if we are on win32
738
if sys.platform != 'win32':
739
agent = paramiko.Agent()
740
for key in agent.get_keys():
741
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
743
transport.auth_publickey(username, key)
745
except paramiko.SSHException, e:
435
748
# okay, try finding id_rsa or id_dss? (posix only)
436
if self._try_pkey_auth(transport, paramiko.RSAKey, 'id_rsa'):
749
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
438
if self._try_pkey_auth(transport, paramiko.DSSKey, 'id_dsa'):
751
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
441
754
if self._password:
443
transport.auth_password(self._username, self._password)
756
transport.auth_password(username, self._password)
445
758
except paramiko.SSHException, e:
761
# FIXME: Don't keep a password held in memory if you can help it
762
#self._password = None
448
764
# give up and ask for a password
449
password = getpass.getpass('SSH %s@%s password: ' % (self._username, self._host))
765
password = bzrlib.ui.ui_factory.get_password(
766
prompt='SSH %(user)s@%(host)s password',
767
user=username, host=self._host)
451
transport.auth_password(self._username, password)
452
except paramiko.SSHException:
453
raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
454
(self._username, self._host))
769
transport.auth_password(username, password)
770
except paramiko.SSHException, e:
771
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
772
(username, self._host), e)
456
def _try_pkey_auth(self, transport, pkey_class, filename):
774
def _try_pkey_auth(self, transport, pkey_class, username, filename):
457
775
filename = os.path.expanduser('~/.ssh/' + filename)
459
777
key = pkey_class.from_private_key_file(filename)
460
transport.auth_publickey(self._username, key)
778
transport.auth_publickey(username, key)
462
780
except paramiko.PasswordRequiredException:
463
password = getpass.getpass('SSH %s password: ' % (os.path.basename(filename),))
781
password = bzrlib.ui.ui_factory.get_password(
782
prompt='SSH %(filename)s password',
465
785
key = pkey_class.from_private_key_file(filename, password)
466
transport.auth_publickey(self._username, key)
786
transport.auth_publickey(username, key)
468
788
except paramiko.SSHException:
469
789
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
796
def _sftp_open_exclusive(self, abspath, mode=None):
797
"""Open a remote path exclusively.
799
SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
800
the file already exists. However it does not expose this
801
at the higher level of SFTPClient.open(), so we have to
804
WARNING: This breaks the SFTPClient abstraction, so it
805
could easily break against an updated version of paramiko.
807
:param abspath: The remote absolute path where the file should be opened
808
:param mode: The mode permissions bits for the new file
810
path = self._sftp._adjust_cwd(abspath)
811
attr = SFTPAttributes()
814
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
815
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
817
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
819
raise TransportError('Expected an SFTP handle')
820
handle = msg.get_string()
821
return SFTPFile(self._sftp, handle, 'wb', -1)
822
except (paramiko.SSHException, IOError), e:
823
self._translate_io_exception(e, abspath, ': unable to open',
824
failure_exc=FileExists)
827
# ------------- server test implementation --------------
831
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
833
STUB_SERVER_KEY = """
834
-----BEGIN RSA PRIVATE KEY-----
835
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
836
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
837
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
838
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
839
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
840
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
841
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
842
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
843
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
844
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
845
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
846
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
847
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
848
-----END RSA PRIVATE KEY-----
852
class SingleListener(threading.Thread):
854
def __init__(self, callback):
855
threading.Thread.__init__(self)
856
self._callback = callback
857
self._socket = socket.socket()
858
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
859
self._socket.bind(('localhost', 0))
860
self._socket.listen(1)
861
self.port = self._socket.getsockname()[1]
862
self.stop_event = threading.Event()
865
s, _ = self._socket.accept()
866
# now close the listen socket
869
self._callback(s, self.stop_event)
871
pass #Ignore socket errors
873
# probably a failed test
874
warning('Exception from within unit test server thread: %r' % x)
877
self.stop_event.set()
878
# use a timeout here, because if the test fails, the server thread may
879
# never notice the stop_event.
883
class SFTPServer(Server):
884
"""Common code for SFTP server facilities."""
887
self._original_vendor = None
889
self._server_homedir = None
890
self._listener = None
892
self._vendor = 'none'
896
def _get_sftp_url(self, path):
897
"""Calculate an sftp url to this server for path."""
898
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
900
def log(self, message):
901
"""StubServer uses this to log when a new server is created."""
902
self.logs.append(message)
904
def _run_server(self, s, stop_event):
905
ssh_server = paramiko.Transport(s)
906
key_file = os.path.join(self._homedir, 'test_rsa.key')
907
file(key_file, 'w').write(STUB_SERVER_KEY)
908
host_key = paramiko.RSAKey.from_private_key_file(key_file)
909
ssh_server.add_server_key(host_key)
910
server = StubServer(self)
911
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
912
StubSFTPServer, root=self._root,
913
home=self._server_homedir)
914
event = threading.Event()
915
ssh_server.start_server(event, server)
917
stop_event.wait(30.0)
921
self._original_vendor = _ssh_vendor
922
_ssh_vendor = self._vendor
923
self._homedir = os.getcwdu()
924
if self._server_homedir is None:
925
self._server_homedir = self._homedir
927
# FIXME WINDOWS: _root should be _server_homedir[0]:/
928
self._listener = SingleListener(self._run_server)
929
self._listener.setDaemon(True)
930
self._listener.start()
933
"""See bzrlib.transport.Server.tearDown."""
935
self._listener.stop()
936
_ssh_vendor = self._original_vendor
939
class SFTPFullAbsoluteServer(SFTPServer):
940
"""A test server for sftp transports, using absolute urls and ssh."""
943
"""See bzrlib.transport.Server.get_url."""
944
return self._get_sftp_url(urlescape(self._homedir[1:]))
947
class SFTPServerWithoutSSH(SFTPServer):
948
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
951
super(SFTPServerWithoutSSH, self).__init__()
952
self._vendor = 'loopback'
954
def _run_server(self, sock, stop_event):
955
class FakeChannel(object):
956
def get_transport(self):
958
def get_log_channel(self):
962
def get_hexdump(self):
965
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
966
root=self._root, home=self._server_homedir)
967
server.start_subsystem('sftp', None, sock)
968
server.finish_subsystem()
971
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
972
"""A test server for sftp transports, using absolute urls."""
975
"""See bzrlib.transport.Server.get_url."""
976
return self._get_sftp_url(urlescape(self._homedir[1:]))
979
class SFTPHomeDirServer(SFTPServerWithoutSSH):
980
"""A test server for sftp transports, using homedir relative urls."""
983
"""See bzrlib.transport.Server.get_url."""
984
return self._get_sftp_url("~/")
987
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
988
"""A test servere for sftp transports, using absolute urls to non-home."""
991
self._server_homedir = '/dev/noone/runs/tests/here'
992
super(SFTPSiblingAbsoluteServer, self).setUp()
995
def get_test_permutations():
996
"""Return the permutations to be used in testing."""
997
return [(SFTPTransport, SFTPAbsoluteServer),
998
(SFTPTransport, SFTPHomeDirServer),
999
(SFTPTransport, SFTPSiblingAbsoluteServer),