~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

(vila) Fix bzrlib.tests.test_gpg.TestVerify.test_verify_revoked_signature
 with recent versions of gpg. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 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
from __future__ import absolute_import
 
21
 
 
22
import errno
 
23
import getpass
 
24
import logging
 
25
import os
 
26
import socket
 
27
import subprocess
 
28
import sys
 
29
from binascii import hexlify
 
30
 
 
31
from bzrlib import (
 
32
    config,
 
33
    errors,
 
34
    osutils,
 
35
    trace,
 
36
    ui,
 
37
    )
 
38
 
 
39
try:
 
40
    import paramiko
 
41
except ImportError, e:
 
42
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
 
43
    # access
 
44
    paramiko = None
 
45
else:
 
46
    from paramiko.sftp_client import SFTPClient
 
47
 
 
48
 
 
49
SYSTEM_HOSTKEYS = {}
 
50
BZR_HOSTKEYS = {}
 
51
 
 
52
 
 
53
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
54
 
 
55
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
 
56
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
57
# so we get an AttributeError exception. So we will not try to
 
58
# connect to an agent if we are on win32 and using Paramiko older than 1.6
 
59
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
60
 
 
61
 
 
62
class SSHVendorManager(object):
 
63
    """Manager for manage SSH vendors."""
 
64
 
 
65
    # Note, although at first sign the class interface seems similar to
 
66
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
67
    # the Registry because the class just has "get()" interface instead of the
 
68
    # Registry's "get(key)".
 
69
 
 
70
    def __init__(self):
 
71
        self._ssh_vendors = {}
 
72
        self._cached_ssh_vendor = None
 
73
        self._default_ssh_vendor = None
 
74
 
 
75
    def register_default_vendor(self, vendor):
 
76
        """Register default SSH vendor."""
 
77
        self._default_ssh_vendor = vendor
 
78
 
 
79
    def register_vendor(self, name, vendor):
 
80
        """Register new SSH vendor by name."""
 
81
        self._ssh_vendors[name] = vendor
 
82
 
 
83
    def clear_cache(self):
 
84
        """Clear previously cached lookup result."""
 
85
        self._cached_ssh_vendor = None
 
86
 
 
87
    def _get_vendor_by_environment(self, environment=None):
 
88
        """Return the vendor or None based on BZR_SSH environment variable.
 
89
 
 
90
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
91
                            unknown vendor name
 
92
        """
 
93
        if environment is None:
 
94
            environment = os.environ
 
95
        if 'BZR_SSH' in environment:
 
96
            vendor_name = environment['BZR_SSH']
 
97
            try:
 
98
                vendor = self._ssh_vendors[vendor_name]
 
99
            except KeyError:
 
100
                vendor = self._get_vendor_from_path(vendor_name)
 
101
                if vendor is None:
 
102
                    raise errors.UnknownSSH(vendor_name)
 
103
                vendor.executable_path = vendor_name
 
104
            return vendor
 
105
        return None
 
106
 
 
107
    def _get_ssh_version_string(self, args):
 
108
        """Return SSH version string from the subprocess."""
 
109
        try:
 
110
            p = subprocess.Popen(args,
 
111
                                 stdout=subprocess.PIPE,
 
112
                                 stderr=subprocess.PIPE,
 
113
                                 **os_specific_subprocess_params())
 
114
            stdout, stderr = p.communicate()
 
115
        except OSError:
 
116
            stdout = stderr = ''
 
117
        return stdout + stderr
 
118
 
 
119
    def _get_vendor_by_version_string(self, version, progname):
 
120
        """Return the vendor or None based on output from the subprocess.
 
121
 
 
122
        :param version: The output of 'ssh -V' like command.
 
123
        :param args: Command line that was run.
 
124
        """
 
125
        vendor = None
 
126
        if 'OpenSSH' in version:
 
127
            trace.mutter('ssh implementation is OpenSSH')
 
