~toddy/bzr/bzr.i18n

« back to all changes in this revision

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

  • Committer: Tobias Toedter
  • Date: 2007-12-30 18:52:13 UTC
  • mfrom: (2438.1.708 +trunk)
  • Revision ID: t.toedter@gmx.net-20071230185213-7xiqpbtshmnsf073
Merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006 Canonical Ltd
 
1
# Copyright (C) 2006,2007 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
20
20
 
21
21
For instance, we create a new HTTPConnection and HTTPSConnection that inherit
22
22
from the original urllib2.HTTP(s)Connection objects, but also have a new base
23
 
which implements a custom getresponse and fake_close handlers.
 
23
which implements a custom getresponse and cleanup_pipe handlers.
24
24
 
25
25
And then we implement custom HTTPHandler and HTTPSHandler classes, that use
26
26
the custom HTTPConnection classes.
78
78
    # Some responses have bodies in which we have no interest
79
79
    _body_ignored_responses = [301,302, 303, 307, 401, 403, 404]
80
80
 
81
 
    def __init__(self, *args, **kwargs):
82
 
        httplib.HTTPResponse.__init__(self, *args, **kwargs)
83
 
 
84
81
    def begin(self):
85
82
        """Begin to read the response from the server.
86
83
 
123
120
            # below we keep the socket with the server opened.
124
121
            self.will_close = False
125
122
 
 
123
    # in finish() below, we may have to discard several MB in the worst
 
124
    # case. To avoid buffering that much, we read and discard by chunks
 
125
    # instead. The underlying file is either a socket or a StringIO, so reading
 
126
    # 8k chunks should be fine.
 
127
    _discarded_buf_size = 8192
 
128
 
 
129
    def finish(self):
 
130
        """Finish reading the body.
 
131
 
 
132
        In some cases, the client may have left some bytes to read in the
 
133
        body. That will block the next request to succeed if we use a
 
134
        persistent connection. If we don't use a persistent connection, well,
 
135
        nothing will block the next request since a new connection will be
 
136
        issued anyway.
 
137
 
 
138
        :return: the number of bytes left on the socket (may be None)
 
139
        """
 
140
        pending = None
 
141
        if not self.isclosed():
 
142
            # Make sure nothing was left to be read on the socket
 
143
            pending = 0
 
144
            while self.length and self.length > self._discarded_buf_size:
 
145
                data = self.read(self._discarded_buf_size)
 
146
                pending += len(data)
 
147
            if self.length:
 
148
                data = self.read(self.length)
 
149
                pending += len(data)
 
150
            if pending:
 
151
                trace.mutter(
 
152
                    "bogus http server didn't give body length,"
 
153
                    "%s bytes left on the socket",
 
154
                    pending)
 
155
            self.close()
 
156
        return pending
 
157
 
126
158
 
127
159
# Not inheriting from 'object' because httplib.HTTPConnection doesn't.
128
160
class AbstractHTTPConnection:
131
163
    response_class = Response
132
164
    strict = 1 # We don't support HTTP/0.9
133
165
 
134
 
    def fake_close(self):
135
 
        """Make the connection believes the response have been fully handled.
136
 
 
137
 
        That makes the httplib.HTTPConnection happy
