/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/http/_pycurl.py

  • Committer: Robert Collins
  • Date: 2008-08-20 02:07:36 UTC
  • mfrom: (3640 +trunk)
  • mto: This revision was merged to the branch mainline in revision 3682.
  • Revision ID: robertc@robertcollins.net-20080820020736-g2xe4921zzxtymle
Merge bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
"""http/https transport using pycurl"""
18
18
 
19
19
# TODO: test reporting of http errors
20
 
 
 
20
#
21
21
# TODO: Transport option to control caching of particular requests; broadly we
22
22
# would want to offer "caching allowed" or "must revalidate", depending on
23
23
# whether we expect a particular file will be modified after it's committed.
24
24
# It's probably safer to just always revalidate.  mbp 20060321
25
25
 
 
26
# TODO: Some refactoring could be done to avoid the strange idiom
 
27
# used to capture data and headers while setting up the request
 
28
# (and having to pass 'header' to _curl_perform to handle
 
29
# redirections) . This could be achieved by creating a
 
30
# specialized Curl object and returning code, headers and data
 
31
# from _curl_perform.  Not done because we may deprecate pycurl in the
 
32
# future -- vila 20070212
 
33
 
26
34
import os
27
 
from StringIO import StringIO
 
35
from cStringIO import StringIO
 
36
import httplib
 
37
import sys
28
38
 
 
39
from bzrlib import (
 
40
    debug,
 
41
    errors,
 
42
    trace,
 
43
    __version__ as bzrlib_version,
 
44
    )
29
45
import bzrlib
30
 
from bzrlib.errors import (TransportNotPossible, NoSuchFile,
31
 
                           TransportError, ConnectionError,
32
 
                           DependencyNotPresent)
33
46
from bzrlib.trace import mutter
34
 
from bzrlib.transport import register_urlparse_netloc_protocol
35
 
from bzrlib.transport.http import HttpTransportBase, extract_auth, HttpServer
 
47
from bzrlib.transport.http import (
 
48
    ca_bundle,
 
49
    HttpTransportBase,
 
50
    response,
 
51
    )
36
52
 
37
53
try:
38
54
    import pycurl
39
55
except ImportError, e:
40
56
    mutter("failed to import pycurl: %s", e)
41
 
    raise DependencyNotPresent('pycurl', e)
 
57
    raise errors.DependencyNotPresent('pycurl', e)
42
58
 
43
59
try:
44
60
    # see if we can actually initialize PyCurl - sometimes it will load but
53
69
    pycurl.Curl()
54
70
except pycurl.error, e:
55
71
    mutter("failed to initialize pycurl: %s", e)
56
 
    raise DependencyNotPresent('pycurl', e)
57
 
 
58
 
 
59
 
register_urlparse_netloc_protocol('http+pycurl')
 
72
    raise errors.DependencyNotPresent('pycurl', e)
 
73
 
 
74
 
 
75
 
 
76
 
 
77
def _get_pycurl_errcode(symbol, default):
 
78
    """
 
79
    Returns the numerical error code for a symbol defined by pycurl.
 
80
 
 
81
    Different pycurl implementations define different symbols for error
 
82
    codes. Old versions never define some symbols (wether they can return the
 
83
    corresponding error code or not). The following addresses the problem by
 
84
    defining the symbols we care about.  Note: this allows to define symbols
 
85
    for errors that older versions will never return, which is fine.
 
86
    """
 
87
    return pycurl.__dict__.get(symbol, default)
 
88
 
 
89
CURLE_SSL_CACERT_BADFILE = _get_pycurl_errcode('E_SSL_CACERT_BADFILE', 77)
 
90
CURLE_COULDNT_CONNECT = _get_pycurl_errcode('E_COULDNT_CONNECT', 7)
 
91
CURLE_COULDNT_RESOLVE_HOST = _get_pycurl_errcode('E_COULDNT_RESOLVE_HOST', 6)
 
92
CURLE_COULDNT_RESOLVE_PROXY = _get_pycurl_errcode('E_COULDNT_RESOLVE_PROXY', 5)
 
93
CURLE_GOT_NOTHING = _get_pycurl_errcode('E_GOT_NOTHING', 52)
 
94
CURLE_PARTIAL_FILE = _get_pycurl_errcode('E_PARTIAL_FILE', 18)
60
95
 
