~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

Replace remaining to unittest.TestResult methods with super

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-2010 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
        # As plink user prompts are not handled currently, don't auto-detect
 
130
        # it by inspection below, but keep this vendor detection for if a path
 
131
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
132
        elif 'plink' in version and progname == 'plink':
126
133
            # Checking if "plink" was the executed argument as Windows
127
134
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
128
135
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
132
139
 
133
140
    def _get_vendor_by_inspection(self):
134
141
        """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
 
142
        version = self._get_ssh_version_string(['ssh', '-V'])
 
143
        return self._get_vendor_by_version_string(version, "ssh")
 
144
 
 
145
    def _get_vendor_from_path(self, path):
 
146
        """Return the vendor or None using the program at the given path"""
 
147
        version = self._get_ssh_version_string([path, '-V'])
 
148
        return self._get_vendor_by_version_string(version, 
 
149
            os.path.splitext(os.path.basename(path))[0])
141
150
 
142
151
    def get_vendor(self, environment=None):
143
152
        """Find out what version of SSH is on the system.
164
173
register_ssh_vendor = _ssh_vendor_manager.register_vendor
165
174
 
166
175
 
167
 
def _ignore_sigint():
 
176
def _ignore_signals():
168
177
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
169
178
    # doesn't handle it itself.
170
179
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
171
180
    import signal
172
181
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
182
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
 
183
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
 
184
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
173
185
 
174
186
 
175
187
class SocketAsChannelAdapter(object):
180
192
 
181
193
    def get_name(self):
182
194
        return "bzr SocketAsChannelAdapter"
183
 
    
 
195
 
184
196
    def send(self, data):
185
197
        return self.__socket.send(data)
186
198
 
211
223
 
212
224
    def connect_sftp(self, username, password, host, port):
213
225
        """Make an SSH connection, and return an SFTPClient.
214
 
        
 
226
 
215
227
        :param username: an ascii string
216
228
        :param password: an ascii string
217
229
        :param host: a host name as an ascii string
226
238
 
227
239
    def connect_ssh(self, username, password, host, port, command):
228
240
        """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.
 
241
 
 
242
        :returns: an SSHConnection.
232
243
        """
233
244
        raise NotImplementedError(self.connect_ssh)
234
245
 
257
268
register_ssh_vendor('loopback', LoopbackVendor())
258
269
 
259
270
 
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
271
class ParamikoVendor(SSHVendor):
272
272
    """Vendor that uses paramiko."""
273
273
 
351
351
    """Abstract base class for vendors that use pipes to a subprocess."""
352
352
 
353
353
    def _connect(self, argv):
354
 
        proc = subprocess.Popen(argv,
355
 
                                stdin=subprocess.PIPE,
356
 
                                stdout=subprocess.PIPE,
 
354
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
355
        # subprocess.  We prefer sockets to pipes because they support
 
356
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
357
        # whatever) chunks.
 
358
        try:
 
359
            my_sock, subproc_sock = socket.socketpair()
 
360
        except (AttributeError, socket.error):
 
361
            # This platform doesn't support socketpair(), so just use ordinary
 
362
            # pipes instead.
 
363
            stdin = stdout = subprocess.PIPE
 
364
            sock = None
 
365
        else:
 
366
            stdin = stdout = subproc_sock
 
367
            sock = my_sock
 
368
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
357
369
                                **os_specific_subprocess_params())
358
 
        return SSHSubprocess(proc)
 
370
        return SSHSubprocessConnection(proc, sock=sock)
359
371
 
360
372
    def connect_sftp(self, username, password, host, port):
361
373
        try:
391
403
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
392
404
                                  command=None):
393
405
        """Returns the argument list to run the subprocess with.
394
 
        
 
406
 
395
407
        Exactly one of 'subsystem' and 'command' must be specified.
