~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-10 15:46:03 UTC
  • mfrom: (4985.3.21 update)
  • mto: This revision was merged to the branch mainline in revision 5021.
  • Revision ID: v.ladeuil+lp@free.fr-20100210154603-k4no1gvfuqpzrw7p
Update performs two merges in a more logical order but stop on conflicts

Show diffs side-by-side

added added

removed removed

Lines of Context:
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.
180
189
 
181
190
    def get_name(self):
182
191
        return "bzr SocketAsChannelAdapter"
183
 
    
 
192
 
184
193
    def send(self, data):
185
194
        return self.__socket.send(data)
186
195
 
211
220
 
212
221
    def connect_sftp(self, username, password, host, port):
213
222
        """Make an SSH connection, and return an SFTPClient.
214
 
        
 
223
 
215
224
        :param username: an ascii string
216
225
        :param password: an ascii string
217
226
        :param host: a host name as an ascii string
226
235
 
227
236
    def connect_ssh(self, username, password, host, port, command):
228
237
        """Make an SSH connection.
229
 
        
 
238
 
230
239
        :returns: something with a `close` method, and a `get_filelike_channels`
231
240
            method that returns a pair of (read, write) filelike objects.
232
241
        """
391
400
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
392
401
                                  command=None):
393
402
        """Returns the argument list to run the subprocess with.
394
 
        
 
403
 
395
404
        Exactly one of 'subsystem' and 'command' must be specified.
