~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: John Arbash Meinel
  • Date: 2008-08-25 21:50:11 UTC
  • mfrom: (0.11.3 tools)
  • mto: This revision was merged to the branch mainline in revision 3659.
  • Revision ID: john@arbash-meinel.com-20080825215011-de9esmzgkue3e522
Merge in Lukáš's helper scripts.
Update the packaging documents to describe how to do the releases
using bzr-builddeb to package all distro platforms
simultaneously.

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
 
138
133
    def _get_vendor_by_inspection(self):
139
134
        """Return the vendor or None by checking for known SSH implementations."""
140
 
        for args in [['ssh', '-V'], ['plink', '-V']]:
 
135
        for args in (['ssh', '-V'], ['plink', '-V']):
141
136
            version = self._get_ssh_version_string(args)
142
137
            vendor = self._get_vendor_by_version_string(version, args)
143
138
            if vendor is not None:
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
 
177
172
    signal.signal(signal.SIGINT, signal.SIG_IGN)
178
173
 
179
174
 
180
 
class LoopbackSFTP(object):
 
175
class SocketAsChannelAdapter(object):
181
176
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
182
177
 
183
178
    def __init__(self, sock):
184
179
        self.__socket = sock
185
 
 
 
180
 
 
181
    def get_name(self):
 
182
        return "bzr SocketAsChannelAdapter"
 
183
    
186
184
    def send(self, data):
187
185
        return self.__socket.send(data)
188
186
 
189
187
    def recv(self, n):
190
 
        return self.__socket.recv(n)
 
188
        try:
 
189
            return self.__socket.recv(n)
 
190
        except socket.error, e:
 
191
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
192
                             errno.EBADF):
 
193
                # Connection has closed.  Paramiko expects an empty string in
 
194
                # this case, not an exception.
 
195
                return ''
 
196
            raise
191
197
 
192
198
    def recv_ready(self):
 
199
        # TODO: jam 20051215 this function is necessary to support the
 
200
        # pipelined() function. In reality, it probably should use
 
201
        # poll() or select() to actually return if there is data
 
202
        # available, otherwise we probably don't get any benefit
193
203
        return True
194
204
 
195
205
    def close(self):
198
208
 
199
209
class SSHVendor(object):
200
210
    """Abstract base class for SSH vendor implementations."""
201
 
    
 
211
 
202
212
    def connect_sftp(self, username, password, host, port):
203
213
        """Make an SSH connection, and return an SFTPClient.
204
214
        
221
231
            method that returns a pair of (read, write) filelike objects.
222
232
        """
223
233
        raise NotImplementedError(self.connect_ssh)
224
 
        
 
234
 
225
235
    def _raise_connection_error(self, host, port=None, orig_error=None,
226
236
                                msg='Unable to connect to SSH host'):
227
237
        """Raise a SocketConnectionError with properly formatted host.
229
239
        This just unifies all the locations that try to raise ConnectionError,
230
240
        so that they format things properly.
231
241
        """
232
 
        raise SocketConnectionError(host=host, port=port, msg=msg,
233
 
                                    orig_error=orig_error)
 
242
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
243
                                           orig_error=orig_error)
234
244
 
235
245
 
236
246
class LoopbackVendor(SSHVendor):
237
247
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
238
 
    
 
248
 
239
249
    def connect_sftp(self, username, password, host, port):
240
250
        sock = socket.socket()
241
251
        try:
242
252
            sock.connect((host, port))
243
253
        except socket.error, e:
244
254
            self._raise_connection_error(host, port=port, orig_error=e)
245
 
        return SFTPClient(LoopbackSFTP(sock))
 
255
        return SFTPClient(SocketAsChannelAdapter(sock))
246
256
 
247
257
register_ssh_vendor('loopback', LoopbackVendor())
248
258
 
263
273
 
264
274
    def _connect(self, username, password, host, port):
265
275
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
266
 
        
 
276
 
267
277
        load_host_keys()
268
278
 
269
279
        try:
272
282
            t.start_client()
273
283
        except (paramiko.SSHException, socket.error), e:
274
284
            self._raise_connection_error(host, port=port, orig_error=e)
275
 
            
 
285
 
276
286
        server_key = t.get_remote_server_key()
277
287
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
278
288
        keytype = server_key.get_name()
279
289
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
280
290
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
281
 
            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())
