~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-03-17 07:05:37 UTC
  • mfrom: (4152.1.2 branch.stacked.streams)
  • Revision ID: pqm@pqm.ubuntu.com-20090317070537-zaud24vjs2szna87
(robertc) Add client-side streaming from stacked branches (over
        bzr:// protocols) when the sort order is compatible with doing
        that. (Robert Collins, Andrew Bennetts)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  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
23
22
import os
24
23
import socket
25
24
import subprocess
94
93
            try:
95
94
                vendor = self._ssh_vendors[vendor_name]
96
95
            except KeyError:
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
 
96
                raise errors.UnknownSSH(vendor_name)
101
97
            return vendor
102
98
        return None
103
99
 
113
109
            stdout = stderr = ''
114
110
        return stdout + stderr
115
111
 
116
 
    def _get_vendor_by_version_string(self, version, progname):
 
112
    def _get_vendor_by_version_string(self, version, args):
117
113
        """Return the vendor or None based on output from the subprocess.
118
114
 
119
115
        :param version: The output of 'ssh -V' like command.
126
122
        elif 'SSH Secure Shell' in version:
127
123
            trace.mutter('ssh implementation is SSH Corp.')
128
124
            vendor = SSHCorpSubprocessVendor()
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':
 
125
        elif 'plink' in version and args[0] == 'plink':
133
126
            # Checking if "plink" was the executed argument as Windows
134
127
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
135
128
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
139
132
 
140
133
    def _get_vendor_by_inspection(self):
141
134
        """Return the vendor or None by checking for known SSH implementations."""
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])
 
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
150
141
 
151
142
    def get_vendor(self, environment=None):
152
143
        """Find out what version of SSH is on the system.
173
164
register_ssh_vendor = _ssh_vendor_manager.register_vendor
174
165
 
175
166
 
176
 
def _ignore_signals():
 
167
def _ignore_sigint():
177
168
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
178
169
    # doesn't handle it itself.
179
170
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
180
171
    import signal
181
172
    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)
185
173
 
186
174
 
187
175
class SocketAsChannelAdapter(object):
239
227
    def connect_ssh(self, username, password, host, port, command):
240
228
        """Make an SSH connection.
241
229
 
242
 
        :returns: an SSHConnection.
 
230
        :returns: something with a `close` method, and a `get_filelike_channels`
 
231
            method that returns a pair of (read, write) filelike objects.
243
232
        """
244
233
        raise NotImplementedError(self.connect_ssh)
245
234
 
268
257
register_ssh_vendor('loopback', LoopbackVendor())
269
258
 
270
259
 
 
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
 
        # 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,
 
354
        proc = subprocess.Popen(argv,
 
355
                                stdin=subprocess.PIPE,
 
356
                                stdout=subprocess.PIPE,
369
357
                                **os_specific_subprocess_params())
370
 
        return SSHSubprocessConnection(proc, sock=sock)
 
358
        return SSHSubprocess(proc)
371
359
 
372
360
    def connect_sftp(self, username, password, host, port):
373
361
        try:
412
400
class OpenSSHSubprocessVendor(SubprocessVendor):
413
401
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
414
402
 
415
 
    executable_path = 'ssh'
416
 
 
417
403
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
404
                                  command=None):
419
 
        args = [self.executable_path,
 
405
        args = ['ssh',
420
406
                '-oForwardX11=no', '-oForwardAgent=no',
421
407
                '-oClearAllForwardings=yes', '-oProtocol=2',
422
408
                '-oNoHostAuthenticationForLocalhost=yes']
436
422
class SSHCorpSubprocessVendor(SubprocessVendor):
437
423
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
438
424
 
439
 
    executable_path = 'ssh'
440
 
 
441
425
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
442
426
                                  command=None):
443
 
        args = [self.executable_path, '-x']
 
427
        args = ['ssh', '-x']
444
428
        if port is not None:
445
429
            args.extend(['-p', str(port)])
446
430
        if username is not None:
451
435
            args.extend([host] + command)
452
436
        return args
453
437
 
454
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
438
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
455
439
 
456
440
 
457
441
class PLinkSubprocessVendor(SubprocessVendor):
458
442
    """SSH vendor that uses the 'plink' executable from Putty."""
459
443
 
460
 
    executable_path = 'plink'
461
 
 
462
444
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
445
                                  command=None):
464
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
446
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
465
447
        if port is not None:
466
448
            args.extend(['-P', str(port)])
467
449
        if username is not None:
476
458
 
477
459
 
478
460
def _paramiko_auth(username, password, host, port, paramiko_transport):
479
 
    auth = config.AuthenticationConfig()
480
461
    # paramiko requires a username, but it might be none if nothing was
481
462
    # supplied.  If so, use the local username.
482
463
    if username is None:
483
 
        username = auth.get_user('ssh', host, port=port,
484
 
                                 default=getpass.getuser())
 
464
        username = getpass.getuser()
 
465
 
485
466
    if _use_ssh_agent:
486
467
        agent = paramiko.Agent()
487
468
        for key in agent.get_keys():
499
480
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
500
481
        return
501
482
 
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
 
 
535
483
    if password:
536
484
        try:
537
485
            paramiko_transport.auth_password(username, password)
540
488
            pass
541
489
 
542
490
    # give up and ask for a password
 
491
    auth = config.AuthenticationConfig()
543
492
    password = auth.get_password('ssh', host, username, port=port)
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))
 
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
498
 
556
499
 
557
500
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
637
580
        # Running it in a separate process group is not good because then it
638
581
        # can't get non-echoed input of a password or passphrase.
639
582
        # <https://launchpad.net/products/bzr/+bug/40508>
640
 
        return {'preexec_fn': _ignore_signals,
 
583
        return {'preexec_fn': _ignore_sigint,
641
584
                'close_fds': True,
642
585
                }
643
586
 
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
 
        """
 
587
 
 
588
class SSHSubprocess(object):
 
589
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
590
 
 
591
    def __init__(self, proc):
687
592
        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))
695
593
 
696
594
    def send(self, 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)
 
595
        return os.write(self.proc.stdin.fileno(), data)
701
596
 
702
597
    def recv(self, count):
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
 
 
 
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)
730
607