~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robert Collins
  • Date: 2007-09-25 08:41:29 UTC
  • mto: This revision was merged to the branch mainline in revision 2862.
  • Revision ID: robertc@robertcollins.net-20070925084129-ca0kd25h23dmunrs
Review feedback.

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