~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Martin Packman
  • Date: 2011-12-08 19:00:14 UTC
  • mto: This revision was merged to the branch mainline in revision 6359.
  • Revision ID: martin.packman@canonical.com-20111208190014-mi8jm6v7jygmhb0r
Use --include-duplicates for make update-pot which already combines multiple msgid strings prettily

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
import errno
 
21
import getpass
 
22
import logging
 
23
import os
 
24
import socket
 
25
import subprocess
 
26
import sys
 
27
 
 
28
from bzrlib import (
 
29
    config,
 
30
    errors,
 
31
    osutils,
 
32
    trace,
 
33
    ui,
 
34
    )
 
35
 
 
36
try:
 
37
    import paramiko
 
38
except ImportError, e:
 
39
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
 
40
    # access
 
41
    paramiko = None
 
42
else:
 
43
    from paramiko.sftp_client import SFTPClient
 
44
 
 
45
 
 
46
SYSTEM_HOSTKEYS = {}
 
47
BZR_HOSTKEYS = {}
 
48
 
 
49
 
 
50
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
51
 
 
52
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
 
53
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
54
# so we get an AttributeError exception. So we will not try to
 
55
# connect to an agent if we are on win32 and using Paramiko older than 1.6
 
56
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
57
 
 
58
 
 
59
class SSHVendorManager(object):
 
60
    """Manager for manage SSH vendors."""
 
61
 
 
62
    # Note, although at first sign the class interface seems similar to
 
63
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
64
    # the Registry because the class just has "get()" interface instead of the
 
65
    # Registry's "get(key)".
 
66
 
 
67
    def __init__(self):
 
68
        self._ssh_vendors = {}
 
69
        self._cached_ssh_vendor = None
 
70
        self._default_ssh_vendor = None
 
71
 
 
72
    def register_default_vendor(self, vendor):
 
73
        """Register default SSH vendor."""
 
74
        self._default_ssh_vendor = vendor
 
75
 
 
76
    def register_vendor(self, name, vendor):
 
77
        """Register new SSH vendor by name."""
 
78
        self._ssh_vendors[name] = vendor
 
79
 
 
80
    def clear_cache(self):
 
81
        """Clear previously cached lookup result."""
 
82
        self._cached_ssh_vendor = None
 
83
 
 
84
    def _get_vendor_by_environment(self, environment=None):
 
85
        """Return the vendor or None based on BZR_SSH environment variable.
 
86
 
 
87
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
88
                            unknown vendor name
 
89
        """
 
90
        if environment is None:
 
91
            environment = os.environ
 
92
        if 'BZR_SSH' in environment:
 
93
            vendor_name = environment['BZR_SSH']
 
94
            try:
 
95
                vendor = self._ssh_vendors[vendor_name]
 
96
            except KeyError:
 
97
                vendor = self._get_vendor_from_path(vendor_name)
 
98
                if vendor is None:
 
99
                    raise errors.UnknownSSH(vendor_name)
 
100
                vendor.executable_path = vendor_name
 
101
            return vendor
 
102
        return None
 
103
 
 
104
    def _get_ssh_version_string(self, args):
 
105
        """Return SSH version string from the subprocess."""
 
106
        try:
 
107
            p = subprocess.Popen(args,
 
108
                                 stdout=subprocess.PIPE,
 
109
                                 stderr=subprocess.PIPE,
 
110
                                 **os_specific_subprocess_params())
 
111
            stdout, stderr = p.communicate()
 
112
        except OSError:
 
113
            stdout = stderr = ''
 
114
        return stdout + stderr
 
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 BZR_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 BZR_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
_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, 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, e:
 
268
            self._raise_connection_error(host, port=port, orig_error=e)
 
269
        return SFTPClient(SocketAsChannelAdapter(sock))
 
270
 
 
271
register_ssh_vendor('loopback', LoopbackVendor())
 
272
 
 
273
 
 
274
class ParamikoVendor(SSHVendor):
 
275
    """Vendor that uses paramiko."""
 
276
 
 
277
    def _connect(self, username, password, host, port):
 
278
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
279
 
 
280
        load_host_keys()
 
281
 
 
282
        try:
 
283
            t = paramiko.Transport((host, port or 22))
 
284
            t.set_log_channel('bzr.paramiko')
 
285
            t.start_client()
 
286
        except (paramiko.SSHException, socket.error), e:
 
287
            self._raise_connection_error(host, port=port, orig_error=e)
 
