1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
# Copyright (C) 2005, 2006 Canonical Ltd
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Implementation of Transport over SFTP, using paramiko."""
35
from bzrlib.config import config_dir, ensure_config_dir_exists
36
from bzrlib.errors import (ConnectionError,
38
TransportNotPossible, NoSuchFile, PathNotChild,
44
from bzrlib.osutils import pathjoin, fancy_rename
45
from bzrlib.trace import mutter, warning, error
46
from bzrlib.transport import (
47
register_urlparse_netloc_protocol,
53
import bzrlib.urlutils as urlutils
57
except ImportError, e:
58
raise ParamikoNotPresent(e)
60
from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
61
SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
63
from paramiko.sftp_attr import SFTPAttributes
64
from paramiko.sftp_file import SFTPFile
65
from paramiko.sftp_client import SFTPClient
68
register_urlparse_netloc_protocol('sftp')
72
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
73
# doesn't handle it itself.
74
# <https://launchpad.net/products/bzr/+bug/41433/+index>
76
signal.signal(signal.SIGINT, signal.SIG_IGN)
79
def os_specific_subprocess_params():
80
"""Get O/S specific subprocess parameters."""
81
if sys.platform == 'win32':
82
# setting the process group and closing fds is not supported on
86
# We close fds other than the pipes as the child process does not need
89
# We also set the child process to ignore SIGINT. Normally the signal
90
# would be sent to every process in the foreground process group, but
91
# this causes it to be seen only by bzr and not by ssh. Python will
92
# generate a KeyboardInterrupt in bzr, and we will then have a chance
93
# to release locks or do other cleanup over ssh before the connection
95
# <https://launchpad.net/products/bzr/+bug/5987>
97
# Running it in a separate process group is not good because then it
98
# can't get non-echoed input of a password or passphrase.
99
# <https://launchpad.net/products/bzr/+bug/40508>
100
return {'preexec_fn': _ignore_sigint,
105
# don't use prefetch unless paramiko version >= 1.5.2 (there were bugs earlier)
106
_default_do_prefetch = False
107
if getattr(paramiko, '__version_info__', (0, 0, 0)) >= (1, 5, 5):
108
_default_do_prefetch = True
112
def _get_ssh_vendor():
113
"""Find out what version of SSH is on the system."""
115
if _ssh_vendor is not None:
120
if 'BZR_SSH' in os.environ:
121
_ssh_vendor = os.environ['BZR_SSH']
122
if _ssh_vendor == 'paramiko':
127
p = subprocess.Popen(['ssh', '-V'],
128
stdin=subprocess.PIPE,
129
stdout=subprocess.PIPE,
130
stderr=subprocess.PIPE,
131
**os_specific_subprocess_params())
132
returncode = p.returncode
133
stdout, stderr = p.communicate()
137
if 'OpenSSH' in stderr:
138
mutter('ssh implementation is OpenSSH')
139
_ssh_vendor = 'openssh'
140
elif 'SSH Secure Shell' in stderr:
141
mutter('ssh implementation is SSH Corp.')
144
if _ssh_vendor != 'none':
147
# XXX: 20051123 jamesh
148
# A check for putty's plink or lsh would go here.
150
mutter('falling back to paramiko implementation')
154
class SFTPSubprocess:
155
"""A socket-like object that talks to an ssh subprocess via pipes."""
156
def __init__(self, hostname, vendor, port=None, user=None):
157
assert vendor in ['openssh', 'ssh']
158
if vendor == 'openssh':
160
'-oForwardX11=no', '-oForwardAgent=no',
161
'-oClearAllForwardings=yes', '-oProtocol=2',
162
'-oNoHostAuthenticationForLocalhost=yes']
164
args.extend(['-p', str(port)])
166
args.extend(['-l', user])
167
args.extend(['-s', hostname, 'sftp'])
168
elif vendor == 'ssh':
171
args.extend(['-p', str(port)])
173
args.extend(['-l', user])
174
args.extend(['-s', 'sftp', hostname])
176
self.proc = subprocess.Popen(args,
177
stdin=subprocess.PIPE,
178
stdout=subprocess.PIPE,
179
**os_specific_subprocess_params())
181
def send(self, data):
182
return os.write(self.proc.stdin.fileno(), data)
184
def recv_ready(self):
185
# TODO: jam 20051215 this function is necessary to support the
186
# pipelined() function. In reality, it probably should use
187
# poll() or select() to actually return if there is data
188
# available, otherwise we probably don't get any benefit
191
def recv(self, count):
192
return os.read(self.proc.stdout.fileno(), count)
195
self.proc.stdin.close()
196
self.proc.stdout.close()
200
class LoopbackSFTP(object):
201
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
203
def __init__(self, sock):
206
def send(self, data):
207
return self.__socket.send(data)
210
return self.__socket.recv(n)
212
def recv_ready(self):
216
self.__socket.close()
222
# This is a weakref dictionary, so that we can reuse connections
223
# that are still active. Long term, it might be nice to have some
224
# sort of expiration policy, such as disconnect if inactive for
225
# X seconds. But that requires a lot more fanciness.
226
_connected_hosts = weakref.WeakValueDictionary()
228
def clear_connection_cache():
229
"""Remove all hosts from the SFTP connection cache.
231
Primarily useful for test cases wanting to force garbage collection.
233
_connected_hosts.clear()
236
def load_host_keys():
238
Load system host keys (probably doesn't work on windows) and any
239
"discovered" keys from previous sessions.
241
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
243
SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
245
mutter('failed to load system host keys: ' + str(e))
246
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
248
BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
250
mutter('failed to load bzr host keys: ' + str(e))
254
def save_host_keys():
256
Save "discovered" host keys in $(config)/ssh_host_keys/.
258
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
259
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
260
ensure_config_dir_exists()
263
f = open(bzr_hostkey_path, 'w')
264
f.write('# SSH host keys collected by bzr\n')
265
for hostname, keys in BZR_HOSTKEYS.iteritems():
266
for keytype, key in keys.iteritems():
267
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
270
mutter('failed to save bzr host keys: ' + str(e))
273
class SFTPLock(object):
274
"""This fakes a lock in a remote location."""
275
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
276
def __init__(self, path, transport):
277
assert isinstance(transport, SFTPTransport)
279
self.lock_file = None
281
self.lock_path = path + '.write-lock'
282
self.transport = transport
284
# RBC 20060103 FIXME should we be using private methods here ?
285
abspath = transport._remote_path(self.lock_path)
286
self.lock_file = transport._sftp_open_exclusive(abspath)
288
raise LockError('File %r already locked' % (self.path,))
291
"""Should this warn, or actually try to cleanup?"""
293
warning("SFTPLock %r not explicitly unlocked" % (self.path,))
297
if not self.lock_file:
299
self.lock_file.close()
300
self.lock_file = None
302
self.transport.delete(self.lock_path)
303
except (NoSuchFile,):
304
# What specific errors should we catch here?
308
class SFTPTransport (Transport):
310
Transport implementation for SFTP access.
312
_do_prefetch = _default_do_prefetch
314
def __init__(self, base, clone_from=None):
315
assert base.startswith('sftp://')
316
self._parse_url(base)
317
base = self._unparse_url()
320
super(SFTPTransport, self).__init__(base)
321
if clone_from is None:
324
# use the same ssh connection, etc
325
self._sftp = clone_from._sftp
326
# super saves 'self.base'
328
def should_cache(self):
330
Return True if the data pulled across should be cached locally.
334
def clone(self, offset=None):
336
Return a new SFTPTransport with root at self.base + offset.
337
We share the same SFTP session between such transports, because it's
338
fairly expensive to set them up.
341
return SFTPTransport(self.base, self)
343
return SFTPTransport(self.abspath(offset), self)
345
def abspath(self, relpath):
347
Return the full url to the given relative path.
349
@param relpath: the relative path or path components
350
@type relpath: str or list
352
return self._unparse_url(self._remote_path(relpath))
354
def _remote_path(self, relpath):
355
"""Return the path to be passed along the sftp protocol for relpath.
357
relpath is a urlencoded string.
359
# FIXME: share the common code across transports
360
assert isinstance(relpath, basestring)
361
relpath = urlutils.unescape(relpath).split('/')
362
basepath = self._path.split('/')
363
if len(basepath) > 0 and basepath[-1] == '':
364
basepath = basepath[:-1]
368
if len(basepath) == 0:
369
# In most filesystems, a request for the parent
370
# of root, just returns root.
378
path = '/'.join(basepath)
381
def relpath(self, abspath):
382
username, password, host, port, path = self._split_url(abspath)
384
if (username != self._username):
385
error.append('username mismatch')
386
if (host != self._host):
387
error.append('host mismatch')
388
if (port != self._port):
389
error.append('port mismatch')
390
if (not path.startswith(self._path)):
391
error.append('path mismatch')
393
extra = ': ' + ', '.join(error)
394
raise PathNotChild(abspath, self.base, extra=extra)
396
return path[pl:].strip('/')
398
def has(self, relpath):
400
Does the target location exist?
403
self._sftp.stat(self._remote_path(relpath))
408
def get(self, relpath):
410
Get the file at the given relative path.
412
:param relpath: The relative path to the file
415
path = self._remote_path(relpath)
416
f = self._sftp.file(path, mode='rb')
417
if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
420
except (IOError, paramiko.SSHException), e:
421
self._translate_io_exception(e, path, ': error retrieving')
423
def get_partial(self, relpath, start, length=None):
425
Get just part of a file.
427
:param relpath: Path to the file, relative to base
428
:param start: The starting position to read from
429
:param length: The length to read. A length of None indicates
430
read to the end of the file.
431
:return: A file-like object containing at least the specified bytes.
432
Some implementations may return objects which can be read
433
past this length, but this is not guaranteed.
435
# TODO: implement get_partial_multi to help with knit support
436
f = self.get(relpath)
438
if self._do_prefetch and hasattr(f, 'prefetch'):
442
def put(self, relpath, f, mode=None):
444
Copy the file-like or string object into the location.
446
:param relpath: Location to put the contents, relative to base.
447
:param f: File-like or string object.
448
:param mode: The final mode for the file
450
final_path = self._remote_path(relpath)
451
self._put(final_path, f, mode=mode)
453
def _put(self, abspath, f, mode=None):
454
"""Helper function so both put() and copy_abspaths can reuse the code"""
455
tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
456
os.getpid(), random.randint(0,0x7FFFFFFF))
457
fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
461
fout.set_pipelined(True)
463
except (IOError, paramiko.SSHException), e:
464
self._translate_io_exception(e, tmp_abspath)
466
self._sftp.chmod(tmp_abspath, mode)
469
self._rename_and_overwrite(tmp_abspath, abspath)
471
# If we fail, try to clean up the temporary file
472
# before we throw the exception
473
# but don't let another exception mess things up
474
# Write out the traceback, because otherwise
475
# the catch and throw destroys it
477
mutter(traceback.format_exc())
481
self._sftp.remove(tmp_abspath)
483
# raise the saved except
485
# raise the original with its traceback if we can.
488
def iter_files_recursive(self):
489
"""Walk the relative paths of all files in this transport."""
490
queue = list(self.list_dir('.'))
492
relpath = urllib.quote(queue.pop(0))
493
st = self.stat(relpath)
494
if stat.S_ISDIR(st.st_mode):
495
for i, basename in enumerate(self.list_dir(relpath)):
496
queue.insert(i, relpath+'/'+basename)
500
def mkdir(self, relpath, mode=None):
501
"""Create a directory at the given path."""
503
path = self._remote_path(relpath)
504
# In the paramiko documentation, it says that passing a mode flag
505
# will filtered against the server umask.
506
# StubSFTPServer does not do this, which would be nice, because it is
507
# what we really want :)
508
# However, real servers do use umask, so we really should do it that way
509
self._sftp.mkdir(path)
511
self._sftp.chmod(path, mode=mode)
512
except (paramiko.SSHException, IOError), e:
513
self._translate_io_exception(e, path, ': unable to mkdir',
514
failure_exc=FileExists)
516
def _translate_io_exception(self, e, path, more_info='',
517
failure_exc=PathError):
518
"""Translate a paramiko or IOError into a friendlier exception.
520
:param e: The original exception
521
:param path: The path in question when the error is raised
522
:param more_info: Extra information that can be included,
523
such as what was going on
524
:param failure_exc: Paramiko has the super fun ability to raise completely
525
opaque errors that just set "e.args = ('Failure',)" with
527
If this parameter is set, it defines the exception
528
to raise in these cases.
530
# paramiko seems to generate detailless errors.
531
self._translate_error(e, path, raise_generic=False)
532
if hasattr(e, 'args'):
533
if (e.args == ('No such file or directory',) or
534
e.args == ('No such file',)):
535
raise NoSuchFile(path, str(e) + more_info)
536
if (e.args == ('mkdir failed',)):
537
raise FileExists(path, str(e) + more_info)
538
# strange but true, for the paramiko server.
539
if (e.args == ('Failure',)):
540
raise failure_exc(path, str(e) + more_info)
541
mutter('Raising exception with args %s', e.args)
542
if hasattr(e, 'errno'):
543
mutter('Raising exception with errno %s', e.errno)
546
def append(self, relpath, f, mode=None):
548
Append the text in the file-like object into the final
552
path = self._remote_path(relpath)
553
fout = self._sftp.file(path, 'ab')
555
self._sftp.chmod(path, mode)
559
except (IOError, paramiko.SSHException), e:
560
self._translate_io_exception(e, relpath, ': unable to append')
562
def rename(self, rel_from, rel_to):
563
"""Rename without special overwriting"""
565
self._sftp.rename(self._remote_path(rel_from),
566
self._remote_path(rel_to))
567
except (IOError, paramiko.SSHException), e:
568
self._translate_io_exception(e, rel_from,
569
': unable to rename to %r' % (rel_to))
571
def _rename_and_overwrite(self, abs_from, abs_to):
572
"""Do a fancy rename on the remote server.
574
Using the implementation provided by osutils.
577
fancy_rename(abs_from, abs_to,
578
rename_func=self._sftp.rename,
579
unlink_func=self._sftp.remove)
580
except (IOError, paramiko.SSHException), e:
581
self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
583
def move(self, rel_from, rel_to):
584
"""Move the item at rel_from to the location at rel_to"""
585
path_from = self._remote_path(rel_from)
586
path_to = self._remote_path(rel_to)
587
self._rename_and_overwrite(path_from, path_to)
589
def delete(self, relpath):
590
"""Delete the item at relpath"""
591
path = self._remote_path(relpath)
593
self._sftp.remove(path)
594
except (IOError, paramiko.SSHException), e:
595
self._translate_io_exception(e, path, ': unable to delete')
598
"""Return True if this store supports listing."""
601
def list_dir(self, relpath):
603
Return a list of all files at the given location.
605
# does anything actually use this?
606
path = self._remote_path(relpath)
608
return self._sftp.listdir(path)
609
except (IOError, paramiko.SSHException), e:
610
self._translate_io_exception(e, path, ': failed to list_dir')
612
def rmdir(self, relpath):
613
"""See Transport.rmdir."""
614
path = self._remote_path(relpath)
616
return self._sftp.rmdir(path)
617
except (IOError, paramiko.SSHException), e:
618
self._translate_io_exception(e, path, ': failed to rmdir')
620
def stat(self, relpath):
621
"""Return the stat information for a file."""
622
path = self._remote_path(relpath)
624
return self._sftp.stat(path)
625
except (IOError, paramiko.SSHException), e:
626
self._translate_io_exception(e, path, ': unable to stat')
628
def lock_read(self, relpath):
630
Lock the given file for shared (read) access.
631
:return: A lock object, which has an unlock() member function
633
# FIXME: there should be something clever i can do here...
634
class BogusLock(object):
635
def __init__(self, path):
639
return BogusLock(relpath)
641
def lock_write(self, relpath):
643
Lock the given file for exclusive (write) access.
644
WARNING: many transports do not support this, so trying avoid using it
646
:return: A lock object, which has an unlock() member function
648
# This is a little bit bogus, but basically, we create a file
649
# which should not already exist, and if it does, we assume
650
# that there is a lock, and if it doesn't, the we assume
651
# that we have taken the lock.
652
return SFTPLock(relpath, self)
654
def _unparse_url(self, path=None):
657
path = urllib.quote(path)
658
# handle homedir paths
659
if not path.startswith('/'):
661
netloc = urllib.quote(self._host)
662
if self._username is not None:
663
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
664
if self._port is not None:
665
netloc = '%s:%d' % (netloc, self._port)
666
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
668
def _split_url(self, url):
669
(scheme, username, password, host, port, path) = split_url(url)
670
assert scheme == 'sftp'
672
# the initial slash should be removed from the path, and treated
673
# as a homedir relative path (the path begins with a double slash
674
# if it is absolute).
675
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
676
# RBC 20060118 we are not using this as its too user hostile. instead
677
# we are following lftp and using /~/foo to mean '~/foo'.
678
# handle homedir paths
679
if path.startswith('/~/'):
683
return (username, password, host, port, path)
685
def _parse_url(self, url):
686
(self._username, self._password,
687
self._host, self._port, self._path) = self._split_url(url)
689
def _sftp_connect(self):
690
"""Connect to the remote sftp server.
691
After this, self._sftp should have a valid connection (or
692
we raise an TransportError 'could not connect').
694
TODO: Raise a more reasonable ConnectionFailed exception
696
global _connected_hosts
698
idx = (self._host, self._port, self._username)
700
self._sftp = _connected_hosts[idx]
705
vendor = _get_ssh_vendor()
706
if vendor == 'loopback':
707
sock = socket.socket()
709
sock.connect((self._host, self._port))
710
except socket.error, e:
711
raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
712
% (self._host, self._port, e))
713
self._sftp = SFTPClient(LoopbackSFTP(sock))
714
elif vendor != 'none':
715
sock = SFTPSubprocess(self._host, vendor, self._port,
717
self._sftp = SFTPClient(sock)
719
self._paramiko_connect()
721
_connected_hosts[idx] = self._sftp
723
def _paramiko_connect(self):
724
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
729
t = paramiko.Transport((self._host, self._port or 22))
730
t.set_log_channel('bzr.paramiko')
732
except paramiko.SSHException, e:
733
raise ConnectionError('Unable to reach SSH host %s:%s: %s'
734
% (self._host, self._port, e))
736
server_key = t.get_remote_server_key()
737
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
738
keytype = server_key.get_name()
739
if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
740
our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
741
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
742
elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
743
our_server_key = BZR_HOSTKEYS[self._host][keytype]
744
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
746
warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
747
if not BZR_HOSTKEYS.has_key(self._host):
748
BZR_HOSTKEYS[self._host] = {}
749
BZR_HOSTKEYS[self._host][keytype] = server_key
750
our_server_key = server_key
751
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
753
if server_key != our_server_key:
754
filename1 = os.path.expanduser('~/.ssh/known_hosts')
755
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
756
raise TransportError('Host keys for %s do not match! %s != %s' % \
757
(self._host, our_server_key_hex, server_key_hex),
758
['Try editing %s or %s' % (filename1, filename2)])
763
self._sftp = t.open_sftp_client()
764
except paramiko.SSHException, e:
765
raise ConnectionError('Unable to start sftp client %s:%d' %
766
(self._host, self._port), e)
768
def _sftp_auth(self, transport):
769
# paramiko requires a username, but it might be none if nothing was supplied
770
# use the local username, just in case.
771
# We don't override self._username, because if we aren't using paramiko,
772
# the username might be specified in ~/.ssh/config and we don't want to
773
# force it to something else
774
# Also, it would mess up the self.relpath() functionality
775
username = self._username or getpass.getuser()
777
# Paramiko tries to open a socket.AF_UNIX in order to connect
778
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
779
# so we get an AttributeError exception. For now, just don't try to
780
# connect to an agent if we are on win32
781
if sys.platform != 'win32':
782
agent = paramiko.Agent()
783
for key in agent.get_keys():
784
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
786
transport.auth_publickey(username, key)
788
except paramiko.SSHException, e:
791
# okay, try finding id_rsa or id_dss? (posix only)
792
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
794
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
799
transport.auth_password(username, self._password)
801
except paramiko.SSHException, e:
804
# FIXME: Don't keep a password held in memory if you can help it
805
#self._password = None
807
# give up and ask for a password
808
password = bzrlib.ui.ui_factory.get_password(
809
prompt='SSH %(user)s@%(host)s password',
810
user=username, host=self._host)
812
transport.auth_password(username, password)
813
except paramiko.SSHException, e:
814
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
815
(username, self._host), e)
817
def _try_pkey_auth(self, transport, pkey_class, username, filename):
818
filename = os.path.expanduser('~/.ssh/' + filename)
820
key = pkey_class.from_private_key_file(filename)
821
transport.auth_publickey(username, key)
823
except paramiko.PasswordRequiredException:
824
password = bzrlib.ui.ui_factory.get_password(
825
prompt='SSH %(filename)s password',
828
key = pkey_class.from_private_key_file(filename, password)
829
transport.auth_publickey(username, key)
831
except paramiko.SSHException:
832
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
833
except paramiko.SSHException:
834
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
839
def _sftp_open_exclusive(self, abspath, mode=None):
840
"""Open a remote path exclusively.
842
SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
843
the file already exists. However it does not expose this
844
at the higher level of SFTPClient.open(), so we have to
847
WARNING: This breaks the SFTPClient abstraction, so it
848
could easily break against an updated version of paramiko.
850
:param abspath: The remote absolute path where the file should be opened
851
:param mode: The mode permissions bits for the new file
853
path = self._sftp._adjust_cwd(abspath)
854
attr = SFTPAttributes()
857
omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE
858
| SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
860
t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
862
raise TransportError('Expected an SFTP handle')
863
handle = msg.get_string()
864
return SFTPFile(self._sftp, handle, 'wb', -1)
865
except (paramiko.SSHException, IOError), e:
866
self._translate_io_exception(e, abspath, ': unable to open',
867
failure_exc=FileExists)
870
# ------------- server test implementation --------------
874
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
876
STUB_SERVER_KEY = """
877
-----BEGIN RSA PRIVATE KEY-----
878
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
879
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
880
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
881
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
882
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
883
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
884
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
885
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
886
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
887
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
888
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
889
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
890
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
891
-----END RSA PRIVATE KEY-----
895
class SocketListener(threading.Thread):
897
def __init__(self, callback):
898
threading.Thread.__init__(self)
899
self._callback = callback
900
self._socket = socket.socket()
901
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
902
self._socket.bind(('localhost', 0))
903
self._socket.listen(1)
904
self.port = self._socket.getsockname()[1]
905
self._stop_event = threading.Event()
908
# called from outside this thread
909
self._stop_event.set()
910
# use a timeout here, because if the test fails, the server thread may
911
# never notice the stop_event.
917
readable, writable_unused, exception_unused = \
918
select.select([self._socket], [], [], 0.1)
919
if self._stop_event.isSet():
921
if len(readable) == 0:
924
s, addr_unused = self._socket.accept()
925
# because the loopback socket is inline, and transports are
926
# never explicitly closed, best to launch a new thread.
927
threading.Thread(target=self._callback, args=(s,)).start()
928
except socket.error, x:
929
sys.excepthook(*sys.exc_info())
930
warning('Socket error during accept() within unit test server'
933
# probably a failed test; unit test thread will log the
935
sys.excepthook(*sys.exc_info())
936
warning('Exception from within unit test server thread: %r' %
940
class SFTPServer(Server):
941
"""Common code for SFTP server facilities."""
944
self._original_vendor = None
946
self._server_homedir = None
947
self._listener = None
949
self._vendor = 'none'
953
def _get_sftp_url(self, path):
954
"""Calculate an sftp url to this server for path."""
955
return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
957
def log(self, message):
958
"""StubServer uses this to log when a new server is created."""
959
self.logs.append(message)
961
def _run_server(self, s):
962
ssh_server = paramiko.Transport(s)
963
key_file = os.path.join(self._homedir, 'test_rsa.key')
964
f = open(key_file, 'w')
965
f.write(STUB_SERVER_KEY)
967
host_key = paramiko.RSAKey.from_private_key_file(key_file)
968
ssh_server.add_server_key(host_key)
969
server = StubServer(self)
970
ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
971
StubSFTPServer, root=self._root,
972
home=self._server_homedir)
973
event = threading.Event()
974
ssh_server.start_server(event, server)
979
self._original_vendor = _ssh_vendor
980
_ssh_vendor = self._vendor
981
self._homedir = os.getcwd()
982
if self._server_homedir is None:
983
self._server_homedir = self._homedir
985
# FIXME WINDOWS: _root should be _server_homedir[0]:/
986
self._listener = SocketListener(self._run_server)
987
self._listener.setDaemon(True)
988
self._listener.start()
991
"""See bzrlib.transport.Server.tearDown."""
993
self._listener.stop()
994
_ssh_vendor = self._original_vendor
996
def get_bogus_url(self):
997
"""See bzrlib.transport.Server.get_bogus_url."""
998
# this is chosen to try to prevent trouble with proxies, wierd dns,
1000
return 'sftp://127.0.0.1:1/'
1004
class SFTPFullAbsoluteServer(SFTPServer):
1005
"""A test server for sftp transports, using absolute urls and ssh."""
1008
"""See bzrlib.transport.Server.get_url."""
1009
return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
1012
class SFTPServerWithoutSSH(SFTPServer):
1013
"""An SFTP server that uses a simple TCP socket pair rather than SSH."""
1016
super(SFTPServerWithoutSSH, self).__init__()
1017
self._vendor = 'loopback'
1019
def _run_server(self, sock):
1020
class FakeChannel(object):
1021
def get_transport(self):
1023
def get_log_channel(self):
1027
def get_hexdump(self):
1032
server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
1033
root=self._root, home=self._server_homedir)
1035
server.start_subsystem('sftp', None, sock)
1036
except socket.error, e:
1037
if (len(e.args) > 0) and (e.args[0] == errno.EPIPE):
1038
# it's okay for the client to disconnect abruptly
1039
# (bug in paramiko 1.6: it should absorb this exception)
1043
except Exception, e:
1044
import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
1045
server.finish_subsystem()
1048
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
1049
"""A test server for sftp transports, using absolute urls."""
1052
"""See bzrlib.transport.Server.get_url."""
1053
return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
1056
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1057
"""A test server for sftp transports, using homedir relative urls."""
1060
"""See bzrlib.transport.Server.get_url."""
1061
return self._get_sftp_url("~/")
1064
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1065
"""A test servere for sftp transports, using absolute urls to non-home."""
1068
self._server_homedir = '/dev/noone/runs/tests/here'
1069
super(SFTPSiblingAbsoluteServer, self).setUp()
1072
def get_test_permutations():
1073
"""Return the permutations to be used in testing."""
1074
return [(SFTPTransport, SFTPAbsoluteServer),
1075
(SFTPTransport, SFTPHomeDirServer),
1076
(SFTPTransport, SFTPSiblingAbsoluteServer),