/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py

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
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
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
 
17
 
 
18
"""Foundation SSH support for SFTP and smart server."""
 
19
 
 
20
import errno
 
21
import getpass
 
22
import os
 
23
import socket
 
24
import subprocess
 
25
import sys
 
26
 
 
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
 
36
 
 
37
try:
 
38
    import paramiko
 
39
except ImportError, e:
 
40
    raise ParamikoNotPresent(e)
 
41
else:
 
42
    from paramiko.sftp_client import SFTPClient
 
43
 
 
44
 
 
45
SYSTEM_HOSTKEYS = {}
 
46
BZR_HOSTKEYS = {}
 
47
 
 
48
 
 
49
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
50
 
 
51
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
 
52
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
53
# so we get an AttributeError exception. So we will not try to
 
54
# connect to an agent if we are on win32 and using Paramiko older than 1.6
 
55
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
56
 
 
57
_ssh_vendors = {}
 
58
 
 
59
def register_ssh_vendor(name, vendor_class):
 
60
    """Register lazy-loaded SSH vendor class.""" 
 
61
    _ssh_vendors[name] = vendor_class
 
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']
 
73
        try:
 
74
            klass = _ssh_vendors[vendor_name]
 
75
        except KeyError:
 
76
            raise UnknownSSH(vendor_name)
 
77
        else:
 
78
            _ssh_vendor = klass()
 
79
        return _ssh_vendor
 
80
 
 
81
    try:
 
82
        p = subprocess.Popen(['ssh', '-V'],
 
83
                             stdin=subprocess.PIPE,
 
84
                             stdout=subprocess.PIPE,
 
85
                             stderr=subprocess.PIPE,
 
86
                             **os_specific_subprocess_params())
 
87
        returncode = p.returncode
 
88
        stdout, stderr = p.communicate()
 
89
    except OSError:
 
90
        returncode = -1
 
91
        stdout = stderr = ''
 
92
    if 'OpenSSH' in stderr:
 
93
        mutter('ssh implementation is OpenSSH')
 
94
        _ssh_vendor = OpenSSHSubprocessVendor()
 
95
    elif 'SSH Secure Shell' in stderr:
 
96
        mutter('ssh implementation is SSH Corp.')
 
97
        _ssh_vendor = SSHCorpSubprocessVendor()
 
98
 
 
99
    if _ssh_vendor is not None:
 
100
        return _ssh_vendor
 
101
 
 
102
    # XXX: 20051123 jamesh
 
103
    # A check for putty's plink or lsh would go here.
 
104
 
 
105
    mutter('falling back to paramiko implementation')
 
106
    _ssh_vendor = ssh.ParamikoVendor()
 
107
    return _ssh_vendor
 
108
 
 
109
 
 
110
 
 
111
def _ignore_sigint():
 
112
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
113
    # doesn't handle it itself.
 
114
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
115
    import signal
 
116
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
117
    
 
118
 
 
119
 
 
120
class LoopbackSFTP(object):
 
121
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
122
 
 
123
    def __init__(self, sock):
 
124
        self.__socket = sock
 
125
 
 
126
    def send(self, data):
 
127
        return self.__socket.send(data)
 
128
 
 
129
    def recv(self, n):
 
130
        return self.__socket.recv(n)
 
131
 
 
132
    def recv_ready(self):
 
133
        return True
 
134
 
 
135
    def close(self):
 
136
        self.__socket.close()
 
137
 
 
138
 
 
139
class SSHVendor(object):
 
140
    """Abstract base class for SSH vendor implementations."""
 
141
    
 
142
    def connect_sftp(self, username, password, host, port):
 
143
        """Make an SSH connection, and return an SFTPClient.
 
144
        
 
145
        :param username: an ascii string
 
146
        :param password: an ascii string
 
147
        :param host: a host name as an ascii string
 
148
        :param port: a port number
 
149
        :type port: int
 
150
 
 
151
        :raises: ConnectionError if it cannot connect.
 
152
 
 
153
        :rtype: paramiko.sftp_client.SFTPClient
 
154
        """
 
