~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2010-08-30 22:49:20 UTC
  • mfrom: (5397.1.6 jam-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20100830224920-w9zw1vhsd5oiyljv
(vila, jam) Get PQM running correctly again (bug #626667),
        skip test_bzr_connect_to_bzr_ssh (bug #626876)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2010 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_signals():
 
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
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
 
183
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
 
184
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
 
185
 
 
186
 
 
187
class SocketAsChannelAdapter(object):
 
188
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
189
 
 
190
    def __init__(self, sock):
 
191
        self.__socket = sock
 
192
 
 
193
    def get_name(self):
 
194
        return "bzr SocketAsChannelAdapter"
 
195
 
 
196
    def send(self, data):
 
197
        return self.__socket.send(data)
 
198
 
 
199
    def recv(self, n):
 
200
        try:
 
201
            return self.__socket.recv(n)
 
202
        except socket.error, e:
 
203
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
204
                             errno.EBADF):
 
205
                # Connection has closed.  Paramiko expects an empty string in
 
206
                # this case, not an exception.
 
207
                return ''
 
208
            raise
 
209
 
 
210
    def recv_ready(self):
 
211
        # TODO: jam 20051215 this function is necessary to support the
 
212
        # pipelined() function. In reality, it probably should use
 
213
        # poll() or select() to actually return if there is data
 
214
        # available, otherwise we probably don't get any benefit
 
215
        return True
 
216
 
 
217
    def close(self):
 
218
        self.__socket.close()
 
219
 
 
220
 
 
221
class SSHVendor(object):
 
222
    """Abstract base class for SSH vendor implementations."""
 
223
 
 
224
    def connect_sftp(self, username, password, host, port):
 
225
        """Make an SSH connection, and return an SFTPClient.
 
226
 
 
227
        :param username: an ascii string
 
228
        :param password: an ascii string
 
229
        :param host: a host name as an ascii string
 
230
        :param port: a port number
 
231
        :type port: int
 
232
 
 
233
        :raises: ConnectionError if it cannot connect.
 
234
 
 
235
        :rtype: paramiko.sftp_client.SFTPClient
 
236
        """
 
237
        raise NotImplementedError(self.connect_sftp)
 
238
 
 
239
    def connect_ssh(self, username, password, host, port, command):
 
240
        """Make an SSH connection.
 
241
 
 
242
        :returns: an SSHConnection.
 
243
        """
 
244
        raise NotImplementedError(self.connect_ssh)
 
245
 
 
246
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
247
                                msg='Unable to connect to SSH host'):
 
248
        """Raise a SocketConnectionError with properly formatted host.
 
249
 
 
250
        This just unifies all the locations that try to raise ConnectionError,
 
251
        so that they format things properly.
 
252
        """
 
253
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
254
                                           orig_error=orig_error)
 
255
 
 
256
 
 
257
class LoopbackVendor(SSHVendor):
 
258
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
259
 
 
260
    def connect_sftp(self, username, password, host, port):
 
261
        sock = socket.socket()
 
262
        try:
 
263
            sock.connect((host, port))
 
264
        except socket.error, e:
 
265
            self._raise_connection_error(host, port=port, orig_error=e)
 
266
        return SFTPClient(SocketAsChannelAdapter(sock))
 
267
 
 
268
register_ssh_vendor('loopback', LoopbackVendor())
 
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
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
355
        # subprocess.  We prefer sockets to pipes because they support
 
356
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
357
        # whatever) chunks.
 
358
        try:
 
359
            my_sock, subproc_sock = socket.socketpair()
 
360
        except (AttributeError, socket.error):
 
361
            # This platform doesn't support socketpair(), so just use ordinary
 
362
            # pipes instead.
 
363
            stdin = stdout = subprocess.PIPE
 
364
            sock = None
 
365
        else:
 
366
            stdin = stdout = subproc_sock
 
367
            sock = my_sock
 
