~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Andrew Bennetts
  • Date: 2007-10-29 08:34:38 UTC
  • mto: (2535.4.22 streaming-smart-fetch)
  • mto: This revision was merged to the branch mainline in revision 2981.
  • Revision ID: andrew.bennetts@canonical.com-20071029083438-ke1vsv97dvgrvup5
ImproveĀ someĀ docstrings.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
15
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
 
from __future__ import absolute_import
21
 
 
22
20
import errno
23
21
import getpass
24
 
import logging
25
22
import os
26
23
import socket
27
24
import subprocess
28
25
import sys
29
 
from binascii import hexlify
30
 
 
31
 
from bzrlib import (
32
 
    config,
33
 
    errors,
34
 
    osutils,
35
 
    trace,
36
 
    ui,
37
 
    )
 
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
38
39
 
39
40
try:
40
41
    import paramiko
97
98
            try:
98
99
                vendor = self._ssh_vendors[vendor_name]
99
100
            except KeyError:
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
 
101
                raise UnknownSSH(vendor_name)
104
102
            return vendor
105
103
        return None
106
104
 
116
114
            stdout = stderr = ''
117
115
        return stdout + stderr
118
116
 
119
 
    def _get_vendor_by_version_string(self, version, progname):
 
117
    def _get_vendor_by_version_string(self, version, args):
120
118
        """Return the vendor or None based on output from the subprocess.
121
119
 
122
120
        :param version: The output of 'ssh -V' like command.
124
122
        """
125
123
        vendor = None
126
124
        if 'OpenSSH' in version:
127
 
            trace.mutter('ssh implementation is OpenSSH')
 
125
            mutter('ssh implementation is OpenSSH')
128
126
            vendor = OpenSSHSubprocessVendor()
129
127
        elif 'SSH Secure Shell' in version:
130
 
            trace.mutter('ssh implementation is SSH Corp.')
 
128
            mutter('ssh implementation is SSH Corp.')
131
129
            vendor = SSHCorpSubprocessVendor()
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.")
 
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.")
143
135
            vendor = PLinkSubprocessVendor()
144
136
        return vendor
145
137
 
146
138
    def _get_vendor_by_inspection(self):
147
139
        """Return the vendor or None by checking for known SSH implementations."""
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])
 
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
156
146
 
157
147
    def get_vendor(self, environment=None):
158
148
        """Find out what version of SSH is on the system.
166
156
            if vendor is None:
167
157
                vendor = self._get_vendor_by_inspection()
168
158
                if vendor is None:
169
 
                    trace.mutter('falling back to default implementation')
 
159
                    mutter('falling back to default implementation')
170
160
                    vendor = self._default_ssh_vendor
171
161
                    if vendor is None:
172
 
                        raise errors.SSHVendorNotFound()
 
162
                        raise SSHVendorNotFound()
173
163
            self._cached_ssh_vendor = vendor
174
164
        return self._cached_ssh_vendor
175
165
 
179
169
register_ssh_vendor = _ssh_vendor_manager.register_vendor
180
170
 
181
171
 
182
 
def _ignore_signals():
 
172
def _ignore_sigint():
183
173
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
184
174
    # doesn't handle it itself.
185
175
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
186
176
    import signal
187
177
    signal.signal(signal.SIGINT, signal.SIG_IGN)
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):
 
178
 
 
179
 
 
180
class LoopbackSFTP(object):
194
181
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
195
182
 
196
183
    def __init__(self, sock):
197
184
        self.__socket = sock
198
 
 
199
 
    def get_name(self):
200
 
        return "bzr SocketAsChannelAdapter"
201
 
 
 
185
 
202
186
    def send(self, data):
203
187
        return self.__socket.send(data)
204
188
 
205
189
    def recv(self, 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
 
190
        return self.__socket.recv(n)
215
191
 
216
192
    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
221
193
        return True
222
194
 
223
195
    def close(self):
226
198
 
227
199
class SSHVendor(object):
228
200
    """Abstract base class for SSH vendor implementations."""
229
 
 
 
201
    
230
202
    def connect_sftp(self, username, password, host, port):
231
203
        """Make an SSH connection, and return an SFTPClient.
232
 
 
 
