~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Aaron Bentley
  • Date: 2008-03-11 14:29:08 UTC
  • mto: This revision was merged to the branch mainline in revision 3264.
  • Revision ID: aaron@aaronbentley.com-20080311142908-yyrvcpn2mldt0fnn
Update documentation to reflect conflict-handling difference

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
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
 
127
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
135
128
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
136
129
            trace.mutter("ssh implementation is Putty's plink.")
137
130
            vendor = PLinkSubprocessVendor()
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
 
 
186
 
 
187
 
class SocketAsChannelAdapter(object):
 
173
 
 
174
 
 
175
class LoopbackSFTP(object):
188
176
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
189
177
 
190
178
    def __init__(self, sock):
191
179
        self.__socket = sock
192
180
 
193
 
    def get_name(self):
194
 
        return "bzr SocketAsChannelAdapter"
195
 
 
196
181
    def send(self, data):
197
182
        return self.__socket.send(data)
198
183
 
199
184
    def recv(self, n):
200
 
        try:
201
 
            return self.__socket.recv(n)
202
 
        except socket.error, e:
203
 
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
204
 
                             errno.EBADF):
205
 
                # Connection has closed.  Paramiko expects an empty string in
206
 
                # this case, not an exception.
207
 
                return ''
208
 
            raise
 
185
        return self.__socket.recv(n)
209
186
 
210
187
    def recv_ready(self):
211
 
        # TODO: jam 20051215 this function is necessary to support the
212
 
        # pipelined() function. In reality, it probably should use
213
 
        # poll() or select() to actually return if there is data
214
 
        # available, otherwise we probably don't get any benefit
215
188
        return True
216
189
 
217
190
    def close(self):
223
196
 
224
197
    def connect_sftp(self, username, password, host, port):
225
198
        """Make an SSH connection, and return an SFTPClient.
226
 
 
 
199
        
227
200
        :param username: an ascii string
228
201
        :param password: an ascii string
229
202
        :param host: a host name as an ascii string
238
211
 
239
212
    def connect_ssh(self, username, password, host, port, command):
240
213
        """Make an SSH connection.
241
 
 
242
 
        :returns: an SSHConnection.
 
214
        
 
215
        :returns: something with a `close` method, and a `get_filelike_channels`
 
216
            method that returns a pair of (read, write) filelike objects.
243
217
        """
244
218
        raise NotImplementedError(self.connect_ssh)
245
219
 
263
237
            sock.connect((host, port))
264
238
        except socket.error, e:
265
239
            self._raise_connection_error(host, port=port, orig_error=e)
266
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
240
        return SFTPClient(LoopbackSFTP(sock))
267
241
 
268
242
register_ssh_vendor('loopback', LoopbackVendor())
269
243
 
270
244
 
 
245
class _ParamikoSSHConnection(object):
 
246
    def __init__(self, channel):
 
247
        self.channel = channel
 
248
 
 
249
    def get_filelike_channels(self):
 
250
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
251
 
 
252
    def close(self):
 
253
        return self.channel.close()
 
254
 
 
255
 
271
256
class ParamikoVendor(SSHVendor):
272
257
    """Vendor that uses paramiko."""
273
258
 
336
321
            self._raise_connection_error(host, port=port, orig_error=e,
337
322
                                         msg='Unable to invoke remote bzr')
338
323
 
339
 
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
340
324
if paramiko is not None:
341
325
    vendor = ParamikoVendor()
342
326
    register_ssh_vendor('paramiko', vendor)
343
327
    register_ssh_vendor('none', vendor)
344
328
    register_default_ssh_vendor(vendor)
345
 
    _ssh_connection_errors += (paramiko.SSHException,)
 
329
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
346
330
    del vendor
 
331
else:
 
332
    _sftp_connection_errors = (EOFError,)
347
333
 
348
334
 
349
335
class SubprocessVendor(SSHVendor):
350
336
    """Abstract base class for vendors that use pipes to a subprocess."""
351
337
 
352
338
    def _connect(self, argv):
353
 
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
354
 
        # subprocess.  We prefer sockets to pipes because they support
355
 
        # non-blocking short reads, allowing us to optimistically read 64k (or
356
 
        # whatever) chunks.
357
 
        try:
358
 
            my_sock, subproc_sock = socket.socketpair()
359
 
        except (AttributeError, socket.error):
360
 
            # This platform doesn't support socketpair(), so just use ordinary
361
 
            # pipes instead.
362
 
            stdin = stdout = subprocess.PIPE
363
 
            sock = None
364
 
        else:
365
 
            stdin = stdout = subproc_sock
366
 
            sock = my_sock
367
 
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
339
        proc = subprocess.Popen(argv,
 
340
                                stdin=subprocess.PIPE,
 
341
                                stdout=subprocess.PIPE,
368
342
                                **os_specific_subprocess_params())
369
 
        return SSHSubprocessConnection(proc, sock=sock)
 
343
        return SSHSubprocess(proc)
370
344
 
371
345
    def connect_sftp(self, username, password, host, port):
372
346
        try:
373
347
            argv = self._get_vendor_specific_argv(username, host, port,
374
348
                                                  subsystem='sftp')
375
349
            sock = self._connect(argv)
376
 
            return SFTPClient(SocketAsChannelAdapter(sock))
377
 
        except _ssh_connection_errors, e:
 
350
            return SFTPClient(sock)
 
351
        except _sftp_connection_errors, e:
 
352
            self._raise_connection_error(host, port=port, orig_error=e)
 
353
        except (OSError, IOError), e:
 
354
            # If the machine is fast enough, ssh can actually exit
 
355
            # before we try and send it the sftp request, which
 
356
            # raises a Broken Pipe
 
357
            if e.errno not in (errno.EPIPE,):
 
358
                raise
378
359
            self._raise_connection_error(host, port=port, orig_error=e)
379
360
 
380
361
    def connect_ssh(self, username, password, host, port, command):
382
363
            argv = self._get_vendor_specific_argv(username, host, port,
383
364
                                                  command=command)
384
365
            return self._connect(argv)
385
 
        except _ssh_connection_errors, e:
 
366
        except (EOFError), 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
386
374
            self._raise_connection_error(host, port=port, orig_error=e)
387
375
 
388
376
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
389
377
                                  command=None):
390
378
        """Returns the argument list to run the subprocess with.