128
            vendor = OpenSSHSubprocessVendor()
 
129
        elif 'SSH Secure Shell' in version:
 
130
            trace.mutter('ssh implementation is SSH Corp.')
 
131
            vendor = SSHCorpSubprocessVendor()
 
132
        elif 'lsh' in version:
 
133
            trace.mutter('ssh implementation is GNU lsh.')
 
134
            vendor = LSHSubprocessVendor()
 
135
        # As plink user prompts are not handled currently, don't auto-detect
 
136
        # it by inspection below, but keep this vendor detection for if a path
 
137
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
138
        elif 'plink' in version and progname == 'plink':
 
139
            # Checking if "plink" was the executed argument as Windows
 
140
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
 
141
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
 
142
            trace.mutter("ssh implementation is Putty's plink.")
 
143
            vendor = PLinkSubprocessVendor()
 
144
        return vendor
 
145
 
 
146
    def _get_vendor_by_inspection(self):
 
147
        """Return the vendor or None by checking for known SSH implementations."""
 
148
        version = self._get_ssh_version_string(['ssh', '-V'])
 
149
        return self._get_vendor_by_version_string(version, "ssh")
 
150
 
 
151
    def _get_vendor_from_path(self, path):
 
152
        """Return the vendor or None using the program at the given path"""
 
153
        version = self._get_ssh_version_string([path, '-V'])
 
154
        return self._get_vendor_by_version_string(version, 
 
155
            os.path.splitext(os.path.basename(path))[0])
 
156
 
 
157
    def get_vendor(self, environment=None):
 
158
        """Find out what version of SSH is on the system.
 
159
 
 
160
        :raises SSHVendorNotFound: if no any SSH vendor is found
 
161
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
162
                            unknown vendor name
 
163
        """
 
164
        if self._cached_ssh_vendor is None:
 
165
            vendor = self._get_vendor_by_environment(environment)
 
166
            if vendor is None:
 
167
                vendor = self._get_vendor_by_inspection()
 
168
                if vendor is None:
 
169
                    trace.mutter('falling back to default implementation')
 
170
                    vendor = self._default_ssh_vendor
 
171
                    if vendor is None:
 
172
                        raise errors.SSHVendorNotFound()
 
173
            self._cached_ssh_vendor = vendor
 
174
        return self._cached_ssh_vendor
 
175
 
 
176
_ssh_vendor_manager = SSHVendorManager()
 
177
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
 
178
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
 
179
register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
180
 
 
181
 
 
182
def _ignore_signals():
 
183
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
184
    # doesn't handle it itself.
 
185
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
186
    import signal
 
187
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
188
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
 
189
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
 
190
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
 
191
 
 
192
 
 
193
class SocketAsChannelAdapter(object):
 
194
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
195
 
 
196
    def __init__(self, sock):
 
197
        self.__socket = sock
 
198
 
 
199
    def get_name(self):
 
200
        return "bzr SocketAsChannelAdapter"
 
201
 
 
202
    def send(self, data):
 
203
        return self.__socket.send(data)
 
204
 
 
205
    def recv(self, n):
 
206
        try:
 
207
            return self.__socket.recv(n)
 
208
        except socket.error, e:
 
209
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
210
                             errno.EBADF):
 
211
                # Connection has closed.  Paramiko expects an empty string in
 
212
                # this case, not an exception.
 
213
                return ''
 
214
            raise
 
215
 
 
216
    def recv_ready(self):
 
217
        # TODO: jam 20051215 this function is necessary to support the
 
218
        # pipelined() function. In reality, it probably should use
 
219
        # poll() or select() to actually return if there is data
 
220
        # available, otherwise we probably don't get any benefit
 
221
        return True
 
222
 
 
223
    def close(self):
 
224
        self.__socket.close()
 
225
 
 
226
 
 
227
class SSHVendor(object):
 