288
 
 
289
        server_key = t.get_remote_server_key()
 
290
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
291
        keytype = server_key.get_name()
 
292
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
 
293
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
294
            our_server_key_hex = paramiko.util.hexify(
 
295
                our_server_key.get_fingerprint())
 
296
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
297
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
298
            our_server_key_hex = paramiko.util.hexify(
 
299
                our_server_key.get_fingerprint())
 
300
        else:
 
301
            trace.warning('Adding %s host key for %s: %s'
 
302
                          % (keytype, host, server_key_hex))
 
303
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
304
            if add is not None: # paramiko >= 1.X.X
 
305
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
306
            else:
 
307
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
308
            our_server_key = server_key
 
309
            our_server_key_hex = paramiko.util.hexify(
 
310
                our_server_key.get_fingerprint())
 
311
            save_host_keys()
 
312
        if server_key != our_server_key:
 
313
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
314
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
315
            raise errors.TransportError(
 
316
                'Host keys for %s do not match!  %s != %s' %
 
317
                (host, our_server_key_hex, server_key_hex),
 
318
                ['Try editing %s or %s' % (filename1, filename2)])
 
319
 
 
320
        _paramiko_auth(username, password, host, port, t)
 
321
        return t
 
322
 
 
323
    def connect_sftp(self, username, password, host, port):
 
324
        t = self._connect(username, password, host, port)
 
325
        try:
 
326
            return t.open_sftp_client()
 
327
        except paramiko.SSHException, e:
 
328
            self._raise_connection_error(host, port=port, orig_error=e,
 
329
                                         msg='Unable to start sftp client')
 
330
 
 
331
    def connect_ssh(self, username, password, host, port, command):
 
332
        t = self._connect(username, password, host, port)
 
333
        try:
 
334
            channel = t.open_session()
 
335
            cmdline = ' '.join(command)
 
336
            channel.exec_command(cmdline)
 
337
            return _ParamikoSSHConnection(channel)
 
338
        except paramiko.SSHException, e:
 
339
            self._raise_connection_error(host, port=port, orig_error=e,
 
340
                                         msg='Unable to invoke remote bzr')
 
341
 
 
342
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
 
343
if paramiko is not None:
 
344
    vendor = ParamikoVendor()
 
345
    register_ssh_vendor('paramiko', vendor)
 
346
    register_ssh_vendor('none', vendor)
 
347
    register_default_ssh_vendor(vendor)
 
348
    _ssh_connection_errors += (paramiko.SSHException,)
 
349
    del vendor
 
350
 
 
351
 
 
352
class SubprocessVendor(SSHVendor):
 
353
    """Abstract base class for vendors that use pipes to a subprocess."""
 
354
 
 
355
    # In general stderr should be inherited from the parent process so prompts
 
356
    # are visible on the terminal. This can be overriden to another file for
 
357
    # tests, but beware of using PIPE which may hang due to not being read.
 
358
    _stderr_target = None
 
359
 
 
360
    def _connect(self, argv):
 
361
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
362
        # subprocess.  We prefer sockets to pipes because they support
 
363
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
364
        # whatever) chunks.
 
365
        try:
 
366
            my_sock, subproc_sock = socket.socketpair()
 
367
            osutils.set_fd_cloexec(my_sock)
 
368
        except (AttributeError, socket.error):
 
369
            # This platform doesn't support socketpair(), so just use ordinary
 
370
            # pipes instead.
 
371
            stdin = stdout = subprocess.PIPE
 
372
            my_sock, subproc_sock = None, None
 
373
        else:
 
374
            stdin = stdout = subproc_sock
 
375
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
376
                                stderr=self._stderr_target,
 
377
                                **os_specific_subprocess_params())
 
378
        if subproc_sock is not None:
 
379
            subproc_sock.close()
 
380
        return SSHSubprocessConnection(proc, sock=my_sock)
 
381
 
 
382
    def connect_sftp(self, username, password, host, port):
 
383
        try:
 
384
            argv = self._get_vendor_specific_argv(username, host, port,
 
385
                                                  subsystem='sftp')
 
386
            sock = self._connect(argv)
 
387
            return SFTPClient(SocketAsChannelAdapter(sock))
 
388
        except _ssh_connection_errors, e:
 
389
            self._raise_connection_error(host, port=port, orig_error=e)
 
390
 
 
391
    def connect_ssh(self, username, password, host, port, command):
 
392
        try:
 
