14
14
# You should have received a copy of the GNU General Public License
15
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
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
18
"""Foundation SSH support for SFTP and smart server."""
20
from __future__ import absolute_import
27
from bzrlib.config import config_dir, ensure_config_dir_exists
28
from bzrlib.errors import (ConnectionError,
30
SocketConnectionError,
36
from bzrlib.osutils import pathjoin
37
from bzrlib.trace import mutter, warning
29
from binascii import hexlify
114
116
stdout = stderr = ''
115
117
return stdout + stderr
117
def _get_vendor_by_version_string(self, version):
119
def _get_vendor_by_version_string(self, version, progname):
118
120
"""Return the vendor or None based on output from the subprocess.
120
122
:param version: The output of 'ssh -V' like command.
123
:param args: Command line that was run.
123
126
if 'OpenSSH' in version:
124
mutter('ssh implementation is OpenSSH')
127
trace.mutter('ssh implementation is OpenSSH')
125
128
vendor = OpenSSHSubprocessVendor()
126
129
elif 'SSH Secure Shell' in version:
127
mutter('ssh implementation is SSH Corp.')
130
trace.mutter('ssh implementation is SSH Corp.')
128
131
vendor = SSHCorpSubprocessVendor()
129
elif 'plink' in version:
130
mutter("ssh implementation is Putty's plink.")
132
elif 'lsh' in version:
133
trace.mutter('ssh implementation is GNU lsh.')
134
vendor = LSHSubprocessVendor()
135
# As plink user prompts are not handled currently, don't auto-detect
136
# it by inspection below, but keep this vendor detection for if a path
137
# is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
138
elif 'plink' in version and progname == 'plink':
139
# Checking if "plink" was the executed argument as Windows
140
# sometimes reports 'ssh -V' incorrectly with 'plink' in its
141
# version. See https://bugs.launchpad.net/bzr/+bug/107155
142
trace.mutter("ssh implementation is Putty's plink.")
131
143
vendor = PLinkSubprocessVendor()
134
146
def _get_vendor_by_inspection(self):
135
147
"""Return the vendor or None by checking for known SSH implementations."""
136
for args in [['ssh', '-V'], ['plink', '-V']]:
137
version = self._get_ssh_version_string(args)
138
vendor = self._get_vendor_by_version_string(version)
139
if vendor is not None:
148
version = self._get_ssh_version_string(['ssh', '-V'])
149
return self._get_vendor_by_version_string(version, "ssh")
151
def _get_vendor_from_path(self, path):
152
"""Return the vendor or None using the program at the given path"""
153
version = self._get_ssh_version_string([path, '-V'])
154
return self._get_vendor_by_version_string(version,
155
os.path.splitext(os.path.basename(path))[0])
143
157
def get_vendor(self, environment=None):
144
158
"""Find out what version of SSH is on the system.
165
179
register_ssh_vendor = _ssh_vendor_manager.register_vendor
168
def _ignore_sigint():
182
def _ignore_signals():
169
183
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
170
184
# doesn't handle it itself.
171
185
# <https://launchpad.net/products/bzr/+bug/41433/+index>
173
187
signal.signal(signal.SIGINT, signal.SIG_IGN)
176
class LoopbackSFTP(object):
188
# GZ 2010-02-19: Perhaps make this check if breakin is installed instead
189
if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
190
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
193
class SocketAsChannelAdapter(object):
177
194
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
179
196
def __init__(self, sock):
180
197
self.__socket = sock
200
return "bzr SocketAsChannelAdapter"
182
202
def send(self, data):
183
203
return self.__socket.send(data)
185
205
def recv(self, n):
186
return self.__socket.recv(n)
207
return self.__socket.recv(n)
208
except socket.error, e:
209
if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
211
# Connection has closed. Paramiko expects an empty string in
212
# this case, not an exception.
188
216
def recv_ready(self):
217
# TODO: jam 20051215 this function is necessary to support the
218
# pipelined() function. In reality, it probably should use
219
# poll() or select() to actually return if there is data
220
# available, otherwise we probably don't get any benefit
225
256
This just unifies all the locations that try to raise ConnectionError,
226
257
so that they format things properly.
228
raise SocketConnectionError(host=host, port=port, msg=msg,
229
orig_error=orig_error)
259
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
260
orig_error=orig_error)
232
263
class LoopbackVendor(SSHVendor):
233
264
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
235
266
def connect_sftp(self, username, password, host, port):
236
267
sock = socket.socket()
238
269
sock.connect((host, port))
239
270
except socket.error, e:
240
271
self._raise_connection_error(host, port=port, orig_error=e)
241
return SFTPClient(LoopbackSFTP(sock))
272
return SFTPClient(SocketAsChannelAdapter(sock))
243
274
register_ssh_vendor('loopback', LoopbackVendor())
246
class _ParamikoSSHConnection(object):
247
def __init__(self, channel):
248
self.channel = channel
250
def get_filelike_channels(self):
251
return self.channel.makefile('rb'), self.channel.makefile('wb')
254
return self.channel.close()
257
277
class ParamikoVendor(SSHVendor):
258
278
"""Vendor that uses paramiko."""
280
def _hexify(self, s):
281
return hexlify(s).upper()
260
283
def _connect(self, username, password, host, port):
261
284
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
269
292
except (paramiko.SSHException, socket.error), e:
270
293
self._raise_connection_error(host, port=port, orig_error=e)
272
295
server_key = t.get_remote_server_key()
273
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
296
server_key_hex = self._hexify(server_key.get_fingerprint())
274
297
keytype = server_key.get_name()
275
298
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
276
299
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
277
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
300
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
278
301
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
279
302
our_server_key = BZR_HOSTKEYS[host][keytype]
280
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
303
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
282
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
305
trace.warning('Adding %s host key for %s: %s'
306
% (keytype, host, server_key_hex))
283
307
add = getattr(BZR_HOSTKEYS, 'add', None)
284
308
if add is not None: # paramiko >= 1.X.X
285
309
BZR_HOSTKEYS.add(host, keytype, server_key)
287
311
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
288
312
our_server_key = server_key
289
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
313
our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
291
315
if server_key != our_server_key:
292
316
filename1 = os.path.expanduser('~/.ssh/known_hosts')
293
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
294
raise TransportError('Host keys for %s do not match! %s != %s' % \
317
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
318
raise errors.TransportError(
319
'Host keys for %s do not match! %s != %s' %
295
320
(host, our_server_key_hex, server_key_hex),
296
321
['Try editing %s or %s' % (filename1, filename2)])
298
_paramiko_auth(username, password, host, t)
323
_paramiko_auth(username, password, host, port, t)
301
326
def connect_sftp(self, username, password, host, port):
302
327
t = self._connect(username, password, host, port)
317
342
self._raise_connection_error(host, port=port, orig_error=e,
318
343
msg='Unable to invoke remote bzr')
345
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
320
346
if paramiko is not None:
321
347
vendor = ParamikoVendor()
322
348
register_ssh_vendor('paramiko', vendor)
323
349
register_ssh_vendor('none', vendor)
324
350
register_default_ssh_vendor(vendor)
351
_ssh_connection_errors += (paramiko.SSHException,)
328
355
class SubprocessVendor(SSHVendor):
329
356
"""Abstract base class for vendors that use pipes to a subprocess."""
358
# In general stderr should be inherited from the parent process so prompts
359
# are visible on the terminal. This can be overriden to another file for
360
# tests, but beware of using PIPE which may hang due to not being read.
361
_stderr_target = None
331
363
def _connect(self, argv):
332
proc = subprocess.Popen(argv,
333
stdin=subprocess.PIPE,
334
stdout=subprocess.PIPE,
364
# Attempt to make a socketpair to use as stdin/stdout for the SSH
365
# subprocess. We prefer sockets to pipes because they support
366
# non-blocking short reads, allowing us to optimistically read 64k (or
369
my_sock, subproc_sock = socket.socketpair()
370
osutils.set_fd_cloexec(my_sock)
371
except (AttributeError, socket.error):
372
# This platform doesn't support socketpair(), so just use ordinary
374
stdin = stdout = subprocess.PIPE
375
my_sock, subproc_sock = None, None
377
stdin = stdout = subproc_sock
378
proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
379
stderr=self._stderr_target,
335
380
**os_specific_subprocess_params())
336
return SSHSubprocess(proc)
381
if subproc_sock is not None:
383
return SSHSubprocessConnection(proc, sock=my_sock)
338
385
def connect_sftp(self, username, password, host, port):
340
387
argv = self._get_vendor_specific_argv(username, host, port,
341
388
subsystem='sftp')
342
389
sock = self._connect(argv)
343
return SFTPClient(sock)
344
except (EOFError, paramiko.SSHException), e:
345
self._raise_connection_error(host, port=port, orig_error=e)
346
except (OSError, IOError), e:
347
# If the machine is fast enough, ssh can actually exit
348
# before we try and send it the sftp request, which
349
# raises a Broken Pipe
350
if e.errno not in (errno.EPIPE,):
390
return SFTPClient(SocketAsChannelAdapter(sock))
391
except _ssh_connection_errors, e:
352
392
self._raise_connection_error(host, port=port, orig_error=e)
354
394
def connect_ssh(self, username, password, host, port, command):
356
396
argv = self._get_vendor_specific_argv(username, host, port,
358
398
return self._connect(argv)
359
except (EOFError), e:
360
self._raise_connection_error(host, port=port, orig_error=e)
361
except (OSError, IOError), e:
362
# If the machine is fast enough, ssh can actually exit
363
# before we try and send it the sftp request, which
364
# raises a Broken Pipe
365
if e.errno not in (errno.EPIPE,):
399
except _ssh_connection_errors, e:
367
400
self._raise_connection_error(host, port=port, orig_error=e)
369
402
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
371
404
"""Returns the argument list to run the subprocess with.
373
406
Exactly one of 'subsystem' and 'command' must be specified.
375
408
raise NotImplementedError(self._get_vendor_specific_argv)
378
411
class OpenSSHSubprocessVendor(SubprocessVendor):
379
412
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
414
executable_path = 'ssh'
381
416
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
383
assert subsystem is not None or command is not None, (
384
'Must specify a command or subsystem')
385
if subsystem is not None:
386
assert command is None, (
387
'subsystem and command are mutually exclusive')
418
args = [self.executable_path,
389
419
'-oForwardX11=no', '-oForwardAgent=no',
390
'-oClearAllForwardings=yes', '-oProtocol=2',
420
'-oClearAllForwardings=yes',
391
421
'-oNoHostAuthenticationForLocalhost=yes']
392
422
if port is not None:
393
423
args.extend(['-p', str(port)])
423
450
args.extend([host] + command)
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
453
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
456
class LSHSubprocessVendor(SubprocessVendor):
457
"""SSH vendor that uses the 'lsh' executable from GNU"""
459
executable_path = 'lsh'
461
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
args = [self.executable_path]
465
args.extend(['-p', str(port)])
466
if username is not None:
467
args.extend(['-l', username])
468
if subsystem is not None:
469
args.extend(['--subsystem', subsystem, host])
471
args.extend([host] + command)
474
register_ssh_vendor('lsh', LSHSubprocessVendor())
429
477
class PLinkSubprocessVendor(SubprocessVendor):
430
478
"""SSH vendor that uses the 'plink' executable from Putty."""
480
executable_path = 'plink'
432
482
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
434
assert subsystem is not None or command is not None, (
435
'Must specify a command or subsystem')
436
if subsystem is not None:
437
assert command is None, (
438
'subsystem and command are mutually exclusive')
439
args = ['plink', '-x', '-a', '-ssh', '-2']
484
args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
440
485
if port is not None:
441
486
args.extend(['-P', str(port)])
442
487
if username is not None:
450
495
register_ssh_vendor('plink', PLinkSubprocessVendor())
453
def _paramiko_auth(username, password, host, paramiko_transport):
454
# paramiko requires a username, but it might be none if nothing was supplied
455
# use the local username, just in case.
456
# We don't override username, because if we aren't using paramiko,
457
# the username might be specified in ~/.ssh/config and we don't want to
458
# force it to something else
459
# Also, it would mess up the self.relpath() functionality
460
username = username or getpass.getuser()
498
def _paramiko_auth(username, password, host, port, paramiko_transport):
499
auth = config.AuthenticationConfig()
500
# paramiko requires a username, but it might be none if nothing was
501
# supplied. If so, use the local username.
503
username = auth.get_user('ssh', host, port=port,
504
default=getpass.getuser())
462
505
if _use_ssh_agent:
463
506
agent = paramiko.Agent()
464
507
for key in agent.get_keys():
465
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
508
trace.mutter('Trying SSH agent key %s'
509
% self._hexify(key.get_fingerprint()))
467
511
paramiko_transport.auth_publickey(username, key)
469
513
except paramiko.SSHException, e:
472
516
# okay, try finding id_rsa or id_dss? (posix only)
473
517
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
475
519
if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
522
# If we have gotten this far, we are about to try for passwords, do an
523
# auth_none check to see if it is even supported.
524
supported_auth_types = []
526
# Note that with paramiko <1.7.5 this logs an INFO message:
527
# Authentication type (none) not permitted.
528
# So we explicitly disable the logging level for this action
529
old_level = paramiko_transport.logger.level
530
paramiko_transport.logger.setLevel(logging.WARNING)
532
paramiko_transport.auth_none(username)
534
paramiko_transport.logger.setLevel(old_level)
535
except paramiko.BadAuthenticationType, e:
536
# Supported methods are in the exception
537
supported_auth_types = e.allowed_types
538
except paramiko.SSHException, e:
539
# Don't know what happened, but just ignore it
541
# We treat 'keyboard-interactive' and 'password' auth methods identically,
542
# because Paramiko's auth_password method will automatically try
543
# 'keyboard-interactive' auth (using the password as the response) if
544
# 'password' auth is not available. Apparently some Debian and Gentoo
545
# OpenSSH servers require this.
546
# XXX: It's possible for a server to require keyboard-interactive auth that
547
# requires something other than a single password, but we currently don't
549
if ('password' not in supported_auth_types and
550
'keyboard-interactive' not in supported_auth_types):
551
raise errors.ConnectionError('Unable to authenticate to SSH host as'
552
'\n %s@%s\nsupported auth types: %s'
553
% (username, host, supported_auth_types))
480
557
paramiko_transport.auth_password(username, password)
485
562
# give up and ask for a password
486
password = bzrlib.ui.ui_factory.get_password(
487
prompt='SSH %(user)s@%(host)s password',
488
user=username, host=host)
490
paramiko_transport.auth_password(username, password)
491
except paramiko.SSHException, e:
492
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
563
password = auth.get_password('ssh', host, username, port=port)
564
# get_password can still return None, which means we should not prompt
565
if password is not None:
567
paramiko_transport.auth_password(username, password)
568
except paramiko.SSHException, e:
569
raise errors.ConnectionError(
570
'Unable to authenticate to SSH host as'
571
'\n %s@%s\n' % (username, host), e)
573
raise errors.ConnectionError('Unable to authenticate to SSH host as'
574
' %s@%s' % (username, host))
496
577
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
500
581
paramiko_transport.auth_publickey(username, key)
502
583
except paramiko.PasswordRequiredException:
503
password = bzrlib.ui.ui_factory.get_password(
504
prompt='SSH %(filename)s password',
584
password = ui.ui_factory.get_password(
585
prompt=u'SSH %(filename)s password',
586
filename=filename.decode(osutils._fs_enc))
507
588
key = pkey_class.from_private_key_file(filename, password)
508
589
paramiko_transport.auth_publickey(username, key)
510
591
except paramiko.SSHException:
511
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
592
trace.mutter('SSH authentication via %s key failed.'
593
% (os.path.basename(filename),))
512
594
except paramiko.SSHException:
513
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
595
trace.mutter('SSH authentication via %s key failed.'
596
% (os.path.basename(filename),))
568
652
# this causes it to be seen only by bzr and not by ssh. Python will
569
653
# generate a KeyboardInterrupt in bzr, and we will then have a chance
570
654
# to release locks or do other cleanup over ssh before the connection
572
656
# <https://launchpad.net/products/bzr/+bug/5987>
574
658
# Running it in a separate process group is not good because then it
575
659
# can't get non-echoed input of a password or passphrase.
576
660
# <https://launchpad.net/products/bzr/+bug/40508>
577
return {'preexec_fn': _ignore_sigint,
661
return {'preexec_fn': _ignore_signals,
578
662
'close_fds': True,
582
class SSHSubprocess(object):
583
"""A socket-like object that talks to an ssh subprocess via pipes."""
585
def __init__(self, proc):
666
_subproc_weakrefs = set()
668
def _close_ssh_proc(proc, sock):
669
"""Carefully close stdin/stdout and reap the SSH process.
671
If the pipes are already closed and/or the process has already been
672
wait()ed on, that's ok, and no error is raised. The goal is to do our best
673
to clean up (whether or not a clean up was already tried).
676
for closeable in (proc.stdin, proc.stdout, sock):
677
# We expect that either proc (a subprocess.Popen) will have stdin and
678
# stdout streams to close, or that we will have been passed a socket to
679
# close, with the option not in use being None.
680
if closeable is not None:
681
funcs.append(closeable.close)
682
funcs.append(proc.wait)
687
# It's ok for the pipe to already be closed, or the process to
688
# already be finished.
692
class SSHConnection(object):
693
"""Abstract base class for SSH connections."""
695
def get_sock_or_pipes(self):
696
"""Returns a (kind, io_object) pair.
698
If kind == 'socket', then io_object is a socket.
700
If kind == 'pipes', then io_object is a pair of file-like objects
701
(read_from, write_to).
703
raise NotImplementedError(self.get_sock_or_pipes)
706
raise NotImplementedError(self.close)
709
class SSHSubprocessConnection(SSHConnection):
710
"""A connection to an ssh subprocess via pipes or a socket.
712
This class is also socket-like enough to be used with
713
SocketAsChannelAdapter (it has 'send' and 'recv' methods).
716
def __init__(self, proc, sock=None):
719
:param proc: a subprocess.Popen
720
:param sock: if proc.stdin/out is a socket from a socketpair, then sock
721
should bzrlib's half of that socketpair. If not passed, proc's
722
stdin/out is assumed to be ordinary pipes.
726
# Add a weakref to proc that will attempt to do the same as self.close
727
# to avoid leaving processes lingering indefinitely.
729
_subproc_weakrefs.remove(ref)
730
_close_ssh_proc(proc, sock)
731
_subproc_weakrefs.add(weakref.ref(self, terminate))
588
733
def send(self, data):
589
return os.write(self.proc.stdin.fileno(), data)
591
def recv_ready(self):
592
# TODO: jam 20051215 this function is necessary to support the
593
# pipelined() function. In reality, it probably should use
594
# poll() or select() to actually return if there is data
595
# available, otherwise we probably don't get any benefit
734
if self._sock is not None:
735
return self._sock.send(data)
737
return os.write(self.proc.stdin.fileno(), data)
598
739
def recv(self, count):
599
return os.read(self.proc.stdout.fileno(), count)
602
self.proc.stdin.close()
603
self.proc.stdout.close()
606
def get_filelike_channels(self):
607
return (self.proc.stdout, self.proc.stdin)
740
if self._sock is not None:
741
return self._sock.recv(count)
743
return os.read(self.proc.stdout.fileno(), count)
746
_close_ssh_proc(self.proc, self._sock)
748
def get_sock_or_pipes(self):
749
if self._sock is not None:
750
return 'socket', self._sock
752
return 'pipes', (self.proc.stdout, self.proc.stdin)
755
class _ParamikoSSHConnection(SSHConnection):
756
"""An SSH connection via paramiko."""
758
def __init__(self, channel):
759
self.channel = channel
761
def get_sock_or_pipes(self):
762
return ('socket', self.channel)
765
return self.channel.close()