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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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
115
114
stdout = stderr = ''
116
115
return stdout + stderr
118
def _get_vendor_by_version_string(self, version, progname):
117
def _get_vendor_by_version_string(self, version):
119
118
"""Return the vendor or None based on output from the subprocess.
121
120
:param version: The output of 'ssh -V' like command.
122
:param args: Command line that was run.
125
123
if 'OpenSSH' in version:
126
trace.mutter('ssh implementation is OpenSSH')
124
mutter('ssh implementation is OpenSSH')
127
125
vendor = OpenSSHSubprocessVendor()
128
126
elif 'SSH Secure Shell' in version:
129
trace.mutter('ssh implementation is SSH Corp.')
127
mutter('ssh implementation is SSH Corp.')
130
128
vendor = SSHCorpSubprocessVendor()
131
elif 'lsh' in version:
132
trace.mutter('ssh implementation is GNU lsh.')
133
vendor = LSHSubprocessVendor()
134
# As plink user prompts are not handled currently, don't auto-detect
135
# it by inspection below, but keep this vendor detection for if a path
136
# is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
137
elif 'plink' in version and progname == 'plink':
138
# Checking if "plink" was the executed argument as Windows
139
# sometimes reports 'ssh -V' incorrectly with 'plink' in its
140
# version. See https://bugs.launchpad.net/bzr/+bug/107155
141
trace.mutter("ssh implementation is Putty's plink.")
129
elif 'plink' in version:
130
mutter("ssh implementation is Putty's plink.")
142
131
vendor = PLinkSubprocessVendor()
145
134
def _get_vendor_by_inspection(self):
146
135
"""Return the vendor or None by checking for known SSH implementations."""
147
version = self._get_ssh_version_string(['ssh', '-V'])
148
return self._get_vendor_by_version_string(version, "ssh")
150
def _get_vendor_from_path(self, path):
151
"""Return the vendor or None using the program at the given path"""
152
version = self._get_ssh_version_string([path, '-V'])
153
return self._get_vendor_by_version_string(version,
154
os.path.splitext(os.path.basename(path))[0])
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:
156
143
def get_vendor(self, environment=None):
157
144
"""Find out what version of SSH is on the system.
178
165
register_ssh_vendor = _ssh_vendor_manager.register_vendor
181
def _ignore_signals():
168
def _ignore_sigint():
182
169
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
183
170
# doesn't handle it itself.
184
171
# <https://launchpad.net/products/bzr/+bug/41433/+index>
186
173
signal.signal(signal.SIGINT, signal.SIG_IGN)
187
# GZ 2010-02-19: Perhaps make this check if breakin is installed instead
188
if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
189
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
192
class SocketAsChannelAdapter(object):
176
class LoopbackSFTP(object):
193
177
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
195
179
def __init__(self, sock):
196
180
self.__socket = sock
199
return "bzr SocketAsChannelAdapter"
201
182
def send(self, data):
202
183
return self.__socket.send(data)
204
185
def recv(self, n):
206
return self.__socket.recv(n)
207
except socket.error, e:
208
if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
210
# Connection has closed. Paramiko expects an empty string in
211
# this case, not an exception.
186
return self.__socket.recv(n)
215
188
def recv_ready(self):
216
# TODO: jam 20051215 this function is necessary to support the
217
# pipelined() function. In reality, it probably should use
218
# poll() or select() to actually return if there is data
219
# available, otherwise we probably don't get any benefit
255
225
This just unifies all the locations that try to raise ConnectionError,
256
226
so that they format things properly.
258
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
259
orig_error=orig_error)
228
raise SocketConnectionError(host=host, port=port, msg=msg,
229
orig_error=orig_error)
262
232
class LoopbackVendor(SSHVendor):
263
233
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
265
235
def connect_sftp(self, username, password, host, port):
266
236
sock = socket.socket()
268
238
sock.connect((host, port))
269
239
except socket.error, e:
270
240
self._raise_connection_error(host, port=port, orig_error=e)
271
return SFTPClient(SocketAsChannelAdapter(sock))
241
return SFTPClient(LoopbackSFTP(sock))
273
243
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()
276
257
class ParamikoVendor(SSHVendor):
277
258
"""Vendor that uses paramiko."""
279
260
def _connect(self, username, password, host, port):
280
261
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
288
269
except (paramiko.SSHException, socket.error), e:
289
270
self._raise_connection_error(host, port=port, orig_error=e)
291
272
server_key = t.get_remote_server_key()
292
273
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
293
274
keytype = server_key.get_name()
294
275
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
295
276
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
296
our_server_key_hex = paramiko.util.hexify(
297
our_server_key.get_fingerprint())
277
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
298
278
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
299
279
our_server_key = BZR_HOSTKEYS[host][keytype]
300
our_server_key_hex = paramiko.util.hexify(
301
our_server_key.get_fingerprint())
280
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
303
trace.warning('Adding %s host key for %s: %s'
304
% (keytype, host, server_key_hex))
282
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
305
283
add = getattr(BZR_HOSTKEYS, 'add', None)
306
284
if add is not None: # paramiko >= 1.X.X
307
285
BZR_HOSTKEYS.add(host, keytype, server_key)
309
287
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
310
288
our_server_key = server_key
311
our_server_key_hex = paramiko.util.hexify(
312
our_server_key.get_fingerprint())
289
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
314
291
if server_key != our_server_key:
315
292
filename1 = os.path.expanduser('~/.ssh/known_hosts')
316
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
317
raise errors.TransportError(
318
'Host keys for %s do not match! %s != %s' %
293
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
294
raise TransportError('Host keys for %s do not match! %s != %s' % \
319
295
(host, our_server_key_hex, server_key_hex),
320
296
['Try editing %s or %s' % (filename1, filename2)])
322
_paramiko_auth(username, password, host, port, t)
298
_paramiko_auth(username, password, host, t)
325
301
def connect_sftp(self, username, password, host, port):
326
302
t = self._connect(username, password, host, port)
341
317
self._raise_connection_error(host, port=port, orig_error=e,
342
318
msg='Unable to invoke remote bzr')
344
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
345
320
if paramiko is not None:
346
321
vendor = ParamikoVendor()
347
322
register_ssh_vendor('paramiko', vendor)
348
323
register_ssh_vendor('none', vendor)
349
324
register_default_ssh_vendor(vendor)
350
_ssh_connection_errors += (paramiko.SSHException,)
354
328
class SubprocessVendor(SSHVendor):
355
329
"""Abstract base class for vendors that use pipes to a subprocess."""
357
# In general stderr should be inherited from the parent process so prompts
358
# are visible on the terminal. This can be overriden to another file for
359
# tests, but beware of using PIPE which may hang due to not being read.
360
_stderr_target = None
362
331
def _connect(self, argv):
363
# Attempt to make a socketpair to use as stdin/stdout for the SSH
364
# subprocess. We prefer sockets to pipes because they support
365
# non-blocking short reads, allowing us to optimistically read 64k (or
368
my_sock, subproc_sock = socket.socketpair()
369
osutils.set_fd_cloexec(my_sock)
370
except (AttributeError, socket.error):
371
# This platform doesn't support socketpair(), so just use ordinary
373
stdin = stdout = subprocess.PIPE
374
my_sock, subproc_sock = None, None
376
stdin = stdout = subproc_sock
377
proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
378
stderr=self._stderr_target,
332
proc = subprocess.Popen(argv,
333
stdin=subprocess.PIPE,
334
stdout=subprocess.PIPE,
379
335
**os_specific_subprocess_params())
380
if subproc_sock is not None:
382
return SSHSubprocessConnection(proc, sock=my_sock)
336
return SSHSubprocess(proc)
384
338
def connect_sftp(self, username, password, host, port):
386
340
argv = self._get_vendor_specific_argv(username, host, port,
387
341
subsystem='sftp')
388
342
sock = self._connect(argv)
389
return SFTPClient(SocketAsChannelAdapter(sock))
390
except _ssh_connection_errors, e:
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,):
391
352
self._raise_connection_error(host, port=port, orig_error=e)
393
354
def connect_ssh(self, username, password, host, port, command):
395
356
argv = self._get_vendor_specific_argv(username, host, port,
397
358
return self._connect(argv)
398
except _ssh_connection_errors, e:
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
367
self._raise_connection_error(host, port=port, orig_error=e)
401
369
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
403
371
"""Returns the argument list to run the subprocess with.
405
373
Exactly one of 'subsystem' and 'command' must be specified.
407
375
raise NotImplementedError(self._get_vendor_specific_argv)
410
378
class OpenSSHSubprocessVendor(SubprocessVendor):
411
379
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
413
executable_path = 'ssh'
415
381
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
417
args = [self.executable_path,
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
389
'-oForwardX11=no', '-oForwardAgent=no',
419
'-oClearAllForwardings=yes',
390
'-oClearAllForwardings=yes', '-oProtocol=2',
420
391
'-oNoHostAuthenticationForLocalhost=yes']
421
392
if port is not None:
422
393
args.extend(['-p', str(port)])
449
423
args.extend([host] + command)
452
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
455
class LSHSubprocessVendor(SubprocessVendor):
456
"""SSH vendor that uses the 'lsh' executable from GNU"""
458
executable_path = 'lsh'
460
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
462
args = [self.executable_path]
464
args.extend(['-p', str(port)])
465
if username is not None:
466
args.extend(['-l', username])
467
if subsystem is not None:
468
args.extend(['--subsystem', subsystem, host])
470
args.extend([host] + command)
473
register_ssh_vendor('lsh', LSHSubprocessVendor())
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
476
429
class PLinkSubprocessVendor(SubprocessVendor):
477
430
"""SSH vendor that uses the 'plink' executable from Putty."""
479
executable_path = 'plink'
481
432
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
483
args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
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
440
if port is not None:
485
441
args.extend(['-P', str(port)])
486
442
if username is not None:
494
450
register_ssh_vendor('plink', PLinkSubprocessVendor())
497
def _paramiko_auth(username, password, host, port, paramiko_transport):
498
auth = config.AuthenticationConfig()
499
# paramiko requires a username, but it might be none if nothing was
500
# supplied. If so, use the local username.
502
username = auth.get_user('ssh', host, port=port,
503
default=getpass.getuser())
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()
504
462
if _use_ssh_agent:
505
463
agent = paramiko.Agent()
506
464
for key in agent.get_keys():
507
trace.mutter('Trying SSH agent key %s'
508
% paramiko.util.hexify(key.get_fingerprint()))
465
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
510
467
paramiko_transport.auth_publickey(username, key)
512
469
except paramiko.SSHException, e:
515
472
# okay, try finding id_rsa or id_dss? (posix only)
516
473
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
518
475
if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
521
# If we have gotten this far, we are about to try for passwords, do an
522
# auth_none check to see if it is even supported.
523
supported_auth_types = []
525
# Note that with paramiko <1.7.5 this logs an INFO message:
526
# Authentication type (none) not permitted.
527
# So we explicitly disable the logging level for this action
528
old_level = paramiko_transport.logger.level
529
paramiko_transport.logger.setLevel(logging.WARNING)
531
paramiko_transport.auth_none(username)
533
paramiko_transport.logger.setLevel(old_level)
534
except paramiko.BadAuthenticationType, e:
535
# Supported methods are in the exception
536
supported_auth_types = e.allowed_types
537
except paramiko.SSHException, e:
538
# Don't know what happened, but just ignore it
540
# We treat 'keyboard-interactive' and 'password' auth methods identically,
541
# because Paramiko's auth_password method will automatically try
542
# 'keyboard-interactive' auth (using the password as the response) if
543
# 'password' auth is not available. Apparently some Debian and Gentoo
544
# OpenSSH servers require this.
545
# XXX: It's possible for a server to require keyboard-interactive auth that
546
# requires something other than a single password, but we currently don't
548
if ('password' not in supported_auth_types and
549
'keyboard-interactive' not in supported_auth_types):
550
raise errors.ConnectionError('Unable to authenticate to SSH host as'
551
'\n %s@%s\nsupported auth types: %s'
552
% (username, host, supported_auth_types))
556
480
paramiko_transport.auth_password(username, password)
561
485
# give up and ask for a password
562
password = auth.get_password('ssh', host, username, port=port)
563
# get_password can still return None, which means we should not prompt
564
if password is not None:
566
paramiko_transport.auth_password(username, password)
567
except paramiko.SSHException, e:
568
raise errors.ConnectionError(
569
'Unable to authenticate to SSH host as'
570
'\n %s@%s\n' % (username, host), e)
572
raise errors.ConnectionError('Unable to authenticate to SSH host as'
573
' %s@%s' % (username, host))
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' %
576
496
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
580
500
paramiko_transport.auth_publickey(username, key)
582
502
except paramiko.PasswordRequiredException:
583
password = ui.ui_factory.get_password(
584
prompt=u'SSH %(filename)s password',
585
filename=filename.decode(osutils._fs_enc))
503
password = bzrlib.ui.ui_factory.get_password(
504
prompt='SSH %(filename)s password',
587
507
key = pkey_class.from_private_key_file(filename, password)
588
508
paramiko_transport.auth_publickey(username, key)
590
510
except paramiko.SSHException:
591
trace.mutter('SSH authentication via %s key failed.'
592
% (os.path.basename(filename),))
511
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
593
512
except paramiko.SSHException:
594
trace.mutter('SSH authentication via %s key failed.'
595
% (os.path.basename(filename),))
513
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
651
568
# this causes it to be seen only by bzr and not by ssh. Python will
652
569
# generate a KeyboardInterrupt in bzr, and we will then have a chance
653
570
# to release locks or do other cleanup over ssh before the connection
655
572
# <https://launchpad.net/products/bzr/+bug/5987>
657
574
# Running it in a separate process group is not good because then it
658
575
# can't get non-echoed input of a password or passphrase.
659
576
# <https://launchpad.net/products/bzr/+bug/40508>
660
return {'preexec_fn': _ignore_signals,
577
return {'preexec_fn': _ignore_sigint,
661
578
'close_fds': True,
665
_subproc_weakrefs = set()
667
def _close_ssh_proc(proc, sock):
668
"""Carefully close stdin/stdout and reap the SSH process.
670
If the pipes are already closed and/or the process has already been
671
wait()ed on, that's ok, and no error is raised. The goal is to do our best
672
to clean up (whether or not a clean up was already tried).
675
for closeable in (proc.stdin, proc.stdout, sock):
676
# We expect that either proc (a subprocess.Popen) will have stdin and
677
# stdout streams to close, or that we will have been passed a socket to
678
# close, with the option not in use being None.
679
if closeable is not None:
680
funcs.append(closeable.close)
681
funcs.append(proc.wait)
686
# It's ok for the pipe to already be closed, or the process to
687
# already be finished.
691
class SSHConnection(object):
692
"""Abstract base class for SSH connections."""
694
def get_sock_or_pipes(self):
695
"""Returns a (kind, io_object) pair.
697
If kind == 'socket', then io_object is a socket.
699
If kind == 'pipes', then io_object is a pair of file-like objects
700
(read_from, write_to).
702
raise NotImplementedError(self.get_sock_or_pipes)
705
raise NotImplementedError(self.close)
708
class SSHSubprocessConnection(SSHConnection):
709
"""A connection to an ssh subprocess via pipes or a socket.
711
This class is also socket-like enough to be used with
712
SocketAsChannelAdapter (it has 'send' and 'recv' methods).
715
def __init__(self, proc, sock=None):
718
:param proc: a subprocess.Popen
719
:param sock: if proc.stdin/out is a socket from a socketpair, then sock
720
should bzrlib's half of that socketpair. If not passed, proc's
721
stdin/out is assumed to be ordinary pipes.
582
class SSHSubprocess(object):
583
"""A socket-like object that talks to an ssh subprocess via pipes."""
585
def __init__(self, proc):
725
# Add a weakref to proc that will attempt to do the same as self.close
726
# to avoid leaving processes lingering indefinitely.
728
_subproc_weakrefs.remove(ref)
729
_close_ssh_proc(proc, sock)
730
_subproc_weakrefs.add(weakref.ref(self, terminate))
732
588
def send(self, data):
733
if self._sock is not None:
734
return self._sock.send(data)
736
return os.write(self.proc.stdin.fileno(), 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
738
598
def recv(self, count):
739
if self._sock is not None:
740
return self._sock.recv(count)
742
return os.read(self.proc.stdout.fileno(), count)
745
_close_ssh_proc(self.proc, self._sock)
747
def get_sock_or_pipes(self):
748
if self._sock is not None:
749
return 'socket', self._sock
751
return 'pipes', (self.proc.stdout, self.proc.stdin)
754
class _ParamikoSSHConnection(SSHConnection):
755
"""An SSH connection via paramiko."""
757
def __init__(self, channel):
758
self.channel = channel
760
def get_sock_or_pipes(self):
761
return ('socket', self.channel)
764
return self.channel.close()
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)