228
    """Abstract base class for SSH vendor implementations."""
 
229
 
 
230
    def connect_sftp(self, username, password, host, port):
 
231
        """Make an SSH connection, and return an SFTPClient.
 
232
 
 
233
        :param username: an ascii string
 
234
        :param password: an ascii string
 
235
        :param host: a host name as an ascii string
 
236
        :param port: a port number
 
237
        :type port: int
 
238
 
 
239
        :raises: ConnectionError if it cannot connect.
 
240
 
 
241
        :rtype: paramiko.sftp_client.SFTPClient
 
242
        """
 
243
        raise NotImplementedError(self.connect_sftp)
 
244
 
 
245
    def connect_ssh(self, username, password, host, port, command):
 
246
        """Make an SSH connection.
 
247
 
 
248
        :returns: an SSHConnection.
 
249
        """
 
250
        raise NotImplementedError(self.connect_ssh)
 
251
 
 
252
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
253
                                msg='Unable to connect to SSH host'):
 
254
        """Raise a SocketConnectionError with properly formatted host.
 
255
 
 
256
        This just unifies all the locations that try to raise ConnectionError,
 
257
        so that they format things properly.
 
258
        """
 
259
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
260
                                           orig_error=orig_error)
 
261
 
 
262
 
 
263
class LoopbackVendor(SSHVendor):
 
264
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
265
 
 
266
    def connect_sftp(self, username, password, host, port):
 
267
        sock = socket.socket()
 
268
        try:
 
269
            sock.connect((host, port))
 
270
        except socket.error, e:
 
271
            self._raise_connection_error(host, port=port, orig_error=e)
 
272
        return SFTPClient(SocketAsChannelAdapter(sock))
 
273
 
 
274
register_ssh_vendor('loopback', LoopbackVendor())
 
275
 
 
276
 
 
277
class ParamikoVendor(SSHVendor):
 
278
    """Vendor that uses paramiko."""
 
279
 
 
280
    def _hexify(self, s):
 
281
        return hexlify(s).upper()
 
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 = self._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 = self._hexify(our_server_key.get_fingerprint())
 
301
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
302
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
303
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
304
        else:
 
305
            trace.warning('Adding %s host key for %s: %s'
 
306
                          % (keytype, host, server_key_hex))
 
307
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
308
            if add is not None: # paramiko >= 1.X.X
 
309
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
310
            else:
 
311
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
312
            our_server_key = server_key
 
313
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
314
            save_host_keys()
 
315
        if server_key != our_server_key:
 
316
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
317
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
318
            raise errors.TransportError(
 
319
                'Host keys for %s do not match!  %s != %s' %
 
320
                (host, our_server_key_hex, server_key_hex),
 
321
                ['Try editing %s or %s' % (filename1, filename2)])
 
322
 
 
323
        _paramiko_auth(username, password, host, port, t)
 
324
        return t
 
325
 
 
326
    def connect_sftp(self, username, password, host, port):
 
327
        t = self._connect(username, password, host, port)
 
328
        try:
 
329
            return t.open_sftp_client()
 
330
        except paramiko.SSHException, e:
 
331
            self._raise_connection_error(host, port=port, orig_error=e,
 
332
                                         msg='Unable to start sftp client')
 
333
 
 
334
    def connect_ssh(self, username, password, host, port, command):
 
335
        t = self._connect(username, password, host, port)
 
336
        try:
 
337
            channel = t.open_session()
 
338
            cmdline = ' '.join(command)
 
339
            channel.exec_command(cmdline)
 
340
            return _ParamikoSSHConnection(channel)
 
341
        except paramiko.SSHException, e:
 
342
            self._raise_connection_error(host, port=port, orig_error=e,
 
343
                                         msg='Unable to invoke remote bzr')
 
344
 
 
345
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
 
346
if paramiko is not None:
 
347
    vendor = ParamikoVendor()
 
