~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Ian Clatworthy
  • Date: 2009-09-09 02:30:32 UTC
  • mto: (4634.37.2 prepare-2.0)
  • mto: This revision was merged to the branch mainline in revision 4689.
  • Revision ID: ian.clatworthy@canonical.com-20090909023032-xz7mkx0u4ppe1yvr
proper Contents panel in bzr-es.chm & bzr-ru.chm

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
 
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
17
 
 
18
"""Foundation SSH support for SFTP and smart server."""
 
19
 
 
20
import errno
 
21
import getpass
 
22
import logging
 
23
import os
 
24
import socket
 
25
import subprocess
 
26
import sys
 
27
 
 
28
from bzrlib import (
 
29
    config,
 
30
    errors,
 
31
    osutils,
 
32
    trace,
 
33
    ui,
 
34
    )
 
35
 
 
36
try:
 
37
    import paramiko
 
38
except ImportError, e:
 
39
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
 
40
    # access
 
41
    paramiko = None
 
42
else:
 
43
    from paramiko.sftp_client import SFTPClient
 
44
 
 
45
 
 
46
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))
 
57
 
 
58
 
 
59
class SSHVendorManager(object):
 
60
    """Manager for manage SSH vendors."""
 
61
 
 
62
    # Note, although at first sign the class interface seems similar to
 
63
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
64
    # the Registry because the class just has "get()" interface instead of the
 
65
    # Registry's "get(key)".
 
66
 
 
67
    def __init__(self):
 
68
        self._ssh_vendors = {}
 
69
        self._cached_ssh_vendor = None
 
70
        self._default_ssh_vendor = None
 
71
 
 
72
    def register_default_vendor(self, vendor):
 
73
        """Register default SSH vendor."""
 
74
        self._default_ssh_vendor = vendor
 
75
 
 
76
    def register_vendor(self, name, vendor):
 
77
        """Register new SSH vendor by name."""
 
78
        self._ssh_vendors[name] = vendor
 
79
 
 
80
    def clear_cache(self):
 
81
        """Clear previously cached lookup result."""
 
82
        self._cached_ssh_vendor = None
 
83
 
 
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']
 
94
            try:
 
95
                vendor = self._ssh_vendors[vendor_name]
 
96
            except KeyError:
 
97
                raise errors.UnknownSSH(vendor_name)
 
98
            return vendor
 
99
        return None
 
100
 
 
101
    def _get_ssh_version_string(self, args):
 
102
        """Return SSH version string from the subprocess."""
 
103
        try:
 
104
            p = subprocess.Popen(args,
 
105
                                 stdout=subprocess.PIPE,
 
106
                                 stderr=subprocess.PIPE,
 
107
                                 **os_specific_subprocess_params())
 
108
            stdout, stderr = p.communicate()
 
109
        except OSError:
 
110
            stdout = stderr = ''
 
111
        return stdout + stderr
 
112
 
 
113
    def _get_vendor_by_version_string(self, version, args):
 
114
        """Return the vendor or None based on output from the subprocess.
 
115
 
 
116
        :param version: The output of 'ssh -V' like command.
 
117
        :param args: Command line that was run.
 
118
        """
 
119
        vendor = None
 
120
        if 'OpenSSH' in version:
 
121
            trace.mutter('ssh implementation is OpenSSH')
 
122
            vendor = OpenSSHSubprocessVendor()
 
123
        elif 'SSH Secure Shell' in version:
 
124
            trace.mutter('ssh implementation is SSH Corp.')
 
125
            vendor = SSHCorpSubprocessVendor()
 
126
        # Auto-detect of plink vendor disabled, on Windows recommended
 
127
        # default ssh-client is paramiko
 
128
        # see https://bugs.launchpad.net/bugs/414743
 
129
        #~elif 'plink' in version and args[0] == 'plink':
 
130
        #~    # Checking if "plink" was the executed argument as Windows
 
131
        #~    # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
132
        #~    # version.  See https://bugs.launchpad.net/bzr/+bug/107155
 
133
        #~    trace.mutter("ssh implementation is Putty's plink.")
 
134
        #~    vendor = PLinkSubprocessVendor()
 
135
        return vendor
 
136
 
 
137
    def _get_vendor_by_inspection(self):
 
138
        """Return the vendor or None by checking for known SSH implementations."""
 
139
        for args in (['ssh', '-V'], ['plink', '-V']):
 
140
            version = self._get_ssh_version_string(args)
 
141
            vendor = self._get_vendor_by_version_string(version, args)
 
142
            if vendor is not None:
 
143
                return vendor
 
144
        return None
 
