~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2009-05-01 14:29:06 UTC
  • mto: This revision was merged to the branch mainline in revision 4321.
  • Revision ID: jelmer@samba.org-20090501142906-7zj8hcpp9igzuyi4
Add repository argument to 'repository' info hook, per Roberts review.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 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
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
20
import errno
21
 
import getpass
22
 
import logging
23
21
import os
24
22
import socket
25
23
import subprocess
94
92
            try:
95
93
                vendor = self._ssh_vendors[vendor_name]
96
94
            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
 
95
                raise errors.UnknownSSH(vendor_name)
101
96
            return vendor
102
97
        return None
103
98
 
113
108
            stdout = stderr = ''
114
109
        return stdout + stderr
115
110
 
116
 
    def _get_vendor_by_version_string(self, version, progname):
 
111
    def _get_vendor_by_version_string(self, version, args):
117
112
        """Return the vendor or None based on output from the subprocess.
118
113
 
119
114
        :param version: The output of 'ssh -V' like command.
126
121
        elif 'SSH Secure Shell' in version:
127
122
            trace.mutter('ssh implementation is SSH Corp.')
128
123
            vendor = SSHCorpSubprocessVendor()
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':
 
124
        elif 'plink' in version and args[0] == 'plink':
136
125
            # Checking if "plink" was the executed argument as Windows
137
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
 
126
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
138
127
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
139
128
            trace.mutter("ssh implementation is Putty's plink.")
140
129
            vendor = PLinkSubprocessVendor()
142
131
 
143
132
    def _get_vendor_by_inspection(self):
144
133
        """Return the vendor or None by checking for known SSH implementations."""
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])
 
134
        for args in (['ssh', '-V'], ['plink', '-V']):
 
135
            version = self._get_ssh_version_string(args)
 
136
            vendor = self._get_vendor_by_version_string(version, args)
 
137
            if vendor is not None:
 
138
                return vendor
 
139
        return None
153
140
 
154
141
    def get_vendor(self, environment=None):
155
142
        """Find out what version of SSH is on the system.
176
163
register_ssh_vendor = _ssh_vendor_manager.register_vendor
177
164
 
178
165
 
179
 
def _ignore_signals():
 
166
def _ignore_sigint():
180
167
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
181
168
    # doesn't handle it itself.
182
169
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
183
170
    import signal
184
171
    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)
188
172
 
189
173
 
190
174
class SocketAsChannelAdapter(object):
242
226
    def connect_ssh(self, username, password, host, port, command):
243
227
        """Make an SSH connection.
244
228
 
245
 
        :returns: an SSHConnection.
 
229
        :returns: something with a `close` method, and a `get_filelike_channels`
 
230
            method that returns a pair of (read, write) filelike objects.
246
231
        """
247
232
        raise NotImplementedError(self.connect_ssh)
248
233
 
271
256
register_ssh_vendor('loopback', LoopbackVendor())
272
257
 
273
258
 
 
259
class _ParamikoSSHConnection(object):
 
260
    def __init__(self, channel):
 
261
        self.channel = channel
 
262
 
 
263
    def get_filelike_channels(self):
 
264
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
265
 
 
266
    def close(self):
 
267
        return self.channel.close()
 
268
 
 
269
 
274
270
class ParamikoVendor(SSHVendor):
275
271
    """Vendor that uses paramiko."""
276
272
 
339
335
            self._raise_connection_error(host, port=port, orig_error=e,
340
336
                                         msg='Unable to invoke remote bzr')
341
337
 
342
 
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
343
338
if paramiko is not None:
344
339
    vendor = ParamikoVendor()
345
340
    register_ssh_vendor('paramiko', vendor)
346
341
    register_ssh_vendor('none', vendor)
347
342
    register_default_ssh_vendor(vendor)
348
 
    _ssh_connection_errors += (paramiko.SSHException,)
 
343
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
349
344
    del vendor
 
345
else:
 
346
    _sftp_connection_errors = (EOFError,)
350
347
 
351
348
 
352
349
class SubprocessVendor(SSHVendor):
353
350
    """Abstract base class for vendors that use pipes to a subprocess."""
354
351
 
355
352
    def _connect(self, argv):
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,
 
353
        proc = subprocess.Popen(argv,
 
354
                                stdin=subprocess.PIPE,
 
355
                                stdout=subprocess.PIPE,
371
356
                                **os_specific_subprocess_params())
372
 
        if subproc_sock is not None:
373
 
            subproc_sock.close()
374
 
        return SSHSubprocessConnection(proc, sock=my_sock)
 
357
        return SSHSubprocess(proc)
375
358
 
376
359
    def connect_sftp(self, username, password, host, port):
377
360
        try:
379
362
                                                  subsystem='sftp')
380
363
            sock = self._connect(argv)
381
364
            return SFTPClient(SocketAsChannelAdapter(sock))
382
 
        except _ssh_connection_errors, e:
 
365
        except _sftp_connection_errors, e:
 
366
            self._raise_connection_error(host, port=port, orig_error=e)
 
367
        except (OSError, IOError), e:
 
368
            # If the machine is fast enough, ssh can actually exit
 