282
293
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
283
294
            our_server_key = BZR_HOSTKEYS[host][keytype]
284
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
295
            our_server_key_hex = paramiko.util.hexify(
 
296
                our_server_key.get_fingerprint())
285
297
        else:
286
 
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
 
298
            trace.warning('Adding %s host key for %s: %s'
 
299
                          % (keytype, host, server_key_hex))
287
300
            add = getattr(BZR_HOSTKEYS, 'add', None)
288
301
            if add is not None: # paramiko >= 1.X.X
289
302
                BZR_HOSTKEYS.add(host, keytype, server_key)
290
303
            else:
291
304
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
292
305
            our_server_key = server_key
293
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
306
            our_server_key_hex = paramiko.util.hexify(
 
307
                our_server_key.get_fingerprint())
294
308
            save_host_keys()
295
309
        if server_key != our_server_key:
296
310
            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' % \
 
311
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
312
            raise errors.TransportError(
 
313
                'Host keys for %s do not match!  %s != %s' %
299
314
                (host, our_server_key_hex, server_key_hex),
300
315
                ['Try editing %s or %s' % (filename1, filename2)])
301
316
 
302
 
        _paramiko_auth(username, password, host, t)
 
317
        _paramiko_auth(username, password, host, port, t)
303
318
        return t
304
 
        
 
319
 
305
320
    def connect_sftp(self, username, password, host, port):
306
321
        t = self._connect(username, password, host, port)
307
322
        try:
326
341
    register_ssh_vendor('paramiko', vendor)
327
342
    register_ssh_vendor('none', vendor)
328
343
    register_default_ssh_vendor(vendor)
 
344
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
329
345
    del vendor
 
346
else:
 
347
    _sftp_connection_errors = (EOFError,)
330
348
 
331
349
 
332
350
class SubprocessVendor(SSHVendor):
333
351
    """Abstract base class for vendors that use pipes to a subprocess."""
334
 
    
 
352
 
335
353
    def _connect(self, argv):
336
354
        proc = subprocess.Popen(argv,
337
355
                                stdin=subprocess.PIPE,
344
362
            argv = self._get_vendor_specific_argv(username, host, port,
345
363
                                                  subsystem='sftp')
346
364
            sock = self._connect(argv)
347
 
            return SFTPClient(sock)
348
 
        except (EOFError, paramiko.SSHException), e:
 
365
            return SFTPClient(SocketAsChannelAdapter(sock))
 
366
        except _sftp_connection_errors, e:
349
367
            self._raise_connection_error(host, port=port, orig_error=e)
350
368
        except (OSError, IOError), e:
351
369
            # If the machine is fast enough, ssh can actually exit
381
399
 
382
400
class OpenSSHSubprocessVendor(SubprocessVendor):
383
401
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
384
 
    
 
402
 
385
403
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
386
404
                                  command=None):
387
 
        assert subsystem is not None or command is not None, (
388
 
            'Must specify a command or subsystem')
389
 
        if subsystem is not None:
390
 
            assert command is None, (
391
 
                'subsystem and command are mutually exclusive')
392
405
        args = ['ssh',
393
406
                '-oForwardX11=no', '-oForwardAgent=no',
394
407
                '-oClearAllForwardings=yes', '-oProtocol=2',
411
424
 
412
425
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
413
426
                                  command=None):
414
 
        assert subsystem is not None or command is not None, (
415
 
            'Must specify a command or subsystem')
416
 
        if subsystem is not None:
417
 
            assert command is None, (
418
 
                'subsystem and command are mutually exclusive')
419
427
        args = ['ssh', '-x']
420
428
        if port is not None:
421
429
            args.extend(['-p', str(port)])
426
434
        else:
427
435
            args.extend([host] + command)
428
436
        return args
429
 
    
 
437
 
430
438
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
431
439
 
432
440
 
435
443
 
436
444
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
437
445
                                  command=None):
438
 
        assert subsystem is not None or command is not None, (
439
 
            'Must specify a command or subsystem')
440
 
        if subsystem is not None:
441
 
            assert command is None, (
442
 
                'subsystem and command are mutually exclusive')
443
 
        args = ['plink', '-x', '-a', '-ssh', '-2']
 
446
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
444
447
        if port is not None:
445
448
            args.extend(['-P', str(port)])
446
449
        if username is not None:
454
457
register_ssh_vendor('plink', PLinkSubprocessVendor())
455
458
 
456
459
 
457
 
def _paramiko_auth(username, password, host, paramiko_transport):
 
460
def _paramiko_auth(username, password, host, port, paramiko_transport):
458
461
    # paramiko requires a username, but it might be none if nothing was supplied
459
462
    # use the local username, just in case.
460
463
    # We don't override username, because if we aren't using paramiko,
461
464
    # the username might be specified in ~/.ssh/config and we don't want to
462
465
    # force it to something else
463
466
    # Also, it would mess up the self.relpath() functionality
464
 
    username = username or getpass.getuser()
 
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()
465
473
 
466
474
    if _use_ssh_agent:
467
475
        agent = paramiko.Agent()
468
476
        for key in agent.get_keys():
469
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
477
            trace.mutter('Trying SSH agent key %s'
 
478
                         % paramiko.util.hexify(key.get_fingerprint()))
470
479
            try:
471
480
                paramiko_transport.auth_publickey(username, key)
472
481
                return
473
482
            except paramiko.SSHException, e:
474
483
                pass
475
 
    
 
484
 
476
485
    # okay, try finding id_rsa or id_dss?  (posix only)
477
486
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
478
487
        return
487
496
            pass
488
497
 
489
498
    # 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)
 
499
    password = auth.get_password('ssh', host, username, port=port)
493
500
    try:
494
501
        paramiko_transport.auth_password(username, password)
495
502
    except paramiko.SSHException, e:
496
 
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
497
 
                              (username, host), e)
 
503
        raise errors.ConnectionError(
 
504
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
498
505
 
499
506
 
500
507
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
504
511
        paramiko_transport.auth_publickey(username, key)
505
512
        return True
506
513
    except paramiko.PasswordRequiredException:
507
 
        password = bzrlib.ui.ui_factory.get_password(
508
 
                prompt='SSH %(filename)s password',
509
 
                filename=filename)
 
514
        password = ui.ui_factory.get_password(
 
515
            prompt='SSH %(filename)s password', filename=filename)
510
516
        try:
511
517
            key = pkey_class.from_private_key_file(filename, password)
512
518
            paramiko_transport.auth_publickey(username, key)
513
519
            return True
514
520
        except paramiko.SSHException:
515
 
            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),))
516
523
    except paramiko.SSHException:
517
 
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
524
        trace.mutter('SSH authentication via %s key failed.'
 
525
                     % (os.path.basename(filename),))
518
526
    except IOError:
519
527
        pass
520
528
    return False
527
535
    """
528
536
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
529
537
    try:
530
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
538
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
539
            os.path.expanduser('~/.ssh/known_hosts'))
531
540
    except IOError, e:
532
 
        mutter('failed to load system host keys: ' + str(e))
533
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
541
        trace.mutter('failed to load system host keys: ' + str(e))
 
542
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
534
543
    try:
535
544
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
536
545
    except IOError, e:
537
 
        mutter('failed to load bzr host keys: ' + str(e))
 
546
        trace.mutter('failed to load bzr host keys: ' + str(e))
538
547
        save_host_keys()
539
548
 
540
549
 
543
552
    Save "discovered" host keys in $(config)/ssh_host_keys/.
544
553
    """
545
554
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
546
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
547
 
    ensure_config_dir_exists()
 
555
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
556
    config.ensure_config_dir_exists()
548
557
 
549
558
    try:
550
559
        f = open(bzr_hostkey_path, 'w')
554
563
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
555
564
        f.close()
556
565
    except IOError, e:
557
 
        mutter('failed to save bzr host keys: ' + str(e))
 
566
        trace.mutter('failed to save bzr host keys: ' + str(e))
558
567
 
559
568
 
560
569
def os_specific_subprocess_params():
592
601
    def send(self, data):
593
602
        return os.write(self.proc.stdin.fileno(), data)
594
603
 
595
 
    def recv_ready(self):
596
 
        # TODO: jam 20051215 this function is necessary to support the
597
 
        # pipelined() function. In reality, it probably should use
598
 
        # poll() or select() to actually return if there is data
599
 
        # available, otherwise we probably don't get any benefit
600
 
        return True
601
 
 
602
604
    def recv(self, count):
603
605
        return os.read(self.proc.stdout.fileno(), count)
604
606