58
54
# connect to an agent if we are on win32 and using Paramiko older than 1.6
59
55
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
63
def register_ssh_vendor(name, vendor):
64
"""Register SSH vendor."""
65
_ssh_vendors[name] = vendor
69
def _get_ssh_vendor():
70
"""Find out what version of SSH is on the system."""
72
if _ssh_vendor is not None:
75
if 'BZR_SSH' in os.environ:
76
vendor_name = os.environ['BZR_SSH']
58
class SSHVendorManager(object):
59
"""Manager for manage SSH vendors."""
61
# Note, although at first sign the class interface seems similar to
62
# bzrlib.registry.Registry it is not possible/convenient to directly use
63
# the Registry because the class just has "get()" interface instead of the
64
# Registry's "get(key)".
67
self._ssh_vendors = {}
68
self._cached_ssh_vendor = None
69
self._default_ssh_vendor = None
71
def register_default_vendor(self, vendor):
72
"""Register default SSH vendor."""
73
self._default_ssh_vendor = vendor
75
def register_vendor(self, name, vendor):
76
"""Register new SSH vendor by name."""
77
self._ssh_vendors[name] = vendor
79
def clear_cache(self):
80
"""Clear previously cached lookup result."""
81
self._cached_ssh_vendor = None
83
def _get_vendor_by_environment(self, environment=None):
84
"""Return the vendor or None based on BZR_SSH environment variable.
86
:raises UnknownSSH: if the BZR_SSH environment variable contains
89
if environment is None:
90
environment = os.environ
91
if 'BZR_SSH' in environment:
92
vendor_name = environment['BZR_SSH']
94
vendor = self._ssh_vendors[vendor_name]
96
raise errors.UnknownSSH(vendor_name)
100
def _get_ssh_version_string(self, args):
101
"""Return SSH version string from the subprocess."""
78
_ssh_vendor = _ssh_vendors[vendor_name]
80
raise UnknownSSH(vendor_name)
84
p = subprocess.Popen(['ssh', '-V'],
85
stdin=subprocess.PIPE,
86
stdout=subprocess.PIPE,
87
stderr=subprocess.PIPE,
88
**os_specific_subprocess_params())
89
returncode = p.returncode
90
stdout, stderr = p.communicate()
94
if 'OpenSSH' in stderr:
95
mutter('ssh implementation is OpenSSH')
96
_ssh_vendor = OpenSSHSubprocessVendor()
97
elif 'SSH Secure Shell' in stderr:
98
mutter('ssh implementation is SSH Corp.')
99
_ssh_vendor = SSHCorpSubprocessVendor()
101
if _ssh_vendor is not None:
104
# XXX: 20051123 jamesh
105
# A check for putty's plink or lsh would go here.
107
mutter('falling back to paramiko implementation')
108
_ssh_vendor = ParamikoVendor()
103
p = subprocess.Popen(args,
104
stdout=subprocess.PIPE,
105
stderr=subprocess.PIPE,
106
**os_specific_subprocess_params())
107
stdout, stderr = p.communicate()
110
return stdout + stderr
112
def _get_vendor_by_version_string(self, version, args):
113
"""Return the vendor or None based on output from the subprocess.
115
:param version: The output of 'ssh -V' like command.
116
:param args: Command line that was run.
119
if 'OpenSSH' in version:
120
trace.mutter('ssh implementation is OpenSSH')
121
vendor = OpenSSHSubprocessVendor()
122
elif 'SSH Secure Shell' in version:
123
trace.mutter('ssh implementation is SSH Corp.')
124
vendor = SSHCorpSubprocessVendor()
125
elif 'plink' in version and args[0] == 'plink':
126
# Checking if "plink" was the executed argument as Windows
127
# sometimes reports 'ssh -V' incorrectly with 'plink' in it's
128
# version. See https://bugs.launchpad.net/bzr/+bug/107155
129
trace.mutter("ssh implementation is Putty's plink.")
130
vendor = PLinkSubprocessVendor()
133
def _get_vendor_by_inspection(self):
134
"""Return the vendor or None by checking for known SSH implementations."""
135
for args in (['ssh', '-V'], ['plink', '-V']):
136
version = self._get_ssh_version_string(args)
137
vendor = self._get_vendor_by_version_string(version, args)
138
if vendor is not None:
142
def get_vendor(self, environment=None):
143
"""Find out what version of SSH is on the system.
145
:raises SSHVendorNotFound: if no any SSH vendor is found
146
:raises UnknownSSH: if the BZR_SSH environment variable contains
149
if self._cached_ssh_vendor is None:
150
vendor = self._get_vendor_by_environment(environment)
152
vendor = self._get_vendor_by_inspection()
154
trace.mutter('falling back to default implementation')
155
vendor = self._default_ssh_vendor
157
raise errors.SSHVendorNotFound()
158
self._cached_ssh_vendor = vendor
159
return self._cached_ssh_vendor
161
_ssh_vendor_manager = SSHVendorManager()
162
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
163
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
164
register_ssh_vendor = _ssh_vendor_manager.register_vendor
112
167
def _ignore_sigint():
115
170
# <https://launchpad.net/products/bzr/+bug/41433/+index>
117
172
signal.signal(signal.SIGINT, signal.SIG_IGN)
121
class LoopbackSFTP(object):
175
class SocketAsChannelAdapter(object):
122
176
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
124
178
def __init__(self, sock):
125
179
self.__socket = sock
182
return "bzr SocketAsChannelAdapter"
127
184
def send(self, data):
128
185
return self.__socket.send(data)
130
187
def recv(self, n):
131
return self.__socket.recv(n)
189
return self.__socket.recv(n)
190
except socket.error, e:
191
if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
193
# Connection has closed. Paramiko expects an empty string in
194
# this case, not an exception.
133
198
def recv_ready(self):
199
# TODO: jam 20051215 this function is necessary to support the
200
# pipelined() function. In reality, it probably should use
201
# poll() or select() to actually return if there is data
202
# available, otherwise we probably don't get any benefit
170
239
This just unifies all the locations that try to raise ConnectionError,
171
240
so that they format things properly.
173
raise SocketConnectionError(host=host, port=port, msg=msg,
174
orig_error=orig_error)
242
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
243
orig_error=orig_error)
177
246
class LoopbackVendor(SSHVendor):
178
247
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
180
249
def connect_sftp(self, username, password, host, port):
181
250
sock = socket.socket()
183
252
sock.connect((host, port))
184
253
except socket.error, e:
185
254
self._raise_connection_error(host, port=port, orig_error=e)
186
return SFTPClient(LoopbackSFTP(sock))
255
return SFTPClient(SocketAsChannelAdapter(sock))
188
257
register_ssh_vendor('loopback', LoopbackVendor())
214
283
except (paramiko.SSHException, socket.error), e:
215
284
self._raise_connection_error(host, port=port, orig_error=e)
217
286
server_key = t.get_remote_server_key()
218
287
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
219
288
keytype = server_key.get_name()
220
289
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
221
290
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
222
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
291
our_server_key_hex = paramiko.util.hexify(
292
our_server_key.get_fingerprint())
223
293
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
224
294
our_server_key = BZR_HOSTKEYS[host][keytype]
225
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
295
our_server_key_hex = paramiko.util.hexify(
296
our_server_key.get_fingerprint())
227
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
298
trace.warning('Adding %s host key for %s: %s'
299
% (keytype, host, server_key_hex))
228
300
add = getattr(BZR_HOSTKEYS, 'add', None)
229
301
if add is not None: # paramiko >= 1.X.X
230
302
BZR_HOSTKEYS.add(host, keytype, server_key)
232
304
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
233
305
our_server_key = server_key
234
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
306
our_server_key_hex = paramiko.util.hexify(
307
our_server_key.get_fingerprint())
236
309
if server_key != our_server_key:
237
310
filename1 = os.path.expanduser('~/.ssh/known_hosts')
238
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
239
raise TransportError('Host keys for %s do not match! %s != %s' % \
311
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
312
raise errors.TransportError(
313
'Host keys for %s do not match! %s != %s' %
240
314
(host, our_server_key_hex, server_key_hex),
241
315
['Try editing %s or %s' % (filename1, filename2)])
243
_paramiko_auth(username, password, host, t)
317
_paramiko_auth(username, password, host, port, t)
246
320
def connect_sftp(self, username, password, host, port):
247
321
t = self._connect(username, password, host, port)
316
397
raise NotImplementedError(self._get_vendor_specific_argv)
318
register_ssh_vendor('none', ParamikoVendor())
321
400
class OpenSSHSubprocessVendor(SubprocessVendor):
322
401
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
324
403
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
326
assert subsystem is not None or command is not None, (
327
'Must specify a command or subsystem')
328
if subsystem is not None:
329
assert command is None, (
330
'subsystem and command are mutually exclusive')
332
406
'-oForwardX11=no', '-oForwardAgent=no',
333
407
'-oClearAllForwardings=yes', '-oProtocol=2',
366
435
args.extend([host] + command)
369
438
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
372
def _paramiko_auth(username, password, host, paramiko_transport):
373
# paramiko requires a username, but it might be none if nothing was supplied
374
# use the local username, just in case.
375
# We don't override username, because if we aren't using paramiko,
376
# the username might be specified in ~/.ssh/config and we don't want to
377
# force it to something else
378
# Also, it would mess up the self.relpath() functionality
379
username = username or getpass.getuser()
441
class PLinkSubprocessVendor(SubprocessVendor):
442
"""SSH vendor that uses the 'plink' executable from Putty."""
444
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
446
args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
448
args.extend(['-P', str(port)])
449
if username is not None:
450
args.extend(['-l', username])
451
if subsystem is not None:
452
args.extend(['-s', host, subsystem])
454
args.extend([host] + command)
457
register_ssh_vendor('plink', PLinkSubprocessVendor())
460
def _paramiko_auth(username, password, host, port, paramiko_transport):
461
# paramiko requires a username, but it might be none if nothing was
462
# supplied. If so, use the local username.
464
username = getpass.getuser()
381
466
if _use_ssh_agent:
382
467
agent = paramiko.Agent()
383
468
for key in agent.get_keys():
384
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
469
trace.mutter('Trying SSH agent key %s'
470
% paramiko.util.hexify(key.get_fingerprint()))
386
472
paramiko_transport.auth_publickey(username, key)
388
474
except paramiko.SSHException, e:
391
477
# okay, try finding id_rsa or id_dss? (posix only)
392
478
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
404
490
# give up and ask for a password
405
password = bzrlib.ui.ui_factory.get_password(
406
prompt='SSH %(user)s@%(host)s password',
407
user=username, host=host)
491
auth = config.AuthenticationConfig()
492
password = auth.get_password('ssh', host, username, port=port)
409
494
paramiko_transport.auth_password(username, password)
410
495
except paramiko.SSHException, e:
411
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
496
raise errors.ConnectionError(
497
'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
415
500
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
419
504
paramiko_transport.auth_publickey(username, key)
421
506
except paramiko.PasswordRequiredException:
422
password = bzrlib.ui.ui_factory.get_password(
423
prompt='SSH %(filename)s password',
507
password = ui.ui_factory.get_password(
508
prompt='SSH %(filename)s password', filename=filename)
426
510
key = pkey_class.from_private_key_file(filename, password)
427
511
paramiko_transport.auth_publickey(username, key)
429
513
except paramiko.SSHException:
430
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
514
trace.mutter('SSH authentication via %s key failed.'
515
% (os.path.basename(filename),))
431
516
except paramiko.SSHException:
432
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
517
trace.mutter('SSH authentication via %s key failed.'
518
% (os.path.basename(filename),))