~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/_pycurl.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Canonical Ltd
 
1
# Copyright (C) 2006 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
"""http/https transport using pycurl"""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
19
# TODO: test reporting of http errors
22
20
#
23
21
# TODO: Transport option to control caching of particular requests; broadly we
33
31
# from _curl_perform.  Not done because we may deprecate pycurl in the
34
32
# future -- vila 20070212
35
33
 
 
34
import os
36
35
from cStringIO import StringIO
37
36
import httplib
 
37
import sys
38
38
 
39
 
import bzrlib
40
39
from bzrlib import (
41
40
    debug,
42
41
    errors,
43
42
    trace,
 
43
    __version__ as bzrlib_version,
44
44
    )
 
45
import bzrlib
 
46
from bzrlib.trace import mutter
45
47
from bzrlib.transport.http import (
46
48
    ca_bundle,
47
49
    HttpTransportBase,
48
50
    response,
49
 
    unhtml_roughly,
50
51
    )
51
52
 
52
53
try:
53
54
    import pycurl
54
55
except ImportError, e:
55
 
    trace.mutter("failed to import pycurl: %s", e)
 
56
    mutter("failed to import pycurl: %s", e)
56
57
    raise errors.DependencyNotPresent('pycurl', e)
57
58
 
58
59
try:
59
60
    # see if we can actually initialize PyCurl - sometimes it will load but
60
61
    # fail to start up due to this bug:
61
 
    #
 
62
    #  
62
63
    #   32. (At least on Windows) If libcurl is built with c-ares and there's
63
64
    #   no DNS server configured in the system, the ares_init() call fails and
64
65
    #   thus curl_easy_init() fails as well. This causes weird effects for
67
68
    # reported by Alexander Belchenko, 2006-04-26
68
69
    pycurl.Curl()
69
70
except pycurl.error, e:
70
 
    trace.mutter("failed to initialize pycurl: %s", e)
 
71
    mutter("failed to initialize pycurl: %s", e)
71
72
    raise errors.DependencyNotPresent('pycurl', e)
72
73
 
73
74
 
85
86
    """
86
87
    return pycurl.__dict__.get(symbol, default)
87
88
 
 
89
CURLE_SSL_CACERT_BADFILE = _get_pycurl_errcode('E_SSL_CACERT_BADFILE', 77)
88
90
CURLE_COULDNT_CONNECT = _get_pycurl_errcode('E_COULDNT_CONNECT', 7)
89
91
CURLE_COULDNT_RESOLVE_HOST = _get_pycurl_errcode('E_COULDNT_RESOLVE_HOST', 6)
90
92
CURLE_COULDNT_RESOLVE_PROXY = _get_pycurl_errcode('E_COULDNT_RESOLVE_PROXY', 5)
91
93
CURLE_GOT_NOTHING = _get_pycurl_errcode('E_GOT_NOTHING', 52)
92
94
CURLE_PARTIAL_FILE = _get_pycurl_errcode('E_PARTIAL_FILE', 18)
93
95
CURLE_SEND_ERROR = _get_pycurl_errcode('E_SEND_ERROR', 55)
94
 
CURLE_RECV_ERROR = _get_pycurl_errcode('E_RECV_ERROR', 56)
95
 
CURLE_SSL_CACERT = _get_pycurl_errcode('E_SSL_CACERT', 60)
96
 
CURLE_SSL_CACERT_BADFILE = _get_pycurl_errcode('E_SSL_CACERT_BADFILE', 77)
97
96
 
98
97
 
99
98
class PyCurlTransport(HttpTransportBase):
106
105
    """
107
106
 
108
107
    def __init__(self, base, _from_transport=None):
