~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
                           SocketConnectionError,
 
31
                           SSHVendorNotFound,
 
32
                           TransportError,
 
33
                           UnknownSSH,
 
34
                           )
 
35
 
 
36
from bzrlib.osutils import pathjoin
 
37
from bzrlib.trace import mutter, warning
 
38
import bzrlib.ui
35
39
 
36
40
try:
37
41
    import paramiko
94
98
            try:
95
99
                vendor = self._ssh_vendors[vendor_name]
96
100
            except KeyError:
97
 
                raise errors.UnknownSSH(vendor_name)
 
101
                raise UnknownSSH(vendor_name)
98
102
            return vendor
99
103
        return None
100
104
 
110
114
            stdout = stderr = ''
111
115
        return stdout + stderr
112
116
 
113
 
    def _get_vendor_by_version_string(self, version, args):
 
117
    def _get_vendor_by_version_string(self, version):
114
118
        """Return the vendor or None based on output from the subprocess.
115
119
 
116
120
        :param version: The output of 'ssh -V' like command.
117
 
        :param args: Command line that was run.
118
121
        """
119
122
        vendor = None
120
123
        if 'OpenSSH' in version:
121
 
            trace.mutter('ssh implementation is OpenSSH')
 
124
            mutter('ssh implementation is OpenSSH')
122
125
            vendor = OpenSSHSubprocessVendor()
123
126
        elif 'SSH Secure Shell' in version:
124
 
            trace.mutter('ssh implementation is SSH Corp.')
 
127
            mutter('ssh implementation is SSH Corp.')
125
128
            vendor = SSHCorpSubprocessVendor()
126
 
        elif 'plink' in version and args[0] == 'plink':
127
 
            # Checking if "plink" was the executed argument as Windows
128
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
129
 
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
130
 
            trace.mutter("ssh implementation is Putty's plink.")
 
129
        elif 'plink' in version:
 
130
            mutter("ssh implementation is Putty's plink.")
131
131
            vendor = PLinkSubprocessVendor()
132
132
        return vendor
133
133
 
134
134
    def _get_vendor_by_inspection(self):
135
135
        """Return the vendor or None by checking for known SSH implementations."""
136
 
        for args in (['ssh', '-V'], ['plink', '-V']):
 
136
        for args in [['ssh', '-V'], ['plink', '-V']]:
137
137
            version = self._get_ssh_version_string(args)
138
 
            vendor = self._get_vendor_by_version_string(version, args)
 
138
            vendor = self._get_vendor_by_version_string(version)
139
139
            if vendor is not None:
140
140
                return vendor
141
141
        return None
152
152
            if vendor is None:
153
153
                vendor = self._get_vendor_by_inspection()
154
154
                if vendor is None:
155
 
                    trace.mutter('falling back to default implementation')
 
155
                    mutter('falling back to default implementation')
156
156
                    vendor = self._default_ssh_vendor
157
157
                    if vendor is None:
158
 
                        raise errors.SSHVendorNotFound()
 
158
                        raise SSHVendorNotFound()
159
159
            self._cached_ssh_vendor = vendor
160
160
        return self._cached_ssh_vendor
161
161
 
173
173
    signal.signal(signal.SIGINT, signal.SIG_IGN)
174
174
 
175
175
 
176
 
class SocketAsChannelAdapter(object):
 
176
class LoopbackSFTP(object):
177
177
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
178
178
 
179
179
    def __init__(self, sock):
180
180
        self.__socket = sock
181
 
 
182
 
    def get_name(self):
183
 
        return "bzr SocketAsChannelAdapter"
184
 
 
 
181
 
185
182
    def send(self, data):
186
183
        return self.__socket.send(data)
187
184
 
188
185
    def recv(self, n):
189
 
        try:
190
 
            return self.__socket.recv(n)
191
 
        except socket.error, e:
192
 
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
193
 
                             errno.EBADF):
194
 
                # Connection has closed.  Paramiko expects an empty string in
195
 
                # this case, not an exception.
196
 
                return ''
197
 
            raise
 
186
        return self.__socket.recv(n)
198
187
 
199
188
    def recv_ready(self):
200
 
        # TODO: jam 20051215 this function is necessary to support the
201
 
        # pipelined() function. In reality, it probably should use
202
 
        # poll() or select() to actually return if there is data
203
 
        # available, otherwise we probably don't get any benefit
204
189
        return True
205
190
 
206
191
    def close(self):
209
194
 
210
195
class SSHVendor(object):
211
196
    """Abstract base class for SSH vendor implementations."""
212
 
 
 
197
    
213
198
    def connect_sftp(self, username, password, host, port):
