~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

[merge] bzr.dev

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
import weakref
29
31
 
30
32
from bzrlib.errors import (FileExists, 
46
48
                               CMD_HANDLE, CMD_OPEN)
47
49
    from paramiko.sftp_attr import SFTPAttributes
48
50
    from paramiko.sftp_file import SFTPFile
 
51
    from paramiko.sftp_client import SFTPClient
 
52
 
 
53
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
54
 
 
55
 
 
56
_ssh_vendor = None
 
57
def _get_ssh_vendor():
 
58
    """Find out what version of SSH is on the system."""
 
59
    global _ssh_vendor
 
60
    if _ssh_vendor is not None:
 
61
        return _ssh_vendor
 
62
 
 
63
    _ssh_vendor = 'none'
 
64
 
 
65
    try:
 
66
        p = subprocess.Popen(['ssh', '-V'],
 
67
                             close_fds=True,
 
68
                             stdin=subprocess.PIPE,
 
69
                             stdout=subprocess.PIPE,
 
70
                             stderr=subprocess.PIPE)
 
71
        returncode = p.returncode
 
72
        stdout, stderr = p.communicate()
 
73
    except OSError:
 
74
        returncode = -1
 
75
        stdout = stderr = ''
 
76
    if 'OpenSSH' in stderr:
 
77
        mutter('ssh implementation is OpenSSH')
 
78
        _ssh_vendor = 'openssh'
 
79
    elif 'SSH Secure Shell' in stderr:
 
80
        mutter('ssh implementation is SSH Corp.')
 
81
        _ssh_vendor = 'ssh'
 
82
 
 
83
    if _ssh_vendor != 'none':
 
84
        return _ssh_vendor
 
85
 
 
86
    # XXX: 20051123 jamesh
 
87
    # A check for putty's plink or lsh would go here.
 
88
 
 
89
    mutter('falling back to paramiko implementation')
 
90
    return _ssh_vendor
 
91
 
 
92
 
 
93
class SFTPSubprocess:
 
94
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
95
    def __init__(self, hostname, port=None, user=None):
 
96
        vendor = _get_ssh_vendor()
 
97
        assert vendor in ['openssh', 'ssh']
 
98
        if vendor == 'openssh':
 
99
            args = ['ssh',
 
100
                    '-oForwardX11=no', '-oForwardAgent=no',
 
101
                    '-oClearAllForwardings=yes', '-oProtocol=2',
 
102
                    '-oNoHostAuthenticationForLocalhost=yes']
 
103
            if port is not None:
 
104
                args.extend(['-p', str(port)])
 
105
            if user is not None:
 
106
                args.extend(['-l', user])
 
107
            args.extend(['-s', hostname, 'sftp'])
 
108
        elif vendor == 'ssh':
 
109
            args = ['ssh', '-x']
 
110
            if port is not None:
 
111
                args.extend(['-p', str(port)])
 
112
            if user is not None:
 
113
                args.extend(['-l', user])
 
114
            args.extend(['-s', 'sftp', hostname])
 
115
 
 
116
        self.proc = subprocess.Popen(args, close_fds=True,
 
117
                                     stdin=subprocess.PIPE,
 
118
                                     stdout=subprocess.PIPE)
 
119
 
 
120
    def send(self, data):
 
121
        return os.write(self.proc.stdin.fileno(), data)
 
122
 
 
123
    def recv(self, count):
 
124
        return os.read(self.proc.stdout.fileno(), count)
 
125
 
 
126
    def close(self):
 
127
        self.proc.stdin.close()
 
128
        self.proc.stdout.close()
 
129
        self.proc.wait()
49
130
 
50
131
 
51
132
SYSTEM_HOSTKEYS = {}
134
215
    Transport implementation for SFTP access.
135
216
    """
136
217
 
137
 
    _url_matcher = re.compile(r'^sftp://([^:@]*(:[^@]*)?@)?(.*?)(:[^/]+)?(/.*)?$')
138
 
    
139
218
    def __init__(self, base, clone_from=None):
140
219
        assert base.startswith('sftp://')
141
220
        self._parse_url(base)
200
279
        return path
201
280
 
202
281
    def relpath(self, abspath):
203
 
        # FIXME: this is identical to HttpTransport -- share it
204
 
        m = self._url_matcher.match(abspath)
205
 
        path = m.group(5)
206
 
        if not path.startswith(self._path):
 
282
        username, password, host, port, path = self._split_url(abspath)
 
283
        if (username != self._username or host != self._host or
 
284
            port != self._port or not path.startswith(self._path)):
207
285
            raise NonRelativePath('path %r is not under base URL %r'
208
286
                           % (abspath, self.base))
209
 
        pl = len(self.base)
210
 
        return abspath[pl:].lstrip('/')
 
287
        pl = len(self._path)
 
288
        return path[pl:].lstrip('/')
211
289
 
212
290
    def has(self, relpath):
213
291
        """
228
306
        try:
229
307
            path = self._abspath(relpath)
230
308
            f = self._sftp.file(path)
 
309
            # TODO: Don't prefetch until paramiko fixes itself
231
310
            if hasattr(f, 'prefetch'):
232
311
                f.prefetch()
233
312
            return f
272
351
            except IOError, e:
273
352
                self._translate_io_exception(e, relpath)
274
353
            except paramiko.SSHException, x:
275
 
                raise SFTPTransportError('Unable to write file %r' % (path,), x)
 
