~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

(jameinel) Allow 'bzr serve' to interpret SIGHUP as a graceful shutdown.
 (bug #795025) (John A Meinel)

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
13
13
#
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
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
20
import errno
21
21
import getpass
 
22
import logging
22
23
import os
23
24
import socket
24
25
import subprocess
93
94
            try:
94
95
                vendor = self._ssh_vendors[vendor_name]
95
96
            except KeyError:
96
 
                raise errors.UnknownSSH(vendor_name)
 
97
                vendor = self._get_vendor_from_path(vendor_name)
 
98
                if vendor is None:
 
99
                    raise errors.UnknownSSH(vendor_name)
 
100
                vendor.executable_path = vendor_name
97
101
            return vendor
98
102
        return None
99
103
 
109
113
            stdout = stderr = ''
110
114
        return stdout + stderr
111
115
 
112
 
    def _get_vendor_by_version_string(self, version, args):
 
116
    def _get_vendor_by_version_string(self, version, progname):
113
117
        """Return the vendor or None based on output from the subprocess.
114
118
 
115
119
        :param version: The output of 'ssh -V' like command.
122
126
        elif 'SSH Secure Shell' in version:
123
127
            trace.mutter('ssh implementation is SSH Corp.')
124
128
            vendor = SSHCorpSubprocessVendor()
125
 
        elif 'plink' in version and args[0] == 'plink':
 
129
        elif 'lsh' in version:
 
130
            trace.mutter('ssh implementation is GNU lsh.')
 
131
            vendor = LSHSubprocessVendor()
 
132
        # As plink user prompts are not handled currently, don't auto-detect
 
133
        # it by inspection below, but keep this vendor detection for if a path
 
134
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
135
        elif 'plink' in version and progname == 'plink':
126
136
            # Checking if "plink" was the executed argument as Windows
127
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
137
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
128
138
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
129
139
            trace.mutter("ssh implementation is Putty's plink.")
130
140
            vendor = PLinkSubprocessVendor()
132
142
 
133
143
    def _get_vendor_by_inspection(self):
134
144
        """Return the vendor or None by checking for known SSH implementations."""
135
 
        for args in (['ssh', '-V'], ['plink', '-V']):
136
 
            version = self._get_ssh_version_string(args)
137
 
            vendor = self._get_vendor_by_version_string(version, args)
138
 
            if vendor is not None:
139
 
                return vendor
140
 
        return None
 
145
        version = self._get_ssh_version_string(['ssh', '-V'])
 
146
        return self._get_vendor_by_version_string(version, "ssh")
 
147
 
 
148
    def _get_vendor_from_path(self, path):
 
149
        """Return the vendor or None using the program at the given path"""
 
150
        version = self._get_ssh_version_string([path, '-V'])
 
151
        return self._get_vendor_by_version_string(version, 
 
152
            os.path.splitext(os.path.basename(path))[0])
141
153
 
142
154
    def get_vendor(self, environment=None):
143
155
        """Find out what version of SSH is on the system.
164
176
register_ssh_vendor = _ssh_vendor_manager.register_vendor
165
177
 
166
178
 
167
 
def _ignore_sigint():
 
179
def _ignore_signals():
168
180
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
169
181
    # doesn't handle it itself.
170
182
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
171
183
    import signal
172
184
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
185
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
 
186
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
 
187
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
173
188
 
174
189
 
175
190
class SocketAsChannelAdapter(object):
180
195
 
181
196
    def get_name(self):
182
197
        return "bzr SocketAsChannelAdapter"
183
 
    
 
198
 
184
199
    def send(self, data):
185
200
        return self.__socket.send(data)
186
201
 
211
226
 
212
227
    def connect_sftp(self, username, password, host, port):
213
228
        """Make an SSH connection, and return an SFTPClient.
214
 
        
 
229
 
215
230
        :param username: an ascii string
216
231
        :param password: an ascii string
217
232
        :param host: a host name as an ascii string
226
241
 
227
242
    def connect_ssh(self, username, password, host, port, command):
228
243
        """Make an SSH connection.
229
 
        
230
 
        :returns: something with a `close` method, and a `get_filelike_channels`
231
 
            method that returns a pair of (read, write) filelike objects.
 
244
 
 
245
        :returns: an SSHConnection.
232
246
        """
233
247
        raise NotImplementedError(self.connect_ssh)
234
248
 
257
271
register_ssh_vendor('loopback', LoopbackVendor())
258
272
 
259
273
 
260
 
class _ParamikoSSHConnection(object):
261
 
    def __init__(self, channel):
262
 
        self.channel = channel
263
 
 
264
 
    def get_filelike_channels(self):
265
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
266
 
 
267
 
    def close(self):
268
 
        return self.channel.close()
269
 
 
270
 
 
271
274
class ParamikoVendor(SSHVendor):
272
275
    """Vendor that uses paramiko."""
273
276
 
336
339
            self._raise_connection_error(host, port=port, orig_error=e,
337
340
                                         msg='Unable to invoke remote bzr')
338
341
 
 
342
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
339
343
if paramiko is not None:
340
344
    vendor = ParamikoVendor()
341
345
    register_ssh_vendor('paramiko', vendor)
342
346
    register_ssh_vendor('none', vendor)
343
347
    register_default_ssh_vendor(vendor)
344
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
348
    _ssh_connection_errors += (paramiko.SSHException,)
345
349
    del vendor
346
 
else:
347
 
    _sftp_connection_errors = (EOFError,)
348
350
 
349
351
 
350
352
class SubprocessVendor(SSHVendor):
351
353
    """Abstract base class for vendors that use pipes to a subprocess."""
352
354
 
353
355
    def _connect(self, argv):
354
 
        proc = subprocess.Popen(argv,
355
 
                                stdin=subprocess.PIPE,
356
 
                                stdout=subprocess.PIPE,
 
356
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
357
        # subprocess.  We prefer sockets to pipes because they support
 
358
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
359
        # whatever) chunks.
 
360
        try:
 
361
            my_sock, subproc_sock = socket.socketpair()
 
362
            osutils.set_fd_cloexec(my_sock)
 
363
        except (AttributeError, socket.error):
 
364
            # This platform doesn't support socketpair(), so just use ordinary
 
365
            # pipes instead.
 
366
            stdin = stdout = subprocess.PIPE
 
367
            my_sock, subproc_sock = None, None
 
368
        else:
 
369
            stdin = stdout = subproc_sock
 
370
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
357
371
                                **os_specific_subprocess_params())
358
 
        return SSHSubprocess(proc)
 
372
        if subproc_sock is not None:
 
373
            subproc_sock.close()
 
374
        return SSHSubprocessConnection(proc, sock=my_sock)
359
375
 
360
376
    def connect_sftp(self, username, password, host, port):
361
377
        try:
363
379
                                                  subsystem='sftp')
364
380
            sock = self._connect(argv)
365
381
            return SFTPClient(SocketAsChannelAdapter(sock))
366
 
        except _sftp_connection_errors, e:
367
 
            self._raise_connection_error(host, port=port, orig_error=e)
368
 
        except (OSError, IOError), e:
369
 
            # If the machine is fast enough, ssh can actually exit
370
 
            # before we try and send it the sftp request, which
371
 
            # raises a Broken Pipe
372
 
            if e.errno not in (errno.EPIPE,):
373
 
                raise
 
382
        except _ssh_connection_errors, e:
374
383
            self._raise_connection_error(host, port=port, orig_error=e)
375
384
 
376
385
    def connect_ssh(self, username, password, host, port, command):
378
387
            argv = self._get_vendor_specific_argv(username, host, port,
379
388
                                                  command=command)
380
389
            return self._connect(argv)
381
 
        except (EOFError), e:
382
 
            self._raise_connection_error(host, port=port, orig_error=e)
383
 
        except (OSError, IOError), e:
384
 
            # If the machine is fast enough, ssh can actually exit
385
 
            # before we try and send it the sftp request, which
386
 
            # raises a Broken Pipe
387
 
            if e.errno not in (errno.EPIPE,):
388
 
                raise
 
390
        except _ssh_connection_errors, e:
389
391
            self._raise_connection_error(host, port=port, orig_error=e)
390
392
 
391
393
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
392
394
                                  command=None):
393
395
        """Returns the argument list to run the subprocess with.
394
 
        
 
396
 
395
397
        Exactly one of 'subsystem' and 'command' must be specified.
396
398
        """
397
399
        raise NotImplementedError(self._get_vendor_specific_argv)
400
402
class OpenSSHSubprocessVendor(SubprocessVendor):
401
403
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
402
404
 
 
405
    executable_path = 'ssh'
 
406
 
403
407
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
404
408
                                  command=None):
405
 
        args = ['ssh',
 
409
        args = [self.executable_path,
406
410
                '-oForwardX11=no', '-oForwardAgent=no',
407
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
411
                '-oClearAllForwardings=yes',
408
412
                '-oNoHostAuthenticationForLocalhost=yes']
409
413
        if port is not None:
410
414
            args.extend(['-p', str(port)])
422
426
class SSHCorpSubprocessVendor(SubprocessVendor):
423
427
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
424
428
 
 
429
    executable_path = 'ssh'
 
430
 
425
431
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
426
432
                                  command=None):
427
 
        args = ['ssh', '-x']
 
433
        args = [self.executable_path, '-x']
428
434
        if port is not None:
429
435
            args.extend(['-p', str(port)])
430
436
        if username is not None:
435
441
            args.extend([host] + command)
436
442
        return args
437
443
 
438
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
444
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
445
 
 
446
 
 
447
class LSHSubprocessVendor(SubprocessVendor):
 
448
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
449
 
 
450
    executable_path = 'lsh'
 
451
 
 
452
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
453
                                  command=None):
 
454
        args = [self.executable_path]
 
455
        if port is not None:
 
456
            args.extend(['-p', str(port)])
 
457
        if username is not None:
 
458
            args.extend(['-l', username])
 
459
        if subsystem is not None:
 
460
            args.extend(['--subsystem', subsystem, host])
 
461
        else:
 
462
            args.extend([host] + command)
 
463
        return args
 
464
 
 
465
register_ssh_vendor('lsh', LSHSubprocessVendor())
439
466
 
440
467
 
441
468
class PLinkSubprocessVendor(SubprocessVendor):
442
469
    """SSH vendor that uses the 'plink' executable from Putty."""
443
470
 
 
471
    executable_path = 'plink'
 
472
 
444
473
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
445
474
                                  command=None):
446
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
475
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
447
476
        if port is not None:
448
477
            args.extend(['-P', str(port)])
449
478
        if username is not None:
458
487
 
459
488
 
460
489
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
490
    auth = config.AuthenticationConfig()
461
491
    # paramiko requires a username, but it might be none if nothing was
462
492
    # supplied.  If so, use the local username.
463
493
    if username is None:
464
 
        username = getpass.getuser()
465
 
 
 
494
        username = auth.get_user('ssh', host, port=port,
 
495
                                 default=getpass.getuser())
466
496
    if _use_ssh_agent:
467
497
        agent = paramiko.Agent()
468
498
        for key in agent.get_keys():
480
510
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
481
511
        return
482
512
 
 
513
    # If we have gotten this far, we are about to try for passwords, do an
 
514
    # auth_none check to see if it is even supported.
 
515
    supported_auth_types = []
 
516
    try:
 
517
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
518
        #    Authentication type (none) not permitted.
 
519
        # So we explicitly disable the logging level for this action
 
520
        old_level = paramiko_transport.logger.level
 
521
        paramiko_transport.logger.setLevel(logging.WARNING)
 
522
        try:
 
523
            paramiko_transport.auth_none(username)
 
524
        finally:
 
525
            paramiko_transport.logger.setLevel(old_level)
 
526
    except paramiko.BadAuthenticationType, e:
 
527
        # Supported methods are in the exception
 
528
        supported_auth_types = e.allowed_types
 
529
    except paramiko.SSHException, e:
 
530
        # Don't know what happened, but just ignore it
 
531
        pass
 
532
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
533
    # because Paramiko's auth_password method will automatically try
 
534
    # 'keyboard-interactive' auth (using the password as the response) if
 
535
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
536
    # OpenSSH servers require this.
 
537
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
538
    # requires something other than a single password, but we currently don't
 
539
    # support that.
 
540
    if ('password' not in supported_auth_types and
 
541
        'keyboard-interactive' not in supported_auth_types):
 
542
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
543
            '\n  %s@%s\nsupported auth types: %s'
 
544
            % (username, host, supported_auth_types))
 
545
 
483
546
    if password:
484
547
        try:
485
548
            paramiko_transport.auth_password(username, password)
488
551
            pass
489
552
 
490
553
    # give up and ask for a password
491
 
    auth = config.AuthenticationConfig()
492
554
    password = auth.get_password('ssh', host, username, port=port)
493
 
    try:
494
 
        paramiko_transport.auth_password(username, password)
495
 
    except paramiko.SSHException, e:
496
 
        raise errors.ConnectionError(
497
 
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
 
555
    # get_password can still return None, which means we should not prompt
 
556
    if password is not None:
 
557
        try:
 
558
            paramiko_transport.auth_password(username, password)
 
559
        except paramiko.SSHException, e:
 
560
            raise errors.ConnectionError(
 
561
                'Unable to authenticate to SSH host as'
 
562
                '\n  %s@%s\n' % (username, host), e)
 
563
    else:
 
564
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
565
                                     '  %s@%s' % (username, host))
498
566
 
499
567
 
500
568
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
505
573
        return True
506
574
    except paramiko.PasswordRequiredException:
507
575
        password = ui.ui_factory.get_password(
508
 
            prompt='SSH %(filename)s password', filename=filename)
 
576
            prompt=u'SSH %(filename)s password',
 
577
            filename=filename.decode(osutils._fs_enc))
509
578
        try:
510
579
            key = pkey_class.from_private_key_file(filename, password)
511
580
            paramiko_transport.auth_publickey(username, key)
562
631
def os_specific_subprocess_params():
563
632
    """Get O/S specific subprocess parameters."""
564
633
    if sys.platform == 'win32':
565
 
        # setting the process group and closing fds is not supported on 
 
634
        # setting the process group and closing fds is not supported on
566
635
        # win32
567
636
        return {}
568
637
    else:
569
 
        # We close fds other than the pipes as the child process does not need 
 
638
        # We close fds other than the pipes as the child process does not need
570
639
        # them to be open.
571
640
        #
572
641
        # We also set the child process to ignore SIGINT.  Normally the signal
574
643
        # this causes it to be seen only by bzr and not by ssh.  Python will
575
644
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
576
645
        # to release locks or do other cleanup over ssh before the connection
577
 
        # goes away.  
 
646
        # goes away.
578
647
        # <https://launchpad.net/products/bzr/+bug/5987>
579
648
        #
580
649
        # Running it in a separate process group is not good because then it
581
650
        # can't get non-echoed input of a password or passphrase.
582
651
        # <https://launchpad.net/products/bzr/+bug/40508>
583
 
        return {'preexec_fn': _ignore_sigint,
 
652
        return {'preexec_fn': _ignore_signals,
584
653
                'close_fds': True,
585
654
                }
586
655
 
587
 
 
588
 
class SSHSubprocess(object):
589
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
590
 
 
591
 
    def __init__(self, proc):
 
656
import weakref
 
657
_subproc_weakrefs = set()
 
658
 
 
659
def _close_ssh_proc(proc, sock):
 
660
    """Carefully close stdin/stdout and reap the SSH process.
 
661
 
 
662
    If the pipes are already closed and/or the process has already been
 
663
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
664
    to clean up (whether or not a clean up was already tried).
 
665
    """
 
666
    funcs = []
 
667
    for closeable in (proc.stdin, proc.stdout, sock):
 
668
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
669
        # stdout streams to close, or that we will have been passed a socket to
 
670
        # close, with the option not in use being None.
 
671
        if closeable is not None:
 
672
            funcs.append(closeable.close)
 
673
    funcs.append(proc.wait)
 
674
    for func in funcs:
 
675
        try:
 
676
            func()
 
677
        except OSError:
 
678
            # It's ok for the pipe to already be closed, or the process to
 
679
            # already be finished.
 
680
            continue
 
681
 
 
682
 
 
683
class SSHConnection(object):
 
684
    """Abstract base class for SSH connections."""
 
685
 
 
686
    def get_sock_or_pipes(self):
 
687
        """Returns a (kind, io_object) pair.
 
688
 
 
689
        If kind == 'socket', then io_object is a socket.
 
690
 
 
691
        If kind == 'pipes', then io_object is a pair of file-like objects
 
692
        (read_from, write_to).
 
693
        """
 
694
        raise NotImplementedError(self.get_sock_or_pipes)
 
695
 
 
696
    def close(self):
 
697
        raise NotImplementedError(self.close)
 
698
 
 
699
 
 
700
class SSHSubprocessConnection(SSHConnection):
 
701
    """A connection to an ssh subprocess via pipes or a socket.
 
702
 
 
703
    This class is also socket-like enough to be used with
 
704
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
705
    """
 
706
 
 
707
    def __init__(self, proc, sock=None):
 
708
        """Constructor.
 
709
 
 
710
        :param proc: a subprocess.Popen
 
711
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
712
            should bzrlib's half of that socketpair.  If not passed, proc's
 
713
            stdin/out is assumed to be ordinary pipes.
 
714
        """
592
715
        self.proc = proc
 
716
        self._sock = sock
 
717
        # Add a weakref to proc that will attempt to do the same as self.close
 
718
        # to avoid leaving processes lingering indefinitely.
 
719
        def terminate(ref):
 
720
            _subproc_weakrefs.remove(ref)
 
721
            _close_ssh_proc(proc, sock)
 
722
        _subproc_weakrefs.add(weakref.ref(self, terminate))
593
723
 
594
724
    def send(self, data):
595
 
        return os.write(self.proc.stdin.fileno(), data)
 
725
        if self._sock is not None:
 
726
            return self._sock.send(data)
 
727
        else:
 
728
            return os.write(self.proc.stdin.fileno(), data)
596
729
 
597
730
    def recv(self, count):
598
 
        return os.read(self.proc.stdout.fileno(), count)
599
 
 
600
 
    def close(self):
601
 
        self.proc.stdin.close()
602
 
        self.proc.stdout.close()
603
 
        self.proc.wait()
604
 
 
605
 
    def get_filelike_channels(self):
606
 
        return (self.proc.stdout, self.proc.stdin)
 
731
        if self._sock is not None:
 
732
            return self._sock.recv(count)
 
733
        else:
 
734
            return os.read(self.proc.stdout.fileno(), count)
 
735
 
 
736
    def close(self):
 
737
        _close_ssh_proc(self.proc, self._sock)
 
738
 
 
739
    def get_sock_or_pipes(self):
 
740
        if self._sock is not None:
 
741
            return 'socket', self._sock
 
742
        else:
 
743
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
744
 
 
745
 
 
746
class _ParamikoSSHConnection(SSHConnection):
 
747
    """An SSH connection via paramiko."""
 
748
 
 
749
    def __init__(self, channel):
 
750
        self.channel = channel
 
751
 
 
752
    def get_sock_or_pipes(self):
 
753
        return ('socket', self.channel)
 
754
 
 
755
    def close(self):
 
756
        return self.channel.close()
 
757
 
607
758