63
59
CMD_HANDLE, CMD_OPEN)
64
60
from paramiko.sftp_attr import SFTPAttributes
65
61
from paramiko.sftp_file import SFTPFile
66
from paramiko.sftp_client import SFTPClient
69
64
register_urlparse_netloc_protocol('sftp')
73
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
74
# doesn't handle it itself.
75
# <https://launchpad.net/products/bzr/+bug/41433/+index>
77
signal.signal(signal.SIGINT, signal.SIG_IGN)
80
def os_specific_subprocess_params():
81
"""Get O/S specific subprocess parameters."""
82
if sys.platform == 'win32':
83
# setting the process group and closing fds is not supported on
87
# We close fds other than the pipes as the child process does not need
90
# We also set the child process to ignore SIGINT. Normally the signal
91
# would be sent to every process in the foreground process group, but
92
# this causes it to be seen only by bzr and not by ssh. Python will
93
# generate a KeyboardInterrupt in bzr, and we will then have a chance
94
# to release locks or do other cleanup over ssh before the connection
96
# <https://launchpad.net/products/bzr/+bug/5987>
98
# Running it in a separate process group is not good because then it
99
# can't get non-echoed input of a password or passphrase.
100
# <https://launchpad.net/products/bzr/+bug/40508>
101
return {'preexec_fn': _ignore_sigint,
106
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
107
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
108
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
110
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
111
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
112
# so we get an AttributeError exception. So we will not try to
113
# connect to an agent if we are on win32 and using Paramiko older than 1.6
114
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
118
def _get_ssh_vendor():
119
"""Find out what version of SSH is on the system."""
121
if _ssh_vendor is not None:
126
if 'BZR_SSH' in os.environ:
127
_ssh_vendor = os.environ['BZR_SSH']
128
if _ssh_vendor == 'paramiko':
133
p = subprocess.Popen(['ssh', '-V'],
134
stdin=subprocess.PIPE,
135
stdout=subprocess.PIPE,
136
stderr=subprocess.PIPE,
137
**os_specific_subprocess_params())
138
returncode = p.returncode
139
stdout, stderr = p.communicate()
143
if 'OpenSSH' in stderr:
144
mutter('ssh implementation is OpenSSH')
145
_ssh_vendor = 'openssh'
146
elif 'SSH Secure Shell' in stderr:
147
mutter('ssh implementation is SSH Corp.')
150
if _ssh_vendor != 'none':
153
# XXX: 20051123 jamesh
154
# A check for putty's plink or lsh would go here.
156
mutter('falling back to paramiko implementation')
160
class SFTPSubprocess:
161
"""A socket-like object that talks to an ssh subprocess via pipes."""
162
def __init__(self, hostname, vendor, port=None, user=None):
163
assert vendor in ['openssh', 'ssh']
164
if vendor == 'openssh':
166
'-oForwardX11=no', '-oForwardAgent=no',
167
'-oClearAllForwardings=yes', '-oProtocol=2',
168
'-oNoHostAuthenticationForLocalhost=yes']
170
args.extend(['-p', str(port)])
172
args.extend(['-l', user])
173
args.extend(['-s', hostname, 'sftp'])
174
elif vendor == 'ssh':
177
args.extend(['-p', str(port)])
179
args.extend(['-l', user])
180
args.extend(['-s', 'sftp', hostname])
182
self.proc = subprocess.Popen(args,
183
stdin=subprocess.PIPE,
184
stdout=subprocess.PIPE,
185
**os_specific_subprocess_params())
187
def send(self, data):
188
return os.write(self.proc.stdin.fileno(), data)
190
def recv_ready(self):
191
# TODO: jam 20051215 this function is necessary to support the
192
# pipelined() function. In reality, it probably should use
193
# poll() or select() to actually return if there is data
194
# available, otherwise we probably don't get any benefit
197
def recv(self, count):
198
return os.read(self.proc.stdout.fileno(), count)
201
self.proc.stdin.close()
202
self.proc.stdout.close()
206
class LoopbackSFTP(object):
207
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
209
def __init__(self, sock):
212
def send(self, data):
213
return self.__socket.send(data)
216
return self.__socket.recv(n)
218
def recv_ready(self):
222
self.__socket.close()
228
67
# This is a weakref dictionary, so that we can reuse connections
229
68
# that are still active. Long term, it might be nice to have some
230
69
# sort of expiration policy, such as disconnect if inactive for
231
70
# X seconds. But that requires a lot more fanciness.
232
71
_connected_hosts = weakref.WeakValueDictionary()
74
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
75
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
76
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
234
79
def clear_connection_cache():
235
80
"""Remove all hosts from the SFTP connection cache.
239
84
_connected_hosts.clear()
242
def load_host_keys():
244
Load system host keys (probably doesn't work on windows) and any
245
"discovered" keys from previous sessions.
247
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
249
SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
251
mutter('failed to load system host keys: ' + str(e))
252
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
254
BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
256
mutter('failed to load bzr host keys: ' + str(e))
260
def save_host_keys():
262
Save "discovered" host keys in $(config)/ssh_host_keys/.
264
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
265
bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
266
ensure_config_dir_exists()
269
f = open(bzr_hostkey_path, 'w')
270
f.write('# SSH host keys collected by bzr\n')
271
for hostname, keys in BZR_HOSTKEYS.iteritems():
272
for keytype, key in keys.iteritems():
273
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
276
mutter('failed to save bzr host keys: ' + str(e))
279
87
class SFTPLock(object):
280
88
"""This fakes a lock in a remote location."""
281
89
__slots__ = ['path', 'lock_path', 'lock_file', 'transport']
817
625
TODO: Raise a more reasonable ConnectionFailed exception
819
global _connected_hosts
821
idx = (self._host, self._port, self._username)
823
self._sftp = _connected_hosts[idx]
828
vendor = _get_ssh_vendor()
829
if vendor == 'loopback':
830
sock = socket.socket()
832
sock.connect((self._host, self._port))
833
except socket.error, e:
834
raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
835
% (self._host, self._port, e))
836
self._sftp = SFTPClient(LoopbackSFTP(sock))
837
elif vendor != 'none':
839
sock = SFTPSubprocess(self._host, vendor, self._port,
841
self._sftp = SFTPClient(sock)
842
except (EOFError, paramiko.SSHException), e:
843
raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
844
% (self._host, self._port, e))
845
except (OSError, IOError), e:
846
# If the machine is fast enough, ssh can actually exit
847
# before we try and send it the sftp request, which
848
# raises a Broken Pipe
849
if e.errno not in (errno.EPIPE,):
851
raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
852
% (self._host, self._port, e))
854
self._paramiko_connect()
856
_connected_hosts[idx] = self._sftp
858
def _paramiko_connect(self):
859
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
864
t = paramiko.Transport((self._host, self._port or 22))
865
t.set_log_channel('bzr.paramiko')
867
except (paramiko.SSHException, socket.error), e:
868
raise ConnectionError('Unable to reach SSH host %s:%s: %s'
869
% (self._host, self._port, e))
871
server_key = t.get_remote_server_key()
872
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
873
keytype = server_key.get_name()
874
if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
875
our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
876
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
877
elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
878
our_server_key = BZR_HOSTKEYS[self._host][keytype]
879
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
881
warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
882
if not BZR_HOSTKEYS.has_key(self._host):
883
BZR_HOSTKEYS[self._host] = {}
884
BZR_HOSTKEYS[self._host][keytype] = server_key
885
our_server_key = server_key
886
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
888
if server_key != our_server_key:
889
filename1 = os.path.expanduser('~/.ssh/known_hosts')
890
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
891
raise TransportError('Host keys for %s do not match! %s != %s' % \
892
(self._host, our_server_key_hex, server_key_hex),
893
['Try editing %s or %s' % (filename1, filename2)])
898
self._sftp = t.open_sftp_client()
899
except paramiko.SSHException, e:
900
raise ConnectionError('Unable to start sftp client %s:%d' %
901
(self._host, self._port), e)
903
def _sftp_auth(self, transport):
904
# paramiko requires a username, but it might be none if nothing was supplied
905
# use the local username, just in case.
906
# We don't override self._username, because if we aren't using paramiko,
907
# the username might be specified in ~/.ssh/config and we don't want to
908
# force it to something else
909
# Also, it would mess up the self.relpath() functionality
910
username = self._username or getpass.getuser()
913
agent = paramiko.Agent()
914
for key in agent.get_keys():
915
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
917
transport.auth_publickey(username, key)
919
except paramiko.SSHException, e:
922
# okay, try finding id_rsa or id_dss? (posix only)
923
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
925
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
930
transport.auth_password(username, self._password)
932
except paramiko.SSHException, e:
935
# FIXME: Don't keep a password held in memory if you can help it
936
#self._password = None
938
# give up and ask for a password
939
password = bzrlib.ui.ui_factory.get_password(
940
prompt='SSH %(user)s@%(host)s password',
941
user=username, host=self._host)
943
transport.auth_password(username, password)
944
except paramiko.SSHException, e:
945
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
946
(username, self._host), e)
948
def _try_pkey_auth(self, transport, pkey_class, username, filename):
949
filename = os.path.expanduser('~/.ssh/' + filename)
951
key = pkey_class.from_private_key_file(filename)
952
transport.auth_publickey(username, key)
954
except paramiko.PasswordRequiredException:
955
password = bzrlib.ui.ui_factory.get_password(
956
prompt='SSH %(filename)s password',
959
key = pkey_class.from_private_key_file(filename, password)
960
transport.auth_publickey(username, key)
962
except paramiko.SSHException:
963
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
964
except paramiko.SSHException:
965
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
627
self._sftp = _sftp_connect(self._host, self._port, self._username,
970
630
def _sftp_open_exclusive(self, abspath, mode=None):
971
631
"""Open a remote path exclusively.
1298
959
super(SFTPSiblingAbsoluteServer, self).setUp()
962
def _sftp_connect(host, port, username, password):
963
"""Connect to the remote sftp server.
965
:raises: a TransportError 'could not connect'.
967
:returns: an paramiko.sftp_client.SFTPClient
969
TODO: Raise a more reasonable ConnectionFailed exception
971
idx = (host, port, username)
973
return _connected_hosts[idx]
977
sftp = _sftp_connect_uncached(host, port, username, password)
978
_connected_hosts[idx] = sftp
981
def _sftp_connect_uncached(host, port, username, password):
982
vendor = ssh._get_ssh_vendor()
983
sftp = vendor.connect_sftp(username, password, host, port)
1301
987
def get_test_permutations():
1302
988
"""Return the permutations to be used in testing."""
1303
989
return [(SFTPTransport, SFTPAbsoluteServer),