/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: Robert Collins
  • Date: 2005-10-19 10:11:57 UTC
  • mfrom: (1185.16.78)
  • mto: This revision was merged to the branch mainline in revision 1470.
  • Revision ID: robertc@robertcollins.net-20051019101157-17438d311e746b4f
mergeĀ fromĀ upstream

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