393
            argv = self._get_vendor_specific_argv(username, host, port,
 
394
                                                  command=command)
 
395
            return self._connect(argv)
 
396
        except _ssh_connection_errors, e:
 
397
            self._raise_connection_error(host, port=port, orig_error=e)
 
398
 
 
399
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
400
                                  command=None):
 
401
        """Returns the argument list to run the subprocess with.
 
402
 
 
403
        Exactly one of 'subsystem' and 'command' must be specified.
 
404
        """
 
405
        raise NotImplementedError(self._get_vendor_specific_argv)
 
406
 
 
407
 
 
408
class OpenSSHSubprocessVendor(SubprocessVendor):
 
409
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
410
 
 
411
    executable_path = 'ssh'
 
412
 
 
413
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
414
                                  command=None):
 
415
        args = [self.executable_path,
 
416
                '-oForwardX11=no', '-oForwardAgent=no',
 
417
                '-oClearAllForwardings=yes',
 
418
                '-oNoHostAuthenticationForLocalhost=yes']
 
419
        if port is not None:
 
420
            args.extend(['-p', str(port)])
 
421
        if username is not None:
 
422
            args.extend(['-l', username])
 
423
        if subsystem is not None:
 
424
            args.extend(['-s', host, subsystem])
 
425
        else:
 
426
            args.extend([host] + command)
 
427
        return args
 
428
 
 
429
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
430
 
 
431
 
 
432
class SSHCorpSubprocessVendor(SubprocessVendor):
 
433
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
434
 
 
435
    executable_path = 'ssh'
 
436
 
 
437
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
438
                                  command=None):
 
439
        args = [self.executable_path, '-x']
 
440
        if port is not None:
 
441
            args.extend(['-p', str(port)])
 
442
        if username is not None:
 
443
            args.extend(['-l', username])
 
444
        if subsystem is not None:
 
445
            args.extend(['-s', subsystem, host])
 
446
        else:
 
447
            args.extend([host] + command)
 
448
        return args
 
449
 
 
450
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
451
 
 
452
 
 
453
class LSHSubprocessVendor(SubprocessVendor):
 
454
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
455
 
 
456
    executable_path = 'lsh'
 
457
 
 
458
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
459
                                  command=None):
 
460
        args = [self.executable_path]
 
461
        if port is not None:
 
462
            args.extend(['-p', str(port)])
 
463
        if username is not None:
 
464
            args.extend(['-l', username])
 
465
        if subsystem is not None:
 
466
            args.extend(['--subsystem', subsystem, host])
 
467
        else:
 
468
            args.extend([host] + command)
 
469
        return args
 
470
 
 
471
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
472
 
 
473
 
 
474
class PLinkSubprocessVendor(SubprocessVendor):
 
475
    """SSH vendor that uses the 'plink' executable from Putty."""
 
476
 
 
477
    executable_path = 'plink'
 
478
 
 
479
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
480
                                  command=None):
 
481
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
482
        if port is not None:
 
483
            args.extend(['-P', str(port)])
 
484
        if username is not None:
 
485
            args.extend(['-l', username])
 
486
        if subsystem is not None:
 
487
            args.extend(['-s', host, subsystem])
 
488
        else:
 
489
            args.extend([host] + command)
 
490
        return args
 
491
 
 
492
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
493
 
 
494
 
 
495
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
496
    auth = config.AuthenticationConfig()
 
497
    # paramiko requires a username, but it might be none if nothing was
 
498
    # supplied.  If so, use the local username.
 
499
    if username is None:
 
500
        username = auth.get_user('ssh', host, port=port,
 
501
                                 default=getpass.getuser())
 
502
    if _use_ssh_agent:
 
503
        agent = paramiko.Agent()
 
504
        for key in agent.get_keys():
 
505
            trace.mutter('Trying SSH agent key %s'
 
506
                         % paramiko.util.hexify(key.get_fingerprint()))
 
507
            try:
 
508
                paramiko_transport.auth_publickey(username, key)
 
509
                return
 
510
            except paramiko.SSHException, e:
 
511
                pass
 
512
 
 
513
    # okay, try finding id_rsa or id_dss?  (posix only)
 
514
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
515
        return
 
516
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
517
        return
 
518
 
 
519
    # If we have gotten this far, we are about to try for passwords, do an
 
520
    # auth_none check to see if it is even supported.
 
521
    supported_auth_types = []
 
522
    try:
 
523
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
524
        #    Authentication type (none) not permitted.
 
