~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

Fix BzrDir.create_workingtree for NULL_REVISION

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):
 
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']
 
73
        try:
 
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():
 
110
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
111
    # doesn't handle it itself.
 
112
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
113
    import signal
 
114
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
115
    
 
116
 
 
117
 
 
118
class LoopbackSFTP(object):
 
119
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
120
 
 
121
    def __init__(self, sock):
 
122
        self.__socket = sock
 
123
 
 
124
    def send(self, data):
 
125
        return self.__socket.send(data)
 
126
 
 
127
    def recv(self, n):
 
128
        return self.__socket.recv(n)
 
129
 
 
130
    def recv_ready(self):
 
131
        return True
 
132
 
 
133
    def close(self):
 
134
        self.__socket.close()
 
135
 
 
136
 
 
137
class SSHVendor(object):
 
138
    """Abstract base class for SSH vendor implementations."""
 
139
    
 
140
    def connect_sftp(self, username, password, host, port):
 
141
        """Make an SSH connection, and return an SFTPClient.
 
142
        
 
143
        :param username: an ascii string
 
144
        :param password: an ascii string
 
145
        :param host: a host name as an ascii string
 
146
        :param port: a port number
 
147
        :type port: int
 
148
 
 
149
        :raises: ConnectionError if it cannot connect.
 
150
 
 
151
        :rtype: paramiko.sftp_client.SFTPClient
 
152
        """
 
153
        raise NotImplementedError(self.connect_sftp)
 
154
 
 
155
    def connect_ssh(self, username, password, host, port, command):
 
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.)
 
160
        """
 
161
        raise NotImplementedError(self.connect_ssh)
 
162
        
 
163
 
 
164
class LoopbackVendor(SSHVendor):
 
165
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
166
    
 
167
    def connect_sftp(self, username, password, host, port):
 
168
        sock = socket.socket()
 
169
        try:
 
170
            sock.connect((host, port))
 
171
        except socket.error, e:
 
172
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
173
                                  % (host, port, e))
 
174
        return SFTPClient(LoopbackSFTP(sock))
 
175
 
 
176
register_ssh_vendor('loopback', LoopbackVendor())
 
177
 
 
178
 
 
179
class ParamikoVendor(SSHVendor):
 
180
    """Vendor that uses paramiko."""
 
181
 
 
182
    def connect_sftp(self, username, password, host, port):
 
183
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
184
        
 
185
        load_host_keys()
 
186
 
 
187
        try:
 
188
            t = paramiko.Transport((host, port or 22))
 
189
            t.set_log_channel('bzr.paramiko')
 
190
            t.start_client()
 
191
        except (paramiko.SSHException, socket.error), e:
 
192
            raise ConnectionError('Unable to reach SSH host %s:%s: %s' 
 
193
                                  % (host, port, e))
 
194
            
 
195
        server_key = t.get_remote_server_key()
 
196
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
197
        keytype = server_key.get_name()
 
198
        if SYSTEM_HOSTKEYS.has_key(host) and SYSTEM_HOSTKEYS[host].has_key(keytype):
 
199
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
200
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
201
        elif BZR_HOSTKEYS.has_key(host) and BZR_HOSTKEYS[host].has_key(keytype):
 
202
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
203
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
204
        else:
 
205
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
 
206
            if not BZR_HOSTKEYS.has_key(host):
 
207
                BZR_HOSTKEYS[host] = {}
 
208
            BZR_HOSTKEYS[host][keytype] = server_key
 
209
            our_server_key = server_key
 
210
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
211
            save_host_keys()
 
212
        if server_key != our_server_key:
 
213
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
214
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
215
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
 
216
                (host, our_server_key_hex, server_key_hex),
 
217
                ['Try editing %s or %s' % (filename1, filename2)])
 
218
 
 
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())
 
229
 
 
230
 
 
231
class SubprocessVendor(SSHVendor):
 
232
    """Abstract base class for vendors that use pipes to a subprocess."""
 
233
    
 
234
    def connect_sftp(self, username, password, host, port):
 
235
        try:
 
236
            argv = self._get_vendor_specific_argv(username, host, port,
 
237
                                                  subsystem='sftp')
 
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))
 
255
 
 
256
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
257
                                  command=None):
 
258
        """Returns the argument list to run the subprocess with.
 
259
        
 
260
        Exactly one of 'subsystem' and 'command' must be specified.
 
