/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-08-12 20:24:50 UTC
  • mto: (7290.1.35 work)
  • mto: This revision was merged to the branch mainline in revision 7405.
  • Revision ID: jelmer@jelmer.uk-20190812202450-vdpamxay6sebo93w
Fix path to brz.

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
 
 
173
_ssh_vendor_manager = SSHVendorManager()
 
174
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
 
175
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
 
176
register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
177
 
 
178
 
 
179
def _ignore_signals():
 
180
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
181
    # doesn't handle it itself.
 
182
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
183
    import signal
 
184
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
185
    # GZ 2010-02-19: Perhaps make this check if breakin is installed instead
 
186
    if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
 
187
        signal.signal(signal.SIGQUIT, signal.SIG_IGN)
 
188
 
 
189
 
 
190
class SocketAsChannelAdapter(object):
 
191
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
192
 
 
193
    def __init__(self, sock):
 
194
        self.__socket = sock
 
195
 
 
196
    def get_name(self):
 
197
        return "bzr SocketAsChannelAdapter"
 
198
 
 
199
    def send(self, data):
 
200
        return self.__socket.send(data)
 
201
 
 
202
    def recv(self, n):
 
203
        try:
 
204
            return self.__socket.recv(n)
 
205
        except socket.error as e:
 
206
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
207
                             errno.EBADF):
 
208
                # Connection has closed.  Paramiko expects an empty string in
 
209
                # this case, not an exception.
 
210
                return ''
 
211
            raise
 
212
 
 
213
    def recv_ready(self):
 
214
        # TODO: jam 20051215 this function is necessary to support the
 
215
        # pipelined() function. In reality, it probably should use
 
216
        # poll() or select() to actually return if there is data
 
217
        # available, otherwise we probably don't get any benefit
 
218
        return True
 
219
 
 
220
    def close(self):
 
221
        self.__socket.close()
 
222
 
 
223
 
 
224
class SSHVendor(object):
 
225
    """Abstract base class for SSH vendor implementations."""
 
226
 
 
227
    def connect_sftp(self, username, password, host, port):
 
228
        """Make an SSH connection, and return an SFTPClient.
 
229
 
 
230
        :param username: an ascii string
 
231
        :param password: an ascii string
 
232
        :param host: a host name as an ascii string
 
233
        :param port: a port number
 
234
        :type port: int
 
235
 
 
236
        :raises: ConnectionError if it cannot connect.
 
237
 
 
238
        :rtype: paramiko.sftp_client.SFTPClient
 
239
        """
 
240
        raise NotImplementedError(self.connect_sftp)
 
241
 
 
242
    def connect_ssh(self, username, password, host, port, command):
 
243
        """Make an SSH connection.
 
244
 
 
245
        :returns: an SSHConnection.
 
246
        """
 
247
        raise NotImplementedError(self.connect_ssh)
 
248
 
 
249
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
250
                                msg='Unable to connect to SSH host'):
 
251
        """Raise a SocketConnectionError with properly formatted host.
 
252
 
 
253
        This just unifies all the locations that try to raise ConnectionError,
 
254
        so that they format things properly.
 
255
        """
 
256
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
257
                                           orig_error=orig_error)
 
258
 
 
259
 
 
260
class LoopbackVendor(SSHVendor):
 
261
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
262
 
 
263
    def connect_sftp(self, username, password, host, port):
 
264
        sock = socket.socket()
 
265
        try:
 
266
            sock.connect((host, port))
 
267
        except socket.error as e:
 
268
            self._raise_connection_error(host, port=port, orig_error=e)
 
269
        return SFTPClient(SocketAsChannelAdapter(sock))
 
270
 
 
271
 
 
272
register_ssh_vendor('loopback', LoopbackVendor())
 
273
 
 
274
 
 
275
class ParamikoVendor(SSHVendor):
 
276
    """Vendor that uses paramiko."""
 
277
 
 
278
    def _hexify(self, s):
 
279
        return hexlify(s).upper()
 
280
 
 
281
    def _connect(self, username, password, host, port):
 
282
        global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
283
 
 
284
        load_host_keys()
 
285
 
 
286
        try:
 
287
            t = paramiko.Transport((host, port or 22))
 
288
            t.set_log_channel('bzr.paramiko')
 
