~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Eric Holmberg
  • Date: 2008-05-06 15:02:27 UTC
  • mto: This revision was merged to the branch mainline in revision 3449.
  • Revision ID: eholmberg@arrow.com-20080506150227-l3arq1yntdvnoxum
Fix for Bug #215426 in which bzr can cause a MemoryError in socket.recv while
downloading large packs over http.  This patch limits the request size for
socket.recv to avoid this problem.

Changes:
Added mock file object bzrlib.tests.file_utils.
Added new parameters to bzrlib.osutils.pumpfile.
Added unit tests for bzrlib.osutils.pumpfile.
Added usage of bzrlib.osutils.pumpfile to bzrlib.transport.http.response.

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