396
405
        """
397
406
        raise NotImplementedError(self._get_vendor_specific_argv)
400
409
class OpenSSHSubprocessVendor(SubprocessVendor):
401
410
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
402
411
 
 
412
    executable_path = 'ssh'
 
413
 
403
414
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
404
415
                                  command=None):
405
 
        args = ['ssh',
 
416
        args = [self.executable_path,
406
417
                '-oForwardX11=no', '-oForwardAgent=no',
407
418
                '-oClearAllForwardings=yes', '-oProtocol=2',
408
419
                '-oNoHostAuthenticationForLocalhost=yes']
422
433
class SSHCorpSubprocessVendor(SubprocessVendor):
423
434
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
424
435
 
 
436
    executable_path = 'ssh'
 
437
 
425
438
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
426
439
                                  command=None):
427
 
        args = ['ssh', '-x']
 
440
        args = [self.executable_path, '-x']
428
441
        if port is not None:
429
442
            args.extend(['-p', str(port)])
430
443
        if username is not None:
435
448
            args.extend([host] + command)
436
449
        return args
437
450
 
438
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
451
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
439
452
 
440
453
 
441
454
class PLinkSubprocessVendor(SubprocessVendor):
442
455
    """SSH vendor that uses the 'plink' executable from Putty."""
443
456
 
 
457
    executable_path = 'plink'
 
458
 
444
459
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
445
460
                                  command=None):
446
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
461
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
447
462
        if port is not None:
448
463
            args.extend(['-P', str(port)])
449
464
        if username is not None:
458
473
 
459
474
 
460
475
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
476
    auth = config.AuthenticationConfig()
461
477
    # paramiko requires a username, but it might be none if nothing was
462
478
    # supplied.  If so, use the local username.
463
479
    if username is None:
464
 
        username = getpass.getuser()
465
 
 
 
480
        username = auth.get_user('ssh', host, port=port,
 
481
                                 default=getpass.getuser())
466
482
    if _use_ssh_agent:
467
483
        agent = paramiko.Agent()
468
484
        for key in agent.get_keys():
480
496
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
481
497
        return
482
498
 
 
499
    # If we have gotten this far, we are about to try for passwords, do an
 
500
    # auth_none check to see if it is even supported.
 
501
    supported_auth_types = []
 
502
    try:
 
503
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
504
        #    Authentication type (none) not permitted.
 
505
        # So we explicitly disable the logging level for this action
 
506
        old_level = paramiko_transport.logger.level
 
507
        paramiko_transport.logger.setLevel(logging.WARNING)
 
508
        try:
 
509
            paramiko_transport.auth_none(username)
 
510
        finally:
 
511
            paramiko_transport.logger.setLevel(old_level)
 
512
    except paramiko.BadAuthenticationType, e:
 
513
        # Supported methods are in the exception
 
514
        supported_auth_types = e.allowed_types
 
515
    except paramiko.SSHException, e:
 
516
        # Don't know what happened, but just ignore it
 
517
        pass
 
518
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
519
    # because Paramiko's auth_password method will automatically try
 
520
    # 'keyboard-interactive' auth (using the password as the response) if
 
521
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
522
    # OpenSSH servers require this.
 
523
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
524
    # requires something other than a single password, but we currently don't
 
525
    # support that.
 
526
    if ('password' not in supported_auth_types and
 
527
        'keyboard-interactive' not in supported_auth_types):
 
528
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
529
            '\n  %s@%s\nsupported auth types: %s'
 
530
            % (username, host, supported_auth_types))
 
531
 
483
532
    if password:
484
533
        try:
485
534
            paramiko_transport.auth_password(username, password)
488
537
            pass
489
538
 
490
539
    # give up and ask for a password
491
 
    auth = config.AuthenticationConfig()
492
540
    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)
 
541
    # get_password can still return None, which means we should not prompt
 
542
    if password is not None:
 
543
        try:
 
544
            paramiko_transport.auth_password(username, password)
 
545
        except paramiko.SSHException, e:
 
546
            raise errors.ConnectionError(
 
547
                'Unable to authenticate to SSH host as'
 
548
                '\n  %s@%s\n' % (username, host), e)
 
549
    else:
 
550
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
551
                                     '  %s@%s' % (username, host))
498
552
 
499
553
 
500
554
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
562
616
def os_specific_subprocess_params():
563
617
    """Get O/S specific subprocess parameters."""
564
618
    if sys.platform == 'win32':
565
 
        # setting the process group and closing fds is not supported on 
 
619
        # setting the process group and closing fds is not supported on
566
620
        # win32
567
621
        return {}
568
622
    else:
569
 
        # We close fds other than the pipes as the child process does not need 
 
623
        # We close fds other than the pipes as the child process does not need
570
624
        # them to be open.
571
625
        #
572
626
        # We also set the child process to ignore SIGINT.  Normally the signal
574
628
        # this causes it to be seen only by bzr and not by ssh.  Python will
575
629
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
576
630
        # to release locks or do other cleanup over ssh before the connection
577
 
        # goes away.  
 
631
        # goes away.
578
632
        # <https://launchpad.net/products/bzr/+bug/5987>
579
633
        #
580
634
        # Running it in a separate process group is not good because then it
584
638
                'close_fds': True,
585
639
                }
586
640
 
 
641
import weakref
 
642
_subproc_weakrefs = set()
 
643
 
 
644
def _close_ssh_proc(proc):
 
645
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
646
        try:
 
647
            func()
 
648
        except OSError:
 
649
            pass
 
650
 
587
651
 
588
652
class SSHSubprocess(object):
589
653
    """A socket-like object that talks to an ssh subprocess via pipes."""
590
654
 
591
655
    def __init__(self, proc):
592
656
        self.proc = proc
 
657
        # Add a weakref to proc that will attempt to do the same as self.close
 
658
        # to avoid leaving processes lingering indefinitely.
 
659
        def terminate(ref):
 
660
            _subproc_weakrefs.remove(ref)
 
661
            _close_ssh_proc(proc)
 
662
        _subproc_weakrefs.add(weakref.ref(self, terminate))
593
663
 
594
664
    def send(self, data):
595
665
        return os.write(self.proc.stdin.fileno(), data)
598
668
        return os.read(self.proc.stdout.fileno(), count)
599
669
 
600
670
    def close(self):
601
 
        self.proc.stdin.close()
602
 
        self.proc.stdout.close()
603
 
        self.proc.wait()
 
671
        _close_ssh_proc(self.proc)
604
672
 
605
673
    def get_filelike_channels(self):
606
674
        return (self.proc.stdout, self.proc.stdin)