~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Ian Clatworthy
  • Date: 2007-08-13 14:33:10 UTC
  • mto: (2733.1.1 ianc-integration)
  • mto: This revision was merged to the branch mainline in revision 2734.
  • Revision ID: ian.clatworthy@internode.on.net-20070813143310-twhj4la0qnupvze8
Added Quick Start Summary

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
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
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
20
import errno
21
21
import getpass
22
 
import logging
23
22
import os
24
23
import socket
25
24
import subprocess
26
25
import sys
27
26
 
28
 
from bzrlib import (
29
 
    config,
30
 
    errors,
31
 
    osutils,
32
 
    trace,
33
 
    ui,
34
 
    )
 
27
from bzrlib.config import config_dir, ensure_config_dir_exists
 
28
from bzrlib.errors import (ConnectionError,
 
29
                           ParamikoNotPresent,
 
30
                           SocketConnectionError,
 
31
                           SSHVendorNotFound,
 
32
                           TransportError,
 
33
                           UnknownSSH,
 
34
                           )
 
35
 
 
36
from bzrlib.osutils import pathjoin
 
37
from bzrlib.trace import mutter, warning
 
38
import bzrlib.ui
35
39
 
36
40
try:
37
41
    import paramiko
94
98
            try:
95
99
                vendor = self._ssh_vendors[vendor_name]
96
100
            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
                raise UnknownSSH(vendor_name)
101
102
            return vendor
102
103
        return None
103
104
 
113
114
            stdout = stderr = ''
114
115
        return stdout + stderr
115
116
 
116
 
    def _get_vendor_by_version_string(self, version, progname):
 
117
    def _get_vendor_by_version_string(self, version):
117
118
        """Return the vendor or None based on output from the subprocess.
118
119
 
119
120
        :param version: The output of 'ssh -V' like command.
120
 
        :param args: Command line that was run.
121
121
        """
122
122
        vendor = None
123
123
        if 'OpenSSH' in version:
124
 
            trace.mutter('ssh implementation is OpenSSH')
 
124
            mutter('ssh implementation is OpenSSH')
125
125
            vendor = OpenSSHSubprocessVendor()
126
126
        elif 'SSH Secure Shell' in version:
127
 
            trace.mutter('ssh implementation is SSH Corp.')
 
127
            mutter('ssh implementation is SSH Corp.')
128
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.")
 
129
        elif 'plink' in version:
 
130
            mutter("ssh implementation is Putty's plink.")
137
131
            vendor = PLinkSubprocessVendor()
138
132
        return vendor
139
133
 
140
134
    def _get_vendor_by_inspection(self):
141
135
        """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])
 
136
        for args in [['ssh', '-V'], ['plink', '-V']]:
 
137
            version = self._get_ssh_version_string(args)
 
138
            vendor = self._get_vendor_by_version_string(version)
 
139
            if vendor is not None:
 
140
                return vendor
 
141
        return None
150
142
 
151
143
    def get_vendor(self, environment=None):
152
144
        """Find out what version of SSH is on the system.
160
152
            if vendor is None:
161
153
                vendor = self._get_vendor_by_inspection()
162
154
                if vendor is None:
163
 
                    trace.mutter('falling back to default implementation')
 
155
                    mutter('falling back to default implementation')
164
156
                    vendor = self._default_ssh_vendor
165
157
                    if vendor is None:
166
 
                        raise errors.SSHVendorNotFound()
 
158
                        raise SSHVendorNotFound()
167
159
            self._cached_ssh_vendor = vendor
168
160
        return self._cached_ssh_vendor
169
161
 
173
165
register_ssh_vendor = _ssh_vendor_manager.register_vendor
174
166
 
175
167
 
176
 
def _ignore_signals():
 
168
def _ignore_sigint():
177
169
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
178
170
    # doesn't handle it itself.
179
171
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
180
172
    import signal
181
173
    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):
 
174
 
 
175
 
 
176
class LoopbackSFTP(object):
188
177
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
189
178
 