369
            # before we try and send it the sftp request, which
 
370
            # raises a Broken Pipe
 
371
            if e.errno not in (errno.EPIPE,):
 
372
                raise
383
373
            self._raise_connection_error(host, port=port, orig_error=e)
384
374
 
385
375
    def connect_ssh(self, username, password, host, port, command):
387
377
            argv = self._get_vendor_specific_argv(username, host, port,
388
378
                                                  command=command)
389
379
            return self._connect(argv)
390
 
        except _ssh_connection_errors, e:
 
380
        except (EOFError), e:
 
381
            self._raise_connection_error(host, port=port, orig_error=e)
 
382
        except (OSError, IOError), e:
 
383
            # If the machine is fast enough, ssh can actually exit
 
384
            # before we try and send it the sftp request, which
 
385
            # raises a Broken Pipe
 
386
            if e.errno not in (errno.EPIPE,):
 
387
                raise
391
388
            self._raise_connection_error(host, port=port, orig_error=e)
392
389
 
393
390
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
402
399
class OpenSSHSubprocessVendor(SubprocessVendor):
403
400
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
404
401
 
405
 
    executable_path = 'ssh'
406
 
 
407
402
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
408
403
                                  command=None):
409
 
        args = [self.executable_path,
 
404
        args = ['ssh',
410
405
                '-oForwardX11=no', '-oForwardAgent=no',
411
 
                '-oClearAllForwardings=yes',
 
406
                '-oClearAllForwardings=yes', '-oProtocol=2',
412
407
                '-oNoHostAuthenticationForLocalhost=yes']
413
408
        if port is not None:
414
409
            args.extend(['-p', str(port)])
426
421
class SSHCorpSubprocessVendor(SubprocessVendor):
427
422
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
428
423
 
429
 
    executable_path = 'ssh'
430
 
 
431
424
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
432
425
                                  command=None):
433
 
        args = [self.executable_path, '-x']
 
426
        args = ['ssh', '-x']
434
427
        if port is not None:
435
428
            args.extend(['-p', str(port)])
436
429
        if username is not None:
441
434
            args.extend([host] + command)
442
435
        return args
443
436
 
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())
 
437
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
466
438
 
467
439
 
468
440
class PLinkSubprocessVendor(SubprocessVendor):
469
441
    """SSH vendor that uses the 'plink' executable from Putty."""
470
442
 
471
 
    executable_path = 'plink'
472
 
 
473
443
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
474
444
                                  command=None):
475
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
445
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
476
446
        if port is not None:
477
447
            args.extend(['-P', str(port)])
478
448
        if username is not None:
491
461
    # paramiko requires a username, but it might be none if nothing was
492
462
    # supplied.  If so, use the local username.
493
463
    if username is None:
494
 
        username = auth.get_user('ssh', host, port=port,
495
 
                                 default=getpass.getuser())
 
464
        username = auth.get_user('ssh', host, port=port)
 
465
 
496
466
    if _use_ssh_agent:
497
467
        agent = paramiko.Agent()
498
468
        for key in agent.get_keys():
510
480
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
511
481
        return
512
482
 
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
 
 
546
483
    if password:
547
484
        try:
548
485
            paramiko_transport.auth_password(username, password)
552
489
 
553
490
    # give up and ask for a password
554
491
    password = auth.get_password('ssh', host, username, port=port)
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))
 
492
    try:
 
493
        paramiko_transport.auth_password(username, password)
 
494
    except paramiko.SSHException, e:
 
495
        raise errors.ConnectionError(
 
496
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
566
497
 
567
498
 
568
499
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
573
504
        return True
574
505
    except paramiko.PasswordRequiredException:
575
506
        password = ui.ui_factory.get_password(
576
 
            prompt=u'SSH %(filename)s password',
577
 
            filename=filename.decode(osutils._fs_enc))
 
507
            prompt='SSH %(filename)s password', filename=filename)
578
508
        try:
579
509
            key = pkey_class.from_private_key_file(filename, password)
580
510
            paramiko_transport.auth_publickey(username, key)
649
579
        # Running it in a separate process group is not good because then it
650
580
        # can't get non-echoed input of a password or passphrase.
651
581
        # <https://launchpad.net/products/bzr/+bug/40508>
652
 
        return {'preexec_fn': _ignore_signals,
 
582
        return {'preexec_fn': _ignore_sigint,
653
583
                'close_fds': True,
654
584
                }
655
585
 
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
 
        """
 
586
 
 
587
class SSHSubprocess(object):
 
588
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
589
 
 
590
    def __init__(self, proc):
715
591
        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))
723
592
 
724
593
    def send(self, 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)
 
594
        return os.write(self.proc.stdin.fileno(), data)
729
595
 
730
596
    def recv(self, count):
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
 
 
 
597
        return os.read(self.proc.stdout.fileno(), count)
 
598
 
 
599
    def close(self):
 
600
        self.proc.stdin.close()
 
601
        self.proc.stdout.close()
 
602
        self.proc.wait()
 
603
 
 
604
    def get_filelike_channels(self):
 
605
        return (self.proc.stdout, self.proc.stdin)
758
606