~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Ian Clatworthy
  • Date: 2007-11-28 00:07:56 UTC
  • mto: (3054.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 3055.
  • Revision ID: ian.clatworthy@internode.on.net-20071128000756-kqm8iqmc9281roii
more cleanups and creation of tutorials directory

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
        # detection of plink vendor is disabled because of bug #107593
 
136
        # https://bugs.launchpad.net/bzr/+bug/107593
 
137
        # who want plink should explicitly enable it with BZR_SSH environment
 
138
        # variable.
 
139
        #~for args in (['ssh', '-V'], ['plink', '-V']):
 
140
        for args in (['ssh', '-V'],):
 
141
            version = self._get_ssh_version_string(args)
 
142
            vendor = self._get_vendor_by_version_string(version, args)
 
143
            if vendor is not None:
 
144
                return vendor
 
145
        return None
150
146
 
151
147
    def get_vendor(self, environment=None):
152
148
        """Find out what version of SSH is on the system.
173
169
register_ssh_vendor = _ssh_vendor_manager.register_vendor
174
170
 
175
171
 
176
 
def _ignore_signals():
 
172
def _ignore_sigint():
177
173
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
178
174
    # doesn't handle it itself.
179
175
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
180
176
    import signal
181
177
    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):
 
178
 
 
179
 
 
180
class LoopbackSFTP(object):
188
181
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
189
182
 
190
183
    def __init__(self, sock):
191
184
        self.__socket = sock
192
185
 
193
 
    def get_name(self):
194
 
        return "bzr SocketAsChannelAdapter"
195
 
 
196
186
    def send(self, data):
197
187
        return self.__socket.send(data)
198
188
 
199
189
    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
 
190
        return self.__socket.recv(n)
209
191
 
210
192
    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
193
        return True
216
194
 
217
195
    def close(self):
223
201
 
224
202
    def connect_sftp(self, username, password, host, port):
225
203
        """Make an SSH connection, and return an SFTPClient.
226
 
 
 
204
        
227
205
        :param username: an ascii string
228
206
        :param password: an ascii string
229
207
        :param host: a host name as an ascii string
238
216
 
239
217
    def connect_ssh(self, username, password, host, port, command):
240
218
        """Make an SSH connection.
241
 
 
 
219
        
242
220
        :returns: something with a `close` method, and a `get_filelike_channels`
243
221
            method that returns a pair of (read, write) filelike objects.
244
222
        """
264
242
            sock.connect((host, port))
265
243
        except socket.error, e:
266
244
            self._raise_connection_error(host, port=port, orig_error=e)
267
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
245
        return SFTPClient(LoopbackSFTP(sock))
268
246
 
269
247
register_ssh_vendor('loopback', LoopbackVendor())
270
248
 
353
331
    register_ssh_vendor('paramiko', vendor)
354
332
    register_ssh_vendor('none', vendor)
355
333
    register_default_ssh_vendor(vendor)
356
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
357
334
    del vendor
358
 
else:
359
 
    _sftp_connection_errors = (EOFError,)
360
335
 
361
336
 
362
337
class SubprocessVendor(SSHVendor):
374
349
            argv = self._get_vendor_specific_argv(username, host, port,
375
350
                                                  subsystem='sftp')
376
351
            sock = self._connect(argv)
377
 
            return SFTPClient(SocketAsChannelAdapter(sock))
378
 
        except _sftp_connection_errors, e:
 
352
            return SFTPClient(sock)
 
353
        except (EOFError, paramiko.SSHException), e:
379
354
            self._raise_connection_error(host, port=port, orig_error=e)
380
355
        except (OSError, IOError), e:
381
356
            # If the machine is fast enough, ssh can actually exit
403
378
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
404
379
                                  command=None):
405
380
        """Returns the argument list to run the subprocess with.
406
 
 
 
381
        
407
382
        Exactly one of 'subsystem' and 'command' must be specified.
408
383
        """
409
384
        raise NotImplementedError(self._get_vendor_specific_argv)
412
387
class OpenSSHSubprocessVendor(SubprocessVendor):
413
388
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
414
389
 
415
 
    executable_path = 'ssh'
416
 
 
417
390
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
391
                                  command=None):
419
 
        args = [self.executable_path,
 
392
        assert subsystem is not None or command is not None, (
 
393
            'Must specify a command or subsystem')
 
394
        if subsystem is not None:
 
395
            assert command is None, (
 
396
                'subsystem and command are mutually exclusive')
 
397
        args = ['ssh',
420
398
                '-oForwardX11=no', '-oForwardAgent=no',
421
399
                '-oClearAllForwardings=yes', '-oProtocol=2',
422
400
                '-oNoHostAuthenticationForLocalhost=yes']
436
414
class SSHCorpSubprocessVendor(SubprocessVendor):
437
415
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
438
416
 
439
 
    executable_path = 'ssh'
440
 
 
441
417
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
442
418
                                  command=None):
443
 
        args = [self.executable_path, '-x']
 
419
        assert subsystem is not None or command is not None, (
 
420
            'Must specify a command or subsystem')
 
421
        if subsystem is not None:
 
422
            assert command is None, (
 
423
                'subsystem and command are mutually exclusive')
 
