Show More
@@ -0,0 +1,195 b'' | |||
|
1 | # Copyright 2011, Google Inc. | |
|
2 | # All rights reserved. | |
|
3 | # | |
|
4 | # Redistribution and use in source and binary forms, with or without | |
|
5 | # modification, are permitted provided that the following conditions are | |
|
6 | # met: | |
|
7 | # | |
|
8 | # * Redistributions of source code must retain the above copyright | |
|
9 | # notice, this list of conditions and the following disclaimer. | |
|
10 | # * Redistributions in binary form must reproduce the above | |
|
11 | # copyright notice, this list of conditions and the following disclaimer | |
|
12 | # in the documentation and/or other materials provided with the | |
|
13 | # distribution. | |
|
14 | # * Neither the name of Google Inc. nor the names of its | |
|
15 | # contributors may be used to endorse or promote products derived from | |
|
16 | # this software without specific prior written permission. | |
|
17 | ||
|
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
|
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
|
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
|
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
|
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
|
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
|
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
|
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
|
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
|
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
|
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
|
29 | """Reader objects to abstract out different body response types. | |
|
30 | ||
|
31 | This module is package-private. It is not expected that these will | |
|
32 | have any clients outside of httpplus. | |
|
33 | """ | |
|
34 | ||
|
35 | import httplib | |
|
36 | import itertools | |
|
37 | import logging | |
|
38 | ||
|
39 | logger = logging.getLogger(__name__) | |
|
40 | ||
|
41 | ||
|
42 | class ReadNotReady(Exception): | |
|
43 | """Raised when read() is attempted but not enough data is loaded.""" | |
|
44 | ||
|
45 | ||
|
46 | class HTTPRemoteClosedError(httplib.HTTPException): | |
|
47 | """The server closed the remote socket in the middle of a response.""" | |
|
48 | ||
|
49 | ||
|
50 | class AbstractReader(object): | |
|
51 | """Abstract base class for response readers. | |
|
52 | ||
|
53 | Subclasses must implement _load, and should implement _close if | |
|
54 | it's not an error for the server to close their socket without | |
|
55 | some termination condition being detected during _load. | |
|
56 | """ | |
|
57 | def __init__(self): | |
|
58 | self._finished = False | |
|
59 | self._done_chunks = [] | |
|
60 | ||
|
61 | @property | |
|
62 | def available_data(self): | |
|
63 | return sum(map(len, self._done_chunks)) | |
|
64 | ||
|
65 | def done(self): | |
|
66 | return self._finished | |
|
67 | ||
|
68 | def read(self, amt): | |
|
69 | if self.available_data < amt and not self._finished: | |
|
70 | raise ReadNotReady() | |
|
71 | need = [amt] | |
|
72 | def pred(s): | |
|
73 | needed = need[0] > 0 | |
|
74 | need[0] -= len(s) | |
|
75 | return needed | |
|
76 | blocks = list(itertools.takewhile(pred, self._done_chunks)) | |
|
77 | self._done_chunks = self._done_chunks[len(blocks):] | |
|
78 | over_read = sum(map(len, blocks)) - amt | |
|
79 | if over_read > 0 and blocks: | |
|
80 | logger.debug('need to reinsert %d data into done chunks', over_read) | |
|
81 | last = blocks[-1] | |
|
82 | blocks[-1], reinsert = last[:-over_read], last[-over_read:] | |
|
83 | self._done_chunks.insert(0, reinsert) | |
|
84 | result = ''.join(blocks) | |
|
85 | assert len(result) == amt or (self._finished and len(result) < amt) | |
|
86 | return result | |
|
87 | ||
|
88 | def _load(self, data): # pragma: no cover | |
|
89 | """Subclasses must implement this. | |
|
90 | ||
|
91 | As data is available to be read out of this object, it should | |
|
92 | be placed into the _done_chunks list. Subclasses should not | |
|
93 | rely on data remaining in _done_chunks forever, as it may be | |
|
94 | reaped if the client is parsing data as it comes in. | |
|
95 | """ | |
|
96 | raise NotImplementedError | |
|
97 | ||
|
98 | def _close(self): | |
|
99 | """Default implementation of close. | |
|
100 | ||
|
101 | The default implementation assumes that the reader will mark | |
|
102 | the response as finished on the _finished attribute once the | |
|
103 | entire response body has been read. In the event that this is | |
|
104 | not true, the subclass should override the implementation of | |
|
105 | close (for example, close-is-end responses have to set | |
|
106 | self._finished in the close handler.) | |
|
107 | """ | |
|
108 | if not self._finished: | |
|
109 | raise HTTPRemoteClosedError( | |
|
110 | 'server appears to have closed the socket mid-response') | |
|
111 | ||
|
112 | ||
|
113 | class AbstractSimpleReader(AbstractReader): | |
|
114 | """Abstract base class for simple readers that require no response decoding. | |
|
115 | ||
|
116 | Examples of such responses are Connection: Close (close-is-end) | |
|
117 | and responses that specify a content length. | |
|
118 | """ | |
|
119 | def _load(self, data): | |
|
120 | if data: | |
|
121 | assert not self._finished, ( | |
|
122 | 'tried to add data (%r) to a closed reader!' % data) | |
|
123 | logger.debug('%s read an addtional %d data', self.name, len(data)) | |
|
124 | self._done_chunks.append(data) | |
|
125 | ||
|
126 | ||
|
127 | class CloseIsEndReader(AbstractSimpleReader): | |
|
128 | """Reader for responses that specify Connection: Close for length.""" | |
|
129 | name = 'close-is-end' | |
|
130 | ||
|
131 | def _close(self): | |
|
132 | logger.info('Marking close-is-end reader as closed.') | |
|
133 | self._finished = True | |
|
134 | ||
|
135 | ||
|
136 | class ContentLengthReader(AbstractSimpleReader): | |
|
137 | """Reader for responses that specify an exact content length.""" | |
|
138 | name = 'content-length' | |
|
139 | ||
|
140 | def __init__(self, amount): | |
|
141 | AbstractReader.__init__(self) | |
|
142 | self._amount = amount | |
|
143 | if amount == 0: | |
|
144 | self._finished = True | |
|
145 | self._amount_seen = 0 | |
|
146 | ||
|
147 | def _load(self, data): | |
|
148 | AbstractSimpleReader._load(self, data) | |
|
149 | self._amount_seen += len(data) | |
|
150 | if self._amount_seen >= self._amount: | |
|
151 | self._finished = True | |
|
152 | logger.debug('content-length read complete') | |
|
153 | ||
|
154 | ||
|
155 | class ChunkedReader(AbstractReader): | |
|
156 | """Reader for chunked transfer encoding responses.""" | |
|
157 | def __init__(self, eol): | |
|
158 | AbstractReader.__init__(self) | |
|
159 | self._eol = eol | |
|
160 | self._leftover_skip_amt = 0 | |
|
161 | self._leftover_data = '' | |
|
162 | ||
|
163 | def _load(self, data): | |
|
164 | assert not self._finished, 'tried to add data to a closed reader!' | |
|
165 | logger.debug('chunked read an addtional %d data', len(data)) | |
|
166 | position = 0 | |
|
167 | if self._leftover_data: | |
|
168 | logger.debug('chunked reader trying to finish block from leftover data') | |
|
169 | # TODO: avoid this string concatenation if possible | |
|
170 | data = self._leftover_data + data | |
|
171 | position = self._leftover_skip_amt | |
|
172 | self._leftover_data = '' | |
|
173 | self._leftover_skip_amt = 0 | |
|
174 | datalen = len(data) | |
|
175 | while position < datalen: | |
|
176 | split = data.find(self._eol, position) | |
|
177 | if split == -1: | |
|
178 | self._leftover_data = data | |
|
179 | self._leftover_skip_amt = position | |
|
180 | return | |
|
181 | amt = int(data[position:split], base=16) | |
|
182 | block_start = split + len(self._eol) | |
|
183 | # If the whole data chunk plus the eol trailer hasn't | |
|
184 | # loaded, we'll wait for the next load. | |
|
185 | if block_start + amt + len(self._eol) > len(data): | |
|
186 | self._leftover_data = data | |
|
187 | self._leftover_skip_amt = position | |
|
188 | return | |
|
189 | if amt == 0: | |
|
190 | self._finished = True | |
|
191 | logger.debug('closing chunked redaer due to chunk of length 0') | |
|
192 | return | |
|
193 | self._done_chunks.append(data[block_start:block_start + amt]) | |
|
194 | position = block_start + amt + len(self._eol) | |
|
195 | # no-check-code |
@@ -0,0 +1,70 b'' | |||
|
1 | # Copyright 2010, Google Inc. | |
|
2 | # All rights reserved. | |
|
3 | # | |
|
4 | # Redistribution and use in source and binary forms, with or without | |
|
5 | # modification, are permitted provided that the following conditions are | |
|
6 | # met: | |
|
7 | # | |
|
8 | # * Redistributions of source code must retain the above copyright | |
|
9 | # notice, this list of conditions and the following disclaimer. | |
|
10 | # * Redistributions in binary form must reproduce the above | |
|
11 | # copyright notice, this list of conditions and the following disclaimer | |
|
12 | # in the documentation and/or other materials provided with the | |
|
13 | # distribution. | |
|
14 | # * Neither the name of Google Inc. nor the names of its | |
|
15 | # contributors may be used to endorse or promote products derived from | |
|
16 | # this software without specific prior written permission. | |
|
17 | ||
|
18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
|
19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
|
20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
|
21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
|
22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
|
23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
|
24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
|
25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
|
26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
|
27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
|
28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
|
29 | ||
|
30 | import unittest | |
|
31 | ||
|
32 | from httpplus import _readers | |
|
33 | ||
|
34 | def chunkedblock(x, eol='\r\n'): | |
|
35 | r"""Make a chunked transfer-encoding block. | |
|
36 | ||
|
37 | >>> chunkedblock('hi') | |
|
38 | '2\r\nhi\r\n' | |
|
39 | >>> chunkedblock('hi' * 10) | |
|
40 | '14\r\nhihihihihihihihihihi\r\n' | |
|
41 | >>> chunkedblock('hi', eol='\n') | |
|
42 | '2\nhi\n' | |
|
43 | """ | |
|
44 | return ''.join((hex(len(x))[2:], eol, x, eol)) | |
|
45 | ||
|
46 | corpus = 'foo\r\nbar\r\nbaz\r\n' | |
|
47 | ||
|
48 | ||
|
49 | class ChunkedReaderTest(unittest.TestCase): | |
|
50 | def test_many_block_boundaries(self): | |
|
51 | for step in xrange(1, len(corpus)): | |
|
52 | data = ''.join(chunkedblock(corpus[start:start+step]) for | |
|
53 | start in xrange(0, len(corpus), step)) | |
|
54 | for istep in xrange(1, len(data)): | |
|
55 | rdr = _readers.ChunkedReader('\r\n') | |
|
56 | print 'step', step, 'load', istep | |
|
57 | for start in xrange(0, len(data), istep): | |
|
58 | rdr._load(data[start:start+istep]) | |
|
59 | rdr._load(chunkedblock('')) | |
|
60 | self.assertEqual(corpus, rdr.read(len(corpus) + 1)) | |
|
61 | ||
|
62 | def test_small_chunk_blocks_large_wire_blocks(self): | |
|
63 | data = ''.join(map(chunkedblock, corpus)) + chunkedblock('') | |
|
64 | rdr = _readers.ChunkedReader('\r\n') | |
|
65 | for start in xrange(0, len(data), 4): | |
|
66 | d = data[start:start + 4] | |
|
67 | if d: | |
|
68 | rdr._load(d) | |
|
69 | self.assertEqual(corpus, rdr.read(len(corpus)+100)) | |
|
70 | # no-check-code |
@@ -45,6 +45,7 b' import rfc822' | |||
|
45 | 45 | import select |
|
46 | 46 | import socket |
|
47 | 47 | |
|
48 | import _readers | |
|
48 | 49 | import socketutil |
|
49 | 50 | |
|
50 | 51 | logger = logging.getLogger(__name__) |
@@ -54,8 +55,6 b' logger = logging.getLogger(__name__)' | |||
|
54 | 55 | HTTP_VER_1_0 = 'HTTP/1.0' |
|
55 | 56 | HTTP_VER_1_1 = 'HTTP/1.1' |
|
56 | 57 | |
|
57 | _LEN_CLOSE_IS_END = -1 | |
|
58 | ||
|
59 | 58 | OUTGOING_BUFFER_SIZE = 1 << 15 |
|
60 | 59 | INCOMING_BUFFER_SIZE = 1 << 20 |
|
61 | 60 | |
@@ -83,23 +82,19 b' class HTTPResponse(object):' | |||
|
83 | 82 | The response will continue to load as available. If you need the |
|
84 | 83 | complete response before continuing, check the .complete() method. |
|
85 | 84 | """ |
|
86 | def __init__(self, sock, timeout): | |
|
85 | def __init__(self, sock, timeout, method): | |
|
87 | 86 | self.sock = sock |
|
87 | self.method = method | |
|
88 | 88 | self.raw_response = '' |
|
89 | self._body = None | |
|
90 | 89 | self._headers_len = 0 |
|
91 | self._content_len = 0 | |
|
92 | 90 | self.headers = None |
|
93 | 91 | self.will_close = False |
|
94 | 92 | self.status_line = '' |
|
95 | 93 | self.status = None |
|
94 | self.continued = False | |
|
96 | 95 | self.http_version = None |
|
97 | 96 | self.reason = None |
|
98 |
self._ |
|
|
99 | self._chunked_done = False | |
|
100 | self._chunked_until_next = 0 | |
|
101 | self._chunked_skip_bytes = 0 | |
|
102 | self._chunked_preloaded_block = None | |
|
97 | self._reader = None | |
|
103 | 98 | |
|
104 | 99 | self._read_location = 0 |
|
105 | 100 | self._eol = EOL |
@@ -117,11 +112,12 b' class HTTPResponse(object):' | |||
|
117 | 112 | socket is closed, this will nearly always return False, even |
|
118 | 113 | in cases where all the data has actually been loaded. |
|
119 | 114 | """ |
|
120 |
if self._ |
|
|
121 |
return self._ |
|
|
122 | if self._content_len == _LEN_CLOSE_IS_END: | |
|
123 | return False | |
|
124 | return self._body is not None and len(self._body) >= self._content_len | |
|
115 | if self._reader: | |
|
116 | return self._reader.done() | |
|
117 | ||
|
118 | def _close(self): | |
|
119 | if self._reader is not None: | |
|
120 | self._reader._close() | |
|
125 | 121 | |
|
126 | 122 | def readline(self): |
|
127 | 123 | """Read a single line from the response body. |
@@ -129,30 +125,34 b' class HTTPResponse(object):' | |||
|
129 | 125 | This may block until either a line ending is found or the |
|
130 | 126 | response is complete. |
|
131 | 127 | """ |
|
132 | eol = self._body.find('\n', self._read_location) | |
|
133 | while eol == -1 and not self.complete(): | |
|
128 | # TODO: move this into the reader interface where it can be | |
|
129 | # smarter (and probably avoid copies) | |
|
130 | bytes = [] | |
|
131 | while not bytes: | |
|
132 | try: | |
|
133 | bytes = [self._reader.read(1)] | |
|
134 | except _readers.ReadNotReady: | |
|
135 | self._select() | |
|
136 | while bytes[-1] != '\n' and not self.complete(): | |
|
134 | 137 | self._select() |
|
135 | eol = self._body.find('\n', self._read_location) | |
|
136 |
if |
|
|
137 | eol += 1 | |
|
138 | else: | |
|
139 | eol = len(self._body) | |
|
140 | data = self._body[self._read_location:eol] | |
|
141 | self._read_location = eol | |
|
142 |
return |
|
|
138 | bytes.append(self._reader.read(1)) | |
|
139 | if bytes[-1] != '\n': | |
|
140 | next = self._reader.read(1) | |
|
141 | while next and next != '\n': | |
|
142 | bytes.append(next) | |
|
143 | next = self._reader.read(1) | |
|
144 | bytes.append(next) | |
|
145 | return ''.join(bytes) | |
|
143 | 146 | |
|
144 | 147 | def read(self, length=None): |
|
145 | 148 | # if length is None, unbounded read |
|
146 | 149 | while (not self.complete() # never select on a finished read |
|
147 | 150 | and (not length # unbounded, so we wait for complete() |
|
148 | or (self._read_location + length) > len(self._body))): | |
|
151 | or length > self._reader.available_data)): | |
|
149 | 152 | self._select() |
|
150 | 153 | if not length: |
|
151 |
length = |
|
|
152 | elif len(self._body) < (self._read_location + length): | |
|
153 | length = len(self._body) - self._read_location | |
|
154 | r = self._body[self._read_location:self._read_location + length] | |
|
155 | self._read_location += len(r) | |
|
154 | length = self._reader.available_data | |
|
155 | r = self._reader.read(length) | |
|
156 | 156 | if self.complete() and self.will_close: |
|
157 | 157 | self.sock.close() |
|
158 | 158 | return r |
@@ -160,93 +160,35 b' class HTTPResponse(object):' | |||
|
160 | 160 | def _select(self): |
|
161 | 161 | r, _, _ = select.select([self.sock], [], [], self._timeout) |
|
162 | 162 | if not r: |
|
163 |
# socket was not readable. If the response is not |
|
|
164 |
# |
|
|
165 | # If we are a _LEN_CLOSE_IS_END response and we have no data, | |
|
166 | # raise a timeout. | |
|
167 | if not (self.complete() or | |
|
168 | (self._content_len == _LEN_CLOSE_IS_END and self._body)): | |
|
163 | # socket was not readable. If the response is not | |
|
164 | # complete, raise a timeout. | |
|
165 | if not self.complete(): | |
|
169 | 166 | logger.info('timed out with timeout of %s', self._timeout) |
|
170 | 167 | raise HTTPTimeoutException('timeout reading data') |
|
171 | logger.info('cl: %r body: %r', self._content_len, self._body) | |
|
172 | 168 | try: |
|
173 | 169 | data = self.sock.recv(INCOMING_BUFFER_SIZE) |
|
174 | # If the socket was readable and no data was read, that | |
|
175 | # means the socket was closed. If this isn't a | |
|
176 | # _CLOSE_IS_END socket, then something is wrong if we're | |
|
177 | # here (we shouldn't enter _select() if the response is | |
|
178 | # complete), so abort. | |
|
179 | if not data and self._content_len != _LEN_CLOSE_IS_END: | |
|
180 | raise HTTPRemoteClosedError( | |
|
181 | 'server appears to have closed the socket mid-response') | |
|
182 | 170 | except socket.sslerror, e: |
|
183 | 171 | if e.args[0] != socket.SSL_ERROR_WANT_READ: |
|
184 | 172 | raise |
|
185 | 173 | logger.debug('SSL_WANT_READ in _select, should retry later') |
|
186 | 174 | return True |
|
187 | 175 | logger.debug('response read %d data during _select', len(data)) |
|
176 | # If the socket was readable and no data was read, that means | |
|
177 | # the socket was closed. Inform the reader (if any) so it can | |
|
178 | # raise an exception if this is an invalid situation. | |
|
188 | 179 | if not data: |
|
189 | if self.headers and self._content_len == _LEN_CLOSE_IS_END: | |
|
190 |
self._ |
|
|
180 | if self._reader: | |
|
181 | self._reader._close() | |
|
191 | 182 | return False |
|
192 | 183 | else: |
|
193 | 184 | self._load_response(data) |
|
194 | 185 | return True |
|
195 | 186 | |
|
196 | def _chunked_parsedata(self, data): | |
|
197 | if self._chunked_preloaded_block: | |
|
198 | data = self._chunked_preloaded_block + data | |
|
199 | self._chunked_preloaded_block = None | |
|
200 | while data: | |
|
201 | logger.debug('looping with %d data remaining', len(data)) | |
|
202 | # Slice out anything we should skip | |
|
203 | if self._chunked_skip_bytes: | |
|
204 | if len(data) <= self._chunked_skip_bytes: | |
|
205 | self._chunked_skip_bytes -= len(data) | |
|
206 | data = '' | |
|
207 | break | |
|
208 | else: | |
|
209 | data = data[self._chunked_skip_bytes:] | |
|
210 | self._chunked_skip_bytes = 0 | |
|
211 | ||
|
212 | # determine how much is until the next chunk | |
|
213 | if self._chunked_until_next: | |
|
214 | amt = self._chunked_until_next | |
|
215 | logger.debug('reading remaining %d of existing chunk', amt) | |
|
216 | self._chunked_until_next = 0 | |
|
217 | body = data | |
|
218 | else: | |
|
219 | try: | |
|
220 | amt, body = data.split(self._eol, 1) | |
|
221 | except ValueError: | |
|
222 | self._chunked_preloaded_block = data | |
|
223 | logger.debug('saving %r as a preloaded block for chunked', | |
|
224 | self._chunked_preloaded_block) | |
|
225 | return | |
|
226 | amt = int(amt, base=16) | |
|
227 | logger.debug('reading chunk of length %d', amt) | |
|
228 | if amt == 0: | |
|
229 | self._chunked_done = True | |
|
230 | ||
|
231 | # read through end of what we have or the chunk | |
|
232 | self._body += body[:amt] | |
|
233 | if len(body) >= amt: | |
|
234 | data = body[amt:] | |
|
235 | self._chunked_skip_bytes = len(self._eol) | |
|
236 | else: | |
|
237 | self._chunked_until_next = amt - len(body) | |
|
238 | self._chunked_skip_bytes = 0 | |
|
239 | data = '' | |
|
240 | ||
|
241 | 187 | def _load_response(self, data): |
|
242 | if self._chunked: | |
|
243 | self._chunked_parsedata(data) | |
|
244 | return | |
|
245 | elif self._body is not None: | |
|
246 | self._body += data | |
|
247 | return | |
|
248 | ||
|
249 | # We haven't seen end of headers yet | |
|
188 | # Being here implies we're not at the end of the headers yet, | |
|
189 | # since at the end of this method if headers were completely | |
|
190 | # loaded we replace this method with the load() method of the | |
|
191 | # reader we created. | |
|
250 | 192 | self.raw_response += data |
|
251 | 193 | # This is a bogus server with bad line endings |
|
252 | 194 | if self._eol not in self.raw_response: |
@@ -270,6 +212,7 b' class HTTPResponse(object):' | |||
|
270 | 212 | http_ver, status = hdrs.split(' ', 1) |
|
271 | 213 | if status.startswith('100'): |
|
272 | 214 | self.raw_response = body |
|
215 | self.continued = True | |
|
273 | 216 | logger.debug('continue seen, setting body to %r', body) |
|
274 | 217 | return |
|
275 | 218 | |
@@ -289,23 +232,46 b' class HTTPResponse(object):' | |||
|
289 | 232 | if self._eol != EOL: |
|
290 | 233 | hdrs = hdrs.replace(self._eol, '\r\n') |
|
291 | 234 | headers = rfc822.Message(cStringIO.StringIO(hdrs)) |
|
235 | content_len = None | |
|
292 | 236 | if HDR_CONTENT_LENGTH in headers: |
|
293 |
|
|
|
237 | content_len = int(headers[HDR_CONTENT_LENGTH]) | |
|
294 | 238 | if self.http_version == HTTP_VER_1_0: |
|
295 | 239 | self.will_close = True |
|
296 | 240 | elif HDR_CONNECTION_CTRL in headers: |
|
297 | 241 | self.will_close = ( |
|
298 | 242 | headers[HDR_CONNECTION_CTRL].lower() == CONNECTION_CLOSE) |
|
299 | if self._content_len == 0: | |
|
300 | self._content_len = _LEN_CLOSE_IS_END | |
|
301 | 243 | if (HDR_XFER_ENCODING in headers |
|
302 | 244 | and headers[HDR_XFER_ENCODING].lower() == XFER_ENCODING_CHUNKED): |
|
303 | self._body = '' | |
|
304 | self._chunked_parsedata(body) | |
|
305 | self._chunked = True | |
|
306 | if self._body is None: | |
|
307 | self._body = body | |
|
245 | self._reader = _readers.ChunkedReader(self._eol) | |
|
246 | logger.debug('using a chunked reader') | |
|
247 | else: | |
|
248 | # HEAD responses are forbidden from returning a body, and | |
|
249 | # it's implausible for a CONNECT response to use | |
|
250 | # close-is-end logic for an OK response. | |
|
251 | if (self.method == 'HEAD' or | |
|
252 | (self.method == 'CONNECT' and content_len is None)): | |
|
253 | content_len = 0 | |
|
254 | if content_len is not None: | |
|
255 | logger.debug('using a content-length reader with length %d', | |
|
256 | content_len) | |
|
257 | self._reader = _readers.ContentLengthReader(content_len) | |
|
258 | else: | |
|
259 | # Response body had no length specified and is not | |
|
260 | # chunked, so the end of the body will only be | |
|
261 | # identifiable by the termination of the socket by the | |
|
262 | # server. My interpretation of the spec means that we | |
|
263 | # are correct in hitting this case if | |
|
264 | # transfer-encoding, content-length, and | |
|
265 | # connection-control were left unspecified. | |
|
266 | self._reader = _readers.CloseIsEndReader() | |
|
267 | logger.debug('using a close-is-end reader') | |
|
268 | self.will_close = True | |
|
269 | ||
|
270 | if body: | |
|
271 | self._reader._load(body) | |
|
272 | logger.debug('headers complete') | |
|
308 | 273 | self.headers = headers |
|
274 | self._load_response = self._reader._load | |
|
309 | 275 | |
|
310 | 276 | |
|
311 | 277 | class HTTPConnection(object): |
@@ -382,13 +348,14 b' class HTTPConnection(object):' | |||
|
382 | 348 | {}, HTTP_VER_1_0) |
|
383 | 349 | sock.send(data) |
|
384 | 350 | sock.setblocking(0) |
|
385 | r = self.response_class(sock, self.timeout) | |
|
351 | r = self.response_class(sock, self.timeout, 'CONNECT') | |
|
386 | 352 | timeout_exc = HTTPTimeoutException( |
|
387 | 353 | 'Timed out waiting for CONNECT response from proxy') |
|
388 | 354 | while not r.complete(): |
|
389 | 355 | try: |
|
390 | 356 | if not r._select(): |
|
391 |
|
|
|
357 | if not r.complete(): | |
|
358 | raise timeout_exc | |
|
392 | 359 | except HTTPTimeoutException: |
|
393 | 360 | # This raise/except pattern looks goofy, but |
|
394 | 361 | # _select can raise the timeout as well as the |
@@ -527,7 +494,7 b' class HTTPConnection(object):' | |||
|
527 | 494 | out = outgoing_headers or body |
|
528 | 495 | blocking_on_continue = False |
|
529 | 496 | if expect_continue and not outgoing_headers and not ( |
|
530 | response and response.headers): | |
|
497 | response and (response.headers or response.continued)): | |
|
531 | 498 | logger.info( |
|
532 | 499 | 'waiting up to %s seconds for' |
|
533 | 500 | ' continue response from server', |
@@ -550,11 +517,6 b' class HTTPConnection(object):' | |||
|
550 | 517 | 'server, optimistically sending request body') |
|
551 | 518 | else: |
|
552 | 519 | raise HTTPTimeoutException('timeout sending data') |
|
553 | # TODO exceptional conditions with select? (what are those be?) | |
|
554 | # TODO if the response is loading, must we finish sending at all? | |
|
555 | # | |
|
556 | # Certainly not if it's going to close the connection and/or | |
|
557 | # the response is already done...I think. | |
|
558 | 520 | was_first = first |
|
559 | 521 | |
|
560 | 522 | # incoming data |
@@ -572,11 +534,11 b' class HTTPConnection(object):' | |||
|
572 | 534 | logger.info('socket appears closed in read') |
|
573 | 535 | self.sock = None |
|
574 | 536 | self._current_response = None |
|
537 | if response is not None: | |
|
538 | response._close() | |
|
575 | 539 | # This if/elif ladder is a bit subtle, |
|
576 | 540 | # comments in each branch should help. |
|
577 | if response is not None and ( | |
|
578 | response.complete() or | |
|
579 | response._content_len == _LEN_CLOSE_IS_END): | |
|
541 | if response is not None and response.complete(): | |
|
580 | 542 | # Server responded completely and then |
|
581 | 543 | # closed the socket. We should just shut |
|
582 | 544 | # things down and let the caller get their |
@@ -605,7 +567,7 b' class HTTPConnection(object):' | |||
|
605 | 567 | 'response was missing or incomplete!') |
|
606 | 568 | logger.debug('read %d bytes in request()', len(data)) |
|
607 | 569 | if response is None: |
|
608 | response = self.response_class(r[0], self.timeout) | |
|
570 | response = self.response_class(r[0], self.timeout, method) | |
|
609 | 571 | response._load_response(data) |
|
610 | 572 | # Jump to the next select() call so we load more |
|
611 | 573 | # data if the server is still sending us content. |
@@ -613,10 +575,6 b' class HTTPConnection(object):' | |||
|
613 | 575 | except socket.error, e: |
|
614 | 576 | if e[0] != errno.EPIPE and not was_first: |
|
615 | 577 | raise |
|
616 | if (response._content_len | |
|
617 | and response._content_len != _LEN_CLOSE_IS_END): | |
|
618 | outgoing_headers = sent_data + outgoing_headers | |
|
619 | reconnect('read') | |
|
620 | 578 | |
|
621 | 579 | # outgoing data |
|
622 | 580 | if w and out: |
@@ -661,7 +619,7 b' class HTTPConnection(object):' | |||
|
661 | 619 | # close if the server response said to or responded before eating |
|
662 | 620 | # the whole request |
|
663 | 621 | if response is None: |
|
664 | response = self.response_class(self.sock, self.timeout) | |
|
622 | response = self.response_class(self.sock, self.timeout, method) | |
|
665 | 623 | complete = response.complete() |
|
666 | 624 | data_left = bool(outgoing_headers or body) |
|
667 | 625 | if data_left: |
@@ -679,7 +637,8 b' class HTTPConnection(object):' | |||
|
679 | 637 | raise httplib.ResponseNotReady() |
|
680 | 638 | r = self._current_response |
|
681 | 639 | while r.headers is None: |
|
682 | r._select() | |
|
640 | if not r._select() and not r.complete(): | |
|
641 | raise _readers.HTTPRemoteClosedError() | |
|
683 | 642 | if r.will_close: |
|
684 | 643 | self.sock = None |
|
685 | 644 | self._current_response = None |
@@ -705,7 +664,7 b' class HTTPProxyConnectFailedException(ht' | |||
|
705 | 664 | class HTTPStateError(httplib.HTTPException): |
|
706 | 665 | """Invalid internal state encountered.""" |
|
707 | 666 | |
|
708 | ||
|
709 | class HTTPRemoteClosedError(httplib.HTTPException): | |
|
710 | """The server closed the remote socket in the middle of a response.""" | |
|
667 | # Forward this exception type from _readers since it needs to be part | |
|
668 | # of the public API. | |
|
669 | HTTPRemoteClosedError = _readers.HTTPRemoteClosedError | |
|
711 | 670 | # no-check-code |
@@ -29,7 +29,7 b'' | |||
|
29 | 29 | import socket |
|
30 | 30 | import unittest |
|
31 | 31 | |
|
32 | import http | |
|
32 | import httpplus | |
|
33 | 33 | |
|
34 | 34 | # relative import to ease embedding the library |
|
35 | 35 | import util |
@@ -38,7 +38,7 b' import util' | |||
|
38 | 38 | class SimpleHttpTest(util.HttpTestBase, unittest.TestCase): |
|
39 | 39 | |
|
40 | 40 | def _run_simple_test(self, host, server_data, expected_req, expected_data): |
|
41 | con = http.HTTPConnection(host) | |
|
41 | con = httpplus.HTTPConnection(host) | |
|
42 | 42 | con._connect() |
|
43 | 43 | con.sock.data = server_data |
|
44 | 44 | con.request('GET', '/') |
@@ -47,9 +47,9 b' class SimpleHttpTest(util.HttpTestBase, ' | |||
|
47 | 47 | self.assertEqual(expected_data, con.getresponse().read()) |
|
48 | 48 | |
|
49 | 49 | def test_broken_data_obj(self): |
|
50 | con = http.HTTPConnection('1.2.3.4:80') | |
|
50 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
51 | 51 | con._connect() |
|
52 | self.assertRaises(http.BadRequestData, | |
|
52 | self.assertRaises(httpplus.BadRequestData, | |
|
53 | 53 | con.request, 'POST', '/', body=1) |
|
54 | 54 | |
|
55 | 55 | def test_no_keepalive_http_1_0(self): |
@@ -74,7 +74,7 b' store' | |||
|
74 | 74 | fncache |
|
75 | 75 | dotencode |
|
76 | 76 | """ |
|
77 | con = http.HTTPConnection('localhost:9999') | |
|
77 | con = httpplus.HTTPConnection('localhost:9999') | |
|
78 | 78 | con._connect() |
|
79 | 79 | con.sock.data = [expected_response_headers, expected_response_body] |
|
80 | 80 | con.request('GET', '/remote/.hg/requires', |
@@ -95,7 +95,7 b' dotencode' | |||
|
95 | 95 | self.assert_(resp.sock.closed) |
|
96 | 96 | |
|
97 | 97 | def test_multiline_header(self): |
|
98 | con = http.HTTPConnection('1.2.3.4:80') | |
|
98 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
99 | 99 | con._connect() |
|
100 | 100 | con.sock.data = ['HTTP/1.1 200 OK\r\n', |
|
101 | 101 | 'Server: BogusServer 1.0\r\n', |
@@ -122,7 +122,7 b' dotencode' | |||
|
122 | 122 | self.assertEqual(con.sock.closed, False) |
|
123 | 123 | |
|
124 | 124 | def testSimpleRequest(self): |
|
125 | con = http.HTTPConnection('1.2.3.4:80') | |
|
125 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
126 | 126 | con._connect() |
|
127 | 127 | con.sock.data = ['HTTP/1.1 200 OK\r\n', |
|
128 | 128 | 'Server: BogusServer 1.0\r\n', |
@@ -149,12 +149,13 b' dotencode' | |||
|
149 | 149 | resp.headers.getheaders('server')) |
|
150 | 150 | |
|
151 | 151 | def testHeaderlessResponse(self): |
|
152 | con = http.HTTPConnection('1.2.3.4', use_ssl=False) | |
|
152 | con = httpplus.HTTPConnection('1.2.3.4', use_ssl=False) | |
|
153 | 153 | con._connect() |
|
154 | 154 | con.sock.data = ['HTTP/1.1 200 OK\r\n', |
|
155 | 155 | '\r\n' |
|
156 | 156 | '1234567890' |
|
157 | 157 | ] |
|
158 | con.sock.close_on_empty = True | |
|
158 | 159 | con.request('GET', '/') |
|
159 | 160 | |
|
160 | 161 | expected_req = ('GET / HTTP/1.1\r\n' |
@@ -169,7 +170,30 b' dotencode' | |||
|
169 | 170 | self.assertEqual(resp.status, 200) |
|
170 | 171 | |
|
171 | 172 | def testReadline(self): |
|
172 | con = http.HTTPConnection('1.2.3.4') | |
|
173 | con = httpplus.HTTPConnection('1.2.3.4') | |
|
174 | con._connect() | |
|
175 | con.sock.data = ['HTTP/1.1 200 OK\r\n', | |
|
176 | 'Server: BogusServer 1.0\r\n', | |
|
177 | 'Connection: Close\r\n', | |
|
178 | '\r\n' | |
|
179 | '1\n2\nabcdefg\n4\n5'] | |
|
180 | con.sock.close_on_empty = True | |
|
181 | ||
|
182 | expected_req = ('GET / HTTP/1.1\r\n' | |
|
183 | 'Host: 1.2.3.4\r\n' | |
|
184 | 'accept-encoding: identity\r\n\r\n') | |
|
185 | ||
|
186 | con.request('GET', '/') | |
|
187 | self.assertEqual(('1.2.3.4', 80), con.sock.sa) | |
|
188 | self.assertEqual(expected_req, con.sock.sent) | |
|
189 | r = con.getresponse() | |
|
190 | for expected in ['1\n', '2\n', 'abcdefg\n', '4\n', '5']: | |
|
191 | actual = r.readline() | |
|
192 | self.assertEqual(expected, actual, | |
|
193 | 'Expected %r, got %r' % (expected, actual)) | |
|
194 | ||
|
195 | def testReadlineTrickle(self): | |
|
196 | con = httpplus.HTTPConnection('1.2.3.4') | |
|
173 | 197 | con._connect() |
|
174 | 198 | # make sure it trickles in one byte at a time |
|
175 | 199 | # so that we touch all the cases in readline |
@@ -179,6 +203,7 b' dotencode' | |||
|
179 | 203 | 'Connection: Close\r\n', |
|
180 | 204 | '\r\n' |
|
181 | 205 | '1\n2\nabcdefg\n4\n5'])) |
|
206 | con.sock.close_on_empty = True | |
|
182 | 207 | |
|
183 | 208 | expected_req = ('GET / HTTP/1.1\r\n' |
|
184 | 209 | 'Host: 1.2.3.4\r\n' |
@@ -193,6 +218,59 b' dotencode' | |||
|
193 | 218 | self.assertEqual(expected, actual, |
|
194 | 219 | 'Expected %r, got %r' % (expected, actual)) |
|
195 | 220 | |
|
221 | def testVariousReads(self): | |
|
222 | con = httpplus.HTTPConnection('1.2.3.4') | |
|
223 | con._connect() | |
|
224 | # make sure it trickles in one byte at a time | |
|
225 | # so that we touch all the cases in readline | |
|
226 | con.sock.data = list(''.join( | |
|
227 | ['HTTP/1.1 200 OK\r\n', | |
|
228 | 'Server: BogusServer 1.0\r\n', | |
|
229 | 'Connection: Close\r\n', | |
|
230 | '\r\n' | |
|
231 | '1\n2', | |
|
232 | '\na', 'bc', | |
|
233 | 'defg\n4\n5'])) | |
|
234 | con.sock.close_on_empty = True | |
|
235 | ||
|
236 | expected_req = ('GET / HTTP/1.1\r\n' | |
|
237 | 'Host: 1.2.3.4\r\n' | |
|
238 | 'accept-encoding: identity\r\n\r\n') | |
|
239 | ||
|
240 | con.request('GET', '/') | |
|
241 | self.assertEqual(('1.2.3.4', 80), con.sock.sa) | |
|
242 | self.assertEqual(expected_req, con.sock.sent) | |
|
243 | r = con.getresponse() | |
|
244 | for read_amt, expect in [(1, '1'), (1, '\n'), | |
|
245 | (4, '2\nab'), | |
|
246 | ('line', 'cdefg\n'), | |
|
247 | (None, '4\n5')]: | |
|
248 | if read_amt == 'line': | |
|
249 | self.assertEqual(expect, r.readline()) | |
|
250 | else: | |
|
251 | self.assertEqual(expect, r.read(read_amt)) | |
|
252 | ||
|
253 | def testZeroLengthBody(self): | |
|
254 | con = httpplus.HTTPConnection('1.2.3.4') | |
|
255 | con._connect() | |
|
256 | # make sure it trickles in one byte at a time | |
|
257 | # so that we touch all the cases in readline | |
|
258 | con.sock.data = list(''.join( | |
|
259 | ['HTTP/1.1 200 OK\r\n', | |
|
260 | 'Server: BogusServer 1.0\r\n', | |
|
261 | 'Content-length: 0\r\n', | |
|
262 | '\r\n'])) | |
|
263 | ||
|
264 | expected_req = ('GET / HTTP/1.1\r\n' | |
|
265 | 'Host: 1.2.3.4\r\n' | |
|
266 | 'accept-encoding: identity\r\n\r\n') | |
|
267 | ||
|
268 | con.request('GET', '/') | |
|
269 | self.assertEqual(('1.2.3.4', 80), con.sock.sa) | |
|
270 | self.assertEqual(expected_req, con.sock.sent) | |
|
271 | r = con.getresponse() | |
|
272 | self.assertEqual('', r.read()) | |
|
273 | ||
|
196 | 274 | def testIPv6(self): |
|
197 | 275 | self._run_simple_test('[::1]:8221', |
|
198 | 276 | ['HTTP/1.1 200 OK\r\n', |
@@ -226,7 +304,7 b' dotencode' | |||
|
226 | 304 | '1234567890') |
|
227 | 305 | |
|
228 | 306 | def testEarlyContinueResponse(self): |
|
229 | con = http.HTTPConnection('1.2.3.4:80') | |
|
307 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
230 | 308 | con._connect() |
|
231 | 309 | sock = con.sock |
|
232 | 310 | sock.data = ['HTTP/1.1 403 Forbidden\r\n', |
@@ -240,8 +318,23 b' dotencode' | |||
|
240 | 318 | self.assertEqual("You can't do that.", con.getresponse().read()) |
|
241 | 319 | self.assertEqual(sock.closed, True) |
|
242 | 320 | |
|
321 | def testEarlyContinueResponseNoContentLength(self): | |
|
322 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
323 | con._connect() | |
|
324 | sock = con.sock | |
|
325 | sock.data = ['HTTP/1.1 403 Forbidden\r\n', | |
|
326 | 'Server: BogusServer 1.0\r\n', | |
|
327 | '\r\n' | |
|
328 | "You can't do that."] | |
|
329 | sock.close_on_empty = True | |
|
330 | expected_req = self.doPost(con, expect_body=False) | |
|
331 | self.assertEqual(('1.2.3.4', 80), sock.sa) | |
|
332 | self.assertStringEqual(expected_req, sock.sent) | |
|
333 | self.assertEqual("You can't do that.", con.getresponse().read()) | |
|
334 | self.assertEqual(sock.closed, True) | |
|
335 | ||
|
243 | 336 | def testDeniedAfterContinueTimeoutExpires(self): |
|
244 | con = http.HTTPConnection('1.2.3.4:80') | |
|
337 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
245 | 338 | con._connect() |
|
246 | 339 | sock = con.sock |
|
247 | 340 | sock.data = ['HTTP/1.1 403 Forbidden\r\n', |
@@ -269,7 +362,7 b' dotencode' | |||
|
269 | 362 | self.assertEqual(sock.closed, True) |
|
270 | 363 | |
|
271 | 364 | def testPostData(self): |
|
272 | con = http.HTTPConnection('1.2.3.4:80') | |
|
365 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
273 | 366 | con._connect() |
|
274 | 367 | sock = con.sock |
|
275 | 368 | sock.read_wait_sentinel = 'POST data' |
@@ -286,7 +379,7 b' dotencode' | |||
|
286 | 379 | self.assertEqual(sock.closed, False) |
|
287 | 380 | |
|
288 | 381 | def testServerWithoutContinue(self): |
|
289 | con = http.HTTPConnection('1.2.3.4:80') | |
|
382 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
290 | 383 | con._connect() |
|
291 | 384 | sock = con.sock |
|
292 | 385 | sock.read_wait_sentinel = 'POST data' |
@@ -302,7 +395,7 b' dotencode' | |||
|
302 | 395 | self.assertEqual(sock.closed, False) |
|
303 | 396 | |
|
304 | 397 | def testServerWithSlowContinue(self): |
|
305 | con = http.HTTPConnection('1.2.3.4:80') | |
|
398 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
306 | 399 | con._connect() |
|
307 | 400 | sock = con.sock |
|
308 | 401 | sock.read_wait_sentinel = 'POST data' |
@@ -321,7 +414,7 b' dotencode' | |||
|
321 | 414 | self.assertEqual(sock.closed, False) |
|
322 | 415 | |
|
323 | 416 | def testSlowConnection(self): |
|
324 | con = http.HTTPConnection('1.2.3.4:80') | |
|
417 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
325 | 418 | con._connect() |
|
326 | 419 | # simulate one byte arriving at a time, to check for various |
|
327 | 420 | # corner cases |
@@ -340,12 +433,26 b' dotencode' | |||
|
340 | 433 | self.assertEqual(expected_req, con.sock.sent) |
|
341 | 434 | self.assertEqual('1234567890', con.getresponse().read()) |
|
342 | 435 | |
|
436 | def testCloseAfterNotAllOfHeaders(self): | |
|
437 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
438 | con._connect() | |
|
439 | con.sock.data = ['HTTP/1.1 200 OK\r\n', | |
|
440 | 'Server: NO CARRIER'] | |
|
441 | con.sock.close_on_empty = True | |
|
442 | con.request('GET', '/') | |
|
443 | self.assertRaises(httpplus.HTTPRemoteClosedError, | |
|
444 | con.getresponse) | |
|
445 | ||
|
446 | expected_req = ('GET / HTTP/1.1\r\n' | |
|
447 | 'Host: 1.2.3.4\r\n' | |
|
448 | 'accept-encoding: identity\r\n\r\n') | |
|
449 | ||
|
343 | 450 | def testTimeout(self): |
|
344 | con = http.HTTPConnection('1.2.3.4:80') | |
|
451 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
345 | 452 | con._connect() |
|
346 | 453 | con.sock.data = [] |
|
347 | 454 | con.request('GET', '/') |
|
348 | self.assertRaises(http.HTTPTimeoutException, | |
|
455 | self.assertRaises(httpplus.HTTPTimeoutException, | |
|
349 | 456 | con.getresponse) |
|
350 | 457 | |
|
351 | 458 | expected_req = ('GET / HTTP/1.1\r\n' |
@@ -370,7 +477,7 b' dotencode' | |||
|
370 | 477 | return s |
|
371 | 478 | |
|
372 | 479 | socket.socket = closingsocket |
|
373 | con = http.HTTPConnection('1.2.3.4:80') | |
|
480 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
374 | 481 | con._connect() |
|
375 | 482 | con.request('GET', '/') |
|
376 | 483 | r1 = con.getresponse() |
@@ -381,7 +488,7 b' dotencode' | |||
|
381 | 488 | self.assertEqual(2, len(sockets)) |
|
382 | 489 | |
|
383 | 490 | def test_server_closes_before_end_of_body(self): |
|
384 | con = http.HTTPConnection('1.2.3.4:80') | |
|
491 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
385 | 492 | con._connect() |
|
386 | 493 | s = con.sock |
|
387 | 494 | s.data = ['HTTP/1.1 200 OK\r\n', |
@@ -393,9 +500,9 b' dotencode' | |||
|
393 | 500 | s.close_on_empty = True |
|
394 | 501 | con.request('GET', '/') |
|
395 | 502 | r1 = con.getresponse() |
|
396 | self.assertRaises(http.HTTPRemoteClosedError, r1.read) | |
|
503 | self.assertRaises(httpplus.HTTPRemoteClosedError, r1.read) | |
|
397 | 504 | |
|
398 | 505 | def test_no_response_raises_response_not_ready(self): |
|
399 | con = http.HTTPConnection('foo') | |
|
400 | self.assertRaises(http.httplib.ResponseNotReady, con.getresponse) | |
|
506 | con = httpplus.HTTPConnection('foo') | |
|
507 | self.assertRaises(httpplus.httplib.ResponseNotReady, con.getresponse) | |
|
401 | 508 | # no-check-code |
@@ -34,7 +34,7 b' against that potential insanit.y' | |||
|
34 | 34 | """ |
|
35 | 35 | import unittest |
|
36 | 36 | |
|
37 | import http | |
|
37 | import httpplus | |
|
38 | 38 | |
|
39 | 39 | # relative import to ease embedding the library |
|
40 | 40 | import util |
@@ -43,7 +43,7 b' import util' | |||
|
43 | 43 | class SimpleHttpTest(util.HttpTestBase, unittest.TestCase): |
|
44 | 44 | |
|
45 | 45 | def bogusEOL(self, eol): |
|
46 | con = http.HTTPConnection('1.2.3.4:80') | |
|
46 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
47 | 47 | con._connect() |
|
48 | 48 | con.sock.data = ['HTTP/1.1 200 OK%s' % eol, |
|
49 | 49 | 'Server: BogusServer 1.0%s' % eol, |
@@ -29,7 +29,7 b'' | |||
|
29 | 29 | import cStringIO |
|
30 | 30 | import unittest |
|
31 | 31 | |
|
32 | import http | |
|
32 | import httpplus | |
|
33 | 33 | |
|
34 | 34 | # relative import to ease embedding the library |
|
35 | 35 | import util |
@@ -50,7 +50,7 b" def chunkedblock(x, eol='\\r\\n'):" | |||
|
50 | 50 | |
|
51 | 51 | class ChunkedTransferTest(util.HttpTestBase, unittest.TestCase): |
|
52 | 52 | def testChunkedUpload(self): |
|
53 | con = http.HTTPConnection('1.2.3.4:80') | |
|
53 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
54 | 54 | con._connect() |
|
55 | 55 | sock = con.sock |
|
56 | 56 | sock.read_wait_sentinel = '0\r\n\r\n' |
@@ -77,7 +77,7 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
77 | 77 | self.assertEqual(sock.closed, False) |
|
78 | 78 | |
|
79 | 79 | def testChunkedDownload(self): |
|
80 | con = http.HTTPConnection('1.2.3.4:80') | |
|
80 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
81 | 81 | con._connect() |
|
82 | 82 | sock = con.sock |
|
83 | 83 | sock.data = ['HTTP/1.1 200 OK\r\n', |
@@ -85,14 +85,31 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
85 | 85 | 'transfer-encoding: chunked', |
|
86 | 86 | '\r\n\r\n', |
|
87 | 87 | chunkedblock('hi '), |
|
88 |
chunkedblock('there') |
|
|
88 | ] + list(chunkedblock('there')) + [ | |
|
89 | 89 | chunkedblock(''), |
|
90 | 90 | ] |
|
91 | 91 | con.request('GET', '/') |
|
92 | 92 | self.assertStringEqual('hi there', con.getresponse().read()) |
|
93 | 93 | |
|
94 | def testChunkedDownloadOddReadBoundaries(self): | |
|
95 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
96 | con._connect() | |
|
97 | sock = con.sock | |
|
98 | sock.data = ['HTTP/1.1 200 OK\r\n', | |
|
99 | 'Server: BogusServer 1.0\r\n', | |
|
100 | 'transfer-encoding: chunked', | |
|
101 | '\r\n\r\n', | |
|
102 | chunkedblock('hi '), | |
|
103 | ] + list(chunkedblock('there')) + [ | |
|
104 | chunkedblock(''), | |
|
105 | ] | |
|
106 | con.request('GET', '/') | |
|
107 | resp = con.getresponse() | |
|
108 | for amt, expect in [(1, 'h'), (5, 'i the'), (100, 're')]: | |
|
109 | self.assertEqual(expect, resp.read(amt)) | |
|
110 | ||
|
94 | 111 | def testChunkedDownloadBadEOL(self): |
|
95 | con = http.HTTPConnection('1.2.3.4:80') | |
|
112 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
96 | 113 | con._connect() |
|
97 | 114 | sock = con.sock |
|
98 | 115 | sock.data = ['HTTP/1.1 200 OK\n', |
@@ -107,7 +124,7 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
107 | 124 | self.assertStringEqual('hi there', con.getresponse().read()) |
|
108 | 125 | |
|
109 | 126 | def testChunkedDownloadPartialChunkBadEOL(self): |
|
110 | con = http.HTTPConnection('1.2.3.4:80') | |
|
127 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
111 | 128 | con._connect() |
|
112 | 129 | sock = con.sock |
|
113 | 130 | sock.data = ['HTTP/1.1 200 OK\n', |
@@ -122,7 +139,7 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
122 | 139 | con.getresponse().read()) |
|
123 | 140 | |
|
124 | 141 | def testChunkedDownloadPartialChunk(self): |
|
125 | con = http.HTTPConnection('1.2.3.4:80') | |
|
142 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
126 | 143 | con._connect() |
|
127 | 144 | sock = con.sock |
|
128 | 145 | sock.data = ['HTTP/1.1 200 OK\r\n', |
@@ -136,7 +153,7 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
136 | 153 | con.getresponse().read()) |
|
137 | 154 | |
|
138 | 155 | def testChunkedDownloadEarlyHangup(self): |
|
139 | con = http.HTTPConnection('1.2.3.4:80') | |
|
156 | con = httpplus.HTTPConnection('1.2.3.4:80') | |
|
140 | 157 | con._connect() |
|
141 | 158 | sock = con.sock |
|
142 | 159 | broken = chunkedblock('hi'*20)[:-1] |
@@ -149,5 +166,5 b' class ChunkedTransferTest(util.HttpTestB' | |||
|
149 | 166 | sock.close_on_empty = True |
|
150 | 167 | con.request('GET', '/') |
|
151 | 168 | resp = con.getresponse() |
|
152 | self.assertRaises(http.HTTPRemoteClosedError, resp.read) | |
|
169 | self.assertRaises(httpplus.HTTPRemoteClosedError, resp.read) | |
|
153 | 170 | # no-check-code |
@@ -29,13 +29,13 b'' | |||
|
29 | 29 | import unittest |
|
30 | 30 | import socket |
|
31 | 31 | |
|
32 | import http | |
|
32 | import httpplus | |
|
33 | 33 | |
|
34 | 34 | # relative import to ease embedding the library |
|
35 | 35 | import util |
|
36 | 36 | |
|
37 | 37 | |
|
38 | def make_preloaded_socket(data): | |
|
38 | def make_preloaded_socket(data, close=False): | |
|
39 | 39 | """Make a socket pre-loaded with data so it can be read during connect. |
|
40 | 40 | |
|
41 | 41 | Useful for https proxy tests because we have to read from the |
@@ -44,6 +44,7 b' def make_preloaded_socket(data):' | |||
|
44 | 44 | def s(*args, **kwargs): |
|
45 | 45 | sock = util.MockSocket(*args, **kwargs) |
|
46 | 46 | sock.early_data = data[:] |
|
47 | sock.close_on_empty = close | |
|
47 | 48 | return sock |
|
48 | 49 | return s |
|
49 | 50 | |
@@ -51,7 +52,7 b' def make_preloaded_socket(data):' | |||
|
51 | 52 | class ProxyHttpTest(util.HttpTestBase, unittest.TestCase): |
|
52 | 53 | |
|
53 | 54 | def _run_simple_test(self, host, server_data, expected_req, expected_data): |
|
54 | con = http.HTTPConnection(host) | |
|
55 | con = httpplus.HTTPConnection(host) | |
|
55 | 56 | con._connect() |
|
56 | 57 | con.sock.data = server_data |
|
57 | 58 | con.request('GET', '/') |
@@ -60,7 +61,7 b' class ProxyHttpTest(util.HttpTestBase, u' | |||
|
60 | 61 | self.assertEqual(expected_data, con.getresponse().read()) |
|
61 | 62 | |
|
62 | 63 | def testSimpleRequest(self): |
|
63 | con = http.HTTPConnection('1.2.3.4:80', | |
|
64 | con = httpplus.HTTPConnection('1.2.3.4:80', | |
|
64 | 65 | proxy_hostport=('magicproxy', 4242)) |
|
65 | 66 | con._connect() |
|
66 | 67 | con.sock.data = ['HTTP/1.1 200 OK\r\n', |
@@ -88,7 +89,7 b' class ProxyHttpTest(util.HttpTestBase, u' | |||
|
88 | 89 | resp.headers.getheaders('server')) |
|
89 | 90 | |
|
90 | 91 | def testSSLRequest(self): |
|
91 | con = http.HTTPConnection('1.2.3.4:443', | |
|
92 | con = httpplus.HTTPConnection('1.2.3.4:443', | |
|
92 | 93 | proxy_hostport=('magicproxy', 4242)) |
|
93 | 94 | socket.socket = make_preloaded_socket( |
|
94 | 95 | ['HTTP/1.1 200 OK\r\n', |
@@ -124,12 +125,47 b' class ProxyHttpTest(util.HttpTestBase, u' | |||
|
124 | 125 | self.assertEqual(['BogusServer 1.0'], |
|
125 | 126 | resp.headers.getheaders('server')) |
|
126 | 127 | |
|
127 |
def testSSL |
|
|
128 | con = http.HTTPConnection('1.2.3.4:443', | |
|
128 | def testSSLRequestNoConnectBody(self): | |
|
129 | con = httpplus.HTTPConnection('1.2.3.4:443', | |
|
129 | 130 | proxy_hostport=('magicproxy', 4242)) |
|
130 | 131 | socket.socket = make_preloaded_socket( |
|
131 | ['HTTP/1.1 407 Proxy Authentication Required\r\n\r\n']) | |
|
132 | self.assertRaises(http.HTTPProxyConnectFailedException, con._connect) | |
|
133 | self.assertRaises(http.HTTPProxyConnectFailedException, | |
|
132 | ['HTTP/1.1 200 OK\r\n', | |
|
133 | 'Server: BogusServer 1.0\r\n', | |
|
134 | '\r\n']) | |
|
135 | con._connect() | |
|
136 | con.sock.data = ['HTTP/1.1 200 OK\r\n', | |
|
137 | 'Server: BogusServer 1.0\r\n', | |
|
138 | 'Content-Length: 10\r\n', | |
|
139 | '\r\n' | |
|
140 | '1234567890' | |
|
141 | ] | |
|
142 | connect_sent = con.sock.sent | |
|
143 | con.sock.sent = '' | |
|
144 | con.request('GET', '/') | |
|
145 | ||
|
146 | expected_connect = ('CONNECT 1.2.3.4:443 HTTP/1.0\r\n' | |
|
147 | 'Host: 1.2.3.4\r\n' | |
|
148 | 'accept-encoding: identity\r\n' | |
|
149 | '\r\n') | |
|
150 | expected_request = ('GET / HTTP/1.1\r\n' | |
|
151 | 'Host: 1.2.3.4\r\n' | |
|
152 | 'accept-encoding: identity\r\n\r\n') | |
|
153 | ||
|
154 | self.assertEqual(('127.0.0.42', 4242), con.sock.sa) | |
|
155 | self.assertStringEqual(expected_connect, connect_sent) | |
|
156 | self.assertStringEqual(expected_request, con.sock.sent) | |
|
157 | resp = con.getresponse() | |
|
158 | self.assertEqual(resp.status, 200) | |
|
159 | self.assertEqual('1234567890', resp.read()) | |
|
160 | self.assertEqual(['BogusServer 1.0'], | |
|
161 | resp.headers.getheaders('server')) | |
|
162 | ||
|
163 | def testSSLProxyFailure(self): | |
|
164 | con = httpplus.HTTPConnection('1.2.3.4:443', | |
|
165 | proxy_hostport=('magicproxy', 4242)) | |
|
166 | socket.socket = make_preloaded_socket( | |
|
167 | ['HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'], close=True) | |
|
168 | self.assertRaises(httpplus.HTTPProxyConnectFailedException, con._connect) | |
|
169 | self.assertRaises(httpplus.HTTPProxyConnectFailedException, | |
|
134 | 170 | con.request, 'GET', '/') |
|
135 | 171 | # no-check-code |
@@ -28,7 +28,7 b'' | |||
|
28 | 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
29 | 29 | import unittest |
|
30 | 30 | |
|
31 | import http | |
|
31 | import httpplus | |
|
32 | 32 | |
|
33 | 33 | # relative import to ease embedding the library |
|
34 | 34 | import util |
@@ -37,7 +37,7 b' import util' | |||
|
37 | 37 | |
|
38 | 38 | class HttpSslTest(util.HttpTestBase, unittest.TestCase): |
|
39 | 39 | def testSslRereadRequired(self): |
|
40 | con = http.HTTPConnection('1.2.3.4:443') | |
|
40 | con = httpplus.HTTPConnection('1.2.3.4:443') | |
|
41 | 41 | con._connect() |
|
42 | 42 | # extend the list instead of assign because of how |
|
43 | 43 | # MockSSLSocket works. |
@@ -66,7 +66,7 b' class HttpSslTest(util.HttpTestBase, uni' | |||
|
66 | 66 | resp.headers.getheaders('server')) |
|
67 | 67 | |
|
68 | 68 | def testSslRereadInEarlyResponse(self): |
|
69 | con = http.HTTPConnection('1.2.3.4:443') | |
|
69 | con = httpplus.HTTPConnection('1.2.3.4:443') | |
|
70 | 70 | con._connect() |
|
71 | 71 | con.sock.early_data = ['HTTP/1.1 200 OK\r\n', |
|
72 | 72 | 'Server: BogusServer 1.0\r\n', |
@@ -29,7 +29,7 b'' | |||
|
29 | 29 | import difflib |
|
30 | 30 | import socket |
|
31 | 31 | |
|
32 | import http | |
|
32 | import httpplus | |
|
33 | 33 | |
|
34 | 34 | |
|
35 | 35 | class MockSocket(object): |
@@ -57,7 +57,7 b' class MockSocket(object):' | |||
|
57 | 57 | self.remote_closed = self.closed = False |
|
58 | 58 | self.close_on_empty = False |
|
59 | 59 | self.sent = '' |
|
60 | self.read_wait_sentinel = http._END_HEADERS | |
|
60 | self.read_wait_sentinel = httpplus._END_HEADERS | |
|
61 | 61 | |
|
62 | 62 | def close(self): |
|
63 | 63 | self.closed = True |
@@ -86,7 +86,7 b' class MockSocket(object):' | |||
|
86 | 86 | |
|
87 | 87 | @property |
|
88 | 88 | def ready_for_read(self): |
|
89 | return ((self.early_data and http._END_HEADERS in self.sent) | |
|
89 | return ((self.early_data and httpplus._END_HEADERS in self.sent) | |
|
90 | 90 | or (self.read_wait_sentinel in self.sent and self.data) |
|
91 | 91 | or self.closed or self.remote_closed) |
|
92 | 92 | |
@@ -132,7 +132,7 b' class MockSSLSocket(object):' | |||
|
132 | 132 | |
|
133 | 133 | |
|
134 | 134 | def mocksslwrap(sock, keyfile=None, certfile=None, |
|
135 | server_side=False, cert_reqs=http.socketutil.CERT_NONE, | |
|
135 | server_side=False, cert_reqs=httpplus.socketutil.CERT_NONE, | |
|
136 | 136 | ssl_version=None, ca_certs=None, |
|
137 | 137 | do_handshake_on_connect=True, |
|
138 | 138 | suppress_ragged_eofs=True): |
@@ -156,16 +156,16 b' class HttpTestBase(object):' | |||
|
156 | 156 | self.orig_getaddrinfo = socket.getaddrinfo |
|
157 | 157 | socket.getaddrinfo = mockgetaddrinfo |
|
158 | 158 | |
|
159 | self.orig_select = http.select.select | |
|
160 | http.select.select = mockselect | |
|
159 | self.orig_select = httpplus.select.select | |
|
160 | httpplus.select.select = mockselect | |
|
161 | 161 | |
|
162 | self.orig_sslwrap = http.socketutil.wrap_socket | |
|
163 | http.socketutil.wrap_socket = mocksslwrap | |
|
162 | self.orig_sslwrap = httpplus.socketutil.wrap_socket | |
|
163 | httpplus.socketutil.wrap_socket = mocksslwrap | |
|
164 | 164 | |
|
165 | 165 | def tearDown(self): |
|
166 | 166 | socket.socket = self.orig_socket |
|
167 | http.select.select = self.orig_select | |
|
168 | http.socketutil.wrap_socket = self.orig_sslwrap | |
|
167 | httpplus.select.select = self.orig_select | |
|
168 | httpplus.socketutil.wrap_socket = self.orig_sslwrap | |
|
169 | 169 | socket.getaddrinfo = self.orig_getaddrinfo |
|
170 | 170 | |
|
171 | 171 | def assertStringEqual(self, l, r): |
General Comments 0
You need to be logged in to leave comments.
Login now