/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: Jelmer Vernooij
  • Date: 2020-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

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