126
133
elif 'SSH Secure Shell' in version:
127
134
trace.mutter('ssh implementation is SSH Corp.')
128
135
vendor = SSHCorpSubprocessVendor()
136
elif 'lsh' in version:
137
trace.mutter('ssh implementation is GNU lsh.')
138
vendor = LSHSubprocessVendor()
129
139
# As plink user prompts are not handled currently, don't auto-detect
130
140
# it by inspection below, but keep this vendor detection for if a path
131
# is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
141
# is given in BRZ_SSH. See https://bugs.launchpad.net/bugs/414743
132
142
elif 'plink' in version and progname == 'plink':
133
143
# Checking if "plink" was the executed argument as Windows
134
# sometimes reports 'ssh -V' incorrectly with 'plink' in it's
144
# sometimes reports 'ssh -V' incorrectly with 'plink' in its
135
145
# version. See https://bugs.launchpad.net/bzr/+bug/107155
136
146
trace.mutter("ssh implementation is Putty's plink.")
137
147
vendor = PLinkSubprocessVendor()
262
271
sock = socket.socket()
264
273
sock.connect((host, port))
265
except socket.error, e:
274
except socket.error as e:
266
275
self._raise_connection_error(host, port=port, orig_error=e)
267
276
return SFTPClient(SocketAsChannelAdapter(sock))
269
278
register_ssh_vendor('loopback', LoopbackVendor())
272
class _ParamikoSSHConnection(object):
273
def __init__(self, channel):
274
self.channel = channel
276
def get_filelike_channels(self):
277
return self.channel.makefile('rb'), self.channel.makefile('wb')
280
return self.channel.close()
283
281
class ParamikoVendor(SSHVendor):
284
282
"""Vendor that uses paramiko."""
284
def _hexify(self, s):
285
return hexlify(s).upper()
286
287
def _connect(self, username, password, host, port):
287
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
288
global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
292
293
t = paramiko.Transport((host, port or 22))
293
294
t.set_log_channel('bzr.paramiko')
295
except (paramiko.SSHException, socket.error), e:
296
except (paramiko.SSHException, socket.error) as e:
296
297
self._raise_connection_error(host, port=port, orig_error=e)
298
299
server_key = t.get_remote_server_key()
299
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
300
server_key_hex = self._hexify(server_key.get_fingerprint())
300
301
keytype = server_key.get_name()
301
302
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
302
303
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
303
our_server_key_hex = paramiko.util.hexify(
304
our_server_key.get_fingerprint())
305
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
306
our_server_key = BZR_HOSTKEYS[host][keytype]
307
our_server_key_hex = paramiko.util.hexify(
308
our_server_key.get_fingerprint())
304
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
305
elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
306
our_server_key = BRZ_HOSTKEYS[host][keytype]
307
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
310
309
trace.warning('Adding %s host key for %s: %s'
311
310
% (keytype, host, server_key_hex))
312
add = getattr(BZR_HOSTKEYS, 'add', None)
311
add = getattr(BRZ_HOSTKEYS, 'add', None)
313
312
if add is not None: # paramiko >= 1.X.X
314
BZR_HOSTKEYS.add(host, keytype, server_key)
313
BRZ_HOSTKEYS.add(host, keytype, server_key)
316
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
315
BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
317
316
our_server_key = server_key
318
our_server_key_hex = paramiko.util.hexify(
319
our_server_key.get_fingerprint())
317
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
321
319
if server_key != our_server_key:
322
320
filename1 = os.path.expanduser('~/.ssh/known_hosts')
344
342
cmdline = ' '.join(command)
345
343
channel.exec_command(cmdline)
346
344
return _ParamikoSSHConnection(channel)
347
except paramiko.SSHException, e:
345
except paramiko.SSHException as e:
348
346
self._raise_connection_error(host, port=port, orig_error=e,
349
347
msg='Unable to invoke remote bzr')
349
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
351
350
if paramiko is not None:
352
351
vendor = ParamikoVendor()
353
352
register_ssh_vendor('paramiko', vendor)
354
353
register_ssh_vendor('none', vendor)
355
354
register_default_ssh_vendor(vendor)
356
_sftp_connection_errors = (EOFError, paramiko.SSHException)
355
_ssh_connection_errors += (paramiko.SSHException,)
359
_sftp_connection_errors = (EOFError,)
362
359
class SubprocessVendor(SSHVendor):
363
360
"""Abstract base class for vendors that use pipes to a subprocess."""
362
# In general stderr should be inherited from the parent process so prompts
363
# are visible on the terminal. This can be overriden to another file for
364
# tests, but beware of using PIPE which may hang due to not being read.
365
_stderr_target = None
368
def _check_hostname(arg):
369
if arg.startswith('-'):
370
raise StrangeHostname(hostname=arg)
365
372
def _connect(self, argv):
366
proc = subprocess.Popen(argv,
367
stdin=subprocess.PIPE,
368
stdout=subprocess.PIPE,
373
# Attempt to make a socketpair to use as stdin/stdout for the SSH
374
# subprocess. We prefer sockets to pipes because they support
375
# non-blocking short reads, allowing us to optimistically read 64k (or
378
my_sock, subproc_sock = socket.socketpair()
379
osutils.set_fd_cloexec(my_sock)
380
except (AttributeError, socket.error):
381
# This platform doesn't support socketpair(), so just use ordinary
383
stdin = stdout = subprocess.PIPE
384
my_sock, subproc_sock = None, None
386
stdin = stdout = subproc_sock
387
proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
388
stderr=self._stderr_target,
369
389
**os_specific_subprocess_params())
370
return SSHSubprocess(proc)
390
if subproc_sock is not None:
392
return SSHSubprocessConnection(proc, sock=my_sock)
372
394
def connect_sftp(self, username, password, host, port):
375
397
subsystem='sftp')
376
398
sock = self._connect(argv)
377
399
return SFTPClient(SocketAsChannelAdapter(sock))
378
except _sftp_connection_errors, e:
379
self._raise_connection_error(host, port=port, orig_error=e)
380
except (OSError, IOError), e:
381
# If the machine is fast enough, ssh can actually exit
382
# before we try and send it the sftp request, which
383
# raises a Broken Pipe
384
if e.errno not in (errno.EPIPE,):
400
except _ssh_connection_errors as e:
386
401
self._raise_connection_error(host, port=port, orig_error=e)
388
403
def connect_ssh(self, username, password, host, port, command):
390
405
argv = self._get_vendor_specific_argv(username, host, port,
392
407
return self._connect(argv)
393
except (EOFError), e:
394
self._raise_connection_error(host, port=port, orig_error=e)
395
except (OSError, IOError), e:
396
# If the machine is fast enough, ssh can actually exit
397
# before we try and send it the sftp request, which
398
# raises a Broken Pipe
399
if e.errno not in (errno.EPIPE,):
408
except _ssh_connection_errors as e:
401
409
self._raise_connection_error(host, port=port, orig_error=e)
403
411
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
419
427
args = [self.executable_path,
420
428
'-oForwardX11=no', '-oForwardAgent=no',
421
'-oClearAllForwardings=yes', '-oProtocol=2',
429
'-oClearAllForwardings=yes',
422
430
'-oNoHostAuthenticationForLocalhost=yes']
423
431
if port is not None:
424
432
args.extend(['-p', str(port)])
425
433
if username is not None:
426
434
args.extend(['-l', username])
427
435
if subsystem is not None:
428
args.extend(['-s', host, subsystem])
436
args.extend(['-s', '--', host, subsystem])
430
args.extend([host] + command)
438
args.extend(['--', host] + command)
433
441
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
454
463
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
466
class LSHSubprocessVendor(SubprocessVendor):
467
"""SSH vendor that uses the 'lsh' executable from GNU"""
469
executable_path = 'lsh'
471
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
473
self._check_hostname(host)
474
args = [self.executable_path]
476
args.extend(['-p', str(port)])
477
if username is not None:
478
args.extend(['-l', username])
479
if subsystem is not None:
480
args.extend(['--subsystem', subsystem, host])
482
args.extend([host] + command)
485
register_ssh_vendor('lsh', LSHSubprocessVendor())
457
488
class PLinkSubprocessVendor(SubprocessVendor):
458
489
"""SSH vendor that uses the 'plink' executable from Putty."""
583
616
Load system host keys (probably doesn't work on windows) and any
584
617
"discovered" keys from previous sessions.
586
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
619
global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
588
621
SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
589
622
os.path.expanduser('~/.ssh/known_hosts'))
591
624
trace.mutter('failed to load system host keys: ' + str(e))
592
bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
625
brz_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
594
BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
596
trace.mutter('failed to load bzr host keys: ' + str(e))
627
BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
629
trace.mutter('failed to load brz host keys: ' + str(e))
602
635
Save "discovered" host keys in $(config)/ssh_host_keys/.
604
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
637
global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
605
638
bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
606
639
config.ensure_config_dir_exists()
609
f = open(bzr_hostkey_path, 'w')
610
f.write('# SSH host keys collected by bzr\n')
611
for hostname, keys in BZR_HOSTKEYS.iteritems():
612
for keytype, key in keys.iteritems():
613
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
642
with open(bzr_hostkey_path, 'w') as f:
643
f.write('# SSH host keys collected by bzr\n')
644
for hostname, keys in BRZ_HOSTKEYS.items():
645
for keytype, key in keys.items():
646
f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
616
648
trace.mutter('failed to save bzr host keys: ' + str(e))
645
677
_subproc_weakrefs = set()
647
def _close_ssh_proc(proc):
648
for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
679
def _close_ssh_proc(proc, sock):
680
"""Carefully close stdin/stdout and reap the SSH process.
682
If the pipes are already closed and/or the process has already been
683
wait()ed on, that's ok, and no error is raised. The goal is to do our best
684
to clean up (whether or not a clean up was already tried).
687
for closeable in (proc.stdin, proc.stdout, sock):
688
# We expect that either proc (a subprocess.Popen) will have stdin and
689
# stdout streams to close, or that we will have been passed a socket to
690
# close, with the option not in use being None.
691
if closeable is not None:
692
funcs.append(closeable.close)
693
funcs.append(proc.wait)
655
class SSHSubprocess(object):
656
"""A socket-like object that talks to an ssh subprocess via pipes."""
658
def __init__(self, proc):
698
# It's ok for the pipe to already be closed, or the process to
699
# already be finished.
703
class SSHConnection(object):
704
"""Abstract base class for SSH connections."""
706
def get_sock_or_pipes(self):
707
"""Returns a (kind, io_object) pair.
709
If kind == 'socket', then io_object is a socket.
711
If kind == 'pipes', then io_object is a pair of file-like objects
712
(read_from, write_to).
714
raise NotImplementedError(self.get_sock_or_pipes)
717
raise NotImplementedError(self.close)
720
class SSHSubprocessConnection(SSHConnection):
721
"""A connection to an ssh subprocess via pipes or a socket.
723
This class is also socket-like enough to be used with
724
SocketAsChannelAdapter (it has 'send' and 'recv' methods).
727
def __init__(self, proc, sock=None):
730
:param proc: a subprocess.Popen
731
:param sock: if proc.stdin/out is a socket from a socketpair, then sock
732
should breezy's half of that socketpair. If not passed, proc's
733
stdin/out is assumed to be ordinary pipes.
660
737
# Add a weakref to proc that will attempt to do the same as self.close
661
738
# to avoid leaving processes lingering indefinitely.
662
739
def terminate(ref):
663
740
_subproc_weakrefs.remove(ref)
664
_close_ssh_proc(proc)
741
_close_ssh_proc(proc, sock)
665
742
_subproc_weakrefs.add(weakref.ref(self, terminate))
667
744
def send(self, data):
668
return os.write(self.proc.stdin.fileno(), data)
745
if self._sock is not None:
746
return self._sock.send(data)
748
return os.write(self.proc.stdin.fileno(), data)
670
750
def recv(self, count):
671
return os.read(self.proc.stdout.fileno(), count)
674
_close_ssh_proc(self.proc)
676
def get_filelike_channels(self):
677
return (self.proc.stdout, self.proc.stdin)
751
if self._sock is not None:
752
return self._sock.recv(count)
754
return os.read(self.proc.stdout.fileno(), count)
757
_close_ssh_proc(self.proc, self._sock)
759
def get_sock_or_pipes(self):
760
if self._sock is not None:
761
return 'socket', self._sock
763
return 'pipes', (self.proc.stdout, self.proc.stdin)
766
class _ParamikoSSHConnection(SSHConnection):
767
"""An SSH connection via paramiko."""
769
def __init__(self, channel):
770
self.channel = channel
772
def get_sock_or_pipes(self):
773
return ('socket', self.channel)
776
return self.channel.close()