~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-07-12 12:36:57 UTC
  • mfrom: (1732.3.4 bzr.revnoX)
  • Revision ID: pqm@pqm.ubuntu.com-20060712123657-365eeb32b69308bf
(matthieu) revno:x:url revision spec

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 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
 
    def _connect(self, argv):
356
 
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
357
 
        # subprocess.  We prefer sockets to pipes because they support
358
 
        # non-blocking short reads, allowing us to optimistically read 64k (or
359
 
        # whatever) chunks.
360
 
        try:
361
 
            my_sock, subproc_sock = socket.socketpair()
362
 
        except (AttributeError, socket.error):
363
 
            # This platform doesn't support socketpair(), so just use ordinary
364
 
            # pipes instead.
365
 
            stdin = stdout = subprocess.PIPE
366
 
            sock = None
367
 
        else:
368
 
            stdin = stdout = subproc_sock
369
 
            sock = my_sock
370
 
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
371
 
                                **os_specific_subprocess_params())
372
 
        return SSHSubprocessConnection(proc, sock=sock)
373
 
 
374
 
    def connect_sftp(self, username, password, host, port):
375
 
        try:
376
 
            argv = self._get_vendor_specific_argv(username, host, port,
377
 
                                                  subsystem='sftp')
378
 
            sock = self._connect(argv)
379
 
            return SFTPClient(SocketAsChannelAdapter(sock))
380
 
        except _ssh_connection_errors, e:
381
 
            self._raise_connection_error(host, port=port, orig_error=e)
382
 
 
383
 
    def connect_ssh(self, username, password, host, port, command):
384
 
        try:
385
 
            argv = self._get_vendor_specific_argv(username, host, port,
386
 
                                                  command=command)
387
 
            return self._connect(argv)
388
 
        except _ssh_connection_errors, e:
389
 
            self._raise_connection_error(host, port=port, orig_error=e)
390
 
 
391
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
392
 
                                  command=None):
393
 
        """Returns the argument list to run the subprocess with.
394
 
 
395
 
        Exactly one of 'subsystem' and 'command' must be specified.
396
 
        """
397
 
        raise NotImplementedError(self._get_vendor_specific_argv)
398
 
 
399
 
 
400
 
class OpenSSHSubprocessVendor(SubprocessVendor):
401
 
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
402
 
 
403
 
    executable_path = 'ssh'
404
 
 
405
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
406
 
                                  command=None):
407
 
        args = [self.executable_path,
408
 
                '-oForwardX11=no', '-oForwardAgent=no',
409
 
                '-oClearAllForwardings=yes',
410
 
                '-oNoHostAuthenticationForLocalhost=yes']
411
 
        if port is not None:
412
 
            args.extend(['-p', str(port)])
413
 
        if username is not None:
414
 
            args.extend(['-l', username])
415
 
        if subsystem is not None:
416
 
            args.extend(['-s', host, subsystem])
417
 
        else:
418
 
            args.extend([host] + command)
419
 
        return args
420
 
 
421
 
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
422
 
 
423
 
 
424
 
class SSHCorpSubprocessVendor(SubprocessVendor):
425
 
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
426
 
 
427
 
    executable_path = 'ssh'
428
 
 
429
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
430
 
                                  command=None):
431
 
        args = [self.executable_path, '-x']
432
 
        if port is not None:
433
 
            args.extend(['-p', str(port)])
434
 
        if username is not None:
435
 
            args.extend(['-l', username])
436
 
        if subsystem is not None:
437
 
            args.extend(['-s', subsystem, host])
438
 
        else:
439
 
            args.extend([host] + command)
440
 
        return args
441
 
 
442
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
443
 
 
444
 
 
445
 
class LSHSubprocessVendor(SubprocessVendor):
446
 
    """SSH vendor that uses the 'lsh' executable from GNU"""
447
 
 
448
 
    executable_path = 'lsh'