190
179
    def __init__(self, sock):
191
180
        self.__socket = sock
192
 
 
193
 
    def get_name(self):
194
 
        return "bzr SocketAsChannelAdapter"
195
 
 
 
181
 
196
182
    def send(self, data):
197
183
        return self.__socket.send(data)
198
184
 
199
185
    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
 
186
        return self.__socket.recv(n)
209
187
 
210
188
    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
189
        return True
216
190
 
217
191
    def close(self):
220
194
 
221
195
class SSHVendor(object):
222
196
    """Abstract base class for SSH vendor implementations."""
223
 
 
 
197
    
224
198
    def connect_sftp(self, username, password, host, port):
225
199
        """Make an SSH connection, and return an SFTPClient.
226
 
 
 
200
        
227
201
        :param username: an ascii string
228
202
        :param password: an ascii string
229
203
        :param host: a host name as an ascii string
238
212
 
239
213
    def connect_ssh(self, username, password, host, port, command):
240
214
        """Make an SSH connection.
241
 
 
242
 
        :returns: an SSHConnection.
 
215
        
 
216
        :returns: something with a `close` method, and a `get_filelike_channels`
 
217
            method that returns a pair of (read, write) filelike objects.
243
218
        """
244
219
        raise NotImplementedError(self.connect_ssh)
245
 
 
 
220
        
246
221
    def _raise_connection_error(self, host, port=None, orig_error=None,
247
222
                                msg='Unable to connect to SSH host'):
248
223
        """Raise a SocketConnectionError with properly formatted host.
250
225
        This just unifies all the locations that try to raise ConnectionError,
251
226
        so that they format things properly.
