~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-11-04 18:51:39 UTC
  • mfrom: (2961.1.1 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20071104185139-kaio3sneodg2kp71
Authentication ring implementation (read-only)

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.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
 
27
from bzrlib import (
 
28
    config,
 
29
    errors,
 
30
    osutils,
 
31
    trace,
 
32
    ui,
 
33
    )
39
34
 
40
35
try:
41
36
    import paramiko
98
93
            try:
99
94
                vendor = self._ssh_vendors[vendor_name]
100
95
            except KeyError:
101
 
                raise UnknownSSH(vendor_name)
 
96
                raise errors.UnknownSSH(vendor_name)
102
97
            return vendor
103
98
        return None
104
99
 
122
117
        """
123
118
        vendor = None
124
119
        if 'OpenSSH' in version:
125
 
            mutter('ssh implementation is OpenSSH')
 
120
            trace.mutter('ssh implementation is OpenSSH')
126
121
            vendor = OpenSSHSubprocessVendor()
127
122
        elif 'SSH Secure Shell' in version:
128
 
            mutter('ssh implementation is SSH Corp.')
 
123
            trace.mutter('ssh implementation is SSH Corp.')
129
124
            vendor = SSHCorpSubprocessVendor()
130
125
        elif 'plink' in version and args[0] == 'plink':
131
 
            # Checking if "plink" was the executed argument as Windows sometimes 
132
 
            # reports 'ssh -V' incorrectly with 'plink' in it's version. 
133
 
            # See https://bugs.launchpad.net/bzr/+bug/107155
134
 
            mutter("ssh implementation is Putty's 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.")
135
130
            vendor = PLinkSubprocessVendor()
136
131
        return vendor
137
132
 
156
151
            if vendor is None:
157
152
                vendor = self._get_vendor_by_inspection()
158
153
                if vendor is None:
159
 
                    mutter('falling back to default implementation')
 
154
                    trace.mutter('falling back to default implementation')
160
155
                    vendor = self._default_ssh_vendor
161
156
                    if vendor is None:
162
 
                        raise SSHVendorNotFound()
 
157
                        raise errors.SSHVendorNotFound()
163
158
            self._cached_ssh_vendor = vendor
164
159
        return self._cached_ssh_vendor
165
160
 
182
177
 
183
178
    def __init__(self, sock):
184
179
        self.__socket = sock
185
 
 
 
180
 
186
181
    def send(self, data):
187
182
        return self.__socket.send(data)
188
183
 
198
193
 
199
194
class SSHVendor(object):
200
195
    """Abstract base class for SSH vendor implementations."""
201
 
    
 
196
 
202
197
    def connect_sftp(self, username, password, host, port):
203
198
        """Make an SSH connection, and return an SFTPClient.
204
199
        
221
216
            method that returns a pair of (read, write) filelike objects.
222
217
        """
223
218
        raise NotImplementedError(self.connect_ssh)
224
 
        
 
219
 
225
220
    def _raise_connection_error(self, host, port=None, orig_error=None,
226
221
                                msg='Unable to connect to SSH host'):
227
222
        """Raise a SocketConnectionError with properly formatted host.
229
224
        This just unifies all the locations that try to raise ConnectionError,
230
225
        so that they format things properly.
231
226
        """
232
 
        raise SocketConnectionError(host=host, port=port, msg=msg,
233
 
                                    orig_error=orig_error)
 
227
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
228
                                           orig_error=orig_error)
234
229
 
235
230
 
236
231
class LoopbackVendor(SSHVendor):
237
232
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
238
 
    
 
233
 
239
234
    def connect_sftp(self, username, password, host, port):
240
235
        sock = socket.socket()
241
236
        try:
263
258
 
264
259
    def _connect(self, username, password, host, port):
265
260
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
266
 
        
 
261
 
267
262
        load_host_keys()
268
263
 
269
264
        try:
272
267
            t.start_client()
273
268
        except (paramiko.SSHException, socket.error), e:
274
269
            self._raise_connection_error(host, port=port, orig_error=e)
275
 
            
 
270
 
276
271
        server_key = t.get_remote_server_key()
277
272
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
278
273
        keytype = server_key.get_name()
279
274
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
280
275
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
281
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
276
            our_server_key_hex = paramiko.util.hexify(
 
277
                our_server_key.get_fingerprint())
282
278
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
283
279
            our_server_key = BZR_HOSTKEYS[host][keytype]
284
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
280
            our_server_key_hex = paramiko.util.hexify(
 
281
                our_server_key.get_fingerprint())
285
282
        else:
286
 
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
 
283
            trace.warning('Adding %s host key for %s: %s'
 
284
                          % (keytype, host, server_key_hex))
287
285
            add = getattr(BZR_HOSTKEYS, 'add', None)
288
286
            if add is not None: # paramiko >= 1.X.X
289
287
                BZR_HOSTKEYS.add(host, keytype, server_key)
290
288
            else:
291
289
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
292
290
            our_server_key = server_key
293
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
291
            our_server_key_hex = paramiko.util.hexify(
 
292
                our_server_key.get_fingerprint())
294
293
            save_host_keys()
295
294
        if server_key != our_server_key:
296
295
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
297
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
298
 
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
 
296
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
297
            raise errors.TransportError(
 
298
                'Host keys for %s do not match!  %s != %s' %
299
299
                (host, our_server_key_hex, server_key_hex),
300
300
                ['Try editing %s or %s' % (filename1, filename2)])
301
301
 
302
 
        _paramiko_auth(username, password, host, t)
 
302
        _paramiko_auth(username, password, host, port, t)
303
303
        return t
304
 
        
 
304
 
305
305
    def connect_sftp(self, username, password, host, port):
306
306
        t = self._connect(username, password, host, port)
307
307
        try:
331
331
 
332
332
class SubprocessVendor(SSHVendor):
333
333
    """Abstract base class for vendors that use pipes to a subprocess."""
334
 
    
 
334
 
335
335
    def _connect(self, argv):
336
336
        proc = subprocess.Popen(argv,
337
337
                                stdin=subprocess.PIPE,
381
381
 
382
382
class OpenSSHSubprocessVendor(SubprocessVendor):
383
383
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
384
 
    
 
384
 
385
385
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
386
386
                                  command=None):
387
387
        assert subsystem is not None or command is not None, (
426
426
        else:
427
427
            args.extend([host] + command)
428
428
        return args
429
 
    
 
429
 
430
430
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
431
431
 
432
432
 
454
454
register_ssh_vendor('plink', PLinkSubprocessVendor())
455
455
 
456
456
 
457
 
def _paramiko_auth(username, password, host, paramiko_transport):
 
457
def _paramiko_auth(username, password, host, port, paramiko_transport):
458
458
    # paramiko requires a username, but it might be none if nothing was supplied
459
459
    # use the local username, just in case.
460
460
    # We don't override username, because if we aren't using paramiko,
461
461
    # the username might be specified in ~/.ssh/config and we don't want to
462
462
    # force it to something else
463
463
    # Also, it would mess up the self.relpath() functionality
464
 
    username = username or getpass.getuser()
 
464
    auth = config.AuthenticationConfig()
 
465
    if username is None:
 
466
        username = auth.get_user('ssh', host, port=port)
 
467
        if username is None:
 
468
            # Default to local user
 
469
            username = getpass.getuser()
465
470
 
466
471
    if _use_ssh_agent:
467
472
        agent = paramiko.Agent()
468
473
        for key in agent.get_keys():
469
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
474
            trace.mutter('Trying SSH agent key %s'
 
475
                         % paramiko.util.hexify(key.get_fingerprint()))
470
476
            try:
471
477
                paramiko_transport.auth_publickey(username, key)
472
478
                return
473
479
            except paramiko.SSHException, e:
474
480
                pass
475
 
    
 
481
 
476
482
    # okay, try finding id_rsa or id_dss?  (posix only)
477
483
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
478
484
        return
487
493
            pass
488
494
 
489
495
    # give up and ask for a password
490
 
    password = bzrlib.ui.ui_factory.get_password(
491
 
            prompt='SSH %(user)s@%(host)s password',
492
 
            user=username, host=host)
 
496
    password = auth.get_password('ssh', host, username, port=port)
493
497
    try:
494
498
        paramiko_transport.auth_password(username, password)
495
499
    except paramiko.SSHException, e:
496
 
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
497
 
                              (username, host), e)
 
500
        raise errors.ConnectionError(
 
501
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
498
502
 
499
503
 
500
504
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
504
508
        paramiko_transport.auth_publickey(username, key)
505
509
        return True
506
510
    except paramiko.PasswordRequiredException:
507
 
        password = bzrlib.ui.ui_factory.get_password(
508
 
                prompt='SSH %(filename)s password',
509
 
                filename=filename)
 
511
        password = ui.ui_factory.get_password(
 
512
            prompt='SSH %(filename)s password', filename=filename)
510
513
        try:
511
514
            key = pkey_class.from_private_key_file(filename, password)
512
515
            paramiko_transport.auth_publickey(username, key)
513
516
            return True
514
517
        except paramiko.SSHException:
515
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
518
            trace.mutter('SSH authentication via %s key failed.'
 
519
                         % (os.path.basename(filename),))
516
520
    except paramiko.SSHException:
517
 
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
521
        trace.mutter('SSH authentication via %s key failed.'
 
522
                     % (os.path.basename(filename),))
518
523
    except IOError:
519
524
        pass
520
525
    return False
527
532
    """
528
533
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
529
534
    try:
530
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
535
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
536
            os.path.expanduser('~/.ssh/known_hosts'))
531
537
    except IOError, e:
532
 
        mutter('failed to load system host keys: ' + str(e))
533
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
538
        trace.mutter('failed to load system host keys: ' + str(e))
 
539
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
534
540
    try:
535
541
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
536
542
    except IOError, e:
537
 
        mutter('failed to load bzr host keys: ' + str(e))
 
543
        trace.mutter('failed to load bzr host keys: ' + str(e))
538
544
        save_host_keys()
539
545
 
540
546
 
543
549
    Save "discovered" host keys in $(config)/ssh_host_keys/.
544
550
    """
545
551
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
546
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
547
 
    ensure_config_dir_exists()
 
552
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
553
    config.ensure_config_dir_exists()
548
554
 
549
555
    try:
550
556
        f = open(bzr_hostkey_path, 'w')
554
560
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
555
561
        f.close()
556
562
    except IOError, e:
557
 
        mutter('failed to save bzr host keys: ' + str(e))
 
563
        trace.mutter('failed to save bzr host keys: ' + str(e))
558
564
 
559
565
 
560
566
def os_specific_subprocess_params():