145
 
 
146
    def get_vendor(self, environment=None):
 
147
        """Find out what version of SSH is on the system.
 
148
 
 
149
        :raises SSHVendorNotFound: if no any SSH vendor is found
 
150
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
151
                            unknown vendor name
 
152
        """
 
153
        if self._cached_ssh_vendor is None:
 
154
            vendor = self._get_vendor_by_environment(environment)
 
155
            if vendor is None:
 
156
                vendor = self._get_vendor_by_inspection()
 
157
                if vendor is None:
 
158
                    trace.mutter('falling back to default implementation')
 
159
                    vendor = self._default_ssh_vendor
 
160
                    if vendor is None:
 
161
                        raise errors.SSHVendorNotFound()
 
162
            self._cached_ssh_vendor = vendor
 
163
        return self._cached_ssh_vendor
 
164
 
 
165
_ssh_vendor_manager = SSHVendorManager()
 
166
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
 
167
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
 
168
register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
169
 
 
170
 
 
171
def _ignore_sigint():
 
172
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
173
    # doesn't handle it itself.
 
174
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
175
    import signal
 
176
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
177
 
 
178
 
 
179
class SocketAsChannelAdapter(object):
 
180
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
181
 
 
182
    def __init__(self, sock):
 
183
        self.__socket = sock
 
184
 
 
185
    def get_name(self):
 
186
        return "bzr SocketAsChannelAdapter"
 
187
 
 
188
    def send(self, data):
 
189
        return self.__socket.send(data)
 
190
 
 
191
    def recv(self, n):
 
192
        try:
 
193
            return self.__socket.recv(n)
 
194
        except socket.error, e:
 
195
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
196
                             errno.EBADF):
 
197
                # Connection has closed.  Paramiko expects an empty string in
 
198
                # this case, not an exception.
 
199
                return ''
 
200
            raise
 
201
 
 
202
    def recv_ready(self):
 
203
        # TODO: jam 20051215 this function is necessary to support the
 
204
        # pipelined() function. In reality, it probably should use
 
205
        # poll() or select() to actually return if there is data
 
206
        # available, otherwise we probably don't get any benefit
 
207
        return True
 
208
 
 
209
    def close(self):
 
210
        self.__socket.close()
 
211
 
 
212
 
 
213
class SSHVendor(object):
 
214
    """Abstract base class for SSH vendor implementations."""
 
215
 
 
216
    def connect_sftp(self, username, password, host, port):
 
217
        """Make an SSH connection, and return an SFTPClient.
 
218
 
 
219
        :param username: an ascii string
 
220
        :param password: an ascii string
 
221
        :param host: a host name as an ascii string
 
222
        :param port: a port number
 
223
        :type port: int
 
224
 
 
225
        :raises: ConnectionError if it cannot connect.
 
226
 
 
227
        :rtype: paramiko.sftp_client.SFTPClient
 
228
        """
 
229
        raise NotImplementedError(self.connect_sftp)
 
230
 
 
231
    def connect_ssh(self, username, password, host, port, command):
 
232
        """Make an SSH connection.
 
233
 
 
234
        :returns: something with a `close` method, and a `get_filelike_channels`
 
235
            method that returns a pair of (read, write) filelike objects.
 
236
        """
 
237
        raise NotImplementedError(self.connect_ssh)
 
238
 
 
239
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
240
                                msg='Unable to connect to SSH host'):
 
241
        """Raise a SocketConnectionError with properly formatted host.
 
242
 
 
243
        This just unifies all the locations that try to raise ConnectionError,
 
244
        so that they format things properly.
 
245
        """
 
246
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
247
                                           orig_error=orig_error)
 
248
 
 
249
 
 
250
class LoopbackVendor(SSHVendor):
 
251
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
252
 
 
253
    def connect_sftp(self, username, password, host, port):
 
254
        sock = socket.socket()
 
255
        try:
 
256
            sock.connect((host, port))
 
257
        except socket.error, e:
 
258
            self._raise_connection_error(host, port=port, orig_error=e)
 
259
        return SFTPClient(SocketAsChannelAdapter(sock))
 
260
 
 
261
register_ssh_vendor('loopback', LoopbackVendor())
 
262
 
 
263
 
 
264
class _ParamikoSSHConnection(object):
 
265
    def __init__(self, channel):
 
266
        self.channel = channel
 
267
 
 
268
    def get_filelike_channels(self):
 
269
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
270
 
 
271
    def close(self):
 
272
        return self.channel.close()
 
273
 
 
274
 
 
275
class ParamikoVendor(SSHVendor):
 