155
        raise NotImplementedError(self.connect_sftp)
 
156
 
 
157
    def connect_ssh(self, username, password, host, port, command):
 
158
        """Make an SSH connection, and return a pipe-like object."""
 
159
        raise NotImplementedError(self.connect_ssh)
 
160
        
 
161
 
 
162
class LoopbackVendor(SSHVendor):
 
163
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
164
    
 
165
    def connect_sftp(self, username, password, host, port):
 
166
        sock = socket.socket()
 
167
        try:
 
168
            sock.connect((host, port))
 
169
        except socket.error, e:
 
170
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
171
                                  % (host, port, e))
 
172
        return SFTPClient(LoopbackSFTP(sock))
 
173
 
 
174
register_ssh_vendor('loopback', LoopbackVendor)
 
175
 
 
176
 
 
177
class ParamikoVendor(SSHVendor):
 
178
    """Vendor that uses paramiko."""
 
179
 
 
180
    def connect_sftp(self, username, password, host, port):
 
181
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
182
        
 
183
        load_host_keys()
 
184
 
 
185
        try:
 
186
            t = paramiko.Transport((host, port or 22))
 
187
            t.set_log_channel('bzr.paramiko')
 
188
            t.start_client()
 
189
        except (paramiko.SSHException, socket.error), e:
 
190
            raise ConnectionError('Unable to reach SSH host %s:%s: %s' 
 
191
                                  % (host, port, e))
 
192
            
 
193
        server_key = t.get_remote_server_key()
 
194
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
195
        keytype = server_key.get_name()
 
196
        if SYSTEM_HOSTKEYS.has_key(host) and SYSTEM_HOSTKEYS[host].has_key(keytype):
 
197
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
198
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
199
        elif BZR_HOSTKEYS.has_key(host) and BZR_HOSTKEYS[host].has_key(keytype):
 
200
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
201
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
202
        else:
 
203
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
 
204
            if not BZR_HOSTKEYS.has_key(host):
 
205
                BZR_HOSTKEYS[host] = {}
 
206
            BZR_HOSTKEYS[host][keytype] = server_key
 
207
            our_server_key = server_key
 
208
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
209
            save_host_keys()
 
210
        if server_key != our_server_key:
 
211
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
212
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
213
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
 
214
                (host, our_server_key_hex, server_key_hex),
 
215
                ['Try editing %s or %s' % (filename1, filename2)])
 
216
 
 
217
        _paramiko_auth(username, password, host, t)
 
218
        
 
219
        try:
 
220
            sftp = t.open_sftp_client()
 
221
        except paramiko.SSHException, e:
 
222
            raise ConnectionError('Unable to start sftp client %s:%d' %
 
223
                                  (host, port), e)
 
224
        return sftp
 
225
 
 
226
register_ssh_vendor('paramiko', ParamikoVendor)
 
227
 
 
228
 
 
229
class SubprocessVendor(SSHVendor):
 
230
    """Abstract base class for vendors that use pipes to a subprocess."""
 
231
    
 
232
    def connect_sftp(self, username, password, host, port):
 
233
        try:
 
234
            argv = self._get_vendor_specific_argv(username, host, port,
 
235
                                                  subsystem='sftp')
 
236
            proc = subprocess.Popen(argv,
 
237
                                    stdin=subprocess.PIPE,
 
238
                                    stdout=subprocess.PIPE,
 
239
                                    **os_specific_subprocess_params())
 
240
            sock = SSHSubprocess(proc)
 
241
            return SFTPClient(sock)
 
242
        except (EOFError, paramiko.SSHException), e:
 
243
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
244
                                  % (host, port, e))
 
245
        except (OSError, IOError), e:
 
246
            # If the machine is fast enough, ssh can actually exit
 
247
            # before we try and send it the sftp request, which
 
248
            # raises a Broken Pipe
 
