/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/ssh.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-08-23 01:15:41 UTC
  • mfrom: (7520.1.4 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200823011541-nv0oh7nzaganx2qy
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/389690

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) 2006-2011 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
24
24
import socket
25
25
import subprocess
26
26
import sys
 
27
from binascii import hexlify
27
28
 
28
 
from bzrlib import (
 
29
from .. import (
29
30
    config,
 
31
    bedding,
30
32
    errors,
31
33
    osutils,
32
34
    trace,
35
37
 
36
38
try:
37
39
    import paramiko
38
 
except ImportError, e:
 
40
except ImportError as e:
39
41
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
40
42
    # access
41
43
    paramiko = None
43
45
    from paramiko.sftp_client import SFTPClient
44
46
 
45
47
 
 
48
class StrangeHostname(errors.BzrError):
 
49
    _fmt = "Refusing to connect to strange SSH hostname %(hostname)s"
 
50
 
 
51
 
46
52
SYSTEM_HOSTKEYS = {}
47
 
BZR_HOSTKEYS = {}
48
 
 
49
 
 
50
 
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
51
 
 
52
 
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
53
 
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
54
 
# so we get an AttributeError exception. So we will not try to
55
 
# connect to an agent if we are on win32 and using Paramiko older than 1.6
56
 
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
53
BRZ_HOSTKEYS = {}
57
54
 
58
55
 
59
56
class SSHVendorManager(object):
60
57
    """Manager for manage SSH vendors."""
61
58
 
62
59
    # Note, although at first sign the class interface seems similar to
63
 
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
60
    # breezy.registry.Registry it is not possible/convenient to directly use
64
61
    # the Registry because the class just has "get()" interface instead of the
65
62
    # Registry's "get(key)".
66
63
 
81
78
        """Clear previously cached lookup result."""
82
79
        self._cached_ssh_vendor = None
83
80
 
84
 
    def _get_vendor_by_environment(self, environment=None):
85
 
        """Return the vendor or None based on BZR_SSH environment variable.
86
 
 
87
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
88
 
                            unknown vendor name
89
 
        """
90
 
        if environment is None:
91
 
            environment = os.environ
92
 
        if 'BZR_SSH' in environment:
93
 
            vendor_name = environment['BZR_SSH']
 
81
    def _get_vendor_by_config(self):
 
82
        vendor_name = config.GlobalStack().get('ssh')
 
83
        if vendor_name is not None:
94
84
            try:
95
85
                vendor = self._ssh_vendors[vendor_name]
96
86
            except KeyError:
107
97
            p = subprocess.Popen(args,
108
98
                                 stdout=subprocess.PIPE,
109
99
                                 stderr=subprocess.PIPE,
 
100
                                 bufsize=0,
110
101
                                 **os_specific_subprocess_params())
111
102
            stdout, stderr = p.communicate()
112
103
        except OSError:
113
 
            stdout = stderr = ''
114
 
        return stdout + stderr
 
104
            stdout = stderr = b''
 
105
        return (stdout + stderr).decode(osutils.get_terminal_encoding())
115
106
 
116
107
    def _get_vendor_by_version_string(self, version, progname):
117
108
        """Return the vendor or None based on output from the subprocess.
126
117
        elif 'SSH Secure Shell' in version:
127
118
            trace.mutter('ssh implementation is SSH Corp.')
128
119
            vendor = SSHCorpSubprocessVendor()
 
120
        elif 'lsh' in version:
 
121
            trace.mutter('ssh implementation is GNU lsh.')
 
122
            vendor = LSHSubprocessVendor()
129
123
        # As plink user prompts are not handled currently, don't auto-detect
130
124
        # 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
 
125
        # is given in BRZ_SSH. See https://bugs.launchpad.net/bugs/414743
132
126
        elif 'plink' in version and progname == 'plink':
133
127
            # Checking if "plink" was the executed argument as Windows
134
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
128
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
135
129
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
136
130
            trace.mutter("ssh implementation is Putty's plink.")
137
131
            vendor = PLinkSubprocessVendor()
145
139
    def _get_vendor_from_path(self, path):
146
140
        """Return the vendor or None using the program at the given path"""
147
141
        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])
 
142
        return self._get_vendor_by_version_string(version,
 
143
                                                  os.path.splitext(os.path.basename(path))[0])
150
144
 
151
 
    def get_vendor(self, environment=None):
 
145
    def get_vendor(self):
152
146
        """Find out what version of SSH is on the system.
153
147
 
154
148
        :raises SSHVendorNotFound: if no any SSH vendor is found
155
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
149
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
156
150
                            unknown vendor name
157
151
        """
158
152
        if self._cached_ssh_vendor is None:
159
 
            vendor = self._get_vendor_by_environment(environment)
 
153
            vendor = self._get_vendor_by_config()
160
154
            if vendor is None:
161
155
                vendor = self._get_vendor_by_inspection()
162
156
                if vendor is None:
167
161
            self._cached_ssh_vendor = vendor
168
162
        return self._cached_ssh_vendor
169
163
 
 
164
 
170
165
_ssh_vendor_manager = SSHVendorManager()
171
166
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
172
167
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
199
194
    def recv(self, n):
200
195
        try:
201
196
            return self.__socket.recv(n)
202
 
        except socket.error, e:
 
197
        except socket.error as e:
203
198
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
204
199
                             errno.EBADF):
205
200
                # Connection has closed.  Paramiko expects an empty string in
239
234
    def connect_ssh(self, username, password, host, port, command):
240
235
        """Make an SSH connection.
241
236
 
242
 
        :returns: something with a `close` method, and a `get_filelike_channels`
243
 
            method that returns a pair of (read, write) filelike objects.
 
237
        :returns: an SSHConnection.
244
238
        """
245
239
        raise NotImplementedError(self.connect_ssh)
246
240
 
262
256
        sock = socket.socket()
263
257
        try:
264
258
            sock.connect((host, port))
265
 
        except socket.error, e:
 
259
        except socket.error as e:
266
260
            self._raise_connection_error(host, port=port, orig_error=e)
267
261
        return SFTPClient(SocketAsChannelAdapter(sock))
268
262
 
 
263
 
269
264
register_ssh_vendor('loopback', LoopbackVendor())
270
265
 
271
266
 
272
 
class _ParamikoSSHConnection(object):
273
 
    def __init__(self, channel):
274
 
        self.channel = channel
275
 
 
276
 
    def get_filelike_channels(self):
277
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
278
 
 
279
 
    def close(self):
280
 
        return self.channel.close()
281
 
 
282
 
 
283
267
class ParamikoVendor(SSHVendor):
284
268
    """Vendor that uses paramiko."""
285
269
 
 
270
    def _hexify(self, s):
 
271
        return hexlify(s).upper()
 
272
 
286
273
    def _connect(self, username, password, host, port):
287
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
274
        global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
288
275
 
289
276
        load_host_keys()
290
277
 
292
279
            t = paramiko.Transport((host, port or 22))
293
280
            t.set_log_channel('bzr.paramiko')
294
281
            t.start_client()
295
 
        except (paramiko.SSHException, socket.error), e:
 
282
        except (paramiko.SSHException, socket.error) as e:
296
283
            self._raise_connection_error(host, port=port, orig_error=e)
297
284
 
298
285
        server_key = t.get_remote_server_key()
299
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
286
        server_key_hex = self._hexify(server_key.get_fingerprint())
300
287
        keytype = server_key.get_name()
301
288
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
302
289
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
303
 
            our_server_key_hex = paramiko.util.hexify(
304
 
                our_server_key.get_fingerprint())
305
 
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
306
 
            our_server_key = BZR_HOSTKEYS[host][keytype]
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
290
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
291
        elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
 
292
            our_server_key = BRZ_HOSTKEYS[host][keytype]
 
293
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
309
294
        else:
310
295
            trace.warning('Adding %s host key for %s: %s'
311
296
                          % (keytype, host, server_key_hex))
312
 
            add = getattr(BZR_HOSTKEYS, 'add', None)
313
 
            if add is not None: # paramiko >= 1.X.X
314
 
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
297
            add = getattr(BRZ_HOSTKEYS, 'add', None)
 
298
            if add is not None:  # paramiko >= 1.X.X
 
299
                BRZ_HOSTKEYS.add(host, keytype, server_key)
315
300
            else:
316
 
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
301
                BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
317
302
            our_server_key = server_key
318
 
            our_server_key_hex = paramiko.util.hexify(
319
 
                our_server_key.get_fingerprint())
 
303
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
320
304
            save_host_keys()
321
305
        if server_key != our_server_key:
322
306
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
323
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
307
            filename2 = _ssh_host_keys_config_dir()
324
308
            raise errors.TransportError(
325
309
                'Host keys for %s do not match!  %s != %s' %
326
310
                (host, our_server_key_hex, server_key_hex),
333
317
        t = self._connect(username, password, host, port)
334
318
        try:
335
319
            return t.open_sftp_client()
336
 
        except paramiko.SSHException, e:
 
320
        except paramiko.SSHException as e:
337
321
            self._raise_connection_error(host, port=port, orig_error=e,
338
322
                                         msg='Unable to start sftp client')
339
323
 
344
328
            cmdline = ' '.join(command)
345
329
            channel.exec_command(cmdline)
346
330
            return _ParamikoSSHConnection(channel)
347
 
        except paramiko.SSHException, e:
 
331
        except paramiko.SSHException as e:
348
332
            self._raise_connection_error(host, port=port, orig_error=e,
349
333
                                         msg='Unable to invoke remote bzr')
350
334
 
 
335
 
 
336
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
351
337
if paramiko is not None:
352
338
    vendor = ParamikoVendor()
353
339
    register_ssh_vendor('paramiko', vendor)
354
340
    register_ssh_vendor('none', vendor)
355
341
    register_default_ssh_vendor(vendor)
356
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
342
    _ssh_connection_errors += (paramiko.SSHException,)
357
343
    del vendor
358
 
else:
359
 
    _sftp_connection_errors = (EOFError,)
360
344
 
361
345
 
362
346
class SubprocessVendor(SSHVendor):
363
347
    """Abstract base class for vendors that use pipes to a subprocess."""
364
348
 
 
349
    # In general stderr should be inherited from the parent process so prompts
 
350
    # are visible on the terminal. This can be overriden to another file for
 
351
    # tests, but beware of using PIPE which may hang due to not being read.
 
352
    _stderr_target = None
 
353
 
 
354
    @staticmethod
 
355
    def _check_hostname(arg):
 
356
        if arg.startswith('-'):
 
357
            raise StrangeHostname(hostname=arg)
 
358
 
365
359
    def _connect(self, argv):
366
 
        proc = subprocess.Popen(argv,
367
 
                                stdin=subprocess.PIPE,
368
 
                                stdout=subprocess.PIPE,
 
360
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
361
        # subprocess.  We prefer sockets to pipes because they support
 
362
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
363
        # whatever) chunks.
 
364
        try:
 
365
            my_sock, subproc_sock = socket.socketpair()
 
366
            osutils.set_fd_cloexec(my_sock)
 
367
        except (AttributeError, socket.error):
 
368
            # This platform doesn't support socketpair(), so just use ordinary
 
369
            # pipes instead.
 
370
            stdin = stdout = subprocess.PIPE
 
371
            my_sock, subproc_sock = None, None
 
372
        else:
 
373
            stdin = stdout = subproc_sock
 
374
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
375
                                stderr=self._stderr_target,
 
376
                                bufsize=0,
369
377
                                **os_specific_subprocess_params())
370
 
        return SSHSubprocess(proc)
 
378
        if subproc_sock is not None:
 
379
            subproc_sock.close()
 
380
        return SSHSubprocessConnection(proc, sock=my_sock)
371
381
 
372
382
    def connect_sftp(self, username, password, host, port):
373
383
        try:
375
385
                                                  subsystem='sftp')
376
386
            sock = self._connect(argv)
377
387
            return SFTPClient(SocketAsChannelAdapter(sock))
378
 
        except _sftp_connection_errors, e:
379
 
            self._raise_connection_error(host, port=port, orig_error=e)
380
 
        except (OSError, IOError), e:
381
 
            # If the machine is fast enough, ssh can actually exit
382
 
            # before we try and send it the sftp request, which
383
 
            # raises a Broken Pipe
384
 
            if e.errno not in (errno.EPIPE,):
385
 
                raise
 
388
        except _ssh_connection_errors as e:
386
389
            self._raise_connection_error(host, port=port, orig_error=e)
387
390
 
388
391
    def connect_ssh(self, username, password, host, port, command):
390
393
            argv = self._get_vendor_specific_argv(username, host, port,
391
394
                                                  command=command)
392
395
            return self._connect(argv)
393
 
        except (EOFError), e:
394
 
            self._raise_connection_error(host, port=port, orig_error=e)
395
 
        except (OSError, IOError), e:
396
 
            # If the machine is fast enough, ssh can actually exit
397
 
            # before we try and send it the sftp request, which
398
 
            # raises a Broken Pipe
399
 
            if e.errno not in (errno.EPIPE,):
400
 
                raise
 
396
        except _ssh_connection_errors as e:
401
397
            self._raise_connection_error(host, port=port, orig_error=e)
402
398
 
403
399
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
414
                                  command=None):
419
415
        args = [self.executable_path,
420
416
                '-oForwardX11=no', '-oForwardAgent=no',
421
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
417
                '-oClearAllForwardings=yes',
422
418
                '-oNoHostAuthenticationForLocalhost=yes']
423
419
        if port is not None:
424
420
            args.extend(['-p', str(port)])
425
421
        if username is not None:
426
422
            args.extend(['-l', username])
427
423
        if subsystem is not None:
428
 
            args.extend(['-s', host, subsystem])
 
424
            args.extend(['-s', '--', host, subsystem])
429
425
        else:
430
 
            args.extend([host] + command)
 
426
            args.extend(['--', host] + command)
431
427
        return args
432
428
 
 
429
 
433
430
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
434
431
 
435
432
 
440
437
 
441
438
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
442
439
                                  command=None):
 
440
        self._check_hostname(host)
443
441
        args = [self.executable_path, '-x']
444
442
        if port is not None:
445
443
            args.extend(['-p', str(port)])
451
449
            args.extend([host] + command)
452
450
        return args
453
451
 
 
452
 
454
453
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
455
454
 
456
455
 
 
456
class LSHSubprocessVendor(SubprocessVendor):
 
457
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
458
 
 
459
    executable_path = 'lsh'
 
460
 
 
461
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
462
                                  command=None):
 
463
        self._check_hostname(host)
 
464
        args = [self.executable_path]
 
465
        if port is not None:
 
466
            args.extend(['-p', str(port)])
 
467
        if username is not None:
 
468
            args.extend(['-l', username])
 
469
        if subsystem is not None:
 
470
            args.extend(['--subsystem', subsystem, host])
 
471
        else:
 
472
            args.extend([host] + command)
 
473
        return args
 
474
 
 
475
 
 
476
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
477
 
 
478
 
457
479
class PLinkSubprocessVendor(SubprocessVendor):
458
480
    """SSH vendor that uses the 'plink' executable from Putty."""
459
481
 
461
483
 
462
484
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
485
                                  command=None):
 
486
        self._check_hostname(host)
464
487
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
465
488
        if port is not None:
466
489
            args.extend(['-P', str(port)])
472
495
            args.extend([host] + command)
473
496
        return args
474
497
 
 
498
 
475
499
register_ssh_vendor('plink', PLinkSubprocessVendor())
476
500
 
477
501
 
482
506
    if username is None:
483
507
        username = auth.get_user('ssh', host, port=port,
484
508
                                 default=getpass.getuser())
485
 
    if _use_ssh_agent:
486
 
        agent = paramiko.Agent()
487
 
        for key in agent.get_keys():
488
 
            trace.mutter('Trying SSH agent key %s'
489
 
                         % paramiko.util.hexify(key.get_fingerprint()))
490
 
            try:
491
 
                paramiko_transport.auth_publickey(username, key)
492
 
                return
493
 
            except paramiko.SSHException, e:
494
 
                pass
 
509
    agent = paramiko.Agent()
 
510
    for key in agent.get_keys():
 
511
        trace.mutter('Trying SSH agent key %s'
 
512
                     % hexlify(key.get_fingerprint()).upper())
 
513
        try:
 
514
            paramiko_transport.auth_publickey(username, key)
 
515
            return
 
516
        except paramiko.SSHException as e:
 
517
            pass
495
518
 
496
519
    # okay, try finding id_rsa or id_dss?  (posix only)
497
520
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
512
535
            paramiko_transport.auth_none(username)
513
536
        finally:
514
537
            paramiko_transport.logger.setLevel(old_level)
515
 
    except paramiko.BadAuthenticationType, e:
 
538
    except paramiko.BadAuthenticationType as e:
516
539
        # Supported methods are in the exception
517
540
        supported_auth_types = e.allowed_types
518
 
    except paramiko.SSHException, e:
 
541
    except paramiko.SSHException as e:
519
542
        # Don't know what happened, but just ignore it
520
543
        pass
521
544
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
527
550
    # requires something other than a single password, but we currently don't
528
551
    # support that.
529
552
    if ('password' not in supported_auth_types and
530
 
        'keyboard-interactive' not in supported_auth_types):
 
553
            'keyboard-interactive' not in supported_auth_types):
531
554
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
532
 
            '\n  %s@%s\nsupported auth types: %s'
533
 
            % (username, host, supported_auth_types))
 
555
                                     '\n  %s@%s\nsupported auth types: %s'
 
556
                                     % (username, host, supported_auth_types))
534
557
 
535
558
    if password:
536
559
        try:
537
560
            paramiko_transport.auth_password(username, password)
538
561
            return
539
 
        except paramiko.SSHException, e:
 
562
        except paramiko.SSHException as e:
540
563
            pass
541
564
 
542
565
    # give up and ask for a password
545
568
    if password is not None:
546
569
        try:
547
570
            paramiko_transport.auth_password(username, password)
548
 
        except paramiko.SSHException, e:
 
571
        except paramiko.SSHException as e:
549
572
            raise errors.ConnectionError(
550
573
                'Unable to authenticate to SSH host as'
551
574
                '\n  %s@%s\n' % (username, host), e)
562
585
        return True
563
586
    except paramiko.PasswordRequiredException:
564
587
        password = ui.ui_factory.get_password(
565
 
            prompt='SSH %(filename)s password', filename=filename)
 
588
            prompt=u'SSH %(filename)s password',
 
589
            filename=filename.decode(osutils._fs_enc))
566
590
        try:
567
591
            key = pkey_class.from_private_key_file(filename, password)
568
592
            paramiko_transport.auth_publickey(username, key)
578
602
    return False
579
603
 
580
604
 
 
605
def _ssh_host_keys_config_dir():
 
606
    return osutils.pathjoin(bedding.config_dir(), 'ssh_host_keys')
 
607
 
 
608
 
581
609
def load_host_keys():
582
610
    """
583
611
    Load system host keys (probably doesn't work on windows) and any
584
612
    "discovered" keys from previous sessions.
585
613
    """
586
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
614
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
587
615
    try:
588
616
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
589
617
            os.path.expanduser('~/.ssh/known_hosts'))
590
 
    except IOError, e:
 
618
    except IOError as e:
591
619
        trace.mutter('failed to load system host keys: ' + str(e))
592
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
620
    brz_hostkey_path = _ssh_host_keys_config_dir()
593
621
    try:
594
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
595
 
    except IOError, e:
596
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
622
        BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
 
623
    except IOError as e:
 
624
        trace.mutter('failed to load brz host keys: ' + str(e))
597
625
        save_host_keys()
598
626
 
599
627
 
601
629
    """
602
630
    Save "discovered" host keys in $(config)/ssh_host_keys/.
603
631
    """
604
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
605
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
606
 
    config.ensure_config_dir_exists()
 
632
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
633
    bzr_hostkey_path = _ssh_host_keys_config_dir()
 
634
    bedding.ensure_config_dir_exists()
607
635
 
608
636
    try:
609
 
        f = open(bzr_hostkey_path, 'w')
610
 
        f.write('# SSH host keys collected by bzr\n')
611
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
612
 
            for keytype, key in keys.iteritems():
613
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
614
 
        f.close()
615
 
    except IOError, e:
 
637
        with open(bzr_hostkey_path, 'w') as f:
 
638
            f.write('# SSH host keys collected by bzr\n')
 
639
            for hostname, keys in BRZ_HOSTKEYS.items():
 
640
                for keytype, key in keys.items():
 
641
                    f.write('%s %s %s\n' %
 
642
                            (hostname, keytype, key.get_base64()))
 
643
    except IOError as e:
616
644
        trace.mutter('failed to save bzr host keys: ' + str(e))
617
645
 
618
646
 
641
669
                'close_fds': True,
642
670
                }
643
671
 
 
672
 
644
673
import weakref
645
674
_subproc_weakrefs = set()
646
675
 
647
 
def _close_ssh_proc(proc):
648
 
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
676
 
 
677
def _close_ssh_proc(proc, sock):
 
678
    """Carefully close stdin/stdout and reap the SSH process.
 
679
 
 
680
    If the pipes are already closed and/or the process has already been
 
681
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
682
    to clean up (whether or not a clean up was already tried).
 
683
    """
 
684
    funcs = []
 
685
    for closeable in (proc.stdin, proc.stdout, sock):
 
686
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
687
        # stdout streams to close, or that we will have been passed a socket to
 
688
        # close, with the option not in use being None.
 
689
        if closeable is not None:
 
690
            funcs.append(closeable.close)
 
691
    funcs.append(proc.wait)
 
692
    for func in funcs:
649
693
        try:
650
694
            func()
651
695
        except OSError:
652
 
            pass
653
 
 
654
 
 
655
 
class SSHSubprocess(object):
656
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
657
 
 
658
 
    def __init__(self, proc):
 
696
            # It's ok for the pipe to already be closed, or the process to
 
697
            # already be finished.
 
698
            continue
 
699
 
 
700
 
 
701
class SSHConnection(object):
 
702
    """Abstract base class for SSH connections."""
 
703
 
 
704
    def get_sock_or_pipes(self):
 
705
        """Returns a (kind, io_object) pair.
 
706
 
 
707
        If kind == 'socket', then io_object is a socket.
 
708
 
 
709
        If kind == 'pipes', then io_object is a pair of file-like objects
 
710
        (read_from, write_to).
 
711
        """
 
712
        raise NotImplementedError(self.get_sock_or_pipes)
 
713
 
 
714
    def close(self):
 
715
        raise NotImplementedError(self.close)
 
716
 
 
717
 
 
718
class SSHSubprocessConnection(SSHConnection):
 
719
    """A connection to an ssh subprocess via pipes or a socket.
 
720
 
 
721
    This class is also socket-like enough to be used with
 
722
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
723
    """
 
724
 
 
725
    def __init__(self, proc, sock=None):
 
726
        """Constructor.
 
727
 
 
728
        :param proc: a subprocess.Popen
 
729
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
730
            should breezy's half of that socketpair.  If not passed, proc's
 
731
            stdin/out is assumed to be ordinary pipes.
 
732
        """
659
733
        self.proc = proc
 
734
        self._sock = sock
660
735
        # Add a weakref to proc that will attempt to do the same as self.close
661
736
        # to avoid leaving processes lingering indefinitely.
 
737
 
662
738
        def terminate(ref):
663
739
            _subproc_weakrefs.remove(ref)
664
 
            _close_ssh_proc(proc)
 
740
            _close_ssh_proc(proc, sock)
665
741
        _subproc_weakrefs.add(weakref.ref(self, terminate))
666
742
 
667
743
    def send(self, data):
668
 
        return os.write(self.proc.stdin.fileno(), data)
 
744
        if self._sock is not None:
 
745
            return self._sock.send(data)
 
746
        else:
 
747
            return os.write(self.proc.stdin.fileno(), data)
669
748
 
670
749
    def recv(self, count):
671
 
        return os.read(self.proc.stdout.fileno(), count)
672
 
 
673
 
    def close(self):
674
 
        _close_ssh_proc(self.proc)
675
 
 
676
 
    def get_filelike_channels(self):
677
 
        return (self.proc.stdout, self.proc.stdin)
678
 
 
 
750
        if self._sock is not None:
 
751
            return self._sock.recv(count)
 
752
        else:
 
753
            return os.read(self.proc.stdout.fileno(), count)
 
754
 
 
755
    def close(self):
 
756
        _close_ssh_proc(self.proc, self._sock)
 
757
 
 
758
    def get_sock_or_pipes(self):
 
759
        if self._sock is not None:
 
760
            return 'socket', self._sock
 
761
        else:
 
762
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
763
 
 
764
 
 
765
class _ParamikoSSHConnection(SSHConnection):
 
766
    """An SSH connection via paramiko."""
 
767
 
 
768
    def __init__(self, channel):
 
769
        self.channel = channel
 
770
 
 
771
    def get_sock_or_pipes(self):
 
772
        return ('socket', self.channel)
 
773
 
 
774
    def close(self):
 
775
        return self.channel.close()