~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Martin Packman
  • Date: 2011-12-23 19:38:22 UTC
  • mto: This revision was merged to the branch mainline in revision 6405.
  • Revision ID: martin.packman@canonical.com-20111223193822-hesheea4o8aqwexv
Accept and document passing the medium rather than transport for smart connections

Show diffs side-by-side

added added

removed removed

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