54
58
# connect to an agent if we are on win32 and using Paramiko older than 1.6
55
59
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
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."""
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']
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
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()
167
112
def _ignore_sigint():
170
115
# <https://launchpad.net/products/bzr/+bug/41433/+index>
172
117
signal.signal(signal.SIGINT, signal.SIG_IGN)
175
class SocketAsChannelAdapter(object):
121
class LoopbackSFTP(object):
176
122
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
178
124
def __init__(self, sock):
179
125
self.__socket = sock
182
return "bzr SocketAsChannelAdapter"
184
127
def send(self, data):
185
128
return self.__socket.send(data)
187
130
def recv(self, 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.
131
return self.__socket.recv(n)
198
133
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
239
170
This just unifies all the locations that try to raise ConnectionError,
240
171
so that they format things properly.
242
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
243
orig_error=orig_error)
173
raise SocketConnectionError(host=host, port=port, msg=msg,
174
orig_error=orig_error)
246
177
class LoopbackVendor(SSHVendor):
247
178
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
249
180
def connect_sftp(self, username, password, host, port):
250
181
sock = socket.socket()
252
183
sock.connect((host, port))
253
184
except socket.error, e:
254
185
self._raise_connection_error(host, port=port, orig_error=e)
255
return SFTPClient(SocketAsChannelAdapter(sock))
186
return SFTPClient(LoopbackSFTP(sock))
257
188
register_ssh_vendor('loopback', LoopbackVendor())
283
214
except (paramiko.SSHException, socket.error), e:
284
215
self._raise_connection_error(host, port=port, orig_error=e)
286
217
server_key = t.get_remote_server_key()
287
218
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
288
219
keytype = server_key.get_name()
289
220
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
290
221
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
291
our_server_key_hex = paramiko.util.hexify(
292
our_server_key.get_fingerprint())
222
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
293
223
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
294
224
our_server_key = BZR_HOSTKEYS[host][keytype]
295
our_server_key_hex = paramiko.util.hexify(
296
our_server_key.get_fingerprint())
225
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
298
trace.warning('Adding %s host key for %s: %s'
299
% (keytype, host, server_key_hex))
227
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
300
228
add = getattr(BZR_HOSTKEYS, 'add', None)
301
229
if add is not None: # paramiko >= 1.X.X
302
230
BZR_HOSTKEYS.add(host, keytype, server_key)
304
232
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
305
233
our_server_key = server_key
306
our_server_key_hex = paramiko.util.hexify(
307
our_server_key.get_fingerprint())
234
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
309
236
if server_key != our_server_key:
310
237
filename1 = os.path.expanduser('~/.ssh/known_hosts')
311
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
312
raise errors.TransportError(
313
'Host keys for %s do not match! %s != %s' %
238
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
239
raise TransportError('Host keys for %s do not match! %s != %s' % \
314
240
(host, our_server_key_hex, server_key_hex),
315
241
['Try editing %s or %s' % (filename1, filename2)])
317
_paramiko_auth(username, password, host, port, t)
243
_paramiko_auth(username, password, host, t)
320
246
def connect_sftp(self, username, password, host, port):
321
247
t = self._connect(username, password, host, port)
397
316
raise NotImplementedError(self._get_vendor_specific_argv)
318
register_ssh_vendor('none', ParamikoVendor())
400
321
class OpenSSHSubprocessVendor(SubprocessVendor):
401
322
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
403
324
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')
406
332
'-oForwardX11=no', '-oForwardAgent=no',
407
333
'-oClearAllForwardings=yes', '-oProtocol=2',
435
366
args.extend([host] + command)
438
369
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
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):
372
def _paramiko_auth(username, password, host, paramiko_transport):
461
373
# paramiko requires a username, but it might be none if nothing was supplied
462
374
# use the local username, just in case.
463
375
# We don't override username, because if we aren't using paramiko,
464
376
# the username might be specified in ~/.ssh/config and we don't want to
465
377
# force it to something else
466
378
# Also, it would mess up the self.relpath() functionality
467
auth = config.AuthenticationConfig()
469
username = auth.get_user('ssh', host, port=port)
471
# Default to local user
472
username = getpass.getuser()
379
username = username or getpass.getuser()
474
381
if _use_ssh_agent:
475
382
agent = paramiko.Agent()
476
383
for key in agent.get_keys():
477
trace.mutter('Trying SSH agent key %s'
478
% paramiko.util.hexify(key.get_fingerprint()))
384
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
480
386
paramiko_transport.auth_publickey(username, key)
482
388
except paramiko.SSHException, e:
485
391
# okay, try finding id_rsa or id_dss? (posix only)
486
392
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
498
404
# give up and ask for a password
499
password = auth.get_password('ssh', host, username, port=port)
405
password = bzrlib.ui.ui_factory.get_password(
406
prompt='SSH %(user)s@%(host)s password',
407
user=username, host=host)
501
409
paramiko_transport.auth_password(username, password)
502
410
except paramiko.SSHException, e:
503
raise errors.ConnectionError(
504
'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
411
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
507
415
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
511
419
paramiko_transport.auth_publickey(username, key)
513
421
except paramiko.PasswordRequiredException:
514
password = ui.ui_factory.get_password(
515
prompt='SSH %(filename)s password', filename=filename)
422
password = bzrlib.ui.ui_factory.get_password(
423
prompt='SSH %(filename)s password',
517
426
key = pkey_class.from_private_key_file(filename, password)
518
427
paramiko_transport.auth_publickey(username, key)
520
429
except paramiko.SSHException:
521
trace.mutter('SSH authentication via %s key failed.'
522
% (os.path.basename(filename),))
430
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
523
431
except paramiko.SSHException:
524
trace.mutter('SSH authentication via %s key failed.'
525
% (os.path.basename(filename),))
432
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))