276
    """Vendor that uses paramiko."""
 
277
 
 
278
    def _connect(self, username, password, host, port):
 
279
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
280
 
 
281
        load_host_keys()
 
282
 
 
283
        try:
 
284
            t = paramiko.Transport((host, port or 22))
 
285
            t.set_log_channel('bzr.paramiko')
 
286
            t.start_client()
 
287
        except (paramiko.SSHException, socket.error), e:
 
288
            self._raise_connection_error(host, port=port, orig_error=e)
 
289
 
 
290
        server_key = t.get_remote_server_key()
 
291
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
292
        keytype = server_key.get_name()
 
293
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
 
294
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
295
            our_server_key_hex = paramiko.util.hexify(
 
296
                our_server_key.get_fingerprint())
 
297
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
298
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
299
            our_server_key_hex = paramiko.util.hexify(
 
300
                our_server_key.get_fingerprint())
 
301
        else:
 
302
            trace.warning('Adding %s host key for %s: %s'
 
303
                          % (keytype, host, server_key_hex))
 
304
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
305
            if add is not None: # paramiko >= 1.X.X
 
306
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
307
            else:
 
308
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
309
            our_server_key = server_key
 
310
            our_server_key_hex = paramiko.util.hexify(
 
311
                our_server_key.get_fingerprint())
 
312
            save_host_keys()
 
313
        if server_key != our_server_key:
 
314
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
315
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
316
            raise errors.TransportError(
 
317
                'Host keys for %s do not match!  %s != %s' %
 
318
                (host, our_server_key_hex, server_key_hex),
 
319
                ['Try editing %s or %s' % (filename1, filename2)])
 
320
 
 
321
        _paramiko_auth(username, password, host, port, t)
 
322
        return t
 
323
 
 
324
    def connect_sftp(self, username, password, host, port):
 
325
        t = self._connect(username, password, host, port)
 
326
        try:
 
327
            return t.open_sftp_client()
 
328
        except paramiko.SSHException, e:
 
329
            self._raise_connection_error(host, port=port, orig_error=e,
 
330
                                         msg='Unable to start sftp client')
 
331
 
 
332
    def connect_ssh(self, username, password, host, port, command):
 
333
        t = self._connect(username, password, host, port)
 
334
        try:
 
335
            channel = t.open_session()
 
336
            cmdline = ' '.join(command)
 
337
            channel.exec_command(cmdline)
 
338
            return _ParamikoSSHConnection(channel)
 
339
        except paramiko.SSHException, e:
 
340
            self._raise_connection_error(host, port=port, orig_error=e,
 
341
                                         msg='Unable to invoke remote bzr')
 
342
 
 
343
if paramiko is not None:
 
344
    vendor = ParamikoVendor()
 
345
    register_ssh_vendor('paramiko', vendor)
 
346
    register_ssh_vendor('none', vendor)
 
347
    register_default_ssh_vendor(vendor)
 
348
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
349
    del vendor
 
350
else:
 
351
    _sftp_connection_errors = (EOFError,)
 
352
 
 
353
 
 
354
class SubprocessVendor(SSHVendor):
 
355
    """Abstract base class for vendors that use pipes to a subprocess."""
 
356
 
 
357
    def _connect(self, argv):
 
358
        proc = subprocess.Popen(argv,
 
359
                                stdin=subprocess.PIPE,
 
360
                                stdout=subprocess.PIPE,
 
361
                                **os_specific_subprocess_params())
 
362
        return SSHSubprocess(proc)
 
363
 
 
364
    def connect_sftp(self, username, password, host, port):
 
365
        try:
 
366
            argv = self._get_vendor_specific_argv(username, host, port,
 
367
                                                  subsystem='sftp')
 
368
            sock = self._connect(argv)
 
369
            return SFTPClient(SocketAsChannelAdapter(sock))
 
370
        except _sftp_connection_errors, e:
 
371
            self._raise_connection_error(host, port=port, orig_error=e)
 
372
        except (OSError, IOError), e:
 
373
            # If the machine is fast enough, ssh can actually exit
 
374
            # before we try and send it the sftp request, which
 
375
            # raises a Broken Pipe
 
376
            if e.errno not in (errno.EPIPE,):
 
377
                raise
 
378
            self._raise_connection_error(host, port=port, orig_error=e)
 
379
 
 
380
    def connect_ssh(self, username, password, host, port, command):
 
381
        try:
 
382
            argv = self._get_vendor_specific_argv(username, host, port,
 
383
                                                  command=command)
 
384
            return self._connect(argv)
 
385
        except (EOFError), e:
 