138
 
        """
 
166
    # When we detect a server responding with the whole file to range requests,
 
167
    # we want to warn. But not below a given thresold.
 
168
    _range_warning_thresold = 1024 * 1024
 
169
 
 
170
    def __init__(self):
 
171
        self._response = None
 
172
        self._ranges_received_whole_file = None
 
173
 
 
174
    def _mutter_connect(self):
 
175
        netloc = '%s:%s' % (self.host, self.port)
 
176
        if self.proxied_host is not None:
 
177
            netloc += '(proxy for %s)' % self.proxied_host
 
178
        trace.mutter('* About to connect() to %s' % netloc)
 
179
 
 
180
    def getresponse(self):
 
181
        """Capture the response to be able to cleanup"""
 
182
        self._response = httplib.HTTPConnection.getresponse(self)
 
183
        return self._response
 
184
 
 
185
    def cleanup_pipe(self):
 
186
        """Make the connection believe the response has been fully processed."""
 
187
        if self._response is not None:
 
188
            pending = self._response.finish()
 
189
            # Warn the user (once)
 
190
            if (self._ranges_received_whole_file is None
 
191
                and self._response.status == 200
 
192
                and pending and pending > self._range_warning_thresold
 
193
                ):
 
194
                self._ranges_received_whole_file = True
 
195
                trace.warning(
 
196
                    'Got a 200 response when asking for multiple ranges,'
 
197
                    ' does your server at %s:%s support range requests?',
 
198
                    self.host, self.port)
 
199
            self._response = None
139
200
        # Preserve our preciousss
140
201
        sock = self.sock
141
202
        self.sock = None
149
210
 
150
211
    # XXX: Needs refactoring at the caller level.
151
212
    def __init__(self, host, port=None, strict=None, proxied_host=None):
152
 
        if 'http' in debug.debug_flags:
153
 
            netloc = host
154
 
            if port is not None:
155
 
                netloc += '%d' % port
156
 
            if proxied_host is not None:
157
 
                netloc += '(proxy for %s)' % proxied_host
158
 
            trace.mutter('* About to connect() to %s' % netloc)
 
213
        AbstractHTTPConnection.__init__(self)
159
214
        httplib.HTTPConnection.__init__(self, host, port, strict)
160
215
        self.proxied_host = proxied_host
161
216
 
 
217
    def connect(self):
 
218
        if 'http' in debug.debug_flags:
 
219
            self._mutter_connect()
 
220
        httplib.HTTPConnection.connect(self)
 
221
 
162
222
 
163
223
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
164
224
 
165
225
    def __init__(self, host, port=None, key_file=None, cert_file=None,
166
226
                 strict=None, proxied_host=None):
 
227
        AbstractHTTPConnection.__init__(self)
167
228
        httplib.HTTPSConnection.__init__(self, host, port,
168
229
                                         key_file, cert_file, strict)
169
230
        self.proxied_host = proxied_host
170
231
 
171
232
    def connect(self):
 
233
        if 'http' in debug.debug_flags:
 
234
            self._mutter_connect()
172
235
        httplib.HTTPConnection.connect(self)
173
236
        if self.proxied_host is None:
174
237
            self.connect_to_origin()
385
448
                    print '  Will retry, %s %r' % (method, url)
386
449
                request.connection.close()
387
450
                response = self.do_open(http_class, request, False)
388
 
                convert_to_addinfourl = False
389
451
            else:
390
452
                if self._debuglevel > 0:
391
453
                    print 'Received second exception: [%r]' % exc_val
421
483
                    print '  Failed again, %s %r' % (method, url)
422
484
                    print '  Will raise: [%r]' % my_exception
423
485
                raise my_exception, None, exc_tb
424
 
        return response, convert_to_addinfourl
 
486
        return response
425
487
 
426
488
    def do_open(self, http_class, request, first_try=True):
427
489
        """See urllib2.AbstractHTTPHandler.do_open for the general idea.
455
517
            convert_to_addinfourl = True
456
518
        except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
457
519
                socket.error, httplib.HTTPException):
458
 
            response, convert_to_addinfourl = self.retry_or_raise(http_class,
459
 
                                                                  request,
460
 
                                                                  first_try)
 
520
            response = self.retry_or_raise(http_class, request, first_try)
 
521
            convert_to_addinfourl = False
461
522
 
462
523
# FIXME: HTTPConnection does not fully support 100-continue (the
463
524
# server responses are just ignored)
471
532
#            connection.send(body)
472
533
#            response = connection.getresponse()
473
534
 
474
 
        if 'http' in debug.debug_flags:
475
 
            version = 'HTTP/%d.%d'
476
 
            try:
477
 
                version = version % (response.version / 10,
478
 
                                     response.version % 10)
479
 
            except:
480
 
                version = 'HTTP/%r' % version
481
 
            trace.mutter('< %s %s %s' % (version, response.status,
482
 
                                            response.reason))
483
 
            hdrs = [h.rstrip('\r\n') for h in response.msg.headers]
484
 
            trace.mutter('< ' + '\n< '.join(hdrs) + '\n')
485
535
        if self._debuglevel > 0:
486
536
            print 'Receives response: %r' % response