391
 
 
 
379
        
392
380
        Exactly one of 'subsystem' and 'command' must be specified.
393
381
        """
394
382
        raise NotImplementedError(self._get_vendor_specific_argv)
397
385
class OpenSSHSubprocessVendor(SubprocessVendor):
398
386
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
399
387
 
400
 
    executable_path = 'ssh'
401
 
 
402
388
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
403
389
                                  command=None):
404
 
        args = [self.executable_path,
 
390
        assert subsystem is not None or command is not None, (
 
391
            'Must specify a command or subsystem')
 
392
        if subsystem is not None:
 
393
            assert command is None, (
 
394
                'subsystem and command are mutually exclusive')
 
395
        args = ['ssh',
405
396
                '-oForwardX11=no', '-oForwardAgent=no',
406
397
                '-oClearAllForwardings=yes', '-oProtocol=2',
407
398
                '-oNoHostAuthenticationForLocalhost=yes']
421
412
class SSHCorpSubprocessVendor(SubprocessVendor):
422
413
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
423
414
 
424
 
    executable_path = 'ssh'
425
 
 
426
415
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
427
416
                                  command=None):
428
 
        args = [self.executable_path, '-x']
 
417
        assert subsystem is not None or command is not None, (
 
418
            'Must specify a command or subsystem')
 
419
        if subsystem is not None:
 
420
            assert command is None, (
 
421
                'subsystem and command are mutually exclusive')
 
422
        args = ['ssh', '-x']
429
423
        if port is not None:
430
424
            args.extend(['-p', str(port)])
431
425
        if username is not None:
436
430
            args.extend([host] + command)
437
431
        return args
438
432
 
439
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
433
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
440
434
 
441
435
 
442
436
class PLinkSubprocessVendor(SubprocessVendor):
443
437
    """SSH vendor that uses the 'plink' executable from Putty."""
444
438
 
445
 
    executable_path = 'plink'
446
 
 
447
439
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
448
440
                                  command=None):
449
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
441
        assert subsystem is not None or command is not None, (
 
442
            'Must specify a command or subsystem')
 
443
        if subsystem is not None:
 
444
            assert command is None, (
 
445
                'subsystem and command are mutually exclusive')
 
446
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
450
447
        if port is not None:
451
448
            args.extend(['-P', str(port)])
452
449
        if username is not None:
461
458
 
462
459
 
463
460
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
461
    # paramiko requires a username, but it might be none if nothing was supplied
 
462
    # use the local username, just in case.
 
463
    # We don't override username, because if we aren't using paramiko,
 
464
    # the username might be specified in ~/.ssh/config and we don't want to
 
465
    # force it to something else
 
466
    # Also, it would mess up the self.relpath() functionality
464
467
    auth = config.AuthenticationConfig()
465
 
    # paramiko requires a username, but it might be none if nothing was
466
 
    # supplied.  If so, use the local username.
467
468
    if username is None:
468
 
        username = auth.get_user('ssh', host, port=port,
469
 
                                 default=getpass.getuser())
 
469
        username = auth.get_user('ssh', host, port=port)
 
470
        if username is None:
 
471
            # Default to local user
 
472
            username = getpass.getuser()
 
473
 
470
474
    if _use_ssh_agent:
471
475
        agent = paramiko.Agent()
472
476
        for key in agent.get_keys():
484
488
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
485
489
        return
486
490
 
487
 
    # If we have gotten this far, we are about to try for passwords, do an
488
 
    # auth_none check to see if it is even supported.
489
 
    supported_auth_types = []
490
 
    try:
491
 
        # Note that with paramiko <1.7.5 this logs an INFO message:
492
 
        #    Authentication type (none) not permitted.
493
 
        # So we explicitly disable the logging level for this action
494
 
        old_level = paramiko_transport.logger.level
495
 
        paramiko_transport.logger.setLevel(logging.WARNING)
496
 
        try:
497
 
            paramiko_transport.auth_none(username)
498
 
        finally:
499
 
            paramiko_transport.logger.setLevel(old_level)
500
 
    except paramiko.BadAuthenticationType, e:
501
 
        # Supported methods are in the exception
502
 
        supported_auth_types = e.allowed_types
503
 
    except paramiko.SSHException, e:
504
 
        # Don't know what happened, but just ignore it
505
 
        pass
506
 
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
507
 
    # because Paramiko's auth_password method will automatically try
508
 
    # 'keyboard-interactive' auth (using the password as the response) if
509
 
    # 'password' auth is not available.  Apparently some Debian and Gentoo
510
 
    # OpenSSH servers require this.
511
 
    # XXX: It's possible for a server to require keyboard-interactive auth that
512
 
    # requires something other than a single password, but we currently don't
513
 
    # support that.
514
 
    if ('password' not in supported_auth_types and
515
 
        'keyboard-interactive' not in supported_auth_types):
516
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
517
 
            '\n  %s@%s\nsupported auth types: %s'
518
 
            % (username, host, supported_auth_types))
519
 
 
520
491
    if password:
521
492
        try:
522
493
            paramiko_transport.auth_password(username, password)
526
497
 
527
498
    # give up and ask for a password
528
499
    password = auth.get_password('ssh', host, username, port=port)
529
 
    # get_password can still return None, which means we should not prompt
530
 
    if password is not None:
531
 
        try:
532
 
            paramiko_transport.auth_password(username, password)
533
 
        except paramiko.SSHException, e:
534
 
            raise errors.ConnectionError(
535
 
                'Unable to authenticate to SSH host as'
536
 
                '\n  %s@%s\n' % (username, host), e)
537
 
    else:
538
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
539
 
                                     '  %s@%s' % (username, host))
 
500
    try:
 
501
        paramiko_transport.auth_password(username, password)
 
502
    except paramiko.SSHException, e:
 
503
        raise errors.ConnectionError(
 
504
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
540
505
 
541
506
 
542
507
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
604
569
def os_specific_subprocess_params():
605
570
    """Get O/S specific subprocess parameters."""
606
571
    if sys.platform == 'win32':
607
 
        # setting the process group and closing fds is not supported on
 
572
        # setting the process group and closing fds is not supported on 
608
573
        # win32
609
574
        return {}
610
575
    else:
611
 
        # We close fds other than the pipes as the child process does not need
 
576
        # We close fds other than the pipes as the child process does not need 
612
577
        # them to be open.
613
578
        #
614
579
        # We also set the child process to ignore SIGINT.  Normally the signal
616
581
        # this causes it to be seen only by bzr and not by ssh.  Python will
617
582
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
618
583
        # to release locks or do other cleanup over ssh before the connection
619
 
        # goes away.
 
584
        # goes away.  
620
585
        # <https://launchpad.net/products/bzr/+bug/5987>
621
586
        #
622
587
        # Running it in a separate process group is not good because then it
623
588
        # can't get non-echoed input of a password or passphrase.
624
589
        # <https://launchpad.net/products/bzr/+bug/40508>
625
 
        return {'preexec_fn': _ignore_signals,
 
590
        return {'preexec_fn': _ignore_sigint,
626
591
                'close_fds': True,
627
592
                }
628
593
 
629
 
import weakref
630
 
_subproc_weakrefs = set()
631
 
 
632
 
def _close_ssh_proc(proc):
633
 
    """Carefully close stdin/stdout and reap the SSH process.
