~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-09-17 21:19:56 UTC
  • mfrom: (1997.1.6 bind-does-not-push-or-pull)
  • Revision ID: pqm@pqm.ubuntu.com-20060917211956-6e30d07da410fd1a
(Robert Collins) Change the Branch bind method to just bind rather than binding and pushing (fixes #43744 and #39542)

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