487
537
            print '  For: %r(%r)' % (request.get_method(),
496
546
            resp = urllib2.addinfourl(fp, r.msg, req.get_full_url())
497
547
            resp.code = r.status
498
548
            resp.msg = r.reason
 
549
            resp.version = r.version
499
550
            if self._debuglevel > 0:
500
551
                print 'Create addinfourl: %r' % resp
501
552
                print '  For: %r(%r)' % (request.get_method(),
502
553
                                         request.get_full_url())
 
554
            if 'http' in debug.debug_flags:
 
555
                version = 'HTTP/%d.%d'
 
556
                try:
 
557
                    version = version % (resp.version / 10,
 
558
                                         resp.version % 10)
 
559
                except:
 
560
                    version = 'HTTP/%r' % resp.version
 
561
                trace.mutter('< %s %s %s' % (version, resp.code,
 
562
                                             resp.msg))
 
563
                # Use the raw header lines instead of treating resp.info() as a
 
564
                # dict since we may miss duplicated headers otherwise.
 
565
                hdrs = [h.rstrip('\r\n') for h in resp.info().headers]
 
566
                trace.mutter('< ' + '\n< '.join(hdrs) + '\n')
503
567
        else:
504
568
            resp = response
505
569
        return resp
506
570
 
507
 
#       # we need titled headers in a dict but
508
 
#       # response.getheaders returns a list of (lower(header).
509
 
#       # Let's title that because most of bzr handle titled
510
 
#       # headers, but maybe we should switch to lowercased
511
 
#       # headers...
512
 
#        # jam 20060908: I think we actually expect the headers to
513
 
#        #       be similar to mimetools.Message object, which uses
514
 
#        #       case insensitive keys. It lowers() all requests.
515
 
#        #       My concern is that the code may not do perfect title case.
516
 
#        #       For example, it may use Content-type rather than Content-Type
517
 
#
518
 
#        # When we get rid of addinfourl, we must ensure that bzr
519
 
#        # always use titled headers and that any header received
520
 
#        # from server is also titled.
521
 
#
522
 
#        headers = {}
523
 
#        for header, value in (response.getheaders()):
524
 
#            headers[header.title()] = value
525
 
#        # FIXME: Implements a secured .read method
526
 
#        response.code = response.status
527
 
#        response.headers = headers
528
 
#        return response
529
 
 
530
571
 
531
572
class HTTPHandler(AbstractHTTPHandler):
532
573
    """A custom handler that just thunks into HTTPConnection"""
566
607
                raise ConnectionError("Can't connect to %s via proxy %s" % (
567
608
                        connect.proxied_host, self.host))
568
609
            # Housekeeping
569
 
            connection.fake_close()
 
610
            connection.cleanup_pipe()
570
611
            # Establish the connection encryption 
571
612
            connection.connect_to_origin()
572
613
            # Propagate the connection to the original request
637
678
    def http_error_302(self, req, fp, code, msg, headers):
638
679
        """Requests the redirected to URI.
639
680
 
640
 
        Copied from urllib2 to be able to fake_close the
641
 
        associated connection, *before* issuing the redirected
642
 
        request but *after* having eventually raised an error.
 
681
        Copied from urllib2 to be able to clean the pipe of the associated
 
682
        connection, *before* issuing the redirected request but *after* having
 
683
        eventually raised an error.
643
684
        """
644
685
        # Some servers (incorrectly) return multiple Location headers
645
686
        # (so probably same goes for URI).  Use first header.
684
725
        # use it with HTTPError.
685
726
        fp.close()
686
727
        # We have all we need already in the response
687
 
        req.connection.fake_close()
 
728
        req.connection.cleanup_pipe()
688
729
 
689
730
        return self.parent.open(redirected_req)
690
731
 
995
1036
        the prompt, so we build the prompt from the authentication dict which
996
1037
        contains all the needed parts.
997
1038
 
998
 
        Also, hhtp and proxy AuthHandlers present different prompts to the
999
 
        user. The daughter classes hosuld implements a public
 
1039
        Also, http and proxy AuthHandlers present different prompts to the
 
1040
        user. The daughter classes should implements a public
1000
1041
        build_password_prompt using this method.
1001
1042
        """
1002
1043
        prompt = '%s' % auth['protocol'].upper() + ' %(user)s@%(host)s'
1279
1320
    def http_error_default(self, req, fp, code, msg, hdrs):
1280
1321
        if code == 403:
1281
1322
            raise errors.TransportError('Server refuses to fullfil the request')
1282
 
        elif code == 416:
1283
 
            # We don't know which, but one of the ranges we
1284
 
            # specified was wrong. So we raise with 0 for a lack
1285
 
            # of a better magic value.
1286
 
            raise errors.InvalidRange(req.get_full_url(),0)
1287
1323
        else:
1288
1324
            raise errors.InvalidHttpResponse(req.get_full_url(),
1289
1325
                                             'Unable to handle http code %d: %s'