634
 
 
635
 
    If the pipes are already closed and/or the process has already been
636
 
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
637
 
    to clean up (whether or not a clean up was already tried).
638
 
    """
639
 
    dotted_names = ['stdin.close', 'stdout.close', 'wait']
640
 
    for dotted_name in dotted_names:
641
 
        attrs = dotted_name.split('.')
642
 
        try:
643
 
            obj = proc
644
 
            for attr in attrs:
645
 
                obj = getattr(obj, attr)
646
 
        except AttributeError:
647
 
            # It's ok for proc.stdin or proc.stdout to be None.
648
 
            continue
649
 
        try:
650
 
            obj()
651
 
        except OSError:
652
 
            # It's ok for the pipe to already be closed, or the process to
653
 
            # already be finished.
654
 
            continue
655
 
 
656
 
 
657
 
class SSHConnection(object):
658
 
    """Abstract base class for SSH connections."""
659
 
 
660
 
    def get_sock_or_pipes(self):
661
 
        """Returns a (kind, io_object) pair.
662
 
 
663
 
        If kind == 'socket', then io_object is a socket.
664
 
 
665
 
        If kind == 'pipes', then io_object is a pair of file-like objects
666
 
        (read_from, write_to).
667
 
        """
668
 
        raise NotImplementedError(self.get_sock_or_pipes)
669
 
 
670
 
    def close(self):
671
 
        raise NotImplementedError(self.close)
672
 
 
673
 
 
674
 
class SSHSubprocessConnection(SSHConnection):
675
 
    """A connection to an ssh subprocess via pipes or a socket.