386
            self._raise_connection_error(host, port=port, orig_error=e)
 
387
        except (OSError, IOError), e:
 
388
            # If the machine is fast enough, ssh can actually exit
 
389
            # before we try and send it the sftp request, which
 
390
            # raises a Broken Pipe
 
391
            if e.errno not in (errno.EPIPE,):
 
392
                raise
 
393
            self._raise_connection_error(host, port=port, orig_error=e)
 
394
 
 
395
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
396
                                  command=None):
 
397
        """Returns the argument list to run the subprocess with.
 
398
 
 
399
        Exactly one of 'subsystem' and 'command' must be specified.
 
400
        """
 
401
        raise NotImplementedError(self._get_vendor_specific_argv)
 
402
 
 
403
 
 
404
class OpenSSHSubprocessVendor(SubprocessVendor):
 
405
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
406
 
 
407
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
408
                                  command=None):
 
409
        args = ['ssh',
 
410
                '-oForwardX11=no', '-oForwardAgent=no',
 
411
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
412
                '-oNoHostAuthenticationForLocalhost=yes']
 
413
        if port is not None:
 
414
            args.extend(['-p', str(port)])
 
415
        if username is not None:
 
416
            args.extend(['-l', username])
 
417
        if subsystem is not None:
 
418
            args.extend(['-s', host, subsystem])
 
419
        else:
 
420
            args.extend([host] + command)
 
421
        return args
 
422
 
 
423
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
424
 
 
425
 
 
426
class SSHCorpSubprocessVendor(SubprocessVendor):
 
427
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
428
 
 
429
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
430
                                  command=None):
 
431
        args = ['ssh', '-x']
 
432
        if port is not None:
 
433
            args.extend(['-p', str(port)])
 
434
        if username is not None:
 
435
            args.extend(['-l', username])
 
436
        if subsystem is not None:
 
437
            args.extend(['-s', subsystem, host])
 
438
        else:
 
439
            args.extend([host] + command)
 
440
        return args
 
441
 
 
442
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
443
 
 
444
 
 
445
class PLinkSubprocessVendor(SubprocessVendor):
 
446
    """SSH vendor that uses the 'plink' executable from Putty."""
 
447
 
 
448
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
449
                                  command=None):
 
450
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
451
        if port is not None:
 
452
            args.extend(['-P', str(port)])
 
453
        if username is not None:
 
454
            args.extend(['-l', username])
 
455
        if subsystem is not None:
 
456
            args.extend(['-s', host, subsystem])
 
457
        else:
 
458
            args.extend([host] + command)
 
459
        return args
 
460
 
 
461
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
462
 
 
463
 
 
464
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
465
    auth = config.AuthenticationConfig()
 
466
    # paramiko requires a username, but it might be none if nothing was
 
467
    # supplied.  If so, use the local username.
 
468
    if username is None:
 
469
        username = auth.get_user('ssh', host, port=port,
 
470
                                 default=getpass.getuser())
 
471
    if _use_ssh_agent:
 
472
        agent = paramiko.Agent()
 
473
        for key in agent.get_keys():
 
474
            trace.mutter('Trying SSH agent key %s'
 
475
                         % paramiko.util.hexify(key.get_fingerprint()))
 
476
            try:
 
477
                paramiko_transport.auth_publickey(username, key)
 
478
                return
 
479
            except paramiko.SSHException, e:
 
480
                pass
 
481
 
 
482
    # okay, try finding id_rsa or id_dss?  (posix only)
 
483
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
484
        return
 
485
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
486
        return
 
487
 
 
488
    # If we have gotten this far, we are about to try for passwords, do an
 
489
    # auth_none check to see if it is even supported.
 
490
    supported_auth_types = []
 
491
    try:
 
492
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
493
        #    Authentication type (none) not permitted.
 
494
        # So we explicitly disable the logging level for this action
 
495
        old_level = paramiko_transport.logger.level
 
496
        paramiko_transport.logger.setLevel(logging.WARNING)
 
497
        try:
 
498
            paramiko_transport.auth_none(username)
 
499
        finally:
 
500
            paramiko_transport.logger.setLevel(old_level)
 
501
    except paramiko.BadAuthenticationType, e:
 
502
        # Supported methods are in the exception
 
503
        supported_auth_types = e.allowed_types
 
504
    except paramiko.SSHException, e:
 
505
        # Don't know what happened, but just ignore it
 
506
        pass
 
507
    if 'password' not in supported_auth_types:
 
508
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
509
            '\n  %s@%s\nsupported auth types: %s'
 
510
            % (username, host, supported_auth_types))
 
