~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Alexander Belchenko
  • Date: 2007-08-10 09:04:38 UTC
  • mto: This revision was merged to the branch mainline in revision 2694.
  • Revision ID: bialix@ukr.net-20070810090438-0835xdz0rl8825qv
fixes after Ian's review

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
import subprocess
25
25
import sys
26
26
 
27
 
from bzrlib import (
28
 
    config,
29
 
    errors,
30
 
    osutils,
31
 
    trace,
32
 
    ui,
33
 
    )
 
27
from bzrlib.config import config_dir, ensure_config_dir_exists
 
28
from bzrlib.errors import (ConnectionError,
 
29
                           ParamikoNotPresent,
 
30
                           SocketConnectionError,
 
31
                           SSHVendorNotFound,
 
32
                           TransportError,
 
33
                           UnknownSSH,
 
34
                           )
 
35
 
 
36
from bzrlib.osutils import pathjoin
 
37
from bzrlib.trace import mutter, warning
 
38
import bzrlib.ui
34
39
 
35
40
try:
36
41
    import paramiko
93
98
            try:
94
99
                vendor = self._ssh_vendors[vendor_name]
95
100
            except KeyError:
96
 
                raise errors.UnknownSSH(vendor_name)
 
101
                raise UnknownSSH(vendor_name)
97
102
            return vendor
98
103
        return None
99
104
 
109
114
            stdout = stderr = ''
110
115
        return stdout + stderr
111
116
 
112
 
    def _get_vendor_by_version_string(self, version, args):
 
117
    def _get_vendor_by_version_string(self, version):
113
118
        """Return the vendor or None based on output from the subprocess.
114
119
 
115
120
        :param version: The output of 'ssh -V' like command.
116
 
        :param args: Command line that was run.
117
121
        """
118
122
        vendor = None
119
123
        if 'OpenSSH' in version:
120
 
            trace.mutter('ssh implementation is OpenSSH')
 
124
            mutter('ssh implementation is OpenSSH')
121
125
            vendor = OpenSSHSubprocessVendor()
122
126
        elif 'SSH Secure Shell' in version:
123
 
            trace.mutter('ssh implementation is SSH Corp.')
 
127
            mutter('ssh implementation is SSH Corp.')
124
128
            vendor = SSHCorpSubprocessVendor()
125
 
        elif 'plink' in version and args[0] == 'plink':
126
 
            # Checking if "plink" was the executed argument as Windows
127
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
128
 
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
129
 
            trace.mutter("ssh implementation is Putty's plink.")
 
129
        elif 'plink' in version:
 
130
            mutter("ssh implementation is Putty's plink.")
130
131
            vendor = PLinkSubprocessVendor()
131
132
        return vendor
132
133
 
133
134
    def _get_vendor_by_inspection(self):
134
135
        """Return the vendor or None by checking for known SSH implementations."""
135
 
        for args in (['ssh', '-V'], ['plink', '-V']):
 
136
        for args in [['ssh', '-V'], ['plink', '-V']]:
136
137
            version = self._get_ssh_version_string(args)
137
 
            vendor = self._get_vendor_by_version_string(version, args)
 
138
            vendor = self._get_vendor_by_version_string(version)
138
139
            if vendor is not None:
139
140
                return vendor
140
141
        return None
151
152
            if vendor is None:
152
153
                vendor = self._get_vendor_by_inspection()
153
154
                if vendor is None:
154
 
                    trace.mutter('falling back to default implementation')
 
155
                    mutter('falling back to default implementation')
155
156
                    vendor = self._default_ssh_vendor
156
157
                    if vendor is None:
157
 
                        raise errors.SSHVendorNotFound()
 
158
                        raise SSHVendorNotFound()
158
159
            self._cached_ssh_vendor = vendor
159
160
        return self._cached_ssh_vendor
160
161
 
177
178
 
178
179
    def __init__(self, sock):
179
180
        self.__socket = sock
180
 
 
 
181
 
181
182
    def send(self, data):
182
183
        return self.__socket.send(data)
183
184
 
193
194
 
194
195
class SSHVendor(object):
195
196
    """Abstract base class for SSH vendor implementations."""
196
 
 
 
197
    
197
198
    def connect_sftp(self, username, password, host, port):
198
199
        """Make an SSH connection, and return an SFTPClient.
199
200
        
216
217
            method that returns a pair of (read, write) filelike objects.
217
218
        """