289
            t.start_client()
 
290
        except (paramiko.SSHException, socket.error) as e:
 
291
            self._raise_connection_error(host, port=port, orig_error=e)
 
292
 
 
293
        server_key = t.get_remote_server_key()
 
294
        server_key_hex = self._hexify(server_key.get_fingerprint())
 
295
        keytype = server_key.get_name()
 
296
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
 
297
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
298
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
299
        elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
 
300
            our_server_key = BRZ_HOSTKEYS[host][keytype]
 
301
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
302
        else:
 
303
            trace.warning('Adding %s host key for %s: %s'
 
304
                          % (keytype, host, server_key_hex))
 
305
            add = getattr(BRZ_HOSTKEYS, 'add', None)
 
306
            if add is not None:  # paramiko >= 1.X.X
 
307
                BRZ_HOSTKEYS.add(host, keytype, server_key)
 
308
            else:
 
309
                BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
310
            our_server_key = server_key
 
311
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
312
            save_host_keys()
 
313
        if server_key != our_server_key:
 
314
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
315
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
316
            raise errors.TransportError(
 
317
                'Host keys for %s do not match!  %s != %s' %
 
318
                (host, our_server_key_hex, server_key_hex),
 
319
                ['Try editing %s or %s' % (filename1, filename2)])
 
320
 
 
321
        _paramiko_auth(username, password, host, port, t)
 
322
        return t
 
323
 
 
324
    def connect_sftp(self, username, password, host, port):
 
325
        t = self._connect(username, password, host, port)
 
326
        try:
 
327
            return t.open_sftp_client()
 
328
        except paramiko.SSHException as e:
 
329
            self._raise_connection_error(host, port=port, orig_error=e,
 
330
                                         msg='Unable to start sftp client')
 
331
 
 
332
    def connect_ssh(self, username, password, host, port, command):
 
333
        t = self._connect(username, password, host, port)
 
334
        try:
 
335
            channel = t.open_session()
 
336
            cmdline = ' '.join(command)
 
337
            channel.exec_command(cmdline)
 
338
            return _ParamikoSSHConnection(channel)
 
339
        except paramiko.SSHException as e:
 
340
            self._raise_connection_error(host, port=port, orig_error=e,
 
341
                                         msg='Unable to invoke remote bzr')
 
342
 
 
343
 
 
344
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
 
345
if paramiko is not None:
 
346
    vendor = ParamikoVendor()
 
347
    register_ssh_vendor('paramiko', vendor)
 
348
    register_ssh_vendor('none', vendor)
 
349
    register_default_ssh_vendor(vendor)
 
350
    _ssh_connection_errors += (paramiko.SSHException,)
 
351
    del vendor
 
352
 
 
353
 
 
354
class SubprocessVendor(SSHVendor):
 
355
    """Abstract base class for vendors that use pipes to a subprocess."""
 
356
 
 
357
    # In general stderr should be inherited from the parent process so prompts
 
358
    # are visible on the terminal. This can be overriden to another file for
 
359
    # tests, but beware of using PIPE which may hang due to not being read.
 
360
    _stderr_target = None
 
361
 
 
362
    @staticmethod
 
363
    def _check_hostname(arg):
 
364
        if arg.startswith('-'):
 
365
            raise StrangeHostname(hostname=arg)
 
366
 
 
367
    def _connect(self, argv):
 
368
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
369
        # subprocess.  We prefer sockets to pipes because they support
 
370
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
371
        # whatever) chunks.
 
372
        try:
 
373
            my_sock, subproc_sock = socket.socketpair()
 
374
            osutils.set_fd_cloexec(my_sock)
 
375
        except (AttributeError, socket.error):
 
376
            # This platform doesn't support socketpair(), so just use ordinary
 
377
            # pipes instead.
 
378
            stdin = stdout = subprocess.PIPE
 
379
            my_sock, subproc_sock = None, None
 
380
        else:
 
381
            stdin = stdout = subproc_sock
 
382
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
383
                                stderr=self._stderr_target,
 
384
                                bufsize=0,
 
385
                                **os_specific_subprocess_params())
 
386
        if subproc_sock is not None:
 
387
            subproc_sock.close()
 
388
        return SSHSubprocessConnection(proc, sock=my_sock)
 