61
96
 
62
97
class PyCurlTransport(HttpTransportBase):
64
99
 
65
100
    PyCurl is a Python binding to the C "curl" multiprotocol client.
66
101
 
67
 
    This transport can be significantly faster than the builtin Python client. 
68
 
    Advantages include: DNS caching, connection keepalive, and ability to 
69
 
    set headers to allow caching.
 
102
    This transport can be significantly faster than the builtin
 
103
    Python client.  Advantages include: DNS caching.
70
104
    """
71
105
 
72
 
    def __init__(self, base):
73
 
        super(PyCurlTransport, self).__init__(base)
74
 
        mutter('using pycurl %s' % pycurl.version)
 
106
    def __init__(self, base, _from_transport=None):
 
107
        super(PyCurlTransport, self).__init__(base,
 
108
                                              _from_transport=_from_transport)
 
109
        if base.startswith('https'):
 
110
            # Check availability of https into pycurl supported
 
111
            # protocols
 
112
            supported = pycurl.version_info()[8]
 
113
            if 'https' not in supported:
 
114
                raise errors.DependencyNotPresent('pycurl', 'no https support')
 
115
        self.cabundle = ca_bundle.get_ca_path()
75
116
 
76
 
    def should_cache(self):
77
 
        """Return True if the data pulled across should be cached locally.
78
 
        """
79
 
        return True
 
117
    def _get_curl(self):
 
118
        connection = self._get_connection()
 
119
        if connection is None:
 
120
            # First connection ever. There is no credentials for pycurl, either
 
121
            # the password was embedded in the URL or it's not needed. The
 
122
            # connection for pycurl is just the Curl object, it will not
 
123
            # connect to the http server until the first request (which had
 
124
            # just called us).
 
125
            connection = pycurl.Curl()
 
126
            # First request, initialize credentials.
 
127
            auth = self._create_auth()
 
128
            # Proxy handling is out of reach, so we punt
 
129
            self._set_connection(connection, auth)
 
130
        return connection
80
131
 
81
132
    def has(self, relpath):
82
 
        curl = pycurl.Curl()
83
 
        abspath = self._real_abspath(relpath)
 
133
        """See Transport.has()"""
 
134
        # We set NO BODY=0 in _get_full, so it should be safe
 
135
        # to re-use the non-range curl object
 
136
        curl = self._get_curl()
 
137
        abspath = self._remote_path(relpath)
84
138
        curl.setopt(pycurl.URL, abspath)
85
 
        curl.setopt(pycurl.FOLLOWLOCATION, 1) # follow redirect responses
86
139
        self._set_curl_options(curl)
 
140
        curl.setopt(pycurl.HTTPGET, 1)
87
141
        # don't want the body - ie just do a HEAD request
 
142
        # This means "NO BODY" not 'nobody'
88
143
        curl.setopt(pycurl.NOBODY, 1)
89
 
        self._curl_perform(curl)
 
144
        # But we need headers to handle redirections
 
145
        header = StringIO()
 
146
        curl.setopt(pycurl.HEADERFUNCTION, header.write)
 
147
        # In some erroneous cases, pycurl will emit text on
 
148
        # stdout if we don't catch it (see InvalidStatus tests
 
149
        # for one such occurrence).
 
150
        blackhole = StringIO()
 
151
        curl.setopt(pycurl.WRITEFUNCTION, blackhole.write)
 
152
        self._curl_perform(curl, header)
90
153
        code = curl.getinfo(pycurl.HTTP_CODE)
91
154
        if code == 404: # not found
92
155
            return False
93
 
        elif code in (200, 302): # "ok", "found"
 
156
        elif code == 200: # "ok"
94
157
            return True
95
 
        elif code == 0:
96
 
            self._raise_curl_connection_error(curl)
97
158
        else:
98
159
            self._raise_curl_http_error(curl)
99
 
        
100
 
    def _get(self, relpath, ranges):
101
 
        curl = pycurl.Curl()
102
 
        abspath = self._real_abspath(relpath)
103
 
        sio = StringIO()
104
 
        curl.setopt(pycurl.URL, abspath)
105
 
        self._set_curl_options(curl)
106
 
        curl.setopt(pycurl.WRITEFUNCTION, sio.write)
 
160
 
 
161
    def _get(self, relpath, offsets, tail_amount=0):
 
162
        # This just switches based on the type of request
 
163
        if offsets is not None or tail_amount not in (0, None):
 
164
            return self._get_ranged(relpath, offsets, tail_amount=tail_amount)
 
165
        else:
 
166
            return self._get_full(relpath)
 
167
 
 
168
    def _setup_get_request(self, curl, relpath):
 
169
        # Make sure we do a GET request. versions > 7.14.1 also set the
 
170
        # NO BODY flag, but we'll do it ourselves in case it is an older
 
171
        # pycurl version
107
172
        curl.setopt(pycurl.NOBODY, 0)
108
 
        if ranges is not None:
109
 
            assert len(ranges) == 1
110
 
            # multiple ranges not supported yet because we can't decode the
111
 
            # response
112
 
            curl.setopt(pycurl.RANGE, '%d-%d' % ranges[0])
113
 
        self._curl_perform(curl)
 
173
        curl.setopt(pycurl.HTTPGET, 1)
 
174
        return self._setup_request(curl, relpath)
 
175
 
 
176
    def _setup_request(self, curl, relpath):
 
177
        """Do the common setup stuff for making a request
 