348
    register_ssh_vendor('paramiko', vendor)
 
349
    register_ssh_vendor('none', vendor)
 
350
    register_default_ssh_vendor(vendor)
 
351
    _ssh_connection_errors += (paramiko.SSHException,)
 
352
    del vendor
 
353
 
 
354
 
 
355
class SubprocessVendor(SSHVendor):
 
356
    """Abstract base class for vendors that use pipes to a subprocess."""
 
357
 
 
358
    # In general stderr should be inherited from the parent process so prompts
 
359
    # are visible on the terminal. This can be overriden to another file for
 
360
    # tests, but beware of using PIPE which may hang due to not being read.
 
361
    _stderr_target = None
 
362
 
 
363
    def _connect(self, argv):
 
364
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
365
        # subprocess.  We prefer sockets to pipes because they support
 
366
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
367
        # whatever) chunks.
 
368
        try:
 
369
            my_sock, subproc_sock = socket.socketpair()
 
370
            osutils.set_fd_cloexec(my_sock)
 
371
        except (AttributeError, socket.error):
 
372
            # This platform doesn't support socketpair(), so just use ordinary
 
373
            # pipes instead.
 
374
            stdin = stdout = subprocess.PIPE
 
375
            my_sock, subproc_sock = None, None
 
376
        else:
 
377
            stdin = stdout = subproc_sock
 
378
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
379
                                stderr=self._stderr_target,
 
380
                                **os_specific_subprocess_params())
 
381
        if subproc_sock is not None:
 
382
            subproc_sock.close()
 
383
        return SSHSubprocessConnection(proc, sock=my_sock)
 
384
 
 
385
    def connect_sftp(self, username, password, host, port):
 
386
        try:
 
387
            argv = self._get_vendor_specific_argv(username, host, port,
 
388
                                                  subsystem='sftp')
 
389
            sock = self._connect(argv)
 
390
            return SFTPClient(SocketAsChannelAdapter(sock))
 
391
        except _ssh_connection_errors, e:
 
392
            self._raise_connection_error(host, port=port, orig_error=e)
 
393
 
 
394
    def connect_ssh(self, username, password, host, port, command):
 
395
        try:
 
396
            argv = self._get_vendor_specific_argv(username, host, port,
 
397
                                                  command=command)
 
398
            return self._connect(argv)
 
399
        except _ssh_connection_errors, e:
 
400
            self._raise_connection_error(host, port=port, orig_error=e)
 
401
 
 
402
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
403
                                  command=None):
 
404
        """Returns the argument list to run the subprocess with.
 
405
 
 
406
        Exactly one of 'subsystem' and 'command' must be specified.
 
407
        """
 
408
        raise NotImplementedError(self._get_vendor_specific_argv)
 
409
 
 
410
 
 
411
class OpenSSHSubprocessVendor(SubprocessVendor):
 
412
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
413
 
 
414
    executable_path = 'ssh'
 
415
 
 
416
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
417
                                  command=None):
 
418
        args = [self.executable_path,
 
419
                '-oForwardX11=no', '-oForwardAgent=no',
 
420
                '-oClearAllForwardings=yes',
 
421
                '-oNoHostAuthenticationForLocalhost=yes']
 
422
        if port is not None:
 
423
            args.extend(['-p', str(port)])
 
424
        if username is not None:
 
425
            args.extend(['-l', username])
 
426
        if subsystem is not None:
 
427
            args.extend(['-s', host, subsystem])
 
428
        else:
 
429
            args.extend([host] + command)
 
430
        return args
 
431
 
 
432
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
433
 
 
434
 
 
435
class SSHCorpSubprocessVendor(SubprocessVendor):
 
436
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
437
 
 
438
    executable_path = 'ssh'
 
439
 
 
440
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
441
                                  command=None):
 
442
        args = [self.executable_path, '-x']
 
443
        if port is not None:
 
