~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Andrew Bennetts
  • Date: 2010-10-08 04:25:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5472.
  • Revision ID: andrew.bennetts@canonical.com-20101008042510-sg9vdhmnggilzxsk
Fix stray TAB in source.

Show diffs side-by-side

added added

removed removed

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