/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

Merge from bzr.dev

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(self, argv):
 
236
        proc = subprocess.Popen(argv,
 
237
                                stdin=subprocess.PIPE,
 
238
                                stdout=subprocess.PIPE,
 
239
                                **os_specific_subprocess_params())
 
240
        return SSHSubprocess(proc)
 
241
 
 
242
    def connect_sftp(self, username, password, host, port):
 
243
        try:
 
244
            argv = self._get_vendor_specific_argv(username, host, port,
 
245
                                                  subsystem='sftp')
 
246
            sock = self._connect(argv)
 
247
            return SFTPClient(sock)
 
248
        except (EOFError, paramiko.SSHException), e:
 
249
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
250
                                  % (host, port, e))
 
251
        except (OSError, IOError), e:
 
252
            # If the machine is fast enough, ssh can actually exit
 
253
            # before we try and send it the sftp request, which
 
254
            # raises a Broken Pipe
 
255
            if e.errno not in (errno.EPIPE,):
 
256
                raise
 
257
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
258
                                  % (host, port, e))
 
259
 
 
260
    def connect_ssh(self, username, password, host, port, command):
 
261
        try:
 
262
            argv = self._get_vendor_specific_argv(username, host, port,
 
263
                                                  command=command)
 
264
            return self._connect(argv)
 
265
        except (EOFError), e:
 
266
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
267
                                  % (host, port, e))
 
268
        except (OSError, IOError), e:
 
269
            # If the machine is fast enough, ssh can actually exit
 
270
            # before we try and send it the sftp request, which
 
271
            # raises a Broken Pipe
 
272
            if e.errno not in (errno.EPIPE,):
 
273
                raise
 
274
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
 
275
                                  % (host, port, e))
 
276
 
 
277
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
278
                                  command=None):
 
279
        """Returns the argument list to run the subprocess with.
 
280
        
 
281
        Exactly one of 'subsystem' and 'command' must be specified.
 
282
        """
 
283
        raise NotImplementedError(self._get_vendor_specific_argv)
 
284
 
 
285
register_ssh_vendor('none', ParamikoVendor())
 
286
 
 
287
 
 
288
class OpenSSHSubprocessVendor(SubprocessVendor):
 
289
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
290
    
 
291
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
292
                                  command=None):
 
293
        assert subsystem is not None or command is not None, (
 
294
            'Must specify a command or subsystem')
 
295
        if subsystem is not None:
 
296
            assert command is None, (
 
297
                'subsystem and command are mutually exclusive')
 
298
        args = ['ssh',
 
299
                '-oForwardX11=no', '-oForwardAgent=no',
 
300
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
301
                '-oNoHostAuthenticationForLocalhost=yes']
 
302
        if port is not None:
 
303
            args.extend(['-p', str(port)])
 
304
        if username is not None:
 
305
            args.extend(['-l', username])
 
306
        if subsystem is not None:
 
307
            args.extend(['-s', host, subsystem])
 
308
        else:
 
309
            args.extend([host] + command)
 
310
        return args
 
311
 
 
312
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
313
 
 
314
 
 
315
class SSHCorpSubprocessVendor(SubprocessVendor):
 
316
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
317
 
 
318
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
319
                                  command=None):
 
320
        assert subsystem is not None or command is not None, (
 
321
            'Must specify a command or subsystem')
 
322
        if subsystem is not None:
 
323
            assert command is None, (
 
324
                'subsystem and command are mutually exclusive')
 
325
        args = ['ssh', '-x']
 
326
        if port is not None:
 
327
            args.extend(['-p', str(port)])
 
328
        if username is not None:
 
329
            args.extend(['-l', username])
 
330
        if subsystem is not None:
 
331
            args.extend(['-s', subsystem, host])
 
332
        else:
 
333
            args.extend([host] + command)
 
334
        return args
 
335
    
 
336
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
337
 
 
338
 
 
339
def _paramiko_auth(username, password, host, paramiko_transport):
 
340
    # paramiko requires a username, but it might be none if nothing was supplied
 
341
    # use the local username, just in case.
 
342
    # We don't override username, because if we aren't using paramiko,
 
343
    # the username might be specified in ~/.ssh/config and we don't want to
 
344
    # force it to something else
 
345
    # Also, it would mess up the self.relpath() functionality
 
346
    username = username or getpass.getuser()
 
347
 
 
348
    if _use_ssh_agent:
 
349
        agent = paramiko.Agent()
 
350
        for key in agent.get_keys():
 
351
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
352
            try:
 
353
                paramiko_transport.auth_publickey(username, key)
 
354
                return
 
355
            except paramiko.SSHException, e:
 
356
                pass
 