252
227
        """
253
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
254
 
                                           orig_error=orig_error)
 
228
        raise SocketConnectionError(host=host, port=port, msg=msg,
 
229
                                    orig_error=orig_error)
255
230
 
256
231
 
257
232
class LoopbackVendor(SSHVendor):
258
233
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
259
 
 
 
234
    
260
235
    def connect_sftp(self, username, password, host, port):
261
236
        sock = socket.socket()
262
237
        try:
263
238
            sock.connect((host, port))
264
239
        except socket.error, e:
265
240
            self._raise_connection_error(host, port=port, orig_error=e)
266
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
241
        return SFTPClient(LoopbackSFTP(sock))
267
242
 
268
243
register_ssh_vendor('loopback', LoopbackVendor())
269
244
 
270
245
 
 
246
class _ParamikoSSHConnection(object):
 
247
    def __init__(self, channel):
 
248
        self.channel = channel
 
249
 
 
250
    def get_filelike_channels(self):
 
251
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
252
 
 
253
    def close(self):
 
254
        return self.channel.close()
 
255
 
 
256
 
271
257
class ParamikoVendor(SSHVendor):
272
258
    """Vendor that uses paramiko."""
273
259
 
274
260
    def _connect(self, username, password, host, port):
275
261
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
276
 
 
 
262
        
277
263
        load_host_keys()
278
264
 
279
265
        try:
282
268
            t.start_client()
283
269
        except (paramiko.SSHException, socket.error), e:
284
270
            self._raise_connection_error(host, port=port, orig_error=e)
285
 
 
 
271
            
286
272
        server_key = t.get_remote_server_key()
287
273
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
288
274
        keytype = server_key.get_name()
289
275
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
290
276
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
291
 
            our_server_key_hex = paramiko.util.hexify(
292
 
                our_server_key.get_fingerprint())
 
277
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
293
278
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
294
279
            our_server_key = BZR_HOSTKEYS[host][keytype]
295
 
            our_server_key_hex = paramiko.util.hexify(
296
 
                our_server_key.get_fingerprint())
 
280
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
297
281
        else:
298
 
            trace.warning('Adding %s host key for %s: %s'
299
 
                          % (keytype, host, server_key_hex))
 
282
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
300
283
            add = getattr(BZR_HOSTKEYS, 'add', None)
301
284
            if add is not None: # paramiko >= 1.X.X
302
285
                BZR_HOSTKEYS.add(host, keytype, server_key)
303
286
            else:
304
287
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
305
288
            our_server_key = server_key
306
 
            our_server_key_hex = paramiko.util.hexify(
307
 
                our_server_key.get_fingerprint())
 
289
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
308
290
            save_host_keys()
309
291
        if server_key != our_server_key:
310
292
            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' %
 
293
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
294
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
314
295
                (host, our_server_key_hex, server_key_hex),
315
296
                ['Try editing %s or %s' % (filename1, filename2)])
316
297
 
317
 
        _paramiko_auth(username, password, host, port, t)
 
298
        _paramiko_auth(username, password, host, t)
318
299
        return t
319
 
 
 
300
        
320
301
    def connect_sftp(self, username, password, host, port):
321
302
        t = self._connect(username, password, host, port)
322
303
        try:
336
317
            self._raise_connection_error(host, port=port, orig_error=e,
337
318
                                         msg='Unable to invoke remote bzr')
338
319
 
339
 
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
340
320
if paramiko is not None:
341
321
    vendor = ParamikoVendor()
342
322
    register_ssh_vendor('paramiko', vendor)
343
323
    register_ssh_vendor('none', vendor)
344
324
    register_default_ssh_vendor(vendor)
345
 
    _ssh_connection_errors += (paramiko.SSHException,)
346
325
    del vendor
347
326
 
348
327
 
349
328
class SubprocessVendor(SSHVendor):
350
329
    """Abstract base class for vendors that use pipes to a subprocess."""
351
 
 
 
330
    
352
331
    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,
 
332
        proc = subprocess.Popen(argv,
 
333
                                stdin=subprocess.PIPE,
 
334
                                stdout=subprocess.PIPE,
368
335
                                **os_specific_subprocess_params())
369
 
        return SSHSubprocessConnection(proc, sock=sock)
 
336
        return SSHSubprocess(proc)
370
337
 
371
338
    def connect_sftp(self, username, password, host, port):
372
339
        try:
373
340
            argv = self._get_vendor_specific_argv(username, host, port,
374
341
                                                  subsystem='sftp')
375
342
            sock = self._connect(argv)
376
 
            return SFTPClient(SocketAsChannelAdapter(sock))
377
 
        except _ssh_connection_errors, e:
 
343
            return SFTPClient(sock)
 
344
        except (EOFError, paramiko.SSHException), e:
 
345
            self._raise_connection_error(host, port=port, orig_error=e)
 
346
        except (OSError, IOError), e:
 
347
            # If the machine is fast enough, ssh can actually exit
 
348
            # before we try and send it the sftp request, which
 
349
            # raises a Broken Pipe
 
350
            if e.errno not in (errno.EPIPE,):
 
351
                raise
378
352
            self._raise_connection_error(host, port=port, orig_error=e)
379
353
 
380
354
    def connect_ssh(self, username, password, host, port, command):
382
356
            argv = self._get_vendor_specific_argv(username, host, port,
383
357
                                                  command=command)
384
358
            return self._connect(argv)
385
 
        except _ssh_connection_errors, e:
 
359
        except (EOFError), e:
 
360
            self._raise_connection_error(host, port=port, orig_error=e)
 
361
        except (OSError, IOError), e:
 
362
            # If the machine is fast enough, ssh can actually exit
 
363
            # before we try and send it the sftp request, which
 
364
            # raises a Broken Pipe
 
365
            if e.errno not in (errno.EPIPE,):
 
366
                raise
386
367
            self._raise_connection_error(host, port=port, orig_error=e)
387
368
 
388
369
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
389
370
                                  command=None):
390
371
        """Returns the argument list to run the subprocess with.
391
 
 
 
372
        
392
373
        Exactly one of 'subsystem' and 'command' must be specified.
