~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robey Pointer
  • Date: 2006-09-08 18:46:29 UTC
  • mto: This revision was merged to the branch mainline in revision 1996.
  • Revision ID: robey@lag.net-20060908184629-e3fc4c61ca21508c
pychecker is on crack; go back to using 'is None'.

Show diffs side-by-side

added added

removed removed

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