449
 
 
450
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
451
 
                                  command=None):
452
 
        args = [self.executable_path]
453
 
        if port is not None:
454
 
            args.extend(['-p', str(port)])
455
 
        if username is not None:
456
 
            args.extend(['-l', username])
457
 
        if subsystem is not None:
458
 
            args.extend(['--subsystem', subsystem, host])
459
 
        else:
460
 
            args.extend([host] + command)
461
 
        return args
462
 
 
463
 
register_ssh_vendor('lsh', LSHSubprocessVendor())
464
 
 
465
 
 
466
 
class PLinkSubprocessVendor(SubprocessVendor):
467
 
    """SSH vendor that uses the 'plink' executable from Putty."""
468
 
 
469
 
    executable_path = 'plink'
470
 
 
471
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
472
 
                                  command=None):
473
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
474
 
        if port is not None:
475
 
            args.extend(['-P', str(port)])
476
 
        if username is not None:
477
 
            args.extend(['-l', username])
478
 
        if subsystem is not None:
479
 
            args.extend(['-s', host, subsystem])
480
 
        else:
481
 
            args.extend([host] + command)
482
 
        return args
483
 
 
484
 
register_ssh_vendor('plink', PLinkSubprocessVendor())
485
 
 
486
 
 
487
 
def _paramiko_auth(username, password, host, port, paramiko_transport):
488
 
    auth = config.AuthenticationConfig()
489
 
    # paramiko requires a username, but it might be none if nothing was
490
 
    # supplied.  If so, use the local username.
491
 
    if username is None:
492
 
        username = auth.get_user('ssh', host, port=port,
493
 
                                 default=getpass.getuser())
494
 
    if _use_ssh_agent:
495
 
        agent = paramiko.Agent()
496
 
        for key in agent.get_keys():
497
 
            trace.mutter('Trying SSH agent key %s'
498
 
                         % paramiko.util.hexify(key.get_fingerprint()))
499
 
            try:
500
 
                paramiko_transport.auth_publickey(username, key)
501
 
                return
502
 
            except paramiko.SSHException, e:
503
 
                pass
504
 
 
505
 
    # okay, try finding id_rsa or id_dss?  (posix only)
506
 
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
507
 
        return
508
 
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
509
 
        return
510
 
 
511
 
    # If we have gotten this far, we are about to try for passwords, do an
512
 
    # auth_none check to see if it is even supported.
513
 
    supported_auth_types = []
514
 
    try:
515
 
        # Note that with paramiko <1.7.5 this logs an INFO message:
516
 
        #    Authentication type (none) not permitted.
517
 
        # So we explicitly disable the logging level for this action
518
 
        old_level = paramiko_transport.logger.level
519
 
        paramiko_transport.logger.setLevel(logging.WARNING)
520
 
        try:
521
 
            paramiko_transport.auth_none(username)
522
 
        finally:
523
 
            paramiko_transport.logger.setLevel(old_level)
524
 
    except paramiko.BadAuthenticationType, e:
525
 
        # Supported methods are in the exception
526
 
        supported_auth_types = e.allowed_types
527
 
    except paramiko.SSHException, e:
528
 
        # Don't know what happened, but just ignore it
529
 
        pass
530
 
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
531
 
    # because Paramiko's auth_password method will automatically try
532
 
    # 'keyboard-interactive' auth (using the password as the response) if
533
 
    # 'password' auth is not available.  Apparently some Debian and Gentoo
534
 
    # OpenSSH servers require this.
535
 
    # XXX: It's possible for a server to require keyboard-interactive auth that
536
 
    # requires something other than a single password, but we currently don't
537
 
    # support that.
538
 
    if ('password' not in supported_auth_types and
539
 
        'keyboard-interactive' not in supported_auth_types):
540
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
541
 
            '\n  %s@%s\nsupported auth types: %s'
542
 
            % (username, host, supported_auth_types))
543
 
 
544
 
    if password:
545
 
        try:
546
 
            paramiko_transport.auth_password(username, password)
547
 
            return
548
 
        except paramiko.SSHException, e:
549
 
            pass
550
 
 
551
 
    # give up and ask for a password
552
 
    password = auth.get_password('ssh', host, username, port=port)
553
 
    # get_password can still return None, which means we should not prompt
554
 
    if password is not None:
555
 
        try:
556
 
            paramiko_transport.auth_password(username, password)
557
 
        except paramiko.SSHException, e:
558
 
            raise errors.ConnectionError(
559
 
                'Unable to authenticate to SSH host as'
560
 
                '\n  %s@%s\n' % (username, host), e)
561
 
    else:
562
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
563
 
                                     '  %s@%s' % (username, host))
564
 
 
565
 
 
566
 
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
567
 
    filename = os.path.expanduser('~/.ssh/' + filename)
568
 
    try:
569
 
        key = pkey_class.from_private_key_file(filename)
570
 
        paramiko_transport.auth_publickey(username, key)
571
 
        return True
572
 
    except paramiko.PasswordRequiredException:
573
 
        password = ui.ui_factory.get_password(
574
 
            prompt='SSH %(filename)s password', filename=filename)
575
 
        try:
576
 
            key = pkey_class.from_private_key_file(filename, password)
577
 
            paramiko_transport.auth_publickey(username, key)
578
 
            return True
579
 
        except paramiko.SSHException:
580
 
            trace.mutter('SSH authentication via %s key failed.'
581
 
                         % (os.path.basename(filename),))
582
 
    except paramiko.SSHException:
583
 
        trace.mutter('SSH authentication via %s key failed.'
584
 
                     % (os.path.basename(filename),))
585
 
    except IOError:
586
 
        pass
587
 
    return False
588
 
 
589
 
 
590
 
def load_host_keys():
591
 
    """
592
 
    Load system host keys (probably doesn't work on windows) and any
593
 
    "discovered" keys from previous sessions.
594
 
    """
595
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
596
 
    try:
597
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
598
 
            os.path.expanduser('~/.ssh/known_hosts'))
599
 
    except IOError, e:
600
 
        trace.mutter('failed to load system host keys: ' + str(e))
601
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
602
 
    try:
603
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
604
 
    except IOError, e:
605
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
606
 
        save_host_keys()
607
 
 
608
 
 
609
 
def save_host_keys():
610
 
    """
611
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
612
 
    """
613
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
614
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
615
 
    config.ensure_config_dir_exists()
616
 
 
617
 
    try:
618
 
        f = open(bzr_hostkey_path, 'w')
619
 
        f.write('# SSH host keys collected by bzr\n')
620
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
621
 
            for keytype, key in keys.iteritems():
622
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
623
 
        f.close()
624
 
    except IOError, e:
625
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
626
 
 
627
 
 
628
 
def os_specific_subprocess_params():
629
 
    """Get O/S specific subprocess parameters."""
630
 
    if sys.platform == 'win32':
631
 
        # setting the process group and closing fds is not supported on
632
 
        # win32
633
 
        return {}
634
 
    else:
635
 
        # We close fds other than the pipes as the child process does not need
636
 
        # them to be open.
637
 
        #
638
 
        # We also set the child process to ignore SIGINT.  Normally the signal
639
 
        # would be sent to every process in the foreground process group, but
640
 
        # this causes it to be seen only by bzr and not by ssh.  Python will
641
 
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
642
 
        # to release locks or do other cleanup over ssh before the connection
643
 
        # goes away.
644
 
        # <https://launchpad.net/products/bzr/+bug/5987>
645
 
        #
646
 
        # Running it in a separate process group is not good because then it
647
 
        # can't get non-echoed input of a password or passphrase.
648
 
        # <https://launchpad.net/products/bzr/+bug/40508>
649
 
        return {'preexec_fn': _ignore_signals,
650
 
                'close_fds': True,
651
 
                }