444
            args.extend(['-p', str(port)])
 
445
        if username is not None:
 
446
            args.extend(['-l', username])
 
447
        if subsystem is not None:
 
448
            args.extend(['-s', subsystem, host])
 
449
        else:
 
450
            args.extend([host] + command)
 
451
        return args
 
452
 
 
453
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
454
 
 
455
 
 
456
class LSHSubprocessVendor(SubprocessVendor):
 
457
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
458
 
 
459
    executable_path = 'lsh'
 
460
 
 
461
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
462
                                  command=None):
 
463
        args = [self.executable_path]
 
464
        if port is not None:
 
465
            args.extend(['-p', str(port)])
 
466
        if username is not None:
 
467
            args.extend(['-l', username])
 
468
        if subsystem is not None:
 
469
            args.extend(['--subsystem', subsystem, host])
 
470
        else:
 
471
            args.extend([host] + command)
 
472
        return args
 
473
 
 
474
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
475
 
 
476
 
 
477
class PLinkSubprocessVendor(SubprocessVendor):
 
478
    """SSH vendor that uses the 'plink' executable from Putty."""
 
479
 
 
480
    executable_path = 'plink'
 
481
 
 
482
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
483
                                  command=None):
 
484
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
485
        if port is not None:
 
486
            args.extend(['-P', str(port)])
 
487
        if username is not None:
 
488
            args.extend(['-l', username])
 
489
        if subsystem is not None:
 
490
            args.extend(['-s', host, subsystem])
 
491
        else:
 
492
            args.extend([host] + command)
 
493
        return args
 
494
 
 
495
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
496
 
 
497
 
 
498
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
499
    auth = config.AuthenticationConfig()
 
500
    # paramiko requires a username, but it might be none if nothing was
 
501
    # supplied.  If so, use the local username.
 
502
    if username is None:
 
503
        username = auth.get_user('ssh', host, port=port,
 
504
                                 default=getpass.getuser())
 
505
    if _use_ssh_agent:
 
506
        agent = paramiko.Agent()
 
507
        for key in agent.get_keys():
 
508
            trace.mutter('Trying SSH agent key %s'
 
509
                         % self._hexify(key.get_fingerprint()))
 
510
            try:
 
511
                paramiko_transport.auth_publickey(username, key)
 
512
                return
 
513
            except paramiko.SSHException, e:
 
514
                pass
 
515
 
 
516
    # okay, try finding id_rsa or id_dss?  (posix only)
 
517
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
518
        return
 
519
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
520
        return
 
521
 
 
522
    # If we have gotten this far, we are about to try for passwords, do an
 
523
    # auth_none check to see if it is even supported.
 
524
    supported_auth_types = []
 
525
    try:
 
526
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
527
        #    Authentication type (none) not permitted.
 
528
        # So we explicitly disable the logging level for this action
 
529
        old_level = paramiko_transport.logger.level
 
530
        paramiko_transport.logger.setLevel(logging.WARNING)
 
531
        try:
 
532
            paramiko_transport.auth_none(username)
 
533
        finally:
 
534
            paramiko_transport.logger.setLevel(old_level)
 
535
    except paramiko.BadAuthenticationType, e:
 
536
        # Supported methods are in the exception
 
537
        supported_auth_types = e.allowed_types
 
538
    except paramiko.SSHException, e:
 
539
        # Don't know what happened, but just ignore it
 
540
        pass
 
541
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
542
    # because Paramiko's auth_password method will automatically try
 
543
    # 'keyboard-interactive' auth (using the password as the response) if
 
544
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
545
    # OpenSSH servers require this.
 
546
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
547
    # requires something other than a single password, but we currently don't
 
548
    # support that.
 
549
    if ('password' not in supported_auth_types and
 
550
        'keyboard-interactive' not in supported_auth_types):
 
551
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
552
            '\n  %s@%s\nsupported auth types: %s'
 