249
            if e.errno not in (errno.EPIPE,):
 
250
                raise
 
251
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
252
                                  % (host, port, e))
 
253
 
 
254
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
255
                                  command=None):
 
256
        """Returns the argument list to run the subprocess with.
 
257
        
 
258
        Exactly one of 'subsystem' and 'command' must be specified.
 
259
        """
 
260
        raise NotImplementedError(self._get_vendor_specific_argv)
 
261
 
 
262
register_ssh_vendor('none', ParamikoVendor)
 
263
 
 
264
 
 
265
class OpenSSHSubprocessVendor(SubprocessVendor):
 
266
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
267
    
 
268
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
269
                                  command=None):
 
270
        assert subsystem is not None or command is not None, (
 
271
            'Must specify a command or subsystem')
 
272
        if subsystem is not None:
 
273
            assert command is None, (
 
274
                'subsystem and command are mutually exclusive')
 
275
        args = ['ssh',
 
276
                '-oForwardX11=no', '-oForwardAgent=no',
 
277
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
278
                '-oNoHostAuthenticationForLocalhost=yes']
 
279
        if port is not None:
 
280
            args.extend(['-p', str(port)])
 
281
        if username is not None:
 
282
            args.extend(['-l', username])
 
283
        if subsystem is not None:
 
284
            args.extend(['-s', host, subsystem])
 
285
        else:
 
286
            args.extend([host] + command)
 
287
        return args
 
288
 
 
289
register_ssh_vendor('openssh', OpenSSHSubprocessVendor)
 
290
 
 
291
 
 
292
class SSHCorpSubprocessVendor(SubprocessVendor):
 
293
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
294
 
 
295
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
296
                                  command=None):
 
297
        assert subsystem is not None or command is not None, (
 
298
            'Must specify a command or subsystem')
 
299
        if subsystem is not None:
 
300
            assert command is None, (
 
301
                'subsystem and command are mutually exclusive')
 
302
        args = ['ssh', '-x']
 
303
        if port is not None:
 
304
            args.extend(['-p', str(port)])
 
305
        if username is not None:
 
306
            args.extend(['-l', username])
 
307
        if subsystem is not None:
 
308
            args.extend(['-s', subsystem, host])
 
309
        else:
 
310
            args.extend([host] + command)
 
311
        return args
 
312
    
 
313
register_ssh_vendor('ssh', SSHCorpSubprocessVendor)
 
314
 
 
315
 
 
316
def _paramiko_auth(username, password, host, paramiko_transport):
 
317
    # paramiko requires a username, but it might be none if nothing was supplied
 
318
    # use the local username, just in case.
 
319
    # We don't override username, because if we aren't using paramiko,
 
320
    # the username might be specified in ~/.ssh/config and we don't want to
 
321
    # force it to something else
 
322
    # Also, it would mess up the self.relpath() functionality
 
323
    username = username or getpass.getuser()
 
324
 
 
325
    if _use_ssh_agent:
 
326
        agent = paramiko.Agent()
 
327
        for key in agent.get_keys():
 
328
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
329
            try:
 
330
                paramiko_transport.auth_publickey(username, key)
 
331
                return
 
332
            except paramiko.SSHException, e:
 
333
                pass
 
334
    
 
335
    # okay, try finding id_rsa or id_dss?  (posix only)
 
336
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
337
        return
 
338
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
339
        return
 
340
 
 
341
    if password:
 
342
        try:
 
343
            paramiko_transport.auth_password(username, password)
 
344
            return
 
345
        except paramiko.SSHException, e:
 
346
            pass
 
347
 
 
348
    # give up and ask for a password
 
349
    password = bzrlib.ui.ui_factory.get_password(
 
350
            prompt='SSH %(user)s@%(host)s password',
 
351
            user=username, host=host)
 
352
    try:
 
353
        paramiko_transport.auth_password(username, password)
 
354
    except paramiko.SSHException, e:
 
355
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
356
                              (username, host), e)
 