396
408
        """
397
409
        raise NotImplementedError(self._get_vendor_specific_argv)
400
412
class OpenSSHSubprocessVendor(SubprocessVendor):
401
413
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
402
414
 
 
415
    executable_path = 'ssh'
 
416
 
403
417
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
404
418
                                  command=None):
405
 
        args = ['ssh',
 
419
        args = [self.executable_path,
406
420
                '-oForwardX11=no', '-oForwardAgent=no',
407
421
                '-oClearAllForwardings=yes', '-oProtocol=2',
408
422
                '-oNoHostAuthenticationForLocalhost=yes']
422
436
class SSHCorpSubprocessVendor(SubprocessVendor):
423
437
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
424
438
 
 
439
    executable_path = 'ssh'
 
440
 
425
441
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
426
442
                                  command=None):
427
 
        args = ['ssh', '-x']
 
443
        args = [self.executable_path, '-x']
428
444
        if port is not None:
429
445
            args.extend(['-p', str(port)])
430
446
        if username is not None:
435
451
            args.extend([host] + command)
436
452
        return args
437
453
 
438
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
454
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
439
455
 
440
456
 
441
457
class PLinkSubprocessVendor(SubprocessVendor):
442
458
    """SSH vendor that uses the 'plink' executable from Putty."""
443
459
 
 
460
    executable_path = 'plink'
 
461
 
444
462
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
445
463
                                  command=None):
446
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
464
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
447
465
        if port is not None:
448
466
            args.extend(['-P', str(port)])
449
467
        if username is not None:
458
476
 
459
477
 
460
478
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
479
    auth = config.AuthenticationConfig()
461
480
    # paramiko requires a username, but it might be none if nothing was
462
481
    # supplied.  If so, use the local username.
463
482
    if username is None:
464
 
        username = getpass.getuser()
465
 
 
 
483
        username = auth.get_user('ssh', host, port=port,
 
484
                                 default=getpass.getuser())
466
485
    if _use_ssh_agent:
467
486
        agent = paramiko.Agent()
468
487
        for key in agent.get_keys():
480
499
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
481
500
        return
482
501
 
 
502
    # If we have gotten this far, we are about to try for passwords, do an
 
503
    # auth_none check to see if it is even supported.
 
504
    supported_auth_types = []
 
505
    try:
 
506
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
507
        #    Authentication type (none) not permitted.
 
508
        # So we explicitly disable the logging level for this action
 
509
        old_level = paramiko_transport.logger.level
 
510
        paramiko_transport.logger.setLevel(logging.WARNING)
 
511
        try:
 
512
            paramiko_transport.auth_none(username)
 
513
        finally:
 
514
            paramiko_transport.logger.setLevel(old_level)
 
515
    except paramiko.BadAuthenticationType, e:
 
516
        # Supported methods are in the exception
 
517
        supported_auth_types = e.allowed_types
 
518
    except paramiko.SSHException, e:
 
519
        # Don't know what happened, but just ignore it
 
520
        pass
 
521
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
522
    # because Paramiko's auth_password method will automatically try
 
523
    # 'keyboard-interactive' auth (using the password as the response) if
 
524
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
525
    # OpenSSH servers require this.
 
526
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
527
    # requires something other than a single password, but we currently don't
 
528
    # support that.
 
529
    if ('password' not in supported_auth_types and
 
530
        'keyboard-interactive' not in supported_auth_types):
 
531
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
532
            '\n  %s@%s\nsupported auth types: %s'
 
533
            % (username, host, supported_auth_types))
 
534
 
483
535
    if password:
484
536
        try:
485
537
            paramiko_transport.auth_password(username, password)
488
540
            pass
489
541
 
490
542
    # give up and ask for a password
491
 
    auth = config.AuthenticationConfig()
492
543
    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)
 
544
    # get_password can still return None, which means we should not prompt
 
545
    if password is not None:
 
546
        try:
 
547
            paramiko_transport.auth_password(username, password)
 
548
        except paramiko.SSHException, e:
 
549
            raise errors.ConnectionError(
 
550
                'Unable to authenticate to SSH host as'
 
551
                '\n  %s@%s\n' % (username, host), e)
 
552
    else:
 
553
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
554
                                     '  %s@%s' % (username, host))
498
555
 
499
556
 
500
557
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
562
619
def os_specific_subprocess_params():
563
620
    """Get O/S specific subprocess parameters."""
564
621
    if sys.platform == 'win32':
565
 
        # setting the process group and closing fds is not supported on 
 
622
        # setting the process group and closing fds is not supported on
566
623
        # win32
567
624
        return {}
568
625
    else:
569
 
        # We close fds other than the pipes as the child process does not need 
 
626
        # We close fds other than the pipes as the child process does not need
570
627
        # them to be open.
571
628
        #
572
629
        # We also set the child process to ignore SIGINT.  Normally the signal
574
631
        # this causes it to be seen only by bzr and not by ssh.  Python will
575
632
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
576
633
        # to release locks or do other cleanup over ssh before the connection
577
 
        # goes away.  
 
634
        # goes away.
578
635
        # <https://launchpad.net/products/bzr/+bug/5987>
579
636
        #
580
637
        # Running it in a separate process group is not good because then it
581
638
        # can't get non-echoed input of a password or passphrase.
582
639
        # <https://launchpad.net/products/bzr/+bug/40508>
583
 
        return {'preexec_fn': _ignore_sigint,
 
640
        return {'preexec_fn': _ignore_signals,
584
641
                'close_fds': True,
585
642
                }
586
643
 
587
 
 
588
 
class SSHSubprocess(object):
589
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
590
 
 
591
 
    def __init__(self, proc):
 
644
import weakref
 
645
_subproc_weakrefs = set()
 
646
 
 
647
def _close_ssh_proc(proc):
 
648
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
649
        try:
 
650
            func()
 
651
        except OSError:
 
652
            pass
 
653
 
 
654
 
 
655
class SSHConnection(object):
 
656
    """Abstract base class for SSH connections."""
 
657
 
 
658
    def get_sock_or_pipes(self):
 
659
        """Returns a (kind, io_object) pair.
 