204
        
233
205
        :param username: an ascii string
234
206
        :param password: an ascii string
235
207
        :param host: a host name as an ascii string
244
216
 
245
217
    def connect_ssh(self, username, password, host, port, command):
246
218
        """Make an SSH connection.
247
 
 
248
 
        :returns: an SSHConnection.
 
219
        
 
220
        :returns: something with a `close` method, and a `get_filelike_channels`
 
221
            method that returns a pair of (read, write) filelike objects.
249
222
        """
250
223
        raise NotImplementedError(self.connect_ssh)
251
 
 
 
224
        
252
225
    def _raise_connection_error(self, host, port=None, orig_error=None,
253
226
                                msg='Unable to connect to SSH host'):
254
227
        """Raise a SocketConnectionError with properly formatted host.
256
229
        This just unifies all the locations that try to raise ConnectionError,
257
230
        so that they format things properly.
258
231
        """
259
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
260
 
                                           orig_error=orig_error)
 
232
        raise SocketConnectionError(host=host, port=port, msg=msg,
 
233
                                    orig_error=orig_error)
261
234
 
262
235
 
263
236
class LoopbackVendor(SSHVendor):
264
237
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
265
 
 
 
238
    
266
239
    def connect_sftp(self, username, password, host, port):
267
240
        sock = socket.socket()
268
241
        try:
269
242
            sock.connect((host, port))
270
243
        except socket.error, e:
271
244
            self._raise_connection_error(host, port=port, orig_error=e)
272
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
245
        return SFTPClient(LoopbackSFTP(sock))
273
246
 
274
247
register_ssh_vendor('loopback', LoopbackVendor())
275
248
 
276
249
 
 
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
 
277
261
class ParamikoVendor(SSHVendor):
278
262
    """Vendor that uses paramiko."""
279
263
 
280
 
    def _hexify(self, s):
281
 
        return hexlify(s).upper()
282
 
 
283
264
    def _connect(self, username, password, host, port):
284
265
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
285
 
 
 
266
        
286
267
        load_host_keys()
287
268
 
288
269
        try:
291
272
            t.start_client()
292
273
        except (paramiko.SSHException, socket.error), e:
293
274
            self._raise_connection_error(host, port=port, orig_error=e)
294
 
 
 
275
            
295
276
        server_key = t.get_remote_server_key()
296
 
        server_key_hex = self._hexify(server_key.get_fingerprint())
 
277
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
297
278
        keytype = server_key.get_name()
298
279
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
299
280
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
300
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
281
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
301
282
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
302
283
            our_server_key = BZR_HOSTKEYS[host][keytype]
303
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
284
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
304
285
        else:
305
 
            trace.warning('Adding %s host key for %s: %s'
306
 
                          % (keytype, host, server_key_hex))
 
286
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
307
287
            add = getattr(BZR_HOSTKEYS, 'add', None)
308
288
            if add is not None: # paramiko >= 1.X.X
309
289
                BZR_HOSTKEYS.add(host, keytype, server_key)
310
290
            else:
311
291
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
312
292
            our_server_key = server_key
313
 
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
293
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
314
294
            save_host_keys()
315
295
        if server_key != our_server_key:
316
296
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
317
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
318
 
            raise errors.TransportError(
319
 
                'Host keys for %s do not match!  %s != %s' %
 
297
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
298
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
320
299
                (host, our_server_key_hex, server_key_hex),
321
300
                ['Try editing %s or %s' % (filename1, filename2)])
322
301
 
323
 
        _paramiko_auth(username, password, host, port, t)
 
302
        _paramiko_auth(username, password, host, t)
324
303
        return t
325
 
 
 
304
        
326
305
    def connect_sftp(self, username, password, host, port):
327
306
        t = self._connect(username, password, host, port)
328
307
        try:
342
321
            self._raise_connection_error(host, port=port, orig_error=e,
343
322
                                         msg='Unable to invoke remote bzr')
344
323
 
345
 
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
346
324
if paramiko is not None:
347
325
    vendor = ParamikoVendor()
348
326
    register_ssh_vendor('paramiko', vendor)
349
327
    register_ssh_vendor('none', vendor)
