/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-07-26 19:15:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7055.
  • Revision ID: jelmer@jelmer.uk-20180726191527-wniq205k6tzfo1xx
Install fastimport from git.

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