393
374
        """
394
375
        raise NotImplementedError(self._get_vendor_specific_argv)
396
377
 
397
378
class OpenSSHSubprocessVendor(SubprocessVendor):
398
379
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
399
 
 
400
 
    executable_path = 'ssh'
401
 
 
 
380
    
402
381
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
403
382
                                  command=None):
404
 
        args = [self.executable_path,
 
383
        assert subsystem is not None or command is not None, (
 
384
            'Must specify a command or subsystem')
 
385
        if subsystem is not None:
 
386
            assert command is None, (
 
387
                'subsystem and command are mutually exclusive')
 
388
        args = ['ssh',
405
389
                '-oForwardX11=no', '-oForwardAgent=no',
406
390
                '-oClearAllForwardings=yes', '-oProtocol=2',
407
391
                '-oNoHostAuthenticationForLocalhost=yes']
421
405
class SSHCorpSubprocessVendor(SubprocessVendor):
422
406
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
423
407
 
424
 
    executable_path = 'ssh'
425
 
 
426
408
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
427
409
                                  command=None):
428
 
        args = [self.executable_path, '-x']
 
410
        assert subsystem is not None or command is not None, (
 
411
            'Must specify a command or subsystem')
 
412
        if subsystem is not None:
 
413
            assert command is None, (
 
414
                'subsystem and command are mutually exclusive')
 
415
        args = ['ssh', '-x']
429
416
        if port is not None:
430
417
            args.extend(['-p', str(port)])
431
418
        if username is not None:
435
422
        else:
436
423
            args.extend([host] + command)
437
424
        return args
438
 
 
439
 
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
425
    
 
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
440
427
 
441
428
 
442
429
class PLinkSubprocessVendor(SubprocessVendor):
443
430
    """SSH vendor that uses the 'plink' executable from Putty."""
444
431
 
445
 
    executable_path = 'plink'
446
 
 
447
432
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
448
433
                                  command=None):
449
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
434
        assert subsystem is not None or command is not None, (
 
435
            'Must specify a command or subsystem')
 
436
        if subsystem is not None:
 
437
            assert command is None, (
 
438
                'subsystem and command are mutually exclusive')
 
439
        args = ['plink', '-x', '-a', '-ssh', '-2']
450
440
        if port is not None:
451
441
            args.extend(['-P', str(port)])
452
442
        if username is not None:
460
450
register_ssh_vendor('plink', PLinkSubprocessVendor())
461
451
 
462
452
 
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())
 
453
def _paramiko_auth(username, password, host, paramiko_transport):
 
454
    # paramiko requires a username, but it might be none if nothing was supplied
 
455
    # use the local username, just in case.
 
456
    # We don't override username, because if we aren't using paramiko,
 
457
    # the username might be specified in ~/.ssh/config and we don't want to
 
458
    # force it to something else
 
459
    # Also, it would mess up the self.relpath() functionality
 
460
    username = username or getpass.getuser()
 
461
 
470
462
    if _use_ssh_agent:
471
463
        agent = paramiko.Agent()
472
464
        for key in agent.get_keys():
473
 
            trace.mutter('Trying SSH agent key %s'
474
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
465
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
475
466
            try:
476
467
                paramiko_transport.auth_publickey(username, key)
477
468
                return
478
469
            except paramiko.SSHException, e:
479
470
                pass
480
 
 
 
471
    
481
472
    # okay, try finding id_rsa or id_dss?  (posix only)
482
473
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
483
474
        return
484
475
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
485
476
        return
486
477
 
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
478
    if password:
521
479
        try:
522
480
            paramiko_transport.auth_password(username, password)
525
483
            pass
526
484
 
527
485
    # 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))
 
486
    password = bzrlib.ui.ui_factory.get_password(
 
487
            prompt='SSH %(user)s@%(host)s password',
 
488
            user=username, host=host)
 
489
    try:
 
490
        paramiko_transport.auth_password(username, password)
 
491
    except paramiko.SSHException, e:
 
492
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
493
                              (username, host), e)
540
494
 
541
495
 
542
496
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
546
500
        paramiko_transport.auth_publickey(username, key)
547
501
        return True
548
502
    except paramiko.PasswordRequiredException:
549
 
        password = ui.ui_factory.get_password(
550
 
            prompt='SSH %(filename)s password', filename=filename)
 
503
        password = bzrlib.ui.ui_factory.get_password(
 
504
                prompt='SSH %(filename)s password',
 
505
                filename=filename)
551
506
        try:
552
507
            key = pkey_class.from_private_key_file(filename, password)
553
508
            paramiko_transport.auth_publickey(username, key)
554
509
            return True
555
510
        except paramiko.SSHException:
556
 
            trace.mutter('SSH authentication via %s key failed.'
557
 
                         % (os.path.basename(filename),))
 
511
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
558
512
    except paramiko.SSHException:
559
 
        trace.mutter('SSH authentication via %s key failed.'
560
 
                     % (os.path.basename(filename),))
 
513
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
561
514
    except IOError:
562
515
        pass
563
516
    return False
570
523
    """