357
    
 
358
    # okay, try finding id_rsa or id_dss?  (posix only)
 
359
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
360
        return
 
361
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
362
        return
 
363
 
 
364
    if password:
 
365
        try:
 
366
            paramiko_transport.auth_password(username, password)
 
367
            return
 
368
        except paramiko.SSHException, e:
 
369
            pass
 
370
 
 
371
    # give up and ask for a password
 
372
    password = bzrlib.ui.ui_factory.get_password(
 
373
            prompt='SSH %(user)s@%(host)s password',
 
374
            user=username, host=host)
 
375
    try:
 
376
        paramiko_transport.auth_password(username, password)
 
377
    except paramiko.SSHException, e:
 
378
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
379
                              (username, host), e)
 
380
 
 
381
 
 
382
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
383
    filename = os.path.expanduser('~/.ssh/' + filename)
 
384
    try:
 
385
        key = pkey_class.from_private_key_file(filename)
 
386
        paramiko_transport.auth_publickey(username, key)
 
387
        return True
 
388
    except paramiko.PasswordRequiredException:
 
389
        password = bzrlib.ui.ui_factory.get_password(
 
390
                prompt='SSH %(filename)s password',
 
391
                filename=filename)
 
392
        try:
 
393
            key = pkey_class.from_private_key_file(filename, password)
 
394
            paramiko_transport.auth_publickey(username, key)
 
395
            return True
 
396
        except paramiko.SSHException:
 
397
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
398
    except paramiko.SSHException:
 
399
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
400
    except IOError:
 
401
        pass
 
402
    return False
 
403
 
 
404
 
 
405
def load_host_keys():
 
406
    """
 
407
    Load system host keys (probably doesn't work on windows) and any
 
408
    "discovered" keys from previous sessions.
 
409
    """
 
410
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
411
    try:
 
412
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
413
    except Exception, e:
 
414
        mutter('failed to load system host keys: ' + str(e))
 
415
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
416
    try:
 
417
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
418
    except Exception, e:
 
419
        mutter('failed to load bzr host keys: ' + str(e))
 
420
        save_host_keys()
 
421
 
 
422
 
 
423
def save_host_keys():
 
424
    """
 
425
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
426
    """
 
427
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
428
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
429
    ensure_config_dir_exists()
 
430
 
 
431
    try:
 
432
        f = open(bzr_hostkey_path, 'w')
 
433
        f.write('# SSH host keys collected by bzr\n')
 
434
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
435
            for keytype, key in keys.iteritems():
 
436
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
437
        f.close()
 
438
    except IOError, e:
 
439
        mutter('failed to save bzr host keys: ' + str(e))
 
440
 
 
441
 
 
442
def os_specific_subprocess_params():
 
443
    """Get O/S specific subprocess parameters."""
 
444
    if sys.platform == 'win32':
 
445
        # setting the process group and closing fds is not supported on 
 
446
        # win32
 
447
        return {}
 
448
    else:
 
449
        # We close fds other than the pipes as the child process does not need 
 
450
        # them to be open.
 
451
        #
 
452
        # We also set the child process to ignore SIGINT.  Normally the signal
 
453
        # would be sent to every process in the foreground process group, but
 
454
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
455
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
456
        # to release locks or do other cleanup over ssh before the connection
 
457
        # goes away.  
 
458
        # <https://launchpad.net/products/bzr/+bug/5987>
 
459
        #
 
460
        # Running it in a separate process group is not good because then it
 
461
        # can't get non-echoed input of a password or passphrase.
 
462
        # <https://launchpad.net/products/bzr/+bug/40508>
 
463
        return {'preexec_fn': _ignore_sigint,
 
464
                'close_fds': True,
 
465
                }
 
466
 
 
467
 
 
468
class SSHSubprocess(object):
 
469
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
470
 
 
471
    def __init__(self, proc):
 
472
        self.proc = proc
 
473
 
 
474
    def send(self, data):
 
475
        return os.write(self.proc.stdin.fileno(), data)
 
476
 
 
477
    def recv_ready(self):
 
478
        # TODO: jam 20051215 this function is necessary to support the
 
479
        # pipelined() function. In reality, it probably should use
 
480
        # poll() or select() to actually return if there is data
 
481
        # available, otherwise we probably don't get any benefit
 
482
        return True
 
483
 
 
484
    def recv(self, count):
 
485
        return os.read(self.proc.stdout.fileno(), count)
 
486
 
 
487
    def close(self):
 
488
        self.proc.stdin.close()
 
489
        self.proc.stdout.close()
 
490
        self.proc.wait()
 
491
 
 
492
    def get_filelike_channels(self):
 
493
        return (self.proc.stdout, self.proc.stdin)
 
494