525
        # So we explicitly disable the logging level for this action
 
526
        old_level = paramiko_transport.logger.level
 
527
        paramiko_transport.logger.setLevel(logging.WARNING)
 
528
        try:
 
529
            paramiko_transport.auth_none(username)
 
530
        finally:
 
531
            paramiko_transport.logger.setLevel(old_level)
 
532
    except paramiko.BadAuthenticationType, e:
 
533
        # Supported methods are in the exception
 
534
        supported_auth_types = e.allowed_types
 
535
    except paramiko.SSHException, e:
 
536
        # Don't know what happened, but just ignore it
 
537
        pass
 
538
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
539
    # because Paramiko's auth_password method will automatically try
 
540
    # 'keyboard-interactive' auth (using the password as the response) if
 
541
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
542
    # OpenSSH servers require this.
 
543
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
544
    # requires something other than a single password, but we currently don't
 
545
    # support that.
 
546
    if ('password' not in supported_auth_types and
 
547
        'keyboard-interactive' not in supported_auth_types):
 
548
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
549
            '\n  %s@%s\nsupported auth types: %s'
 
550
            % (username, host, supported_auth_types))
 
551
 
 
552
    if password:
 
553
        try:
 
554
            paramiko_transport.auth_password(username, password)
 
555
            return
 
556
        except paramiko.SSHException, e:
 
557
            pass
 
558
 
 
559
    # give up and ask for a password
 
560
    password = auth.get_password('ssh', host, username, port=port)
 
561
    # get_password can still return None, which means we should not prompt
 
562
    if password is not None:
 
563
        try:
 
564
            paramiko_transport.auth_password(username, password)
 
565
        except paramiko.SSHException, e:
 
566
            raise errors.ConnectionError(
 
567
                'Unable to authenticate to SSH host as'
 
568
                '\n  %s@%s\n' % (username, host), e)
 
569
    else:
 
570
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
571
                                     '  %s@%s' % (username, host))
 
572
 
 
573
 
 
574
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
575
    filename = os.path.expanduser('~/.ssh/' + filename)
 
576
    try:
 
577
        key = pkey_class.from_private_key_file(filename)
 
578
        paramiko_transport.auth_publickey(username, key)
 
579
        return True
 
580
    except paramiko.PasswordRequiredException:
 
581
        password = ui.ui_factory.get_password(
 
582
            prompt=u'SSH %(filename)s password',
 
583
            filename=filename.decode(osutils._fs_enc))
 
584
        try:
 
585
            key = pkey_class.from_private_key_file(filename, password)
 
586
            paramiko_transport.auth_publickey(username, key)
 
587
            return True
 
588
        except paramiko.SSHException:
 
589
            trace.mutter('SSH authentication via %s key failed.'
 
590
                         % (os.path.basename(filename),))
 
591
    except paramiko.SSHException:
 
592
        trace.mutter('SSH authentication via %s key failed.'
 
593
                     % (os.path.basename(filename),))
 
594
    except IOError:
 
595
        pass
 
596
    return False
 
597
 
 
598
 
 
599
def load_host_keys():
 
600
    """
 
601
    Load system host keys (probably doesn't work on windows) and any
 
602
    "discovered" keys from previous sessions.
 
603
    """
 
604
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
605
    try:
 
606
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
607
            os.path.expanduser('~/.ssh/known_hosts'))
 
608
    except IOError, e:
 
609
        trace.mutter('failed to load system host keys: ' + str(e))
 
610
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
611
    try:
 
612
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
613
    except IOError, e:
 
614
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
615
        save_host_keys()
 
616
 
 
617
 
 
618
def save_host_keys():
 
619
    """
 
620
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
621
    """
 
622
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
623
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
624
    config.ensure_config_dir_exists()
 
625
 
 
626
    try:
 
627
        f = open(bzr_hostkey_path, 'w')
 
628
        f.write('# SSH host keys collected by bzr\n')
 
629
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
630
            for keytype, key in keys.iteritems():
 
631
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
632
        f.close()
 
633
    except IOError, e:
 
634
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
635
 
 
636
 
 
637
def os_specific_subprocess_params():
 
638
    """Get O/S specific subprocess parameters."""
 
639
    if sys.platform == 'win32':
 
640
        # setting the process group and closing fds is not supported on
 
641
        # win32
 
642
        return {}
 
643
    else:
 
644
        # We close fds other than the pipes as the child process does not need
 