109
 
        super(PyCurlTransport, self).__init__(base, 'pycurl',
 
108
        super(PyCurlTransport, self).__init__(base,
110
109
                                              _from_transport=_from_transport)
111
 
        if self._unqualified_scheme == 'https':
 
110
        if base.startswith('https'):
112
111
            # Check availability of https into pycurl supported
113
112
            # protocols
114
113
            supported = pycurl.version_info()[8]
131
130
            self._set_connection(connection, auth)
132
131
        return connection
133
132
 
134
 
    def disconnect(self):
135
 
        connection = self._get_connection()
136
 
        if connection is not None:
137
 
            connection.close()
138
 
 
139
133
    def has(self, relpath):
140
134
        """See Transport.has()"""
141
135
        # We set NO BODY=0 in _get_full, so it should be safe
185
179
 
186
180
        :param curl: The curl object to place the request on
187
181
        :param relpath: The relative path that we want to get
188
 
        :return: (abspath, data, header)
 
182
        :return: (abspath, data, header) 
189
183
                 abspath: full url
190
184
                 data: file that will be filled with the body
191
185
                 header: file that will be filled with the headers
220
214
 
221
215
    # The parent class use 0 to minimize the requests, but since we can't
222
216
    # exploit the results as soon as they are received (pycurl limitation) we'd
223
 
    # better issue more requests and provide a more responsive UI incurring
224
 
    # more latency costs.
 
217
    # better issue more requests and provide a more responsive UI do the cost
 
218
    # of more latency costs.
225
219
    # If you modify this, think about modifying the comment in http/__init__.py
226
220
    # too.
227
221
    _get_max_size = 4 * 1024 * 1024
270
264
        # We override the Expect: header so that pycurl will send the POST
271
265
        # body immediately.
272
266
        try:
273
 
            self._curl_perform(curl, header,
274
 
                               ['Expect: ',
275
 
                                'Content-Type: application/octet-stream'])
 
267
            self._curl_perform(curl, header, ['Expect: '])
276
268
        except pycurl.error, e:
277
269
            if e[0] == CURLE_SEND_ERROR:
278
270
                # When talking to an HTTP/1.0 server, getting a 400+ error code
284
276
                # error code and the headers are known to be available, we just
285
277
                # swallow the exception, leaving the upper levels handle the
286
278
                # 400+ error.
287
 
                trace.mutter('got pycurl error in POST: %s, %s, %s, url: %s ',
288
 
                             e[0], e[1], e, abspath)
 
279
                mutter('got pycurl error in POST: %s, %s, %s, url: %s ',
 
280
                       e[0], e[1], e, abspath)
289
281
            else:
290
282
                # Re-raise otherwise
291
283
                raise
294
286
        msg = self._parse_headers(header)
295
287
        return code, response.handle_response(abspath, code, msg, data)
296
288
 
297
 
 
298
 
    def _raise_curl_http_error(self, curl, info=None, body=None):
299
 
        """Common curl->bzrlib error translation.
300
 
 
301
 
        Some methods may choose to override this for particular cases.
302
 
 
303
 
        The URL and code are automatically included as appropriate.
304
 
 
305
 
        :param info: Extra information to include in the message.
306
 
 
307
 
        :param body: File-like object from which the body of the page can be
308
 
            read.
309
 
        """
 
289
    def _raise_curl_http_error(self, curl, info=None):
310
290
        code = curl.getinfo(pycurl.HTTP_CODE)
311
291
        url = curl.getinfo(pycurl.EFFECTIVE_URL)
312
 
        if body is not None:
313
 
            response_body = body.read()
314
 
            plaintext_body = unhtml_roughly(response_body)
315
 
        else:
316
 
            response_body = None
317
 
            plaintext_body = ''
 
292
        # Some error codes can be handled the same way for all
 
293
        # requests
318
294
        if code == 403:
319
295
            raise errors.TransportError(
320
296
                'Server refuses to fulfill the request (403 Forbidden)'
321
 
                ' for %s: %s' % (url, plaintext_body))
 
297
                ' for %s' % url)
322
298
        else:
323
299
            if info is None:
324
300
                msg = ''
325
301
            else:
326
302
                msg = ': ' + info
327
303
            raise errors.InvalidHttpResponse(
328
 
                url, 'Unable to handle http code %d%s: %s'
329
 
                % (code, msg, plaintext_body))
330
 
 
331
 
    def _debug_cb(self, kind, text):
332
 
        if kind in (pycurl.INFOTYPE_HEADER_IN, pycurl.INFOTYPE_DATA_IN):
333
 
            self._report_activity(len(text), 'read')
334
 
            if (kind == pycurl.INFOTYPE_HEADER_IN
335
 
                and 'http' in debug.debug_flags):
336
 
                trace.mutter('< %s' % (text.rstrip(),))
337
 
        elif kind in (pycurl.INFOTYPE_HEADER_OUT, pycurl.INFOTYPE_DATA_OUT):
338
 
            self._report_activity(len(text), 'write')
339
 
            if (kind == pycurl.INFOTYPE_HEADER_OUT
340
 
                and 'http' in debug.debug_flags):
341
 
                lines = []
342
 
                for line in text.rstrip().splitlines():
343
 
                    # People are often told to paste -Dhttp output to help
344
 
                    # debug. Don't compromise credentials.
345
 
                    try:
346
 
                        header, details = line.split(':', 1)
347
 
                    except ValueError:
348
 
                        header = None
349
 
                    if header in ('Authorization', 'Proxy-Authorization'):
350
 
                        line = '%s: <masked>' % (header,)
351
 
                    lines.append(line)