676
 
 
677
 
    This class is also socket-like enough to be used with
678
 
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
679
 
    """
680
 
 
681
 
    def __init__(self, proc, sock=None):
682
 
        """Constructor.
683
 
 
684
 
        :param proc: a subprocess.Popen
685
 
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
686
 
            should bzrlib's half of that socketpair.  If not passed, proc's
687
 
            stdin/out is assumed to be ordinary pipes.
688
 
        """
 
594
 
 
595
class SSHSubprocess(object):
 
596
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
597
 
 
598
    def __init__(self, proc):
689
599
        self.proc = proc
690
 
        self._sock = sock
691
 
        # Add a weakref to proc that will attempt to do the same as self.close
692
 
        # to avoid leaving processes lingering indefinitely.
693
 
        def terminate(ref):
694
 
            _subproc_weakrefs.remove(ref)
695
 
            _close_ssh_proc(proc)
696
 
        _subproc_weakrefs.add(weakref.ref(self, terminate))
697
600
 
698
601
    def send(self, data):
699
 
        if self._sock is not None:
700
 
            return self._sock.send(data)
701
 
        else:
702
 
            return os.write(self.proc.stdin.fileno(), data)
 
602
        return os.write(self.proc.stdin.fileno(), data)
 
603
 
 
604
    def recv_ready(self):
 
605
        # TODO: jam 20051215 this function is necessary to support the
 
606
        # pipelined() function. In reality, it probably should use
 
607
        # poll() or select() to actually return if there is data
 
608
        # available, otherwise we probably don't get any benefit
 
609
        return True
703
610
 
704
611
    def recv(self, count):
705
 
        if self._sock is not None:
706
 
            return self._sock.recv(count)
707
 
        else:
708
 
            return os.read(self.proc.stdout.fileno(), count)
709
 
 
710
 
    def close(self):
711
 
        _close_ssh_proc(self.proc)
712
 
 
713
 
    def get_sock_or_pipes(self):
714
 
        if self._sock is not None:
715
 
            return 'socket', self._sock
716
 
        else:
717
 
            return 'pipes', (self.proc.stdout, self.proc.stdin)
718
 
 
719
 
 
720
 
class _ParamikoSSHConnection(SSHConnection):
721
 
    """An SSH connection via paramiko."""
722
 
 
723
 
    def __init__(self, channel):
724
 
        self.channel = channel
725
 
 
726
 
    def get_sock_or_pipes(self):
727
 
        return ('socket', self.channel)
728
 
 
729
 
    def close(self):
730
 
        return self.channel.close()
731
 
 
 
612
        return os.read(self.proc.stdout.fileno(), count)
 
613
 
 
614
    def close(self):
 
615
        self.proc.stdin.close()
 
616
        self.proc.stdout.close()
 
617
        self.proc.wait()
 
618
 
 
619
    def get_filelike_channels(self):
 
620
        return (self.proc.stdout, self.proc.stdin)
732
621