389
 
 
390
    def connect_sftp(self, username, password, host, port):
 
391
        try:
 
392
            argv = self._get_vendor_specific_argv(username, host, port,
 
393
                                                  subsystem='sftp')
 
394
            sock = self._connect(argv)
 
395
            return SFTPClient(SocketAsChannelAdapter(sock))
 
396
        except _ssh_connection_errors as e:
 
397
            self._raise_connection_error(host, port=port, orig_error=e)
 
398
 
 
399
    def connect_ssh(self, username, password, host, port, command):
 
400
        try:
 
401
            argv = self._get_vendor_specific_argv(username, host, port,
 
402
                                                  command=command)
 
403
            return self._connect(argv)
 
404
        except _ssh_connection_errors as e:
 
405
            self._raise_connection_error(host, port=port, orig_error=e)
 
406
 
 
407
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
408
                                  command=None):
 
409
        """Returns the argument list to run the subprocess with.
 
410
 
 
411
        Exactly one of 'subsystem' and 'command' must be specified.
 
412
        """
 
413
        raise NotImplementedError(self._get_vendor_specific_argv)
 
414
 
 
415
 
 
416
class OpenSSHSubprocessVendor(SubprocessVendor):
 
417
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
418
 
 
419
    executable_path = 'ssh'
 
420
 
 
421
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
422
                                  command=None):
 
423
        args = [self.executable_path,
 
424
                '-oForwardX11=no', '-oForwardAgent=no',
 
425
                '-oClearAllForwardings=yes',
 
426
                '-oNoHostAuthenticationForLocalhost=yes']
 
427
        if port is not None:
 
428
            args.extend(['-p', str(port)])
 
429
        if username is not None:
 
430
            args.extend(['-l', username])
 
431
        if subsystem is not None:
 
432
            args.extend(['-s', '--', host, subsystem])
 
433
        else:
 
434
            args.extend(['--', host] + command)
 
435
        return args
 
436
 
 
437
 
 
438
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
439
 
 
440
 
 
441
class SSHCorpSubprocessVendor(SubprocessVendor):
 
442
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
443
 
 
444
    executable_path = 'ssh'
 
445
 
 
446
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
447
                                  command=None):
 
448
        self._check_hostname(host)
 
449
        args = [self.executable_path, '-x']
 
450
        if port is not None:
 
451
            args.extend(['-p', str(port)])
 
452
        if username is not None:
 
453
            args.extend(['-l', username])
 
454
        if subsystem is not None:
 
455
            args.extend(['-s', subsystem, host])
 
456
        else:
 
457
            args.extend([host] + command)
 
458
        return args
 
459
 
 
460
 
 
461
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
462
 
 
463
 
 
464
class LSHSubprocessVendor(SubprocessVendor):
 
465
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
466
 
 
467
    executable_path = 'lsh'
 
468
 
 
469
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
470
                                  command=None):
 
471
        self._check_hostname(host)
 
472
        args = [self.executable_path]
 
473
        if port is not None:
 
474
            args.extend(['-p', str(port)])
 
475
        if username is not None:
 
476
            args.extend(['-l', username])
 
477
        if subsystem is not None:
 
478
            args.extend(['--subsystem', subsystem, host])
 
479
        else:
 
480
            args.extend([host] + command)
 
481
        return args
 
482
 
 
483
 
 
484
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
485
 
 
486
 
 
487
class PLinkSubprocessVendor(SubprocessVendor):
 
488
    """SSH vendor that uses the 'plink' executable from Putty."""
 
489
 
 
490
    executable_path = 'plink'
 
491
 
 
492
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
493
                                  command=None):
 
494
        self._check_hostname(host)
 
495
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
496
        if port is not None:
 
497
            args.extend(['-P', str(port)])
 
498
        if username is not None:
 
499
            args.extend(['-l', username])
 
500
        if subsystem is not None:
 
501
            args.extend(['-s', host, subsystem])
 
502
        else:
 
503
            args.extend([host] + command)
 
504
        return args
 
505
 
 
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
    agent = paramiko.Agent()
 
518
    for key in agent.get_keys():
 
519
        trace.mutter('Trying SSH agent key %s'
 
520
                     % hexlify(key.get_fingerprint()).upper())
 
521
        try:
 