368
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
369
                                **os_specific_subprocess_params())
 
370
        return SSHSubprocessConnection(proc, sock=sock)
 
371
 
 
372
    def connect_sftp(self, username, password, host, port):
 
373
        try:
 
374
            argv = self._get_vendor_specific_argv(username, host, port,
 
375
                                                  subsystem='sftp')
 
376
            sock = self._connect(argv)
 
377
            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
 
386
            self._raise_connection_error(host, port=port, orig_error=e)
 
387
 
 
388
    def connect_ssh(self, username, password, host, port, command):
 
389
        try:
 
390
            argv = self._get_vendor_specific_argv(username, host, port,
 
391
                                                  command=command)
 
392
            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
 
401
            self._raise_connection_error(host, port=port, orig_error=e)
 
402
 
 
403
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
404
                                  command=None):
 
405
        """Returns the argument list to run the subprocess with.
 
406
 
 
407
        Exactly one of 'subsystem' and 'command' must be specified.
 
408
        """
 
409
        raise NotImplementedError(self._get_vendor_specific_argv)
 
410
 
 
411
 
 
412
class OpenSSHSubprocessVendor(SubprocessVendor):
 
413
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
414
 
 
415
    executable_path = 'ssh'
 
416
 
 
417
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
418
                                  command=None):
 
419
        args = [self.executable_path,
 
420
                '-oForwardX11=no', '-oForwardAgent=no',
 
421
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
422
                '-oNoHostAuthenticationForLocalhost=yes']
 
423
        if port is not None:
 
424
            args.extend(['-p', str(port)])
 
425
        if username is not None:
 
426
            args.extend(['-l', username])
 
427
        if subsystem is not None:
 
428
            args.extend(['-s', host, subsystem])
 
429
        else:
 
430
            args.extend([host] + command)
 
431
        return args
 
432
 
 
433
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
434
 
 
435
 
 
436
class SSHCorpSubprocessVendor(SubprocessVendor):
 
437
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
438
 
 
439
    executable_path = 'ssh'
 
440
 
 
441
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
442
                                  command=None):
 
443
        args = [self.executable_path, '-x']
 
444
        if port is not None:
 
445
            args.extend(['-p', str(port)])
 
446
        if username is not None:
 
447
            args.extend(['-l', username])
 
448
        if subsystem is not None:
 
449
            args.extend(['-s', subsystem, host])
 
450
        else:
 
451
            args.extend([host] + command)
 
452
        return args
 
453
 
 
454
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
455
 
 
456
 
 
457
class PLinkSubprocessVendor(SubprocessVendor):
 
458
    """SSH vendor that uses the 'plink' executable from Putty."""
 
459
 
 
460
    executable_path = 'plink'
 
461
 
 
462
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
463
                                  command=None):
 
464
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
465
        if port is not None:
 
466
            args.extend(['-P', str(port)])
 
467
        if username is not None:
 
468
            args.extend(['-l', username])
 
469
        if subsystem is not None:
 
470
            args.extend(['-s', host, subsystem])
 
471
        else:
 
472
            args.extend([host] + command)
 
473
        return args
 
474
 
 
475
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
476
 
 
477
 
 
478
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
479
    auth = config.AuthenticationConfig()
 
480
    # paramiko requires a username, but it might be none if nothing was
 
481
    # supplied.  If so, use the local username.
 
482
    if username is None:
 