214
199
        """Make an SSH connection, and return an SFTPClient.
215
 
 
 
200
        
216
201
        :param username: an ascii string
217
202
        :param password: an ascii string
218
203
        :param host: a host name as an ascii string
227
212
 
228
213
    def connect_ssh(self, username, password, host, port, command):
229
214
        """Make an SSH connection.
230
 
 
 
215
        
231
216
        :returns: something with a `close` method, and a `get_filelike_channels`
232
217
            method that returns a pair of (read, write) filelike objects.
233
218
        """
234
219
        raise NotImplementedError(self.connect_ssh)
235
 
 
 
220
        
236
221
    def _raise_connection_error(self, host, port=None, orig_error=None,
237
222
                                msg='Unable to connect to SSH host'):
238
223
        """Raise a SocketConnectionError with properly formatted host.
240
225
        This just unifies all the locations that try to raise ConnectionError,
241
226
        so that they format things properly.
242
227
        """
243
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
244
 
                                           orig_error=orig_error)
 
228
        raise SocketConnectionError(host=host, port=port, msg=msg,
 
229
                                    orig_error=orig_error)
245
230
 
246
231
 
247
232
class LoopbackVendor(SSHVendor):
248
233
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
249
 
 
 
234
    
250
235
    def connect_sftp(self, username, password, host, port):
251
236
        sock = socket.socket()
252
237
        try:
253
238
            sock.connect((host, port))
254
239
        except socket.error, e:
255
240
            self._raise_connection_error(host, port=port, orig_error=e)
256
 
        return SFTPClient(SocketAsChannelAdapter(sock))
 
241
        return SFTPClient(LoopbackSFTP(sock))
257
242
 
258
243
register_ssh_vendor('loopback', LoopbackVendor())
259
244
 
274
259
 
275
260
    def _connect(self, username, password, host, port):
276
261
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
277
 
 
 
262
        
278
263
        load_host_keys()
279
264
 
280
265
        try:
283
268
            t.start_client()
284
269
        except (paramiko.SSHException, socket.error), e:
285
270
            self._raise_connection_error(host, port=port, orig_error=e)
286
 
 
 
271
            
287
272
        server_key = t.get_remote_server_key()
288
273
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
289
274
        keytype = server_key.get_name()
290
275
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
291
276
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
292
 
            our_server_key_hex = paramiko.util.hexify(
293
 
                our_server_key.get_fingerprint())
 
277
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
294
278
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
295
279
            our_server_key = BZR_HOSTKEYS[host][keytype]
296
 
            our_server_key_hex = paramiko.util.hexify(
297
 
                our_server_key.get_fingerprint())
 
280
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
298
281
        else:
299
 
            trace.warning('Adding %s host key for %s: %s'
300
 
                          % (keytype, host, server_key_hex))
 
282
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
301
283
            add = getattr(BZR_HOSTKEYS, 'add', None)
302
284
            if add is not None: # paramiko >= 1.X.X
303
285
                BZR_HOSTKEYS.add(host, keytype, server_key)
304
286
            else:
305
287
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
306
288
            our_server_key = server_key
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
289
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
309
290
            save_host_keys()
310
291
        if server_key != our_server_key:
311
292
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
312
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
313
 
            raise errors.TransportError(
314
 
                'Host keys for %s do not match!  %s != %s' %
 
293
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
294
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
315
295
                (host, our_server_key_hex, server_key_hex),
316
296
                ['Try editing %s or %s' % (filename1, filename2)])
317
297
 
318
 
        _paramiko_auth(username, password, host, port, t)
 
298
        _paramiko_auth(username, password, host, t)
319
299
        return t
320
 
 
 
300
        
321
301
    def connect_sftp(self, username, password, host, port):
322
302
        t = self._connect(username, password, host, port)
323
303
        try:
342
322
    register_ssh_vendor('paramiko', vendor)
343
323
    register_ssh_vendor('none', vendor)
344
324
    register_default_ssh_vendor(vendor)
345
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
346
325
    del vendor
347
 
else:
348
 
    _sftp_connection_errors = (EOFError,)
349
326
 
350
327
 
351
328
class SubprocessVendor(SSHVendor):
352
329
    """Abstract base class for vendors that use pipes to a subprocess."""
353
 
 
 
330
    
354
331
    def _connect(self, argv):
355
332
        proc = subprocess.Popen(argv,
356
333
                                stdin=subprocess.PIPE,
363
340
            argv = self._get_vendor_specific_argv(username, host, port,
364
341
                                                  subsystem='sftp')
365
342
            sock = self._connect(argv)
366
 
            return SFTPClient(SocketAsChannelAdapter(sock))
367
 
        except _sftp_connection_errors, e:
 
343
            return SFTPClient(sock)
 
344
        except (EOFError, paramiko.SSHException), e:
368
345
            self._raise_connection_error(host, port=port, orig_error=e)
369
346
        except (OSError, IOError), e:
370
347
            # If the machine is fast enough, ssh can actually exit
392
369
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
393
370
                                  command=None):
394
371
        """Returns the argument list to run the subprocess with.
