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
114
115
stdout = stderr = ''
115
116
return stdout + stderr
117
def _get_vendor_by_version_string(self, version):
118
def _get_vendor_by_version_string(self, version, progname):
118
119
"""Return the vendor or None based on output from the subprocess.
120
121
:param version: The output of 'ssh -V' like command.
122
:param args: Command line that was run.
123
125
if 'OpenSSH' in version:
124
mutter('ssh implementation is OpenSSH')
126
trace.mutter('ssh implementation is OpenSSH')
125
127
vendor = OpenSSHSubprocessVendor()
126
128
elif 'SSH Secure Shell' in version:
127
mutter('ssh implementation is SSH Corp.')
129
trace.mutter('ssh implementation is SSH Corp.')
128
130
vendor = SSHCorpSubprocessVendor()
129
elif 'plink' in version:
130
mutter("ssh implementation is Putty's plink.")
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.")
131
142
vendor = PLinkSubprocessVendor()
134
145
def _get_vendor_by_inspection(self):
135
146
"""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:
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])
143
156
def get_vendor(self, environment=None):
144
157
"""Find out what version of SSH is on the system.
165
178
register_ssh_vendor = _ssh_vendor_manager.register_vendor
168
def _ignore_sigint():
181
def _ignore_signals():
169
182
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
170
183
# doesn't handle it itself.
171
184
# <https://launchpad.net/products/bzr/+bug/41433/+index>
173
186
signal.signal(signal.SIGINT, signal.SIG_IGN)
176
class LoopbackSFTP(object):
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):
177
193
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
179
195
def __init__(self, sock):
180
196
self.__socket = sock
199
return "bzr SocketAsChannelAdapter"
182
201
def send(self, data):
183
202
return self.__socket.send(data)
185
204
def recv(self, n):
186
return self.__socket.recv(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.
188
215
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
225
255
This just unifies all the locations that try to raise ConnectionError,
226
256
so that they format things properly.
228
raise SocketConnectionError(host=host, port=port, msg=msg,
229
orig_error=orig_error)
258
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
259
orig_error=orig_error)
232
262
class LoopbackVendor(SSHVendor):
233
263
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
235
265
def connect_sftp(self, username, password, host, port):
236
266
sock = socket.socket()
238
268
sock.connect((host, port))
239
269
except socket.error, e:
240
270
self._raise_connection_error(host, port=port, orig_error=e)
241
return SFTPClient(LoopbackSFTP(sock))
271
return SFTPClient(SocketAsChannelAdapter(sock))
243
273
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
276
class ParamikoVendor(SSHVendor):
258
277
"""Vendor that uses paramiko."""
260
279
def _connect(self, username, password, host, port):
261
280
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
269
288
except (paramiko.SSHException, socket.error), e:
270
289
self._raise_connection_error(host, port=port, orig_error=e)
272
291
server_key = t.get_remote_server_key()
273
292
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
274
293
keytype = server_key.get_name()
275
294
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
276
295
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
277
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
296
our_server_key_hex = paramiko.util.hexify(
297
our_server_key.get_fingerprint())
278
298
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
279
299
our_server_key = BZR_HOSTKEYS[host][keytype]
280
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
300
our_server_key_hex = paramiko.util.hexify(
301
our_server_key.get_fingerprint())
282
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
303
trace.warning('Adding %s host key for %s: %s'
304
% (keytype, host, server_key_hex))
283
305
add = getattr(BZR_HOSTKEYS, 'add', None)
284
306
if add is not None: # paramiko >= 1.X.X
285
307
BZR_HOSTKEYS.add(host, keytype, server_key)
287
309
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
288
310
our_server_key = server_key
289
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
311
our_server_key_hex = paramiko.util.hexify(
312
our_server_key.get_fingerprint())
291
314
if server_key != our_server_key:
292
315
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' % \
316
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
317
raise errors.TransportError(
318
'Host keys for %s do not match! %s != %s' %
295
319
(host, our_server_key_hex, server_key_hex),
296
320
['Try editing %s or %s' % (filename1, filename2)])
298
_paramiko_auth(username, password, host, t)
322
_paramiko_auth(username, password, host, port, t)
301
325
def connect_sftp(self, username, password, host, port):
302
326
t = self._connect(username, password, host, port)
317
341
self._raise_connection_error(host, port=port, orig_error=e,
318
342
msg='Unable to invoke remote bzr')
344
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
320
345
if paramiko is not None:
321
346
vendor = ParamikoVendor()
322
347
register_ssh_vendor('paramiko', vendor)
323
348
register_ssh_vendor('none', vendor)
324
349
register_default_ssh_vendor(vendor)
350
_ssh_connection_errors += (paramiko.SSHException,)
328
354
class SubprocessVendor(SSHVendor):
329
355
"""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
331
362
def _connect(self, argv):
332
proc = subprocess.Popen(argv,
333
stdin=subprocess.PIPE,
334
stdout=subprocess.PIPE,
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,
335
379
**os_specific_subprocess_params())
336
return SSHSubprocess(proc)
380
if subproc_sock is not None:
382
return SSHSubprocessConnection(proc, sock=my_sock)
338
384
def connect_sftp(self, username, password, host, port):
340
386
argv = self._get_vendor_specific_argv(username, host, port,
341
387
subsystem='sftp')
342
388
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,):
389
return SFTPClient(SocketAsChannelAdapter(sock))
390
except _ssh_connection_errors, e:
352
391
self._raise_connection_error(host, port=port, orig_error=e)
354
393
def connect_ssh(self, username, password, host, port, command):
356
395
argv = self._get_vendor_specific_argv(username, host, port,
358
397
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,):
398
except _ssh_connection_errors, e:
367
399
self._raise_connection_error(host, port=port, orig_error=e)
369
401
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
371
403
"""Returns the argument list to run the subprocess with.
373
405
Exactly one of 'subsystem' and 'command' must be specified.
375
407
raise NotImplementedError(self._get_vendor_specific_argv)
378
410
class OpenSSHSubprocessVendor(SubprocessVendor):
379
411
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
413
executable_path = 'ssh'
381
415
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')
417
args = [self.executable_path,
389
418
'-oForwardX11=no', '-oForwardAgent=no',
390
'-oClearAllForwardings=yes', '-oProtocol=2',
419
'-oClearAllForwardings=yes',
391
420
'-oNoHostAuthenticationForLocalhost=yes']
392
421
if port is not None:
393
422
args.extend(['-p', str(port)])
423
449
args.extend([host] + command)
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
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())
429
476
class PLinkSubprocessVendor(SubprocessVendor):
430
477
"""SSH vendor that uses the 'plink' executable from Putty."""
479
executable_path = 'plink'
432
481
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']
483
args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
440
484
if port is not None:
441
485
args.extend(['-P', str(port)])
442
486
if username is not None:
450
494
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()
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())
462
504
if _use_ssh_agent:
463
505
agent = paramiko.Agent()
464
506
for key in agent.get_keys():
465
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
507
trace.mutter('Trying SSH agent key %s'
508
% paramiko.util.hexify(key.get_fingerprint()))
467
510
paramiko_transport.auth_publickey(username, key)
469
512
except paramiko.SSHException, e:
472
515
# okay, try finding id_rsa or id_dss? (posix only)
473
516
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
475
518
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))
480
556
paramiko_transport.auth_password(username, password)
485
561
# 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' %
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))
496
576
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
500
580
paramiko_transport.auth_publickey(username, key)
502
582
except paramiko.PasswordRequiredException:
503
password = bzrlib.ui.ui_factory.get_password(
504
prompt='SSH %(filename)s password',
583
password = ui.ui_factory.get_password(
584
prompt=u'SSH %(filename)s password',
585
filename=filename.decode(osutils._fs_enc))
507
587
key = pkey_class.from_private_key_file(filename, password)
508
588
paramiko_transport.auth_publickey(username, key)
510
590
except paramiko.SSHException:
511
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
591
trace.mutter('SSH authentication via %s key failed.'
592
% (os.path.basename(filename),))
512
593
except paramiko.SSHException:
513
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
594
trace.mutter('SSH authentication via %s key failed.'
595
% (os.path.basename(filename),))
568
651
# this causes it to be seen only by bzr and not by ssh. Python will
569
652
# generate a KeyboardInterrupt in bzr, and we will then have a chance
570
653
# to release locks or do other cleanup over ssh before the connection
572
655
# <https://launchpad.net/products/bzr/+bug/5987>
574
657
# Running it in a separate process group is not good because then it
575
658
# can't get non-echoed input of a password or passphrase.
576
659
# <https://launchpad.net/products/bzr/+bug/40508>
577
return {'preexec_fn': _ignore_sigint,
660
return {'preexec_fn': _ignore_signals,
578
661
'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):
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.
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))
588
732
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
733
if self._sock is not None:
734
return self._sock.send(data)
736
return os.write(self.proc.stdin.fileno(), data)
598
738
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)
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()