660
 
 
661
        If kind == 'socket', then io_object is a socket.
 
662
 
 
663
        If kind == 'pipes', then io_object is a pair of file-like objects
 
664
        (read_from, write_to).
 
665
        """
 
666
        raise NotImplementedError(self.get_sock_or_pipes)
 
667
 
 
668
    def close(self):
 
669
        raise NotImplementedError(self.close)
 
670
 
 
671
 
 
672
class SSHSubprocessConnection(SSHConnection):
 
673
    """A connection to an ssh subprocess via pipes or a socket.
 
674
 
 
675
    This class is also socket-like enough to be used with
 
676
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
677
    """
 
678
 
 
679
    def __init__(self, proc, sock=None):
 
680
        """Constructor.
 
681
 
 
682
        :param proc: a subprocess.Popen
 
683
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
684
            should bzrlib's half of that socketpair.  If not passed, proc's
 
685
            stdin/out is assumed to be ordinary pipes.
 
686
        """
592
687
        self.proc = proc
 
688
        self._sock = sock
 
689
        # Add a weakref to proc that will attempt to do the same as self.close
 
690
        # to avoid leaving processes lingering indefinitely.
 
691
        def terminate(ref):
 
692
            _subproc_weakrefs.remove(ref)
 
693
            _close_ssh_proc(proc)
 
694
        _subproc_weakrefs.add(weakref.ref(self, terminate))
593
695
 
594
696
    def send(self, data):
595
 
        return os.write(self.proc.stdin.fileno(), data)
 
697
        if self._sock is not None:
 
698
            return self._sock.send(data)
 
699
        else:
 
700
            return os.write(self.proc.stdin.fileno(), data)
596
701
 
597
702
    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)
 
703
        if self._sock is not None:
 
704
            return self._sock.recv(count)
 
705
        else:
 
706
            return os.read(self.proc.stdout.fileno(), count)
 
707
 
 
708
    def close(self):
 
709
        _close_ssh_proc(self.proc)
 
710
 
 
711
    def get_sock_or_pipes(self):
 
712
        if self._sock is not None:
 
713
            return 'socket', self._sock
 
714
        else:
 
715
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
716
 
 
717
 
 
718
class _ParamikoSSHConnection(SSHConnection):
 
719
    """An SSH connection via paramiko."""
 
720
 
 
721
    def __init__(self, channel):
 
722
        self.channel = channel
 
723
 
 
724
    def get_sock_or_pipes(self):
 
725
        return ('socket', self.channel)
 
726
 
 
727
    def close(self):
 
728
        return self.channel.close()
 
729
 
607
730