424
        args = ['ssh', '-x']
444
425
        if port is not None:
445
426
            args.extend(['-p', str(port)])
446
427
        if username is not None:
451
432
            args.extend([host] + command)
452
433
        return args
453
434
 
454
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
435
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
455
436
 
456
437
 
457
438
class PLinkSubprocessVendor(SubprocessVendor):
458
439
    """SSH vendor that uses the 'plink' executable from Putty."""
459
440
 
460
 
    executable_path = 'plink'
461
 
 
462
441
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
442
                                  command=None):
464
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
443
        assert subsystem is not None or command is not None, (
 
444
            'Must specify a command or subsystem')
 
445
        if subsystem is not None:
 
446
            assert command is None, (
 
447
                'subsystem and command are mutually exclusive')
 
448
        args = ['plink', '-x', '-a', '-ssh', '-2']
465
449
        if port is not None:
466
450
            args.extend(['-P', str(port)])
467
451
        if username is not None:
476
460
 
477
461
 
478
462
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
463
    # paramiko requires a username, but it might be none if nothing was supplied
 
464
    # use the local username, just in case.
 
465
    # We don't override username, because if we aren't using paramiko,
 
466
    # the username might be specified in ~/.ssh/config and we don't want to
 
467
    # force it to something else
 
468
    # Also, it would mess up the self.relpath() functionality
479
469
    auth = config.AuthenticationConfig()
480
 
    # paramiko requires a username, but it might be none if nothing was
481
 
    # supplied.  If so, use the local username.
482
470
    if username is None:
483
 
        username = auth.get_user('ssh', host, port=port,
484
 
                                 default=getpass.getuser())
 
471
        username = auth.get_user('ssh', host, port=port)
 
472
        if username is None:
 
473
            # Default to local user
 
474
            username = getpass.getuser()
 
475
 
485
476
    if _use_ssh_agent:
486
477
        agent = paramiko.Agent()
487
478
        for key in agent.get_keys():
499
490
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
500
491
        return
501
492
 
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
493
    if password:
536
494
        try:
537
495
            paramiko_transport.auth_password(username, password)
541
499
 
542
500
    # give up and ask for a password
543
501
    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))
 
502
    try:
 
503
        paramiko_transport.auth_password(username, password)
 
504
    except paramiko.SSHException, e:
 
505
        raise errors.ConnectionError(
 
506
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
555
507
 
556
508
 
557
509
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
619
571
def os_specific_subprocess_params():
620
572
    """Get O/S specific subprocess parameters."""
621
573
    if sys.platform == 'win32':
622
 
        # setting the process group and closing fds is not supported on
 
574
        # setting the process group and closing fds is not supported on 
623
575
        # win32
624
576
        return {}
625
577
    else:
626
 
        # We close fds other than the pipes as the child process does not need
 
578
        # We close fds other than the pipes as the child process does not need 
627
579
        # them to be open.
628
580
        #
629
581
        # We also set the child process to ignore SIGINT.  Normally the signal
631
583
        # this causes it to be seen only by bzr and not by ssh.  Python will
632
584
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
633
585
        # to release locks or do other cleanup over ssh before the connection
634
 
        # goes away.
 
586
        # goes away.  
635
587
        # <https://launchpad.net/products/bzr/+bug/5987>
636
588
        #
637
589
        # Running it in a separate process group is not good because then it
638
590
        # can't get non-echoed input of a password or passphrase.
639
591
        # <https://launchpad.net/products/bzr/+bug/40508>
640
 
        return {'preexec_fn': _ignore_signals,
 
592
        return {'preexec_fn': _ignore_sigint,
641
593
                'close_fds': True,
642
594
                }
643
595
 
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
596
 
655
597
class SSHSubprocess(object):
656
598
    """A socket-like object that talks to an ssh subprocess via pipes."""
657
599
 
658
600
    def __init__(self, proc):
659
601
        self.proc = proc
660
 
        # Add a weakref to proc that will attempt to do the same as self.close
661
 
        # to avoid leaving processes lingering indefinitely.
662
 
        def terminate(ref):
663
 
            _subproc_weakrefs.remove(ref)
664
 
            _close_ssh_proc(proc)
665
 
        _subproc_weakrefs.add(weakref.ref(self, terminate))
666
602
 
667
603
    def send(self, data):
668
604
        return os.write(self.proc.stdin.fileno(), data)
669
605
 
 
606
    def recv_ready(self):
 
607
        # TODO: jam 20051215 this function is necessary to support the
 
608
        # pipelined() function. In reality, it probably should use
 
609
        # poll() or select() to actually return if there is data
 
610
        # available, otherwise we probably don't get any benefit
 
611
        return True
 
612
 
670
613
    def recv(self, count):
671
614
        return os.read(self.proc.stdout.fileno(), count)
672
615
 
673
616
    def close(self):
674
 
        _close_ssh_proc(self.proc)
 
617
        self.proc.stdin.close()
 
618
        self.proc.stdout.close()
 
619
        self.proc.wait()
675
620
 
676
621
    def get_filelike_channels(self):
677
622
        return (self.proc.stdout, self.proc.stdin)