~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2006-2011 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
20
22
import errno
21
23
import getpass
22
24
import logging
24
26
import socket
25
27
import subprocess
26
28
import sys
 
29
from binascii import hexlify
27
30
 
28
31
from bzrlib import (
29
32
    config,
94
97
            try:
95
98
                vendor = self._ssh_vendors[vendor_name]
96
99
            except KeyError:
97
 
                raise errors.UnknownSSH(vendor_name)
 
100
                vendor = self._get_vendor_from_path(vendor_name)
 
101
                if vendor is None:
 
102
                    raise errors.UnknownSSH(vendor_name)
 
103
                vendor.executable_path = vendor_name
98
104
            return vendor
99
105
        return None
100
106
 
110
116
            stdout = stderr = ''
111
117
        return stdout + stderr
112
118
 
113
 
    def _get_vendor_by_version_string(self, version, args):
 
119
    def _get_vendor_by_version_string(self, version, progname):
114
120
        """Return the vendor or None based on output from the subprocess.
115
121
 
116
122
        :param version: The output of 'ssh -V' like command.
123
129
        elif 'SSH Secure Shell' in version:
124
130
            trace.mutter('ssh implementation is SSH Corp.')
125
131
            vendor = SSHCorpSubprocessVendor()
126
 
        elif 'plink' in version and args[0] == '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':
127
139
            # Checking if "plink" was the executed argument as Windows
128
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
140
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
129
141
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
130
142
            trace.mutter("ssh implementation is Putty's plink.")
131
143
            vendor = PLinkSubprocessVendor()
133
145
 
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, args)
139
 
            if vendor is not None:
140
 
                return vendor
141
 
        return None
 
148
        version = self._get_ssh_version_string(['ssh', '-V'])
 
149
        return self._get_vendor_by_version_string(version, "ssh")
 
150
 
 
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])
142
156
 
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
166
180
 
167
181
 
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>
172
186
    import signal
173
187
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
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)
174
191
 
175
192
 
176
193
class SocketAsChannelAdapter(object):
228
245
    def connect_ssh(self, username, password, host, port, command):
229
246
        """Make an SSH connection.
230
247
 
231
 
        :returns: something with a `close` method, and a `get_filelike_channels`
232
 
            method that returns a pair of (read, write) filelike objects.
 
248
        :returns: an SSHConnection.
233
249
        """
234
250
        raise NotImplementedError(self.connect_ssh)
235
251
 
258
274
register_ssh_vendor('loopback', LoopbackVendor())
259
275
 
260
276
 
261
 
class _ParamikoSSHConnection(object):
262
 
    def __init__(self, channel):
263
 
        self.channel = channel
264
 
 
265
 
    def get_filelike_channels(self):
266
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
267
 
 
268
 
    def close(self):
269
 
        return self.channel.close()
270
 
 
271
 
 
272
277
class ParamikoVendor(SSHVendor):
273
278
    """Vendor that uses paramiko."""
274
279
 
 
280
    def _hexify(self, s):
 
281
        return hexlify(s).upper()
 
282
 
275
283
    def _connect(self, username, password, host, port):
276
284
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
277
285
 
285
293
            self._raise_connection_error(host, port=port, orig_error=e)
286
294
 
287
295
        server_key = t.get_remote_server_key()
288
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
296
        server_key_hex = self._hexify(server_key.get_fingerprint())
289
297
        keytype = server_key.get_name()
290
298
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
291
299
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
292
 
            our_server_key_hex = paramiko.util.hexify(
293
 
                our_server_key.get_fingerprint())
 
300
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
294
301
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
295
302
            our_server_key = BZR_HOSTKEYS[host][keytype]
296
 
            our_server_key_hex = paramiko.util.hexify(
297
 
                our_server_key.get_fingerprint())
 
303
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
298
304
        else:
299
305
            trace.warning('Adding %s host key for %s: %s'
300
306
                          % (keytype, host, server_key_hex))
304
310
            else:
305
311
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
306
312
            our_server_key = server_key
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
313
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
309
314
            save_host_keys()
310
315
        if server_key != our_server_key:
311
316
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
337
342
            self._raise_connection_error(host, port=port, orig_error=e,
338
343
                                         msg='Unable to invoke remote bzr')
339
344
 
 
345
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
340
346
if paramiko is not None:
341
347
    vendor = ParamikoVendor()
342
348
    register_ssh_vendor('paramiko', vendor)
343
349
    register_ssh_vendor('none', vendor)
344
350
    register_default_ssh_vendor(vendor)
345
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
351
    _ssh_connection_errors += (paramiko.SSHException,)
346
352
    del vendor
347
 
else:
348
 
    _sftp_connection_errors = (EOFError,)
349
353
 
350
354
 
351
355
class SubprocessVendor(SSHVendor):
352
356
    """Abstract base class for vendors that use pipes to a subprocess."""
353
357
 
 
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
 
362
 
354
363
    def _connect(self, argv):
355
 
        proc = subprocess.Popen(argv,
356
 
                                stdin=subprocess.PIPE,
357
 
                                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
 
367
        # whatever) chunks.
 
368
        try:
 
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
 
373
            # pipes instead.
 
374
            stdin = stdout = subprocess.PIPE
 
375
            my_sock, subproc_sock = None, None
 
376
        else:
 
377
            stdin = stdout = subproc_sock
 
378
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
379
                                stderr=self._stderr_target,
358
380
                                **os_specific_subprocess_params())
359
 
        return SSHSubprocess(proc)
 
381
        if subproc_sock is not None:
 
382
            subproc_sock.close()
 
383
        return SSHSubprocessConnection(proc, sock=my_sock)
360
384
 
361
385
    def connect_sftp(self, username, password, host, port):
362
386
        try:
364
388
                                                  subsystem='sftp')
365
389
            sock = self._connect(argv)
366
390
            return SFTPClient(SocketAsChannelAdapter(sock))
367
 
        except _sftp_connection_errors, e:
368
 
            self._raise_connection_error(host, port=port, orig_error=e)
369
 
        except (OSError, IOError), e:
370
 
            # If the machine is fast enough, ssh can actually exit
371
 
            # before we try and send it the sftp request, which
372
 
            # raises a Broken Pipe
373
 
            if e.errno not in (errno.EPIPE,):
374
 
                raise
 
391
        except _ssh_connection_errors, e:
375
392
            self._raise_connection_error(host, port=port, orig_error=e)
376
393
 
377
394
    def connect_ssh(self, username, password, host, port, command):
379
396
            argv = self._get_vendor_specific_argv(username, host, port,
380
397
                                                  command=command)
381
398
            return self._connect(argv)
382
 
        except (EOFError), e:
383
 
            self._raise_connection_error(host, port=port, orig_error=e)
384
 
        except (OSError, IOError), e:
385
 
            # If the machine is fast enough, ssh can actually exit
386
 
            # before we try and send it the sftp request, which
387
 
            # raises a Broken Pipe
388
 
            if e.errno not in (errno.EPIPE,):
389
 
                raise
 
399
        except _ssh_connection_errors, e:
390
400
            self._raise_connection_error(host, port=port, orig_error=e)
391
401
 
392
402
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
401
411
class OpenSSHSubprocessVendor(SubprocessVendor):
402
412
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
403
413
 
 
414
    executable_path = 'ssh'
 
415
 
404
416
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
405
417
                                  command=None):
406
 
        args = ['ssh',
 
418
        args = [self.executable_path,
407
419
                '-oForwardX11=no', '-oForwardAgent=no',
408
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
420
                '-oClearAllForwardings=yes',
409
421
                '-oNoHostAuthenticationForLocalhost=yes']
410
422
        if port is not None:
411
423
            args.extend(['-p', str(port)])
423
435
class SSHCorpSubprocessVendor(SubprocessVendor):
424
436
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
425
437
 
 
438
    executable_path = 'ssh'
 
439
 
426
440
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
427
441
                                  command=None):
428
 
        args = ['ssh', '-x']
 
442
        args = [self.executable_path, '-x']
429
443
        if port is not None:
430
444
            args.extend(['-p', str(port)])
431
445
        if username is not None:
436
450
            args.extend([host] + command)
437
451
        return args
438
452
 
439
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
453
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
454
 
 
455
 
 
456
class LSHSubprocessVendor(SubprocessVendor):
 
457
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
458
 
 
459
    executable_path = 'lsh'
 
460
 
 
461
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
462
                                  command=None):
 
463
        args = [self.executable_path]
 
464
        if port is not None:
 
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])
 
470
        else:
 
471
            args.extend([host] + command)
 
472
        return args
 
473
 
 
474
register_ssh_vendor('lsh', LSHSubprocessVendor())
440
475
 
441
476
 
442
477
class PLinkSubprocessVendor(SubprocessVendor):
443
478
    """SSH vendor that uses the 'plink' executable from Putty."""
444
479
 
 
480
    executable_path = 'plink'
 
481
 
445
482
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
446
483
                                  command=None):
447
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
484
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
448
485
        if port is not None:
449
486
            args.extend(['-P', str(port)])
450
487
        if username is not None:
469
506
        agent = paramiko.Agent()
470
507
        for key in agent.get_keys():
471
508
            trace.mutter('Trying SSH agent key %s'
472
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
509
                         % self._hexify(key.get_fingerprint()))
473
510
            try:
474
511
                paramiko_transport.auth_publickey(username, key)
475
512
                return
501
538
    except paramiko.SSHException, e:
502
539
        # Don't know what happened, but just ignore it
503
540
        pass
504
 
    if 'password' not in supported_auth_types:
 
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
 
548
    # support that.
 
549
    if ('password' not in supported_auth_types and
 
550
        'keyboard-interactive' not in supported_auth_types):
505
551
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
506
552
            '\n  %s@%s\nsupported auth types: %s'
507
553
            % (username, host, supported_auth_types))
536
582
        return True
537
583
    except paramiko.PasswordRequiredException:
538
584
        password = ui.ui_factory.get_password(
539
 
            prompt='SSH %(filename)s password', filename=filename)
 
585
            prompt=u'SSH %(filename)s password',
 
586
            filename=filename.decode(osutils._fs_enc))
540
587
        try:
541
588
            key = pkey_class.from_private_key_file(filename, password)
542
589
            paramiko_transport.auth_publickey(username, key)
611
658
        # Running it in a separate process group is not good because then it
612
659
        # can't get non-echoed input of a password or passphrase.
613
660
        # <https://launchpad.net/products/bzr/+bug/40508>
614
 
        return {'preexec_fn': _ignore_sigint,
 
661
        return {'preexec_fn': _ignore_signals,
615
662
                'close_fds': True,
616
663
                }
617
664
 
618
 
 
619
 
class SSHSubprocess(object):
620
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
621
 
 
622
 
    def __init__(self, proc):
 
665
import weakref
 
666
_subproc_weakrefs = set()
 
667
 
 
668
def _close_ssh_proc(proc, sock):
 
669
    """Carefully close stdin/stdout and reap the SSH process.
 
670
 
 
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).
 
