1
# Copyright (C) 2006 Canonical Ltd
1
# Copyright (C) 2006,2007 Canonical Ltd
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
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.
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]
81
def __init__(self, *args, **kwargs):
82
httplib.HTTPResponse.__init__(self, *args, **kwargs)
85
82
"""Begin to read the response from the server.
123
120
# below we keep the socket with the server opened.
124
121
self.will_close = False
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
130
"""Finish reading the body.
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
138
:return: the number of bytes left on the socket (may be None)
141
if not self.isclosed():
142
# Make sure nothing was left to be read on the socket
144
while self.length and self.length > self._discarded_buf_size:
145
data = self.read(self._discarded_buf_size)
148
data = self.read(self.length)
152
"bogus http server didn't give body length,"
153
"%s bytes left on the socket",
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
134
def fake_close(self):
135
"""Make the connection believes the response have been fully handled.
137
That makes the httplib.HTTPConnection happy
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
171
self._response = None
172
self._ranges_received_whole_file = None
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)
180
def getresponse(self):
181
"""Capture the response to be able to cleanup"""
182
self._response = httplib.HTTPConnection.getresponse(self)
183
return self._response
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
194
self._ranges_received_whole_file = True
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
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:
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
218
if 'http' in debug.debug_flags:
219
self._mutter_connect()
220
httplib.HTTPConnection.connect(self)
163
223
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
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
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
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
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,
520
response = self.retry_or_raise(http_class, request, first_try)
521
convert_to_addinfourl = False
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()
474
if 'http' in debug.debug_flags:
475
version = 'HTTP/%d.%d'
477
version = version % (response.version / 10,
478
response.version % 10)
480
version = 'HTTP/%r' % version
481
trace.mutter('< %s %s %s' % (version, response.status,
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'
557
version = version % (resp.version / 10,
560
version = 'HTTP/%r' % resp.version
561
trace.mutter('< %s %s %s' % (version, resp.code,
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')
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
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
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.
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
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))
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.
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.
644
685
# Some servers (incorrectly) return multiple Location headers
645
686
# (so probably same goes for URI). Use first header.
995
1036
the prompt, so we build the prompt from the authentication dict which
996
1037
contains all the needed parts.
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.
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')
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)
1288
1324
raise errors.InvalidHttpResponse(req.get_full_url(),
1289
1325
'Unable to handle http code %d: %s'