571
524
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
572
525
    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')
 
526
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
527
    except Exception, e:
 
528
        mutter('failed to load system host keys: ' + str(e))
 
529
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
578
530
    try:
579
531
        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))
 
532
    except Exception, e:
 
533
        mutter('failed to load bzr host keys: ' + str(e))
582
534
        save_host_keys()
583
535
 
584
536
 
587
539
    Save "discovered" host keys in $(config)/ssh_host_keys/.
588
540
    """
589
541
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
590
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
591
 
    config.ensure_config_dir_exists()
 
542
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
543
    ensure_config_dir_exists()
592
544
 
593
545
    try:
594
546
        f = open(bzr_hostkey_path, 'w')
598
550
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
599
551
        f.close()
600
552
    except IOError, e:
601
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
553
        mutter('failed to save bzr host keys: ' + str(e))
602
554
 
603
555
 
604
556
def os_specific_subprocess_params():
605
557
    """Get O/S specific subprocess parameters."""
606
558
    if sys.platform == 'win32':
607
 
        # setting the process group and closing fds is not supported on
 
559
        # setting the process group and closing fds is not supported on 
608
560
        # win32
609
561
        return {}
610
562
    else:
611
 
        # We close fds other than the pipes as the child process does not need
 
563
        # We close fds other than the pipes as the child process does not need 
612
564
        # them to be open.
613
565
        #
614
566
        # We also set the child process to ignore SIGINT.  Normally the signal
616
568
        # this causes it to be seen only by bzr and not by ssh.  Python will
617
569
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
618
570
        # to release locks or do other cleanup over ssh before the connection
619
 
        # goes away.
 
571
        # goes away.  
620
572
        # <https://launchpad.net/products/bzr/+bug/5987>
621
573
        #
622
574
        # Running it in a separate process group is not good because then it
623
575
        # can't get non-echoed input of a password or passphrase.
624
576
        # <https://launchpad.net/products/bzr/+bug/40508>
625
 
        return {'preexec_fn': _ignore_signals,
 
577
        return {'preexec_fn': _ignore_sigint,
626
578
                'close_fds': True,
627
579
                }
628
580
 
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
 
        """
 
581
 
 
582
class SSHSubprocess(object):
 
583
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
584
 
 
585
    def __init__(self, proc):
689
586
        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
587
 
698
588
    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)
 
589
        return os.write(self.proc.stdin.fileno(), data)
 
590
 
 
591
    def recv_ready(self):
 
592
        # TODO: jam 20051215 this function is necessary to support the
 
593
        # pipelined() function. In reality, it probably should use
 
594
        # poll() or select() to actually return if there is data
 
595
        # available, otherwise we probably don't get any benefit
 
596
        return True
703
597
 
704
598
    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
 
 
 
599
        return os.read(self.proc.stdout.fileno(), count)
 
600
 
 
601
    def close(self):
 
602
        self.proc.stdin.close()
 
603
        self.proc.stdout.close()
 
604
        self.proc.wait()
 
605
 
 
606
    def get_filelike_channels(self):
 
607
        return (self.proc.stdout, self.proc.stdin)
732
608