~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2012-02-20 14:15:25 UTC
  • mto: (6471.1.4 iter-child-entries)
  • mto: This revision was merged to the branch mainline in revision 6472.
  • Revision ID: jelmer@samba.org-20120220141525-9azkfei62st8yc7w
Use inventories directly in fewer places.

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