/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: 2019-06-29 19:54:32 UTC
  • mto: This revision was merged to the branch mainline in revision 7378.
  • Revision ID: jelmer@jelmer.uk-20190629195432-xuqzgxejnzq6gs2n
Use more ExitStacks.

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