395
 
 
 
372
        
396
373
        Exactly one of 'subsystem' and 'command' must be specified.
397
374
        """
398
375
        raise NotImplementedError(self._get_vendor_specific_argv)
400
377
 
401
378
class OpenSSHSubprocessVendor(SubprocessVendor):
402
379
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
403
 
 
 
380
    
404
381
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
405
382
                                  command=None):
 
383
        assert subsystem is not None or command is not None, (
 
384
            'Must specify a command or subsystem')
 
385
        if subsystem is not None:
 
386
            assert command is None, (
 
387
                'subsystem and command are mutually exclusive')
406
388
        args = ['ssh',
407
389
                '-oForwardX11=no', '-oForwardAgent=no',
408
390
                '-oClearAllForwardings=yes', '-oProtocol=2',
425
407
 
426
408
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
427
409
                                  command=None):
 
410
        assert subsystem is not None or command is not None, (
 
411
            'Must specify a command or subsystem')
 
412
        if subsystem is not None:
 
413
            assert command is None, (
 
414
                'subsystem and command are mutually exclusive')
428
415
        args = ['ssh', '-x']
429
416
        if port is not None:
430
417
            args.extend(['-p', str(port)])
435
422
        else:
436
423
            args.extend([host] + command)
437
424
        return args
438
 
 
 
425
    
439
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
440
427
 
441
428
 
444
431
 
445
432
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
446
433
                                  command=None):
447
 
        args = ['plink', '-x', '-a', '-ssh', '-2', '-batch']
 
434
        assert subsystem is not None or command is not None, (
 
435
            'Must specify a command or subsystem')
 
436
        if subsystem is not None:
 
437
            assert command is None, (
 
438
                'subsystem and command are mutually exclusive')
 
439
        args = ['plink', '-x', '-a', '-ssh', '-2']
448
440
        if port is not None:
449
441
            args.extend(['-P', str(port)])
450
442
        if username is not None:
458
450
register_ssh_vendor('plink', PLinkSubprocessVendor())
459
451
 
460
452
 
461
 
def _paramiko_auth(username, password, host, port, paramiko_transport):
462
 
    auth = config.AuthenticationConfig()
463
 
    # paramiko requires a username, but it might be none if nothing was
464
 
    # supplied.  If so, use the local username.
465
 
    if username is None:
466
 
        username = auth.get_user('ssh', host, port=port,
467
 
                                 default=getpass.getuser())
 
453
def _paramiko_auth(username, password, host, paramiko_transport):
 
454
    # paramiko requires a username, but it might be none if nothing was supplied
 
455
    # use the local username, just in case.
 
456
    # We don't override username, because if we aren't using paramiko,
 
457
    # the username might be specified in ~/.ssh/config and we don't want to
 
458
    # force it to something else
 
459
    # Also, it would mess up the self.relpath() functionality
 
460
    username = username or getpass.getuser()
 
461
 
468
462
    if _use_ssh_agent:
469
463
        agent = paramiko.Agent()
470
464
        for key in agent.get_keys():
471
 
            trace.mutter('Trying SSH agent key %s'
472
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
465
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
473
466
            try:
474
467
                paramiko_transport.auth_publickey(username, key)
475
468
                return
476
469
            except paramiko.SSHException, e:
477
470
                pass
478
 
 
 
471
    
479
472
    # okay, try finding id_rsa or id_dss?  (posix only)
480
473
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
481
474
        return
482
475
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
483
476
        return
484
477
 
485
 
    # If we have gotten this far, we are about to try for passwords, do an
486
 
    # auth_none check to see if it is even supported.
487
 
    supported_auth_types = []
488
 
    try:
489
 
        # Note that with paramiko <1.7.5 this logs an INFO message:
490
 
        #    Authentication type (none) not permitted.
491
 
        # So we explicitly disable the logging level for this action
492
 
        old_level = paramiko_transport.logger.level
493
 
        paramiko_transport.logger.setLevel(logging.WARNING)
494
 
        try:
495
 
            paramiko_transport.auth_none(username)
496
 
        finally:
497
 
            paramiko_transport.logger.setLevel(old_level)
498
 
    except paramiko.BadAuthenticationType, e:
499
 
        # Supported methods are in the exception
500
 
        supported_auth_types = e.allowed_types
501
 
    except paramiko.SSHException, e:
502
 
        # Don't know what happened, but just ignore it
503
 
        pass
504
 
    if 'password' not in supported_auth_types:
505
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
506
 
            '\n  %s@%s\nsupported auth types: %s'
507
 
            % (username, host, supported_auth_types))
508
 
 
509
478
    if password:
510
479
        try:
511
480
            paramiko_transport.auth_password(username, password)
514
483
            pass
515
484
 
516
485
    # give up and ask for a password
517
 
    password = auth.get_password('ssh', host, username, port=port)
518
 
    # get_password can still return None, which means we should not prompt
519
 
    if password is not None:
520
 
        try:
521
 
            paramiko_transport.auth_password(username, password)
522
 
        except paramiko.SSHException, e:
523
 
            raise errors.ConnectionError(
524
 
                'Unable to authenticate to SSH host as'
525
 
                '\n  %s@%s\n' % (username, host), e)
526
 
    else:
527
 
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
528
 
                                     '  %s@%s' % (username, host))
 
486
    password = bzrlib.ui.ui_factory.get_password(
 
487
            prompt='SSH %(user)s@%(host)s password',
 
488
            user=username, host=host)
 
489
    try:
 
490
        paramiko_transport.auth_password(username, password)
 
491
    except paramiko.SSHException, e:
 
492
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
493
                              (username, host), e)
529
494
 
530
495
 
531
496
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
535
500
        paramiko_transport.auth_publickey(username, key)
536
501
        return True
537
502
    except paramiko.PasswordRequiredException:
538
 
        password = ui.ui_factory.get_password(
539
 
            prompt='SSH %(filename)s password', filename=filename)
 
503
        password = bzrlib.ui.ui_factory.get_password(
 
504
                prompt='SSH %(filename)s password',
 
505
                filename=filename)
540
506
        try:
541
507
            key = pkey_class.from_private_key_file(filename, password)
542
508
            paramiko_transport.auth_publickey(username, key)
543
509
            return True
544
510
        except paramiko.SSHException:
545
 
            trace.mutter('SSH authentication via %s key failed.'
546
 
                         % (os.path.basename(filename),))
 
511
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
547
512
    except paramiko.SSHException:
548
 
        trace.mutter('SSH authentication via %s key failed.'
549
 
                     % (os.path.basename(filename),))
 
513
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
550
514
    except IOError:
551
515
        pass
552
516
    return False
559
523
    """