357
 
 
358
 
 
359
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
360
    filename = os.path.expanduser('~/.ssh/' + filename)
 
361
    try:
 
362
        key = pkey_class.from_private_key_file(filename)
 
363
        paramiko_transport.auth_publickey(username, key)
 
364
        return True
 
365
    except paramiko.PasswordRequiredException:
 
366
        password = bzrlib.ui.ui_factory.get_password(
 
367
                prompt='SSH %(filename)s password',
 
368
                filename=filename)
 
369
        try:
 
370
            key = pkey_class.from_private_key_file(filename, password)
 
371
            paramiko_transport.auth_publickey(username, key)
 
372
            return True
 
373
        except paramiko.SSHException:
 
374
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
375
    except paramiko.SSHException:
 
376
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
377
    except IOError:
 
378
        pass
 
379
    return False
 
380
 
 
381
 
 
382
def load_host_keys():
 
383
    """
 
384
    Load system host keys (probably doesn't work on windows) and any
 
385
    "discovered" keys from previous sessions.
 
386
    """
 
387
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
388
    try:
 
389
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
390
    except Exception, e:
 
391
        mutter('failed to load system host keys: ' + str(e))
 
392
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
393
    try:
 
394
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
395
    except Exception, e:
 
396
        mutter('failed to load bzr host keys: ' + str(e))
 
397
        save_host_keys()
 
398
 
 
399
 
 
400
def save_host_keys():
 
401
    """
 
402
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
403
    """
 
404
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
405
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
406
    ensure_config_dir_exists()
 
407
 
 
408
    try:
 
409
        f = open(bzr_hostkey_path, 'w')
 
410
        f.write('# SSH host keys collected by bzr\n')
 
411
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
412
            for keytype, key in keys.iteritems():
 
413
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
414
        f.close()
 
415
    except IOError, e:
 
416
        mutter('failed to save bzr host keys: ' + str(e))
 
417
 
 
418
 
 
419
def os_specific_subprocess_params():
 
420
    """Get O/S specific subprocess parameters."""
 
421
    if sys.platform == 'win32':
 
422
        # setting the process group and closing fds is not supported on 
 
423
        # win32
 
424
        return {}
 
425
    else:
 
426
        # We close fds other than the pipes as the child process does not need 
 
427
        # them to be open.
 
428
        #
 
429
        # We also set the child process to ignore SIGINT.  Normally the signal
 
430
        # would be sent to every process in the foreground process group, but
 
431
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
432
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
433
        # to release locks or do other cleanup over ssh before the connection
 
434
        # goes away.  
 
435
        # <https://launchpad.net/products/bzr/+bug/5987>
 
436
        #
 
437
        # Running it in a separate process group is not good because then it
 
438
        # can't get non-echoed input of a password or passphrase.
 
439
        # <https://launchpad.net/products/bzr/+bug/40508>
 
440
        return {'preexec_fn': _ignore_sigint,
 
441
                'close_fds': True,
 
442
                }
 
443
 
 
444
class SSHSubprocess(object):
 
445
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
446
    # TODO: this class probably belongs in bzrlib/transport/ssh.py
 
447
 
 
448
    def __init__(self, proc):
 
449
        self.proc = proc
 
450
 
 
451
    def send(self, data):
 
452
        return os.write(self.proc.stdin.fileno(), data)
 
453
 
 
454
    def recv_ready(self):
 
455
        # TODO: jam 20051215 this function is necessary to support the
 
456
        # pipelined() function. In reality, it probably should use
 
457
        # poll() or select() to actually return if there is data
 
458
        # available, otherwise we probably don't get any benefit
 
459
        return True
 
460
 
 
461
    def recv(self, count):
 
462
        return os.read(self.proc.stdout.fileno(), count)
 
463
 
 
464
    def close(self):
 
465
        self.proc.stdin.close()
 
466
        self.proc.stdout.close()
 
467
        self.proc.wait()
 
468
 
 
469