553
            % (username, host, supported_auth_types))
 
554
 
 
555
    if password:
 
556
        try:
 
557
            paramiko_transport.auth_password(username, password)
 
558
            return
 
559
        except paramiko.SSHException, e:
 
560
            pass
 
561
 
 
562
    # give up and ask for a password
 
563
    password = auth.get_password('ssh', host, username, port=port)
 
564
    # get_password can still return None, which means we should not prompt
 
565
    if password is not None:
 
566
        try:
 
567
            paramiko_transport.auth_password(username, password)
 
568
        except paramiko.SSHException, e:
 
569
            raise errors.ConnectionError(
 
570
                'Unable to authenticate to SSH host as'
 
571
                '\n  %s@%s\n' % (username, host), e)
 
572
    else:
 
573
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
574
                                     '  %s@%s' % (username, host))
 
575
 
 
576
 
 
577
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
578
    filename = os.path.expanduser('~/.ssh/' + filename)
 
579
    try:
 
580
        key = pkey_class.from_private_key_file(filename)
 
581
        paramiko_transport.auth_publickey(username, key)
 
582
        return True
 
583
    except paramiko.PasswordRequiredException:
 
584
        password = ui.ui_factory.get_password(
 
585
            prompt=u'SSH %(filename)s password',
 
586
            filename=filename.decode(osutils._fs_enc))
 
587
        try:
 
588
            key = pkey_class.from_private_key_file(filename, password)
 
589
            paramiko_transport.auth_publickey(username, key)
 
590
            return True
 
591
        except paramiko.SSHException:
 
592
            trace.mutter('SSH authentication via %s key failed.'
 
593
                         % (os.path.basename(filename),))
 
594
    except paramiko.SSHException:
 
595
        trace.mutter('SSH authentication via %s key failed.'
 
596
                     % (os.path.basename(filename),))
 
597
    except IOError:
 
598
        pass
 
599
    return False
 
600
 
 
601
 
 
602
def load_host_keys():
 
603
    """
 
604
    Load system host keys (probably doesn't work on windows) and any
 
605
    "discovered" keys from previous sessions.
 
606
    """
 
607
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
608
    try:
 
609
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
610
            os.path.expanduser('~/.ssh/known_hosts'))
 
611
    except IOError, e:
 
612
        trace.mutter('failed to load system host keys: ' + str(e))
 
613
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
614
    try:
 
615
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
616
    except IOError, e:
 
617
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
618
        save_host_keys()
 
619
 
 
620
 
 
621
def save_host_keys():
 
622
    """
 
623
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
624
    """
 
625
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
626
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
627
    config.ensure_config_dir_exists()
 
628
 
 
629
    try:
 
630
        f = open(bzr_hostkey_path, 'w')
 
631
        f.write('# SSH host keys collected by bzr\n')
 
632
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
633
            for keytype, key in keys.iteritems():
 
634
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
635
        f.close()
 
636
    except IOError, e:
 
637
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
638
 
 
639
 
 
640
def os_specific_subprocess_params():
 
641
    """Get O/S specific subprocess parameters."""
 
642
    if sys.platform == 'win32':
 
643
        # setting the process group and closing fds is not supported on
 
644
        # win32
 
645
        return {}
 
646
    else:
 
647
        # We close fds other than the pipes as the child process does not need
 
648
        # them to be open.
 
649
        #
 
650
        # We also set the child process to ignore SIGINT.  Normally the signal
 
651
        # would be sent to every process in the foreground process group, but
 
652
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
653
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
654
        # to release locks or do other cleanup over ssh before the connection
 
655
        # goes away.
 
656
        # <https://launchpad.net/products/bzr/+bug/5987>
 
657
        #
 
658
        # Running it in a separate process group is not good because then it
 
659
        # can't get non-echoed input of a password or passphrase.
 
660
        # <https://launchpad.net/products/bzr/+bug/40508>
 