350
328
    register_default_ssh_vendor(vendor)
351
 
    _ssh_connection_errors += (paramiko.SSHException,)
352
329
    del vendor
353
330
 
354
331
 
355
332
class SubprocessVendor(SSHVendor):
356
333
    """Abstract base class for vendors that use pipes to a subprocess."""
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
 
 
 
334
    
363
335
    def _connect(self, argv):
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,
 
336
        proc = subprocess.Popen(argv,
 
337
                                stdin=subprocess.PIPE,
 
338
                                stdout=subprocess.PIPE,
380
339
                                **os_specific_subprocess_params())
381
 
        if subproc_sock is not None:
382
 
            subproc_sock.close()
383
 
        return SSHSubprocessConnection(proc, sock=my_sock)
 
340
        return SSHSubprocess(proc)
384
341
 
385
342
    def connect_sftp(self, username, password, host, port):
386
343
        try:
387
344
            argv = self._get_vendor_specific_argv(username, host, port,
388
345
                                                  subsystem='sftp')
389
346
            sock = self._connect(argv)
390
 
            return SFTPClient(SocketAsChannelAdapter(sock))
391
 
        except _ssh_connection_errors, e:
 
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
392
356
            self._raise_connection_error(host, port=port, orig_error=e)
393
357
 
394
358
    def connect_ssh(self, username, password, host, port, command):
396
360
            argv = self._get_vendor_specific_argv(username, host, port,
397
361
                                                  command=command)
398
362
            return self._connect(argv)
399
 
        except _ssh_connection_errors, e:
 
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
400
371
            self._raise_connection_error(host, port=port, orig_error=e)
401
372
 
402
373
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
403
374
                                  command=None):
404
375
        """Returns the argument list to run the subprocess with.
405
 
 
 
376
        
406
377
        Exactly one of 'subsystem' and 'command' must be specified.
407
378
        """
408
379
        raise NotImplementedError(self._get_vendor_specific_argv)
410
381
 
411
382
class OpenSSHSubprocessVendor(SubprocessVendor):
412
383
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
413
 
 
414
 
    executable_path = 'ssh'
415
 
 
 
384
    
416
385
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
417
386
                                  command=None):
418
 
        args = [self.executable_path,
 
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',
419
393
                '-oForwardX11=no', '-oForwardAgent=no',
420
 
                '-oClearAllForwardings=yes',
 
394
                '-oClearAllForwardings=yes', '-oProtocol=2',
421
395
                '-oNoHostAuthenticationForLocalhost=yes']
422
396
        if port is not None:
423
397
            args.extend(['-p', str(port)])
435
409
class SSHCorpSubprocessVendor(SubprocessVendor):
436
410
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
437
411
 
438
 
    executable_path = 'ssh'
439
 
 
440
412
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
441
413
                                  command=None):
442
 
        args = [self.executable_path, '-x']
 
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']
443
420
        if port is not None:
444
421
            args.extend(['-p', str(port)])
445
422
        if username is not None:
449
426
        else:
450
427
            args.extend([host] + command)
451
428
        return args
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())
 
429
    
 
430
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
475
431
 
476
432
 
477
433
class PLinkSubprocessVendor(SubprocessVendor):
478
434
    """SSH vendor that uses the 'plink' executable from Putty."""
479
435
 
480
 
    executable_path = 'plink'
481
 
 
482
436
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
483
437
                                  command=None):
484
 
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
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']
485
444
        if port is not None:
486
445
            args.extend(['-P', str(port)])
487
446
        if username is not None:
495
454
register_ssh_vendor('plink', PLinkSubprocessVendor())
496
455
 
497
456
 
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())
 
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
 
505
466
    if _use_ssh_agent:
506
467
        agent = paramiko.Agent()
507
468
        for key in agent.get_keys():
508
 
            trace.mutter('Trying SSH agent key %s'
509
 
                         % self._hexify(key.get_fingerprint()))
 
469
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
510
470
            try:
511
471
                paramiko_transport.auth_publickey(username, key)
512
472
                return
513
473
            except paramiko.SSHException, e:
514
474
                pass
515
 
 
 
475
    
516
476
    # okay, try finding id_rsa or id_dss?  (posix only)