522
            paramiko_transport.auth_publickey(username, key)
 
523
            return
 
524
        except paramiko.SSHException as e:
 
525
            pass
 
526
 
 
527
    # okay, try finding id_rsa or id_dss?  (posix only)
 
528
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
529
        return
 
530
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
531
        return
 
532
 
 
533
    # If we have gotten this far, we are about to try for passwords, do an
 
534
    # auth_none check to see if it is even supported.
 
535
    supported_auth_types = []
 
536
    try:
 
537
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
538
        #    Authentication type (none) not permitted.
 
539
        # So we explicitly disable the logging level for this action
 
540
        old_level = paramiko_transport.logger.level
 
541
        paramiko_transport.logger.setLevel(logging.WARNING)
 
542
        try:
 
543
            paramiko_transport.auth_none(username)
 
544
        finally:
 
545
            paramiko_transport.logger.setLevel(old_level)
 
546
    except paramiko.BadAuthenticationType as e:
 
547
        # Supported methods are in the exception
 
548
        supported_auth_types = e.allowed_types
 
549
    except paramiko.SSHException as e:
 
550
        # Don't know what happened, but just ignore it
 
551
        pass
 
552
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
553
    # because Paramiko's auth_password method will automatically try
 
554
    # 'keyboard-interactive' auth (using the password as the response) if
 
555
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
556
    # OpenSSH servers require this.
 
557
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
558
    # requires something other than a single password, but we currently don't
 
559
    # support that.
 
560
    if ('password' not in supported_auth_types and
 
561
            'keyboard-interactive' not in supported_auth_types):
 
562
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
563
                                     '\n  %s@%s\nsupported auth types: %s'
 
564
                                     % (username, host, supported_auth_types))
 
565
 
 
566
    if password:
 
567
        try:
 
568
            paramiko_transport.auth_password(username, password)
 
569
            return
 
570
        except paramiko.SSHException as e:
 
571
            pass
 
572
 
 
573
    # give up and ask for a password
 
574
    password = auth.get_password('ssh', host, username, port=port)
 
575
    # get_password can still return None, which means we should not prompt
 
576
    if password is not None:
 
577
        try:
 
578
            paramiko_transport.auth_password(username, password)
 
579
        except paramiko.SSHException as e:
 
580
            raise errors.ConnectionError(
 
581
                'Unable to authenticate to SSH host as'
 
582
                '\n  %s@%s\n' % (username, host), e)
 
583
    else:
 
584
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
585
                                     '  %s@%s' % (username, host))
 
586
 
 
587
 
 
588
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
589
    filename = os.path.expanduser('~/.ssh/' + filename)
 
590
    try:
 
591
        key = pkey_class.from_private_key_file(filename)
 
592
        paramiko_transport.auth_publickey(username, key)
 
593
        return True
 
594
    except paramiko.PasswordRequiredException:
 
595
        password = ui.ui_factory.get_password(
 
596
            prompt=u'SSH %(filename)s password',
 
597
            filename=filename.decode(osutils._fs_enc))
 
598
        try:
 
599
            key = pkey_class.from_private_key_file(filename, password)
 
600
            paramiko_transport.auth_publickey(username, key)
 
601
            return True
 
602
        except paramiko.SSHException:
 
603
            trace.mutter('SSH authentication via %s key failed.'
 
604
                         % (os.path.basename(filename),))
 
605
    except paramiko.SSHException:
 
606
        trace.mutter('SSH authentication via %s key failed.'
 
607
                     % (os.path.basename(filename),))
 
608
    except IOError:
 
609
        pass
 
610
    return False
 
611
 
 
612
 
 
613
def load_host_keys():
 
614
    """
 
615
    Load system host keys (probably doesn't work on windows) and any
 
616
    "discovered" keys from previous sessions.
 
617
    """
 
618
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
619
    try:
 
620
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
621
            os.path.expanduser('~/.ssh/known_hosts'))
 
622
    except IOError as e:
 
623
        trace.mutter('failed to load system host keys: ' + str(e))
 
624
    brz_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
625
    try:
 
626
        BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
 
627
    except IOError as e:
 
628
        trace.mutter('failed to load brz host keys: ' + str(e))
 
629
        save_host_keys()
 
630
 
 
631
 
 
632
def save_host_keys():
 