674
    """
 
675
    funcs = []
 
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)
 
683
    for func in funcs:
 
684
        try:
 
685
            func()
 
686
        except OSError:
 
687
            # It's ok for the pipe to already be closed, or the process to
 
688
            # already be finished.
 
689
            continue
 
690
 
 
691
 
 
692
class SSHConnection(object):
 
693
    """Abstract base class for SSH connections."""
 
694
 
 
695
    def get_sock_or_pipes(self):
 
696
        """Returns a (kind, io_object) pair.
 
697
 
 
698
        If kind == 'socket', then io_object is a socket.
 
699
 
 
700
        If kind == 'pipes', then io_object is a pair of file-like objects
 
701
        (read_from, write_to).
 
702
        """
 
703
        raise NotImplementedError(self.get_sock_or_pipes)
 
704
 
 
705
    def close(self):
 
706
        raise NotImplementedError(self.close)
 
707
 
 
708
 
 
709
class SSHSubprocessConnection(SSHConnection):
 
710
    """A connection to an ssh subprocess via pipes or a socket.
 
711
 
 
712
    This class is also socket-like enough to be used with
 
713
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
714
    """
 
715
 
 
716
    def __init__(self, proc, sock=None):
 
717
        """Constructor.
 
718
 
 
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.
 
723
        """
623
724
        self.proc = proc
 
725
        self._sock = sock
 
726
        # Add a weakref to proc that will attempt to do the same as self.close
 
727
        # to avoid leaving processes lingering indefinitely.
 
728
        def terminate(ref):
 
729
            _subproc_weakrefs.remove(ref)
 
730
            _close_ssh_proc(proc, sock)
 
731
        _subproc_weakrefs.add(weakref.ref(self, terminate))
624
732
 
625
733
    def send(self, data):
626
 
        return os.write(self.proc.stdin.fileno(), data)
 
734
        if self._sock is not None:
 
735
            return self._sock.send(data)
 
736
        else:
 
737
            return os.write(self.proc.stdin.fileno(), data)
627
738
 
628
739
    def recv(self, count):
629
 
        return os.read(self.proc.stdout.fileno(), count)
630
 
 
631
 
    def close(self):
632
 
        self.proc.stdin.close()
633
 
        self.proc.stdout.close()
634
 
        self.proc.wait()
635
 
 
636
 
    def get_filelike_channels(self):
637
 
        return (self.proc.stdout, self.proc.stdin)
 
740
        if self._sock is not None:
 
741
            return self._sock.recv(count)
 
742
        else:
 
743
            return os.read(self.proc.stdout.fileno(), count)
 
744
 
 
745
    def close(self):
 
746
        _close_ssh_proc(self.proc, self._sock)
 
747
 
 
748
    def get_sock_or_pipes(self):
 
749
        if self._sock is not None:
 
750
            return 'socket', self._sock
 
751
        else:
 
752
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
753
 
 
754
 
 
755
class _ParamikoSSHConnection(SSHConnection):
 
756
    """An SSH connection via paramiko."""
 
757
 
 
758
    def __init__(self, channel):
 
759
        self.channel = channel
 
760
 
 
761
    def get_sock_or_pipes(self):
 
762
        return ('socket', self.channel)
 
763
 
 
764
    def close(self):
 
765
        return self.channel.close()
 
766
 
638
767