517
477
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
518
478
        return
519
479
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
520
480
        return
521
481
 
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
 
 
555
482
    if password:
556
483
        try:
557
484
            paramiko_transport.auth_password(username, password)
560
487
            pass
561
488
 
562
489
    # give up and ask for a password
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))
 
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)
575
498
 
576
499
 
577
500
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
581
504
        paramiko_transport.auth_publickey(username, key)
582
505
        return True
583
506
    except paramiko.PasswordRequiredException:
584
 
        password = ui.ui_factory.get_password(
585
 
            prompt=u'SSH %(filename)s password',
586
 
            filename=filename.decode(osutils._fs_enc))
 
507
        password = bzrlib.ui.ui_factory.get_password(
 
508
                prompt='SSH %(filename)s password',
 
509
                filename=filename)
587
510
        try:
588
511
            key = pkey_class.from_private_key_file(filename, password)
589
512
            paramiko_transport.auth_publickey(username, key)
590
513
            return True
591
514
        except paramiko.SSHException:
592
 
            trace.mutter('SSH authentication via %s key failed.'
593
 
                         % (os.path.basename(filename),))
 
515
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
594
516
    except paramiko.SSHException:
595
 
        trace.mutter('SSH authentication via %s key failed.'
596
 
                     % (os.path.basename(filename),))
 
517
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
597
518
    except IOError:
598
519
        pass
599
520
    return False
606
527
    """
607
528
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
608
529
    try:
609
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
610
 
            os.path.expanduser('~/.ssh/known_hosts'))
 
530
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
611
531
    except IOError, e:
612
 
        trace.mutter('failed to load system host keys: ' + str(e))
613
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
532
        mutter('failed to load system host keys: ' + str(e))
 
533
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
614
534
    try:
615
535
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
616
536
    except IOError, e:
617
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
537
        mutter('failed to load bzr host keys: ' + str(e))
618
538
        save_host_keys()
619
539
 
620
540
 
623
543
    Save "discovered" host keys in $(config)/ssh_host_keys/.
624
544
    """
625
545
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
626
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
627
 
    config.ensure_config_dir_exists()
 
546
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
547
    ensure_config_dir_exists()
628
548
 
629
549
    try:
630
550
        f = open(bzr_hostkey_path, 'w')
634
554
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
635
555
        f.close()
636
556
    except IOError, e:
637
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
557
        mutter('failed to save bzr host keys: ' + str(e))
638
558
 
639
559
 
640
560
def os_specific_subprocess_params():
641
561
    """Get O/S specific subprocess parameters."""
642
562
    if sys.platform == 'win32':
643
 
        # setting the process group and closing fds is not supported on
 
563
        # setting the process group and closing fds is not supported on 
644
564
        # win32
645
565
        return {}
646
566
    else:
647
 
        # We close fds other than the pipes as the child process does not need
 
567
        # We close fds other than the pipes as the child process does not need 
648
568
        # them to be open.
649
569
        #
650
570
        # We also set the child process to ignore SIGINT.  Normally the signal
652
572
        # this causes it to be seen only by bzr and not by ssh.  Python will
653
573
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
654
574
        # to release locks or do other cleanup over ssh before the connection
655
 
        # goes away.
 
575
        # goes away.  
656
576
        # <https://launchpad.net/products/bzr/+bug/5987>
657
577
        #
658
578
        # Running it in a separate process group is not good because then it
659
579
        # can't get non-echoed input of a password or passphrase.
660
580
        # <https://launchpad.net/products/bzr/+bug/40508>
661
 
        return {'preexec_fn': _ignore_signals,
 
581
        return {'preexec_fn': _ignore_sigint,
662
582
                'close_fds': True,
663
583
                }
664
584
 
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
 
        """
 
585
 
 
586
class SSHSubprocess(object):
 
587
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
588
 
 
589
    def __init__(self, proc):
724
590
        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))
732
591
 
733
592
    def send(self, data):
734
 
        if self._sock is not None:
735
 
            return self._sock.send(data)
736
 
        else:
737
 
            return os.write(self.proc.stdin.fileno(), 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
738
601
 
739
602
    def recv(self, count):
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
 
 
 
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)
767
612