652
 
 
653
 
import weakref
654
 
_subproc_weakrefs = set()
655
 
 
656
 
def _close_ssh_proc(proc):
657
 
    """Carefully close stdin/stdout and reap the SSH process.
658
 
 
659
 
    If the pipes are already closed and/or the process has already been
660
 
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
661
 
    to clean up (whether or not a clean up was already tried).
662
 
    """
663
 
    dotted_names = ['stdin.close', 'stdout.close', 'wait']
664
 
    for dotted_name in dotted_names:
665
 
        attrs = dotted_name.split('.')
666
 
        try:
667
 
            obj = proc
668
 
            for attr in attrs:
669
 
                obj = getattr(obj, attr)
670
 
        except AttributeError:
671
 
            # It's ok for proc.stdin or proc.stdout to be None.
672
 
            continue
673
 
        try:
674
 
            obj()
675
 
        except OSError:
676
 
            # It's ok for the pipe to already be closed, or the process to
677
 
            # already be finished.
678
 
            continue
679
 
 
680
 
 
681
 
class SSHConnection(object):
682
 
    """Abstract base class for SSH connections."""
683
 
 
684
 
    def get_sock_or_pipes(self):
685
 
        """Returns a (kind, io_object) pair.
686
 
 
687
 
        If kind == 'socket', then io_object is a socket.
688
 
 
689
 
        If kind == 'pipes', then io_object is a pair of file-like objects
690
 
        (read_from, write_to).
691
 
        """
692
 
        raise NotImplementedError(self.get_sock_or_pipes)
693
 
 
694
 
    def close(self):
695
 
        raise NotImplementedError(self.close)
696
 
 
697
 
 
698
 
class SSHSubprocessConnection(SSHConnection):
699
 
    """A connection to an ssh subprocess via pipes or a socket.
700
 
 
701
 
    This class is also socket-like enough to be used with
702
 
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
703
 
    """
704
 
 
705
 
    def __init__(self, proc, sock=None):
706
 
        """Constructor.
707
 
 
708
 
        :param proc: a subprocess.Popen
709
 
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
710
 
            should bzrlib's half of that socketpair.  If not passed, proc's
711
 
            stdin/out is assumed to be ordinary pipes.
712
 
        """
713
 
        self.proc = proc
714
 
        self._sock = sock
715
 
        # Add a weakref to proc that will attempt to do the same as self.close
716
 
        # to avoid leaving processes lingering indefinitely.
717
 
        def terminate(ref):
718
 
            _subproc_weakrefs.remove(ref)
719
 
            _close_ssh_proc(proc)
720
 
        _subproc_weakrefs.add(weakref.ref(self, terminate))
721
 
 
722
 
    def send(self, data):
723
 
        if self._sock is not None:
724
 
            return self._sock.send(data)
725
 
        else:
726
 
            return os.write(self.proc.stdin.fileno(), data)
727
 
 
728
 
    def recv(self, count):
729
 
        if self._sock is not None:
730
 
            return self._sock.recv(count)
731
 
        else:
732
 
            return os.read(self.proc.stdout.fileno(), count)
733
 
 
734
 
    def close(self):
735
 
        _close_ssh_proc(self.proc)
736
 
 
737
 
    def get_sock_or_pipes(self):
738
 
        if self._sock is not None:
739
 
            return 'socket', self._sock
740
 
        else:
741
 
            return 'pipes', (self.proc.stdout, self.proc.stdin)
742
 
 
743
 
 
744
 
class _ParamikoSSHConnection(SSHConnection):
745
 
    """An SSH connection via paramiko."""
746
 
 
747
 
    def __init__(self, channel):
748
 
        self.channel = channel
749
 
 
750
 
    def get_sock_or_pipes(self):
751
 
        return ('socket', self.channel)
752
 
 
753
 
    def close(self):
754
 
        return self.channel.close()
755
 
 
756