218
219
        raise NotImplementedError(self.connect_ssh)
219
 
 
 
220
        
220
221
    def _raise_connection_error(self, host, port=None, orig_error=None,
221
222
                                msg='Unable to connect to SSH host'):
222
223
        """Raise a SocketConnectionError with properly formatted host.
224
225
        This just unifies all the locations that try to raise ConnectionError,
225
226
        so that they format things properly.
226
227
        """
227
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
228
 
                                           orig_error=orig_error)
 
228
        raise SocketConnectionError(host=host, port=port, msg=msg,
 
229
                                    orig_error=orig_error)
229
230
 
230
231
 
231
232
class LoopbackVendor(SSHVendor):
232
233
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
233
 
 
 
234
    
234
235
    def connect_sftp(self, username, password, host, port):
235
236
        sock = socket.socket()
236
237
        try:
258
259
 
259
260
    def _connect(self, username, password, host, port):
260
261
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
261
 
 
 
262
        
262
263
        load_host_keys()
263
264
 
264
265
        try:
267
268
            t.start_client()
268
269
        except (paramiko.SSHException, socket.error), e:
269
270
            self._raise_connection_error(host, port=port, orig_error=e)
270
 
 
 
271
            
271
272
        server_key = t.get_remote_server_key()
272
273
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
273
274
        keytype = server_key.get_name()
274
275
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
275
276
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
276
 
            our_server_key_hex = paramiko.util.hexify(
277
 
                our_server_key.get_fingerprint())
 
277
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
278
278
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
279
279
            our_server_key = BZR_HOSTKEYS[host][keytype]
280
 
            our_server_key_hex = paramiko.util.hexify(
281
 
                our_server_key.get_fingerprint())
 
280
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
282
281
        else:
283
 
            trace.warning('Adding %s host key for %s: %s'
284
 
                          % (keytype, host, server_key_hex))
 
282
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
285
283
            add = getattr(BZR_HOSTKEYS, 'add', None)
286
284
            if add is not None: # paramiko >= 1.X.X
287
285
                BZR_HOSTKEYS.add(host, keytype, server_key)
288
286
            else:
289
287
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
290
288
            our_server_key = server_key
291
 
            our_server_key_hex = paramiko.util.hexify(
292
 
                our_server_key.get_fingerprint())
 
289
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
293
290
            save_host_keys()
294
291
        if server_key != our_server_key:
295
292
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
296
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
297
 
            raise errors.TransportError(
298
 
                'Host keys for %s do not match!  %s != %s' %
 
293
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
294
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
299
295
                (host, our_server_key_hex, server_key_hex),
300
296
                ['Try editing %s or %s' % (filename1, filename2)])
301
297
 
302
 
        _paramiko_auth(username, password, host, port, t)
 
298
        _paramiko_auth(username, password, host, t)
303
299
        return t
304
 
 
 
300
        
305
301
    def connect_sftp(self, username, password, host, port):
306
302
        t = self._connect(username, password, host, port)
307
303
        try:
326
322
    register_ssh_vendor('paramiko', vendor)
327
323
    register_ssh_vendor('none', vendor)
328
324
    register_default_ssh_vendor(vendor)
329
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
330
325
    del vendor
331
 
else:
332
 
    _sftp_connection_errors = (EOFError,)
333
326
 
334
327
 
335
328
class SubprocessVendor(SSHVendor):
336
329
    """Abstract base class for vendors that use pipes to a subprocess."""
337
 
 
 
330
    
338
331
    def _connect(self, argv):
339
332
        proc = subprocess.Popen(argv,
340
333
                                stdin=subprocess.PIPE,
348
341
                                                  subsystem='sftp')
349
342
            sock = self._connect(argv)
350
343
            return SFTPClient(sock)
351
 
        except _sftp_connection_errors, e:
 
344
        except (EOFError, paramiko.SSHException), e:
352
345
            self._raise_connection_error(host, port=port, orig_error=e)
353
346
        except (OSError, IOError), e:
354
347
            # If the machine is fast enough, ssh can actually exit
384
377
 
385
378
class OpenSSHSubprocessVendor(SubprocessVendor):
386
379
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
387
 
 
 
380
    
388
381
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
389
382
                                  command=None):
