~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] use /usr/bin/ssh if we can (jamesh)

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
import stat
24
24
import sys
25
25
import urllib
 
26
import urlparse
26
27
import time
27
28
import random
 
29
import subprocess
28
30
 
29
31
from bzrlib.errors import (FileExists, 
30
32
                           TransportNotPossible, NoSuchFile, NonRelativePath,
45
47
                               CMD_HANDLE, CMD_OPEN)
46
48
    from paramiko.sftp_attr import SFTPAttributes
47
49
    from paramiko.sftp_file import SFTPFile
 
50
    from paramiko.sftp_client import SFTPClient
 
51
 
 
52
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
53
 
 
54
 
 
55
_ssh_vendor = None
 
56
def _get_ssh_vendor():
 
57
    """Find out what version of SSH is on the system."""
 
58
    global _ssh_vendor
 
59
    if _ssh_vendor is not None:
 
60
        return _ssh_vendor
 
61
 
 
62
    _ssh_vendor = 'none'
 
63
 
 
64
    try:
 
65
        p = subprocess.Popen(['ssh', '-V'],
 
66
                             close_fds=True,
 
67
                             stdin=subprocess.PIPE,
 
68
                             stdout=subprocess.PIPE,
 
69
                             stderr=subprocess.PIPE)
 
70
        returncode = p.returncode
 
71
        stdout, stderr = p.communicate()
 
72
    except OSError:
 
73
        returncode = -1
 
74
        stdout = stderr = ''
 
75
    if 'OpenSSH' in stderr:
 
76
        mutter('ssh implementation is OpenSSH')
 
77
        _ssh_vendor = 'openssh'
 
78
    elif 'SSH Secure Shell' in stderr:
 
79
        mutter('ssh implementation is SSH Corp.')
 
80
        _ssh_vendor = 'ssh'
 
81
 
 
82
    if _ssh_vendor != 'none':
 
83
        return _ssh_vendor
 
84
 
 
85
    # XXX: 20051123 jamesh
 
86
    # A check for putty's plink or lsh would go here.
 
87
 
 
88
    mutter('falling back to paramiko implementation')
 
89
    return _ssh_vendor
 
90
 
 
91
 
 
92
class SFTPSubprocess:
 
93
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
94
    def __init__(self, hostname, port=None, user=None):
 
95
        vendor = _get_ssh_vendor()
 
96
        assert vendor in ['openssh', 'ssh']
 
97
        if vendor == 'openssh':
 
98
            args = ['ssh',
 
99
                    '-oForwardX11=no', '-oForwardAgent=no',
 
100
                    '-oClearAllForwardings=yes', '-oProtocol=2',
 
101
                    '-oNoHostAuthenticationForLocalhost=yes']
 
102
            if port is not None:
 
103
                args.extend(['-p', str(port)])
 
104
            if user is not None:
 
105
                args.extend(['-l', user])
 
106
            args.extend(['-s', hostname, 'sftp'])
 
107
        elif vendor == 'ssh':
 
108
            args = ['ssh', '-x']
 
109
            if port is not None:
 
110
                args.extend(['-p', str(port)])
 
111
            if user is not None:
 
112
                args.extend(['-l', user])
 
113
            args.extend(['-s', 'sftp', hostname])
 
114
 
 
115
        self.proc = subprocess.Popen(args, close_fds=True,
 
116
                                     stdin=subprocess.PIPE,
 
117
                                     stdout=subprocess.PIPE)
 
118
 
 
119
    def send(self, data):
 
120
        return os.write(self.proc.stdin.fileno(), data)
 
121
 
 
122
    def recv(self, count):
 
123
        return os.read(self.proc.stdout.fileno(), count)
 
124
 
 
125
    def close(self):
 
126
        self.proc.stdin.close()
 
127
        self.proc.stdout.close()
 
128
        self.proc.wait()
 
129
 
48
130
 
49
131
 
50
132
SYSTEM_HOSTKEYS = {}
127
209
    Transport implementation for SFTP access.
128
210
    """
129
211
 
130
 
    _url_matcher = re.compile(r'^sftp://([^:@]*(:[^@]*)?@)?(.*?)(:[^/]+)?(/.*)?$')
131
 
    
132
212
    def __init__(self, base, clone_from=None):
133
213
        assert base.startswith('sftp://')
134
214
        super(SFTPTransport, self).__init__(base)
192
272
        return path
193
273
 
194
274
    def relpath(self, abspath):
195
 
        # FIXME: this is identical to HttpTransport -- share it
196
 
        m = self._url_matcher.match(abspath)
197
 
        path = m.group(5)
198
 
        if not path.startswith(self._path):
 
275
        username, password, host, port, path = self._split_url(abspath)
 
276
        if (username != self._username or host != self._host or
 
277
            port != self._port or not path.startswith(self._path)):
199
278
            raise NonRelativePath('path %r is not under base URL %r'
200
279
                           % (abspath, self.base))
201
 
        pl = len(self.base)
202
 
        return abspath[pl:].lstrip('/')
 
280
        pl = len(self._path)
 
281
        return path[pl:].lstrip('/')
203
282
 
204
283
    def has(self, relpath):
205
284
        """
270
349
            except IOError, e:
271
350
                self._translate_io_exception(e, relpath)
272
351
            except paramiko.SSHException, x:
273
 
                raise SFTPTransportError('Unable to write file %r' % (path,), x)
 
352
                raise SFTPTransportError('Unable to write file %r' % (relpath,), x)
274
353
        except Exception, e:
275
354
            # If we fail, try to clean up the temporary file
276
355
            # before we throw the exception
296
375
                self._translate_io_exception(e, relpath)
297
376
            except paramiko.SSHException, x:
298
377
                raise SFTPTransportError('Unable to rename into file %r' 
299
 
                                          % (path,), x)
 
378
                                          % (relpath,), x)
300
379
            if file_existed:
301
380
                self._sftp.unlink(tmp_safety)
302
381
 
435
514
    def _unparse_url(self, path=None):
436
515
        if path is None:
437
516
            path = self._path
438
 
        host = self._host
439
 
        username = urllib.quote(self._username)
440
 
        if self._password:
441
 
            username += ':' + urllib.quote(self._password)
442
 
        if self._port != 22:
443
 
            host += ':%d' % self._port
444
 
        return 'sftp://%s@%s/%s' % (username, host, urllib.quote(path))
 
517
        path = urllib.quote(path)
 
518
        if path.startswith('/'):
 
519
            path = '/%2F' + path[1:]
 
520
        else:
 
521
            path = '/' + path
 
522
        netloc = urllib.quote(self._host)
 
523
        if self._username is not None:
 
524
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
525
        if self._port is not None:
 
526
            netloc = '%s:%d' % (netloc, self._port)
 
527
 
 
528
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
 
529
 
 
530
    def _split_url(self, url):
 
531
        if isinstance(url, unicode):
 
532
            url = url.encode('utf-8')
 
533
        (scheme, netloc, path, params,
 
534
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
535
        assert scheme == 'sftp'
 
536
        username = password = host = port = None
 
537
        if '@' in netloc:
 
538
            username, host = netloc.split('@', 1)
 
539
            if ':' in username:
 
540
                username, password = username.split(':', 1)
 
541
                password = urllib.unquote(password)
 
542
            username = urllib.unquote(username)
 
543
        else:
 
544
            host = netloc
 
545
 
 
546
        if ':' in host:
 
547
            host, port = host.rsplit(':', 1)
 
548
            try:
 
549
                port = int(port)
 
550
            except ValueError:
 
551
                raise SFTPTransportError('%s: invalid port number' % port)
 
552
        host = urllib.unquote(host)
 
553
 
 
554
        path = urllib.unquote(path)
 
555
 
 
556
        # the initial slash should be removed from the path, and treated
 
557
        # as a homedir relative path (the path begins with a double slash
 
558
        # if it is absolute).
 
559
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
560
        if path.startswith('/'):
 
561
            path = path[1:]
 
562
 
 
563
        return (username, password, host, port, path)
445
564
 
446
565
    def _parse_url(self, url):
447
 
        assert url[:7] == 'sftp://'
448
 
        m = self._url_matcher.match(url)
449
 
        if m is None:
450
 
            raise SFTPTransportError('Unable to parse SFTP URL %r' % (url,))
451
 
        self._username, self._password, self._host, self._port, self._path = m.groups()
452
 
        if self._username is None:
453
 
            self._username = getpass.getuser()
454
 
        else:
455
 
            if self._password:
456
 
                # username field is 'user:pass@' in this case, and password is ':pass'
457
 
                username_len = len(self._username) - len(self._password) - 1
458
 
                self._username = urllib.unquote(self._username[:username_len])
459
 
                self._password = urllib.unquote(self._password[1:])
460
 
            else:
461
 
                self._username = urllib.unquote(self._username[:-1])
462
 
        if self._port is None:
463
 
            self._port = 22
464
 
        else:
465
 
            try:
466
 
                self._port = int(self._port[1:])
467
 
            except ValueError:
468
 
                raise SFTPTransportError('%s: invalid port number' % self._port[1:])
 
566
        (self._username, self._password,
 
567
         self._host, self._port, self._path) = self._split_url(url)
 
568
 
 
569
    def _sftp_connect(self):
 
570
        vendor = _get_ssh_vendor()
469
571
        if (self._path is None) or (self._path == ''):
470
572
            self._path = ''
471
573
        else:
472
574
            # remove leading '/'
473
575
            self._path = urllib.unquote(self._path[1:])
 
576
        if vendor != 'none':
 
577
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
578
            self._sftp = SFTPClient(sock)
 
579
        else:
 
580
            self._paramiko_connect()
474
581
 
475
 
    def _sftp_connect(self):
 
582
    def _paramiko_connect(self):
476
583
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
477
584
        
478
585
        load_host_keys()
479
 
        
 
586
 
480
587
        try:
481
 
            t = paramiko.Transport((self._host, self._port))
 
588
            t = paramiko.Transport((self._host, self._port or 22))
482
589
            t.start_client()
483
590
        except paramiko.SSHException:
484
591
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
507
614
                (self._host, our_server_key_hex, server_key_hex),
508
615
                ['Try editing %s or %s' % (filename1, filename2)])
509
616
 
510
 
        self._sftp_auth(t, self._username, self._host)
 
617
        self._sftp_auth(t, self._username or getpass.getuser(), self._host)
511
618
        
512
619
        try:
513
620
            self._sftp = t.open_sftp_client()