560
524
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
561
525
    try:
562
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
563
 
            os.path.expanduser('~/.ssh/known_hosts'))
564
 
    except IOError, e:
565
 
        trace.mutter('failed to load system host keys: ' + str(e))
566
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
526
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
527
    except Exception, e:
 
528
        mutter('failed to load system host keys: ' + str(e))
 
529
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
567
530
    try:
568
531
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
569
 
    except IOError, e:
570
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
532
    except Exception, e:
 
533
        mutter('failed to load bzr host keys: ' + str(e))
571
534
        save_host_keys()
572
535
 
573
536
 
576
539
    Save "discovered" host keys in $(config)/ssh_host_keys/.
577
540
    """
578
541
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
579
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
580
 
    config.ensure_config_dir_exists()
 
542
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
543
    ensure_config_dir_exists()
581
544
 
582
545
    try:
583
546
        f = open(bzr_hostkey_path, 'w')
587
550
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
588
551
        f.close()
589
552
    except IOError, e:
590
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
553
        mutter('failed to save bzr host keys: ' + str(e))
591
554
 
592
555
 
593
556
def os_specific_subprocess_params():
594
557
    """Get O/S specific subprocess parameters."""
595
558
    if sys.platform == 'win32':
596
 
        # setting the process group and closing fds is not supported on
 
559
        # setting the process group and closing fds is not supported on 
597
560
        # win32
598
561
        return {}
599
562
    else:
600
 
        # We close fds other than the pipes as the child process does not need
 
563
        # We close fds other than the pipes as the child process does not need 
601
564
        # them to be open.
602
565
        #
603
566
        # We also set the child process to ignore SIGINT.  Normally the signal
605
568
        # this causes it to be seen only by bzr and not by ssh.  Python will
606
569
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
607
570
        # to release locks or do other cleanup over ssh before the connection
608
 
        # goes away.
 
571
        # goes away.  
609
572
        # <https://launchpad.net/products/bzr/+bug/5987>
610
573
        #
611
574
        # Running it in a separate process group is not good because then it
625
588
    def send(self, data):
626
589
        return os.write(self.proc.stdin.fileno(), data)
627
590
 
 
591
    def recv_ready(self):
 
592
        # TODO: jam 20051215 this function is necessary to support the
 
593
        # pipelined() function. In reality, it probably should use
 
594
        # poll() or select() to actually return if there is data
 
595
        # available, otherwise we probably don't get any benefit
 
596
        return True
 
597
 
628
598
    def recv(self, count):
629
599
        return os.read(self.proc.stdout.fileno(), count)
630
600