~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Patch Queue Manager
  • Date: 2016-02-01 19:13:13 UTC
  • mfrom: (6614.2.2 trunk)
  • Revision ID: pqm@pqm.ubuntu.com-20160201191313-wdfvmfff1djde6oq
(vila) Release 2.7.0 (Vincent Ladeuil)

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