261
        """
 
262
        raise NotImplementedError(self._get_vendor_specific_argv)
 
263
 
 
264
register_ssh_vendor('none', ParamikoVendor())
 
265
 
 
266
 
 
267
class OpenSSHSubprocessVendor(SubprocessVendor):
 
268
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
269
    
 
270
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
271
                                  command=None):
 
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',
 
278
                '-oForwardX11=no', '-oForwardAgent=no',
 
279
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
280
                '-oNoHostAuthenticationForLocalhost=yes']
 
281
        if port is not None:
 
282
            args.extend(['-p', str(port)])
 
283
        if username is not None:
 
284
            args.extend(['-l', username])
 
285
        if subsystem is not None:
 
286
            args.extend(['-s', host, subsystem])
 
287
        else:
 
288
            args.extend([host] + command)
 
289
        return args
 
290
 
 
291
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
292
 
 
293
 
 
294
class SSHCorpSubprocessVendor(SubprocessVendor):
 
295
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
296
 
 
297
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
298
                                  command=None):
 
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']
 
305
        if port is not None:
 
306
            args.extend(['-p', str(port)])
 
307
        if username is not None:
 
308
            args.extend(['-l', username])
 
309
        if subsystem is not None:
 
310
            args.extend(['-s', subsystem, host])
 
311
        else:
 
312
            args.extend([host] + command)
 
313
        return args
 
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
 
 
327
    if _use_ssh_agent:
 
328
        agent = paramiko.Agent()
 
329
        for key in agent.get_keys():
 
330
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
331
            try:
 
332
                paramiko_transport.auth_publickey(username, key)
 
333
                return
 
334
            except paramiko.SSHException, e:
 
335
                pass
 
336
    
 
337
    # okay, try finding id_rsa or id_dss?  (posix only)
 
338
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
339
        return
 
340
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
341
        return
 
342
 
 
343
    if password:
 
344
        try:
 
345
            paramiko_transport.auth_password(username, password)
 
346
            return
 
347
        except paramiko.SSHException, e:
 
348
            pass
 
349
 
 
350
    # give up and ask for a password
 
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)
 
359
 
 
360
 
 
361
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
362
    filename = os.path.expanduser('~/.ssh/' + filename)
 
363
    try:
 
364
        key = pkey_class.from_private_key_file(filename)
 
365
        paramiko_transport.auth_publickey(username, key)
 
366
        return True
 
367
    except paramiko.PasswordRequiredException:
 
368
        password = bzrlib.ui.ui_factory.get_password(
 
369
                prompt='SSH %(filename)s password',
 
370
                filename=filename)
 
371
        try:
 
372
            key = pkey_class.from_private_key_file(filename, password)
 
373
            paramiko_transport.auth_publickey(username, key)
 
374
            return True
 
375
        except paramiko.SSHException:
 
376
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
377
    except paramiko.SSHException:
 
378
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
379
    except IOError:
 
380
        pass
 
381
    return False
 
382
 
 
383
 
 
384
def load_host_keys():
 
385
    """
 
386
    Load system host keys (probably doesn't work on windows) and any
 
387
    "discovered" keys from previous sessions.
 
388
    """
 
389
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
390
    try:
 
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')
 
395
    try:
 
396
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
397
    except Exception, e:
 
398
        mutter('failed to load bzr host keys: ' + str(e))
 
399
        save_host_keys()
 
400
 
 
401
 
 
402
def save_host_keys():
 
403
    """
 
404
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
405
    """
 
406
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
407
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
408
    ensure_config_dir_exists()
 
409
 
 
410
    try:
 
411
        f = open(bzr_hostkey_path, 'w')
 
412
        f.write('# SSH host keys collected by bzr\n')
 
413
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
414
            for keytype, key in keys.iteritems():
 
415
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
416
        f.close()
 
417
    except IOError, e:
 
418
        mutter('failed to save bzr host keys: ' + str(e))
 
419
 
 
420
 
 
421
def os_specific_subprocess_params():
 
422
    """Get O/S specific subprocess parameters."""
 
423
    if sys.platform == 'win32':
 
424
        # setting the process group and closing fds is not supported on 
 
425
        # win32
 
426
        return {}
 
427
    else:
 
428
        # We close fds other than the pipes as the child process does not need 
 
429
        # them to be open.
 
430
        #
 
431
        # We also set the child process to ignore SIGINT.  Normally the signal
 
432
        # would be sent to every process in the foreground process group, but
 
433
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
434
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
435
        # to release locks or do other cleanup over ssh before the connection
 
436
        # goes away.  
 
437
        # <https://launchpad.net/products/bzr/+bug/5987>
 
438
        #
 
439
        # Running it in a separate process group is not good because then it
 
440
        # can't get non-echoed input of a password or passphrase.
 
441
        # <https://launchpad.net/products/bzr/+bug/40508>
 
442
        return {'preexec_fn': _ignore_sigint,
 
443
                'close_fds': True,
 
444
                }
 
445
 
 
446
 
 
447
class SSHSubprocess(object):
 
448
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
449
 
 
450
    def __init__(self, proc):
 
451
        self.proc = proc
 
452
 
 
453
    def send(self, data):
 
454
        return os.write(self.proc.stdin.fileno(), data)
 
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
 
 
463
    def recv(self, count):
 
464
        return os.read(self.proc.stdout.fileno(), count)
 
465
 
 
466
    def close(self):
 
467
        self.proc.stdin.close()
 
468
        self.proc.stdout.close()
 
469
        self.proc.wait()
 
470
 
 
471