178
 
 
179
        :param curl: The curl object to place the request on
 
180
        :param relpath: The relative path that we want to get
 
181
        :return: (abspath, data, header) 
 
182
                 abspath: full url
 
183
                 data: file that will be filled with the body
 
184
                 header: file that will be filled with the headers
 
185
        """
 
186
        abspath = self._remote_path(relpath)
 
187
        curl.setopt(pycurl.URL, abspath)
 
188
        self._set_curl_options(curl)
 
189
 
 
190
        data = StringIO()
 
191
        header = StringIO()
 
192
        curl.setopt(pycurl.WRITEFUNCTION, data.write)
 
193
        curl.setopt(pycurl.HEADERFUNCTION, header.write)
 
194
 
 
195
        return abspath, data, header
 
196
 
 
197
    def _get_full(self, relpath):
 
198
        """Make a request for the entire file"""
 
199
        curl = self._get_curl()
 
200
        abspath, data, header = self._setup_get_request(curl, relpath)
 
201
        self._curl_perform(curl, header)
 
202
 
114
203
        code = curl.getinfo(pycurl.HTTP_CODE)
 
204
        data.seek(0)
 
205
 
115
206
        if code == 404:
116
 
            raise NoSuchFile(abspath)
117
 
        elif code == 200:
118
 
            sio.seek(0)
119
 
            return code, sio
120
 
        elif code == 206 and (ranges is not None):
121
 
            sio.seek(0)
122
 
            return code, sio
123
 
        elif code == 0:
124
 
            self._raise_curl_connection_error(curl)
 
207
            raise errors.NoSuchFile(abspath)
 
208
        if code != 200:
 
209
            self._raise_curl_http_error(
 
210
                curl, 'expected 200 or 404 for full response.')
 
211
 
 
212
        return code, data
 
213
 
 
214
    # The parent class use 0 to minimize the requests, but since we can't
 
215
    # exploit the results as soon as they are received (pycurl limitation) we'd
 
216
    # better issue more requests and provide a more responsive UI do the cost
 
217
    # of more latency costs.
 
218
    # If you modify this, think about modifying the comment in http/__init__.py
 
219
    # too.
 
220
    _get_max_size = 4 * 1024 * 1024
 
221
 
 
222
    def _get_ranged(self, relpath, offsets, tail_amount):
 
223
        """Make a request for just part of the file."""
 
224
        curl = self._get_curl()
 
225
        abspath, data, header = self._setup_get_request(curl, relpath)
 
226
 
 
227
        range_header = self._attempted_range_header(offsets, tail_amount)
 
228
        if range_header is None:
 
229
            # Forget ranges, the server can't handle them
 
230
            return self._get_full(relpath)
 
231
 
 
232
        self._curl_perform(curl, header, ['Range: bytes=%s' % range_header])
 
233
        data.seek(0)
 
234
 
 
235
        code = curl.getinfo(pycurl.HTTP_CODE)
 
236
 
 
237
        if code == 404: # not found
 
238
            raise errors.NoSuchFile(abspath)
 
239
        elif code in (400, 416):
 
240
            # We don't know which, but one of the ranges we specified was
 
241
            # wrong.
 
242
            raise errors.InvalidHttpRange(abspath, range_header,
 
243
                                          'Server return code %d'
 
244
                                          % curl.getinfo(pycurl.HTTP_CODE))
 
245
        msg = self._parse_headers(header)
 
246
        return code, response.handle_response(abspath, code, msg, data)
 
247
 
 
248
    def _parse_headers(self, status_and_headers):
 
249
        """Transform the headers provided by curl into an HTTPMessage"""
 
250
        status_and_headers.seek(0)
 
251
        # Ignore status line
 
252
        status_and_headers.readline()
 
253
        msg = httplib.HTTPMessage(status_and_headers)
 
254
        return msg
 
255
 
 
256
    def _post(self, body_bytes):
 
257
        fake_file = StringIO(body_bytes)
 
258
        curl = self._get_curl()
 
259
        # Other places that use the Curl object (returned by _get_curl)
 
260
        # for GET requests explicitly set HTTPGET, so it should be safe to
 
261
        # re-use the same object for both GETs and POSTs.
 
262
        curl.setopt(pycurl.POST, 1)
 
263
        curl.setopt(pycurl.POSTFIELDSIZE, len(body_bytes))
 
264
        curl.setopt(pycurl.READFUNCTION, fake_file.read)
 
265
        abspath, data, header = self._setup_request(curl, '.bzr/smart')
 
266
        # We override the Expect: header so that pycurl will send the POST
 
267
        # body immediately.
 
268
        self._curl_perform(curl, header, ['Expect: '])
 
269
        data.seek(0)
 
270
        code = curl.getinfo(pycurl.HTTP_CODE)
 
271
        msg = self._parse_headers(header)
 
272
        return code, response.handle_response(abspath, code, msg, data)
 
273
 
 
274
    def _raise_curl_http_error(self, curl, info=None):
 
275
        code = curl.getinfo(pycurl.HTTP_CODE)
 
276
        url = curl.getinfo(pycurl.EFFECTIVE_URL)
 
277
        # Some error codes can be handled the same way for all
 
278
        # requests
 
279
        if code == 403:
 
280
            raise errors.TransportError(
 
281
                'Server refuses to fulfill the request (403 Forbidden)'
 
282
                ' for %s' % url)
125
283
        else:
126
 
            self._raise_curl_http_error(curl)
127
 
 
128
 
    def _raise_curl_connection_error(self, curl):
129
 
        curl_errno = curl.getinfo(pycurl.OS_ERRNO)
130
 
        url = curl.getinfo(pycurl.EFFECTIVE_URL)
131
 
        raise ConnectionError('curl connection error (%s) on %s'
132
 
                              % (os.strerror(curl_errno), url))
133
 
 
134
 
    def _raise_curl_http_error(self, curl):
135
 
        code = curl.getinfo(pycurl.HTTP_CODE)
136
 
        url = curl.getinfo(pycurl.EFFECTIVE_URL)
137
 
        raise TransportError('http error %d probing for %s' %
138
 
                             (code, url))
 
284
            if info is None:
 
285
                msg = ''
 
286
            else:
 
287
                msg = ': ' + info
 
288
            raise errors.InvalidHttpResponse(
 
289
                url, 'Unable to handle http code %d%s' % (code,msg))
139
290
 
140
291
    def _set_curl_options(self, curl):
141
292
        """Set options for all requests"""
142
 
        # There's no way in http/1.0 to say "must revalidate"; we don't want
143
 
        # to force it to always retrieve.  so just turn off the default Pragma
144
 
        # provided by Curl.
145
 
        headers = ['Cache-control: max-age=0',
146
 
                   'Pragma: no-cache']
147
 
        ## curl.setopt(pycurl.VERBOSE, 1)
148
 
        # TODO: maybe include a summary of the pycurl version
149
 
        ua_str = 'bzr/%s (pycurl)' % (bzrlib.__version__)
 
293
        if 'http' in debug.debug_flags:
 
294
            curl.setopt(pycurl.VERBOSE, 1)
 
295
            # pycurl doesn't implement the CURLOPT_STDERR option, so we can't
 
296
            # do : curl.setopt(pycurl.STDERR, trace._trace_file)
 
297
 
 
298
        ua_str = 'bzr/%s (pycurl: %s)' % (bzrlib.__version__, pycurl.version)
150
299
        curl.setopt(pycurl.USERAGENT, ua_str)
151
 
        curl.setopt(pycurl.HTTPHEADER, headers)
152
 
        curl.setopt(pycurl.FOLLOWLOCATION, 1) # follow redirect responses
 
300
        if self.cabundle:
 
301
            curl.setopt(pycurl.CAINFO, self.cabundle)
 
302
        # Set accepted auth methods
 
303
        curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_ANY)
 
304
        curl.setopt(pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)
 
305
        auth = self._get_credentials()
 
306
        user = auth.get('user', None)
 
307
        password = auth.get('password', None)
 
308
        userpass = None
 
309
        if user is not None:
 
310
            userpass = user + ':'
 
311
            if password is not None: # '' is a valid password
 
312
                userpass += password
 
313
            curl.setopt(pycurl.USERPWD, userpass)
153
314
 
154
 
    def _curl_perform(self, curl):
 
315
    def _curl_perform(self, curl, header, more_headers=[]):
155
316
        """Perform curl operation and translate exceptions."""
156
317
        try:
 
318
            # There's no way in http/1.0 to say "must
 
319
            # revalidate"; we don't want to force it to always
 
320
            # retrieve.  so just turn off the default Pragma
 
321
            # provided by Curl.
 
322
            headers = ['Cache-control: max-age=0',
 
323
                       'Pragma: no-cache',
 
324
                       'Connection: Keep-Alive']
 
325
            curl.setopt(pycurl.HTTPHEADER, headers + more_headers)
157
326
            curl.perform()
158
327
        except pycurl.error, e:
159
 
            # XXX: There seem to be no symbolic constants for these values.
160
 
            if e[0] == 6:
161
 
                # couldn't resolve host
162
 
                raise NoSuchFile(curl.getinfo(pycurl.EFFECTIVE_URL), e)
163
 
 
164
 
 
165
 
class HttpServer_PyCurl(HttpServer):
166
 
    """Subclass of HttpServer that gives http+pycurl urls.