354
                raise SFTPTransportError('Unable to write file %r' % (relpath,), x)
276
355
        except Exception, e:
277
356
            # If we fail, try to clean up the temporary file
278
357
            # before we throw the exception
485
564
    def _unparse_url(self, path=None):
486
565
        if path is None:
487
566
            path = self._path
488
 
        host = self._host
489
 
        username = urllib.quote(self._username)
490
 
        if self._port != 22:
491
 
            host += ':%d' % self._port
492
 
        return 'sftp://%s@%s/%s' % (username, host, urllib.quote(path))
 
567
        path = urllib.quote(path)
 
568
        if path.startswith('/'):
 
569
            path = '/%2F' + path[1:]
 
570
        else:
 
571
            path = '/' + path
 
572
        netloc = urllib.quote(self._host)
 
573
        if self._username is not None:
 
574
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
575
        if self._port is not None:
 
576
            netloc = '%s:%d' % (netloc, self._port)
 
577
 
 
578
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
 
579
 
 
580
    def _split_url(self, url):
 
581
        if isinstance(url, unicode):
 
582
            url = url.encode('utf-8')
 
583
        (scheme, netloc, path, params,
 
584
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
585
        assert scheme == 'sftp'
 
586
        username = password = host = port = None
 
587
        if '@' in netloc:
 
588
            username, host = netloc.split('@', 1)
 
589
            if ':' in username:
 
590
                username, password = username.split(':', 1)
 
591
                password = urllib.unquote(password)
 
592
            username = urllib.unquote(username)
 
593
        else:
 
594
            host = netloc
 
595
 
 
596
        if ':' in host:
 
597
            host, port = host.rsplit(':', 1)
 
598
            try:
 
599
                port = int(port)
 
600
            except ValueError:
 
601
                raise SFTPTransportError('%s: invalid port number' % port)
 
602
        host = urllib.unquote(host)
 
603
 
 
604
        path = urllib.unquote(path)
 
605
 
 
606
        # the initial slash should be removed from the path, and treated
 
607
        # as a homedir relative path (the path begins with a double slash
 
608
        # if it is absolute).
 
609
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
610
        if path.startswith('/'):
 
611
            path = path[1:]
 
612
 
 
613
        return (username, password, host, port, path)
493
614
 
494
615
    def _parse_url(self, url):
495
 
        assert url[:7] == 'sftp://'
496
 
        m = self._url_matcher.match(url)
497
 
        if m is None:
498
 
            raise SFTPTransportError('Unable to parse SFTP URL %r' % (url,))
499
 
        self._username, self._password, self._host, self._port, self._path = m.groups()
500
 
        if self._username is None:
501
 
            self._username = getpass.getuser()
502
 
        else:
503
 
            if self._password:
504
 
                # username field is 'user:pass@' in this case, and password is ':pass'
505
 
                username_len = len(self._username) - len(self._password) - 1
506
 
                self._username = urllib.unquote(self._username[:username_len])
507
 
                self._password = urllib.unquote(self._password[1:])
508
 
            else:
509
 
                self._username = urllib.unquote(self._username[:-1])
510
 
        if self._port is None:
511
 
            self._port = 22
512
 
        else:
513
 
            self._port = int(self._port[1:])
514
 
        if (self._path is None) or (self._path == ''):
515
 
            self._path = ''
516
 
        else:
517
 
            # remove leading '/'
518
 
            self._path = urllib.unquote(self._path[1:])
 
616
        (self._username, self._password,
 
617
         self._host, self._port, self._path) = self._split_url(url)
 
618
         if self._port is None:
 
619
             self._port = 22
519
620
 
520
621
    def _sftp_connect(self):
521
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
522
 
        
523
 
        load_host_keys()
 
622
        """Connect to the remote sftp server.
 
623
        After this, self._sftp should have a valid connection (or
 
624
        we raise an SFTPTransportError 'could not connect').
 
625
 
 
626
        TODO: Raise a more reasonable ConnectionFailed exception
 
627
        """
 
628
        global _connected_hosts
524
629
 
525
630
        idx = (self._host, self._port, self._username)
526
631
        try:
529
634
        except KeyError:
530
635
            pass
531
636
        
 
637
        vendor = _get_ssh_vendor()
 
638
        if vendor != 'none':
 
639
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
640
            self._sftp = SFTPClient(sock)
 
641
        else:
 
642
            self._paramiko_connect()
 
643
 
 
644
        _connected_hosts[idx] = self._sftp
 
645
 
 
646
    def _paramiko_connect(self):
 
647
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
648
        
 
649
        load_host_keys()
 
650
 
532
651
        try:
533
652
            t = paramiko.Transport((self._host, self._port))
534
653
            t.start_client()
559
678
                (self._host, our_server_key_hex, server_key_hex),
560
679
                ['Try editing %s or %s' % (filename1, filename2)])
561
680
 
562
 
        self._sftp_auth(t, self._username, self._host)
 
681
        self._sftp_auth(t, self._username or getpass.getuser(), self._host)
563
682
        
564
683
        try:
565
684
            self._sftp = t.open_sftp_client()
566
685
        except paramiko.SSHException:
567
686
            raise BzrError('Unable to find path %s on SFTP server %s' % \
568
687
                (self._path, self._host))
569
 
        else:
570
 
            _connected_hosts[idx] = self._sftp
571
688
 
572
689
    def _sftp_auth(self, transport, username, host):
573
690
        agent = paramiko.Agent()