661
        return {'preexec_fn': _ignore_signals,
 
662
                'close_fds': True,
 
663
                }
 
664
 
 
665
import weakref
 
666
_subproc_weakrefs = set()
 
667
 
 
668
def _close_ssh_proc(proc, sock):
 
669
    """Carefully close stdin/stdout and reap the SSH process.
 
670
 
 
671
    If the pipes are already closed and/or the process has already been
 
672
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
673
    to clean up (whether or not a clean up was already tried).
 
674
    """
 
675
    funcs = []
 
676
    for closeable in (proc.stdin, proc.stdout, sock):
 
677
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
678
        # stdout streams to close, or that we will have been passed a socket to
 
679
        # close, with the option not in use being None.
 
680
        if closeable is not None:
 
681
            funcs.append(closeable.close)
 
682
    funcs.append(proc.wait)
 
683
    for func in funcs:
 
684
        try:
 
685
            func()
 
686
        except OSError:
 
687
            # It's ok for the pipe to already be closed, or the process to
 
688
            # already be finished.
 
689
            continue
 
690
 
 
691
 
 
692
class SSHConnection(object):
 
693
    """Abstract base class for SSH connections."""
 
694
 
 
695
    def get_sock_or_pipes(self):
 
696
        """Returns a (kind, io_object) pair.
 
697
 
 
698
        If kind == 'socket', then io_object is a socket.
 
699
 
 
700
        If kind == 'pipes', then io_object is a pair of file-like objects
 
701
        (read_from, write_to).
 
702
        """
 
703
        raise NotImplementedError(self.get_sock_or_pipes)
 
704
 
 
705
    def close(self):
 
706
        raise NotImplementedError(self.close)
 
707
 
 
708
 
 
709
class SSHSubprocessConnection(SSHConnection):
 
710
    """A connection to an ssh subprocess via pipes or a socket.
 
711
 
 
712
    This class is also socket-like enough to be used with
 
713
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
714
    """
 
715
 
 
716
    def __init__(self, proc, sock=None):
 
717
        """Constructor.
 
718
 
 
719
        :param proc: a subprocess.Popen
 
720
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
721
            should bzrlib's half of that socketpair.  If not passed, proc's
 
722
            stdin/out is assumed to be ordinary pipes.
 
723
        """
 
724
        self.proc = proc
 
725
        self._sock = sock
 
726
        # Add a weakref to proc that will attempt to do the same as self.close
 
727
        # to avoid leaving processes lingering indefinitely.
 
728
        def terminate(ref):
 
729
            _subproc_weakrefs.remove(ref)
 
730
            _close_ssh_proc(proc, sock)
 
731
        _subproc_weakrefs.add(weakref.ref(self, terminate))
 
732
 
 
733
    def send(self, data):
 
734
        if self._sock is not None:
 
735
            return self._sock.send(data)
 
736
        else:
 
737
            return os.write(self.proc.stdin.fileno(), data)
 
738
 
 
739
    def recv(self, count):
 
740
        if self._sock is not None:
 
741
            return self._sock.recv(count)
 
742
        else:
 
743
            return os.read(self.proc.stdout.fileno(), count)
 
744
 
 
745
    def close(self):
 
746
        _close_ssh_proc(self.proc, self._sock)
 
747
 
 
748
    def get_sock_or_pipes(self):
 
749
        if self._sock is not None:
 
750
            return 'socket', self._sock
 
751
        else:
 
752
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
753
 
 
754
 
 
755
class _ParamikoSSHConnection(SSHConnection):
 
756
    """An SSH connection via paramiko."""
 
757
 
 
758
    def __init__(self, channel):
 
759
        self.channel = channel
 
760
 
 
761
    def get_sock_or_pipes(self):
 
762
        return ('socket', self.channel)
 
763
 
 
764
    def close(self):
 
765
        return self.channel.close()
 
766
 
 
767