633
    """
 
634
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
635
    """
 
636
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
 
637
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
638
    config.ensure_config_dir_exists()
 
639
 
 
640
    try:
 
641
        with open(bzr_hostkey_path, 'w') as f:
 
642
            f.write('# SSH host keys collected by bzr\n')
 
643
            for hostname, keys in BRZ_HOSTKEYS.items():
 
644
                for keytype, key in keys.items():
 
645
                    f.write('%s %s %s\n' %
 
646
                            (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
 
 
677
import weakref
 
678
_subproc_weakrefs = set()
 
679
 
 
680
 
 
681
def _close_ssh_proc(proc, sock):
 
682
    """Carefully close stdin/stdout and reap the SSH process.
 
683
 
 
684
    If the pipes are already closed and/or the process has already been
 
685
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
686
    to clean up (whether or not a clean up was already tried).
 
687
    """
 
688
    funcs = []
 
689
    for closeable in (proc.stdin, proc.stdout, sock):
 
690
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
691
        # stdout streams to close, or that we will have been passed a socket to
 
692
        # close, with the option not in use being None.
 
693
        if closeable is not None:
 
694
            funcs.append(closeable.close)
 
695
    funcs.append(proc.wait)
 
696
    for func in funcs:
 
697
        try:
 
698
            func()
 
699
        except OSError:
 
700
            # It's ok for the pipe to already be closed, or the process to
 
701
            # already be finished.
 
702
            continue
 
703
 
 
704
 
 
705
class SSHConnection(object):
 
706
    """Abstract base class for SSH connections."""
 
707
 
 
708
    def get_sock_or_pipes(self):
 
709
        """Returns a (kind, io_object) pair.
 
710
 
 
711
        If kind == 'socket', then io_object is a socket.
 
712
 
 
713
        If kind == 'pipes', then io_object is a pair of file-like objects
 
714
        (read_from, write_to).
 
715
        """
 
716
        raise NotImplementedError(self.get_sock_or_pipes)
 
717
 
 
718
    def close(self):
 
719
        raise NotImplementedError(self.close)
 
720
 
 
721
 
 
722
class SSHSubprocessConnection(SSHConnection):
 
723
    """A connection to an ssh subprocess via pipes or a socket.
 
724
 
 
725
    This class is also socket-like enough to be used with
 
726
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
727
    """
 
728
 
 
729
    def __init__(self, proc, sock=None):
 
730
        """Constructor.
 
731
 
 
732
        :param proc: a subprocess.Popen
 
733
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
734
            should breezy's half of that socketpair.  If not passed, proc's
 
735
            stdin/out is assumed to be ordinary pipes.
 
736
        """
 
737
        self.proc = proc
 
738
        self._sock = sock
 
739
        # Add a weakref to proc that will attempt to do the same as self.close
 
740
        # to avoid leaving processes lingering indefinitely.
 
741
 
 
742
        def terminate(ref):
 
743
            _subproc_weakrefs.remove(ref)
 
744
            _close_ssh_proc(proc, sock)
 
745
        _subproc_weakrefs.add(weakref.ref(self, terminate))
 
746
 
 
747
    def send(self, data):
 
748
        if self._sock is not None:
 
749
            return self._sock.send(data)
 
750
        else:
 
751
            return os.write(self.proc.stdin.fileno(), data)
 
752
 
 
753
    def recv(self, count):
 
754
        if self._sock is not None:
 
755
            return self._sock.recv(count)
 
756
        else:
 
757
            return os.read(self.proc.stdout.fileno(), count)
 
758
 
 
759
    def close(self):
 
760
        _close_ssh_proc(self.proc, self._sock)
 
761
 
 
762
    def get_sock_or_pipes(self):
 
763
        if self._sock is not None:
 
764
            return 'socket', self._sock
 
765
        else:
 
766
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
767
 
 
768
 
 
769
class _ParamikoSSHConnection(SSHConnection):
 
770
    """An SSH connection via paramiko."""
 
771
 
 
772
    def __init__(self, channel):
 
773
        self.channel = channel
 
774
 
 
775
    def get_sock_or_pipes(self):
 
776
        return ('socket', self.channel)
 
777
 
 
778
    def close(self):
 
779
        return self.channel.close()