352
 
                trace.mutter('> ' + '\n> '.join(lines))
353
 
        elif kind == pycurl.INFOTYPE_TEXT and 'http' in debug.debug_flags:
354
 
            trace.mutter('* %s' % text.rstrip())
355
 
        elif (kind in (pycurl.INFOTYPE_TEXT, pycurl.INFOTYPE_SSL_DATA_IN,
356
 
                       pycurl.INFOTYPE_SSL_DATA_OUT)
357
 
              and 'http' in debug.debug_flags):
358
 
            trace.mutter('* %s' % text)
 
304
                url, 'Unable to handle http code %d%s' % (code,msg))
359
305
 
360
306
    def _set_curl_options(self, curl):
361
307
        """Set options for all requests"""
 
308
        if 'http' in debug.debug_flags:
 
309
            curl.setopt(pycurl.VERBOSE, 1)
 
310
            # pycurl doesn't implement the CURLOPT_STDERR option, so we can't
 
311
            # do : curl.setopt(pycurl.STDERR, trace._trace_file)
 
312
 
362
313
        ua_str = 'bzr/%s (pycurl: %s)' % (bzrlib.__version__, pycurl.version)
363
314
        curl.setopt(pycurl.USERAGENT, ua_str)
364
 
        curl.setopt(pycurl.VERBOSE, 1)
365
 
        curl.setopt(pycurl.DEBUGFUNCTION, self._debug_cb)
366
315
        if self.cabundle:
367
316
            curl.setopt(pycurl.CAINFO, self.cabundle)
368
317
        # Set accepted auth methods
392
341
            curl.perform()
393
342
        except pycurl.error, e:
394
343
            url = curl.getinfo(pycurl.EFFECTIVE_URL)
395
 
            trace.mutter('got pycurl error: %s, %s, %s, url: %s ',
396
 
                         e[0], e[1], e, url)
397
 
            if e[0] in (CURLE_COULDNT_RESOLVE_HOST,
398
 
                        CURLE_COULDNT_RESOLVE_PROXY,
 
344
            mutter('got pycurl error: %s, %s, %s, url: %s ',
 
345
                    e[0], e[1], e, url)
 
346
            if e[0] in (CURLE_SSL_CACERT_BADFILE,
 
347
                        CURLE_COULDNT_RESOLVE_HOST,
399
348
                        CURLE_COULDNT_CONNECT,
400
349
                        CURLE_GOT_NOTHING,
401
 
                        CURLE_SSL_CACERT,
402
 
                        CURLE_SSL_CACERT_BADFILE,
403
 
                        ):
 
350
                        CURLE_COULDNT_RESOLVE_PROXY,):
404
351
                raise errors.ConnectionError(
405
352
                    'curl connection error (%s)\non %s' % (e[1], url))
406
 
            elif e[0] == CURLE_RECV_ERROR:
407
 
                raise errors.ConnectionReset(
408
 
                    'curl connection error (%s)\non %s' % (e[1], url))
409
353
            elif e[0] == CURLE_PARTIAL_FILE:
410
354
                # Pycurl itself has detected a short read.  We do not have all
411
355
                # the information for the ShortReadvError, but that should be
422
366
            redirected_to = msg.getheader('location')
423
367
            raise errors.RedirectRequested(url,
424
368
                                           redirected_to,
425
 
                                           is_permanent=(code == 301))
 
369
                                           is_permanent=(code == 301),
 
370
                                           qual_proto=self._scheme)
426
371
 
427
372
 
428
373
def get_test_permutations():
429
374
    """Return the permutations to be used in testing."""
430
 
    from bzrlib.tests import features
431
 
    from bzrlib.tests import http_server
432
 
    permutations = [(PyCurlTransport, http_server.HttpServer_PyCurl),]
433
 
    if features.HTTPSServerFeature.available():
434
 
        from bzrlib.tests import (
435
 
            https_server,
436
 
            ssl_certs,
437
 
            )
438
 
 
439
 
        class HTTPS_pycurl_transport(PyCurlTransport):
440
 
 
441
 
            def __init__(self, base, _from_transport=None):
442
 
                super(HTTPS_pycurl_transport, self).__init__(base,
443
 
                                                             _from_transport)
444
 
                self.cabundle = str(ssl_certs.build_path('ca.crt'))
445
 
 
446
 
        permutations.append((HTTPS_pycurl_transport,
447
 
                             https_server.HTTPSServer_PyCurl))
448
 
    return permutations
 
375
    from bzrlib.tests.http_server import HttpServer_PyCurl
 
376
    return [(PyCurlTransport, HttpServer_PyCurl),
 
377
            ]