390
383
        assert subsystem is not None or command is not None, (
429
422
        else:
430
423
            args.extend([host] + command)
431
424
        return args
432
 
 
 
425
    
433
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
434
427
 
435
428
 
443
436
        if subsystem is not None:
444
437
            assert command is None, (
445
438
                'subsystem and command are mutually exclusive')
446
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
439
        args = ['plink', '-x', '-a', '-ssh', '-2']
447
440
        if port is not None:
448
441
            args.extend(['-P', str(port)])
449
442
        if username is not None:
457
450
register_ssh_vendor('plink', PLinkSubprocessVendor())
458
451
 
459
452
 
460
 
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
453
def _paramiko_auth(username, password, host, paramiko_transport):
461
454
    # paramiko requires a username, but it might be none if nothing was supplied
462
455
    # use the local username, just in case.
463
456
    # We don't override username, because if we aren't using paramiko,
464
457
    # the username might be specified in ~/.ssh/config and we don't want to
465
458
    # force it to something else
466
459
    # Also, it would mess up the self.relpath() functionality
467
 
    auth = config.AuthenticationConfig()
468
 
    if username is None:
469
 
        username = auth.get_user('ssh', host, port=port)
470
 
        if username is None:
471
 
            # Default to local user
472
 
            username = getpass.getuser()
 
460
    username = username or getpass.getuser()
473
461
 
474
462
    if _use_ssh_agent:
475
463
        agent = paramiko.Agent()
476
464
        for key in agent.get_keys():
477
 
            trace.mutter('Trying SSH agent key %s'
478
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
465
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
479
466
            try:
480
467
                paramiko_transport.auth_publickey(username, key)
481
468
                return
482
469
            except paramiko.SSHException, e:
483
470
                pass
484
 
 
 
471
    
485
472
    # okay, try finding id_rsa or id_dss?  (posix only)
486
473
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
487
474
        return
496
483
            pass
497
484
 
498
485
    # give up and ask for a password
499
 
    password = auth.get_password('ssh', host, username, port=port)
 
486
    password = bzrlib.ui.ui_factory.get_password(
 
487
            prompt='SSH %(user)s@%(host)s password',
 
488
            user=username, host=host)
500
489
    try:
501
490
        paramiko_transport.auth_password(username, password)
502
491
    except paramiko.SSHException, e:
503
 
        raise errors.ConnectionError(
504
 
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
 
492
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
493
                              (username, host), e)
505
494
 
506
495
 
507
496
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
511
500
        paramiko_transport.auth_publickey(username, key)
512
501
        return True
513
502
    except paramiko.PasswordRequiredException:
514
 
        password = ui.ui_factory.get_password(
515
 
            prompt='SSH %(filename)s password', filename=filename)
 
503
        password = bzrlib.ui.ui_factory.get_password(
 
504
                prompt='SSH %(filename)s password',
 
505
                filename=filename)
516
506
        try:
517
507
            key = pkey_class.from_private_key_file(filename, password)
518
508
            paramiko_transport.auth_publickey(username, key)
519
509
            return True
520
510
        except paramiko.SSHException:
521
 
            trace.mutter('SSH authentication via %s key failed.'
522
 
                         % (os.path.basename(filename),))
 
511
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
523
512
    except paramiko.SSHException:
524
 
        trace.mutter('SSH authentication via %s key failed.'
525
 
                     % (os.path.basename(filename),))
 
513
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
526
514
    except IOError:
527
515
        pass
528
516
    return False
535
523
    """
536
524
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
537
525
    try:
538
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
539
 
            os.path.expanduser('~/.ssh/known_hosts'))
540
 
    except IOError, e:
541
 
        trace.mutter('failed to load system host keys: ' + str(e))
542
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
526
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
527
    except Exception, e:
 
528
        mutter('failed to load system host keys: ' + str(e))
 
529
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
543
530
    try:
544
531
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
545
 
    except IOError, e:
546
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
532
    except Exception, e:
 
533
        mutter('failed to load bzr host keys: ' + str(e))
547
534
        save_host_keys()
548
535
 
549
536
 
552
539
    Save "discovered" host keys in $(config)/ssh_host_keys/.
553
540
    """
554
541
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
555
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
556
 
    config.ensure_config_dir_exists()
 
542
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
543
    ensure_config_dir_exists()
557
544
 
558
545
    try:
559
546
        f = open(bzr_hostkey_path, 'w')
563
550
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
564
551
        f.close()
565
552
    except IOError, e:
566
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
553
        mutter('failed to save bzr host keys: ' + str(e))
567
554
 
568
555
 
569
556
def os_specific_subprocess_params():