483
        username = auth.get_user('ssh', host, port=port,
 
484
                                 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
 
495
 
 
496
    # okay, try finding id_rsa or id_dss?  (posix only)
 
497
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
498
        return
 
499
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
500
        return
 
501
 
 
502
    # If we have gotten this far, we are about to try for passwords, do an
 
503
    # auth_none check to see if it is even supported.
 
504
    supported_auth_types = []
 
505
    try:
 
506
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
507
        #    Authentication type (none) not permitted.
 
508
        # So we explicitly disable the logging level for this action
 
509
        old_level = paramiko_transport.logger.level
 
510
        paramiko_transport.logger.setLevel(logging.WARNING)
 
511
        try:
 
512
            paramiko_transport.auth_none(username)
 
513
        finally:
 
514
            paramiko_transport.logger.setLevel(old_level)
 
515
    except paramiko.BadAuthenticationType, e:
 
516
        # Supported methods are in the exception
 
517
        supported_auth_types = e.allowed_types
 
518
    except paramiko.SSHException, e:
 
519
        # Don't know what happened, but just ignore it
 
520
        pass
 
521
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
522
    # because Paramiko's auth_password method will automatically try
 
523
    # 'keyboard-interactive' auth (using the password as the response) if
 
524
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
525
    # OpenSSH servers require this.
 
526
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
527
    # requires something other than a single password, but we currently don't
 
528
    # support that.
 
529
    if ('password' not in supported_auth_types and
 
530
        'keyboard-interactive' not in supported_auth_types):
 
531
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
532
            '\n  %s@%s\nsupported auth types: %s'
 
533
            % (username, host, supported_auth_types))
 
534
 
 
535
    if password:
 
536
        try:
 
537
            paramiko_transport.auth_password(username, password)
 
538
            return
 
539
        except paramiko.SSHException, e:
 
540
            pass
 
541
 
 
542
    # give up and ask for a password
 
543
    password = auth.get_password('ssh', host, username, port=port)
 
544
    # get_password can still return None, which means we should not prompt
 
545
    if password is not None:
 
546
        try:
 
547
            paramiko_transport.auth_password(username, password)
 
548
        except paramiko.SSHException, e:
 
549
            raise errors.ConnectionError(
 
550
                'Unable to authenticate to SSH host as'
 
551
                '\n  %s@%s\n' % (username, host), e)
 
552
    else:
 
553
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
554
                                     '  %s@%s' % (username, host))
 
555
 
 
556
 
 
557
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
558
    filename = os.path.expanduser('~/.ssh/' + filename)
 
559
    try:
 
560
        key = pkey_class.from_private_key_file(filename)
 
561
        paramiko_transport.auth_publickey(username, key)
 
562
        return True
 
563
    except paramiko.PasswordRequiredException:
 
564
        password = ui.ui_factory.get_password(
 
565
            prompt='SSH %(filename)s password', filename=filename)
 
566
        try:
 
567
            key = pkey_class.from_private_key_file(filename, password)
 
568
            paramiko_transport.auth_publickey(username, key)
 
569
            return True
 
570
        except paramiko.SSHException:
 
571
            trace.mutter('SSH authentication via %s key failed.'
 
572
                         % (os.path.basename(filename),))
 
573
    except paramiko.SSHException:
 
574
        trace.mutter('SSH authentication via %s key failed.'
 
575
                     % (os.path.basename(filename),))
 
576
    except IOError:
 
577
        pass
 
578
    return False
 
579
 
 
580
 
 
581
def load_host_keys():
 
582
    """
 
583
    Load system host keys (probably doesn't work on windows) and any
 
584
    "discovered" keys from previous sessions.
 
585
    """
 
586
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
587
    try:
 
588
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
589
            os.path.expanduser('~/.ssh/known_hosts'))
 
590
    except IOError, e:
 
591
        trace.mutter('failed to load system host keys: ' + str(e))
 
592
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
593
    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))
 
597
        save_host_keys()
 
598
 
 
599
 
 
600
def save_host_keys():
 
601
    """
 
602
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
603
    """
 
604
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
605
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
606
    config.ensure_config_dir_exists()
 
607
 
 
608
    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:
 
616
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
617
 
 
618
 
 
619
def os_specific_subprocess_params():
 
620
    """Get O/S specific subprocess parameters."""
 
621
    if sys.platform == 'win32':
 
