~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Martin Pool
  • Date: 2005-07-22 22:37:53 UTC
  • Revision ID: mbp@sourcefrog.net-20050722223753-7dced4e32d3ce21d
- add the start of a test for inventory file-id matching

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