511
 
 
512
    if password:
 
513
        try:
 
514
            paramiko_transport.auth_password(username, password)
 
515
            return
 
516
        except paramiko.SSHException, e:
 
517
            pass
 
518
 
 
519
    # give up and ask for a password
 
520
    password = auth.get_password('ssh', host, username, port=port)
 
521
    # get_password can still return None, which means we should not prompt
 
522
    if password is not None:
 
523
        try:
 
524
            paramiko_transport.auth_password(username, password)
 
525
        except paramiko.SSHException, e:
 
526
            raise errors.ConnectionError(
 
527
                'Unable to authenticate to SSH host as'
 
528
                '\n  %s@%s\n' % (username, host), e)
 
529
    else:
 
530
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
531
                                     '  %s@%s' % (username, host))
 
532
 
 
533
 
 
534
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
535
    filename = os.path.expanduser('~/.ssh/' + filename)
 
536
    try:
 
537
        key = pkey_class.from_private_key_file(filename)
 
538
        paramiko_transport.auth_publickey(username, key)
 
539
        return True
 
540
    except paramiko.PasswordRequiredException:
 
541
        password = ui.ui_factory.get_password(
 
542
            prompt='SSH %(filename)s password', filename=filename)
 
543
        try:
 
544
            key = pkey_class.from_private_key_file(filename, password)
 
545
            paramiko_transport.auth_publickey(username, key)
 
546
            return True
 
547
        except paramiko.SSHException:
 
548
            trace.mutter('SSH authentication via %s key failed.'
 
549
                         % (os.path.basename(filename),))
 
550
    except paramiko.SSHException:
 
551
        trace.mutter('SSH authentication via %s key failed.'
 
552
                     % (os.path.basename(filename),))
 
553
    except IOError:
 
554
        pass
 
555
    return False
 
556
 
 
557
 
 
558
def load_host_keys():
 
559
    """
 
560
    Load system host keys (probably doesn't work on windows) and any
 
561
    "discovered" keys from previous sessions.
 
562
    """
 
563
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
564
    try:
 
565
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
566
            os.path.expanduser('~/.ssh/known_hosts'))
 
567
    except IOError, e:
 
568
        trace.mutter('failed to load system host keys: ' + str(e))
 
569
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
570
    try:
 
571
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
572
    except IOError, e:
 
573
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
574
        save_host_keys()
 
575
 
 
576
 
 
577
def save_host_keys():
 
578
    """
 
579
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
580
    """
 
581
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
582
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
583
    config.ensure_config_dir_exists()
 
584
 
 
585
    try:
 
586
        f = open(bzr_hostkey_path, 'w')
 
587
        f.write('# SSH host keys collected by bzr\n')
 
588
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
589
            for keytype, key in keys.iteritems():
 
590
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
591
        f.close()
 
592
    except IOError, e:
 
593
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
594
 
 
595
 
 
596
def os_specific_subprocess_params():
 
597
    """Get O/S specific subprocess parameters."""
 
598
    if sys.platform == 'win32':
 
599
        # setting the process group and closing fds is not supported on
 
600
        # win32
 
601
        return {}
 
602
    else:
 
603
        # We close fds other than the pipes as the child process does not need
 
604
        # them to be open.
 
605
        #
 
606
        # We also set the child process to ignore SIGINT.  Normally the signal
 
607
        # would be sent to every process in the foreground process group, but
 
608
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
609
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
610
        # to release locks or do other cleanup over ssh before the connection
 
611
        # goes away.
 
612
        # <https://launchpad.net/products/bzr/+bug/5987>
 
613
        #
 
614
        # Running it in a separate process group is not good because then it
 
615
        # can't get non-echoed input of a password or passphrase.
 
616
        # <https://launchpad.net/products/bzr/+bug/40508>
 
617
        return {'preexec_fn': _ignore_sigint,
 
618
                'close_fds': True,
 
619
                }
 
620
 
 
621
 
 
622
class SSHSubprocess(object):
 
623
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
624
 
 
625
    def __init__(self, proc):
 
626
        self.proc = proc
 
627
 
 
628
    def send(self, data):
 
629
        return os.write(self.proc.stdin.fileno(), data)
 
630
 
 
631
    def recv(self, count):
 
632
        return os.read(self.proc.stdout.fileno(), count)
 
633
 
 
634
    def close(self):
 
635
        self.proc.stdin.close()
 
636
        self.proc.stdout.close()
 
637
        self.proc.wait()
 
638
 
 
639
    def get_filelike_channels(self):
 
640
        return (self.proc.stdout, self.proc.stdin)
 
641