167
 
 
168
 
    This is for use in testing: connections to this server will always go
169
 
    through pycurl where possible.
170
 
    """
171
 
 
172
 
    # urls returned by this server should require the pycurl client impl
173
 
    _url_protocol = 'http+pycurl'
 
328
            url = curl.getinfo(pycurl.EFFECTIVE_URL)
 
329
            mutter('got pycurl error: %s, %s, %s, url: %s ',
 
330
                    e[0], e[1], e, url)
 
331
            if e[0] in (CURLE_SSL_CACERT_BADFILE,
 
332
                        CURLE_COULDNT_RESOLVE_HOST,
 
333
                        CURLE_COULDNT_CONNECT,
 
334
                        CURLE_GOT_NOTHING,
 
335
                        CURLE_COULDNT_RESOLVE_PROXY,):
 
336
                raise errors.ConnectionError(
 
337
                    'curl connection error (%s)\non %s' % (e[1], url))
 
338
            elif e[0] == CURLE_PARTIAL_FILE:
 
339
                # Pycurl itself has detected a short read.  We do not have all
 
340
                # the information for the ShortReadvError, but that should be
 
341
                # enough
 
342
                raise errors.ShortReadvError(url,
 
343
                                             offset='unknown', length='unknown',
 
344
                                             actual='unknown',
 
345
                                             extra='Server aborted the request')
 
346
            raise
 
347
        code = curl.getinfo(pycurl.HTTP_CODE)
 
348
        if code in (301, 302, 303, 307):
 
349
            url = curl.getinfo(pycurl.EFFECTIVE_URL)
 
350
            msg = self._parse_headers(header)
 
351
            redirected_to = msg.getheader('location')
 
352
            raise errors.RedirectRequested(url,
 
353
                                           redirected_to,
 
354
                                           is_permanent=(code == 301),
 
355
                                           qual_proto=self._scheme)
174
356
 
175
357
 
176
358
def get_test_permutations():
177
359
    """Return the permutations to be used in testing."""
 
360
    from bzrlib.tests.http_server import HttpServer_PyCurl
178
361
    return [(PyCurlTransport, HttpServer_PyCurl),
179
362
            ]