/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2018-05-06 11:48:54 UTC
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@jelmer.uk-20180506114854-h4qd9ojaqy8wxjsd
Move .mailmap to root.

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