645
        # them to be open.
 
646
        #
 
647
        # We also set the child process to ignore SIGINT.  Normally the signal
 
648
        # would be sent to every process in the foreground process group, but
 
649
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
650
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
651
        # to release locks or do other cleanup over ssh before the connection
 
652
        # goes away.
 
653
        # <https://launchpad.net/products/bzr/+bug/5987>
 
654
        #
 
655
        # Running it in a separate process group is not good because then it
 
656
        # can't get non-echoed input of a password or passphrase.
 
657
        # <https://launchpad.net/products/bzr/+bug/40508>
 
658
        return {'preexec_fn': _ignore_signals,
 
659
                'close_fds': True,
 
660
                }
 
661
 
 
662
import weakref
 
663
_subproc_weakrefs = set()
 
664
 
 
665
def _close_ssh_proc(proc, sock):
 
666
    """Carefully close stdin/stdout and reap the SSH process.
 
667
 
 
668
    If the pipes are already closed and/or the process has already been
 
669
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
670
    to clean up (whether or not a clean up was already tried).
 
671
    """
 
672
    funcs = []
 
673
    for closeable in (proc.stdin, proc.stdout, sock):
 
674
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
675
        # stdout streams to close, or that we will have been passed a socket to
 
676
        # close, with the option not in use being None.
 
677
        if closeable is not None:
 
678
            funcs.append(closeable.close)
 
679
    funcs.append(proc.wait)
 
680
    for func in funcs:
 
681
        try:
 
682
            func()
 
683
        except OSError:
 
684
            # It's ok for the pipe to already be closed, or the process to
 
685
            # already be finished.
 
686
            continue
 
687
 
 
688
 
 
689
class SSHConnection(object):
 
690
    """Abstract base class for SSH connections."""
 
691
 
 
692
    def get_sock_or_pipes(self):
 
693
        """Returns a (kind, io_object) pair.
 
694
 
 
695
        If kind == 'socket', then io_object is a socket.
 
696
 
 
697
        If kind == 'pipes', then io_object is a pair of file-like objects
 
698
        (read_from, write_to).
 
699
        """
 
700
        raise NotImplementedError(self.get_sock_or_pipes)
 
701
 
 
702
    def close(self):
 
703
        raise NotImplementedError(self.close)
 
704
 
 
705
 
 
706
class SSHSubprocessConnection(SSHConnection):
 
707
    """A connection to an ssh subprocess via pipes or a socket.
 
708
 
 
709
    This class is also socket-like enough to be used with
 
710
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
711
    """
 
712
 
 
713
    def __init__(self, proc, sock=None):
 
714
        """Constructor.
 
715
 
 
716
        :param proc: a subprocess.Popen
 
717
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
718
            should bzrlib's half of that socketpair.  If not passed, proc's
 
719
            stdin/out is assumed to be ordinary pipes.
 
720
        """
 
721
        self.proc = proc
 
722
        self._sock = sock
 
723
        # Add a weakref to proc that will attempt to do the same as self.close
 
724
        # to avoid leaving processes lingering indefinitely.
 
725
        def terminate(ref):
 
726
            _subproc_weakrefs.remove(ref)
 
727
            _close_ssh_proc(proc, sock)
 
728
        _subproc_weakrefs.add(weakref.ref(self, terminate))
 
729
 
 
730
    def send(self, data):
 
731
        if self._sock is not None:
 
732
            return self._sock.send(data)
 
733
        else:
 
734
            return os.write(self.proc.stdin.fileno(), data)
 
735
 
 
736
    def recv(self, count):
 
737
        if self._sock is not None:
 
738
            return self._sock.recv(count)
 
739
        else:
 
740
            return os.read(self.proc.stdout.fileno(), count)
 
741
 
 
742
    def close(self):
 
743
        _close_ssh_proc(self.proc, self._sock)
 
744
 
 
745
    def get_sock_or_pipes(self):
 
746
        if self._sock is not None:
 
747
            return 'socket', self._sock
 
748
        else:
 
749
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
750
 
 
751
 
 
752
class _ParamikoSSHConnection(SSHConnection):
 
753
    """An SSH connection via paramiko."""
 
754
 
 
755
    def __init__(self, channel):
 
756
        self.channel = channel
 
757
 
 
758
    def get_sock_or_pipes(self):
 
759
        return ('socket', self.channel)
 
760
 
 
761
    def close(self):
 
762
        return self.channel.close()
 
763
 
 
764