~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robert Collins
  • Date: 2006-07-20 13:00:31 UTC
  • mto: (1852.9.1 Tree.compare().)
  • mto: This revision was merged to the branch mainline in revision 1890.
  • Revision ID: robertc@robertcollins.net-20060720130031-d26103a427ea10f3
StartĀ treeĀ implementationĀ tests.

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