622
        # setting the process group and closing fds is not supported on
 
623
        # win32
 
624
        return {}
 
625
    else:
 
626
        # We close fds other than the pipes as the child process does not need
 
627
        # them to be open.
 
628
        #
 
629
        # We also set the child process to ignore SIGINT.  Normally the signal
 
630
        # would be sent to every process in the foreground process group, but
 
631
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
632
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
633
        # to release locks or do other cleanup over ssh before the connection
 
634
        # goes away.
 
635
        # <https://launchpad.net/products/bzr/+bug/5987>
 
636
        #
 
637
        # Running it in a separate process group is not good because then it
 
638
        # can't get non-echoed input of a password or passphrase.
 
639
        # <https://launchpad.net/products/bzr/+bug/40508>
 
640
        return {'preexec_fn': _ignore_signals,
 
641
                'close_fds': True,
 
642
                }
 
643
 
 
644
import weakref
 
645
_subproc_weakrefs = set()
 
646
 
 
647
def _close_ssh_proc(proc):
 
648
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
649
        try:
 
650
            func()
 
651
        except OSError:
 
652
            pass
 
653
 
 
654
 
 
655
class SSHConnection(object):
 
656
    """Abstract base class for SSH connections."""
 
657
 
 
658
    def get_sock_or_pipes(self):
 
659
        """Returns a (kind, io_object) pair.
 
660
 
 
661
        If kind == 'socket', then io_object is a socket.
 
662
 
 
663
        If kind == 'pipes', then io_object is a pair of file-like objects
 
664
        (read_from, write_to).
 
665
        """
 
666
        raise NotImplementedError(self.get_sock_or_pipes)
 
667
 
 
668
    def close(self):
 
669
        raise NotImplementedError(self.close)
 
670
 
 
671
 
 
672
class SSHSubprocessConnection(SSHConnection):
 
673
    """A connection to an ssh subprocess via pipes or a socket.
 
674
 
 
675
    This class is also socket-like enough to be used with
 
676
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
677
    """
 
678
 
 
679
    def __init__(self, proc, sock=None):
 
680
        """Constructor.
 
681
 
 
682
        :param proc: a subprocess.Popen
 
683
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
684
            should bzrlib's half of that socketpair.  If not passed, proc's
 
685
            stdin/out is assumed to be ordinary pipes.
 
686
        """
 
687
        self.proc = proc
 
688
        self._sock = sock
 
689
        # Add a weakref to proc that will attempt to do the same as self.close
 
690
        # to avoid leaving processes lingering indefinitely.
 
691
        def terminate(ref):
 
692
            _subproc_weakrefs.remove(ref)
 
693
            _close_ssh_proc(proc)
 
694
        _subproc_weakrefs.add(weakref.ref(self, terminate))
 
695
 
 
696
    def send(self, data):
 
697
        if self._sock is not None:
 
698
            return self._sock.send(data)
 
699
        else:
 
700
            return os.write(self.proc.stdin.fileno(), data)
 
701
 
 
702
    def recv(self, count):
 
703
        if self._sock is not None:
 
704
            return self._sock.recv(count)
 
705
        else:
 
706
            return os.read(self.proc.stdout.fileno(), count)
 
707
 
 
708
    def close(self):
 
709
        _close_ssh_proc(self.proc)
 
710
 
 
711
    def get_sock_or_pipes(self):
 
712
        if self._sock is not None:
 
713
            return 'socket', self._sock
 
714
        else:
 
715
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
716
 
 
717
 
 
718
class _ParamikoSSHConnection(SSHConnection):
 
719
    """An SSH connection via paramiko."""
 
720
 
 
721
    def __init__(self, channel):
 
722
        self.channel = channel
 
723
 
 
724
    def get_sock_or_pipes(self):
 
725
        return ('socket', self.channel)
 
726
 
 
727
    def close(self):
 
728
        return self.channel.close()
 
729
 
 
730