Merge lp:~pedronis/u1db/enforce-max-request-sizes into lp:u1db
- enforce-max-request-sizes
- Merge into trunk
Proposed by
Samuele Pedroni
Status: | Merged |
---|---|
Approved by: | Samuele Pedroni |
Approved revision: | 260 |
Merged at revision: | 260 |
Proposed branch: | lp:~pedronis/u1db/enforce-max-request-sizes |
Merge into: | lp:u1db |
Diff against target: |
416 lines (+121/-36) 2 files modified
u1db/remote/http_app.py (+26/-11) u1db/tests/test_http_app.py (+95/-25) |
To merge this branch: | bzr merge lp:~pedronis/u1db/enforce-max-request-sizes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel (community) | Approve | ||
Eric Casteleijn (community) | Approve | ||
Review via email: mp+104137@code.launchpad.net |
Commit message
Description of the change
add code to raise/return Bad Request if the overall request body size or single entries (for sync) are larger than supllied maxima
To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'u1db/remote/http_app.py' |
2 | --- u1db/remote/http_app.py 2012-04-18 18:53:08 +0000 |
3 | +++ u1db/remote/http_app.py 2012-04-30 15:54:31 +0000 |
4 | @@ -38,17 +38,19 @@ |
5 | ) |
6 | |
7 | |
8 | +class BadRequest(Exception): |
9 | + """Bad request.""" |
10 | + |
11 | + |
12 | class _FencedReader(object): |
13 | """Read and get lines from a file but not past a given length.""" |
14 | |
15 | MAXCHUNK = 8192 |
16 | |
17 | - # xxx do checks for maximum admissible total size |
18 | - # and maximum admissible line size |
19 | - |
20 | - def __init__(self, rfile, total): |
21 | + def __init__(self, rfile, total, max_entry_size): |
22 | self.rfile = rfile |
23 | self.remaining = total |
24 | + self.max_entry_size = max_entry_size |
25 | self._kept = None |
26 | |
27 | def read_chunk(self, atmost): |
28 | @@ -64,25 +66,28 @@ |
29 | |
30 | def getline(self): |
31 | line_parts = [] |
32 | + size = 0 |
33 | while True: |
34 | chunk = self.read_chunk(self.MAXCHUNK) |
35 | if chunk == '': |
36 | break |
37 | nl = chunk.find("\n") |
38 | if nl != -1: |
39 | + size += nl + 1 |
40 | + if size > self.max_entry_size: |
41 | + raise BadRequest |
42 | line_parts.append(chunk[:nl + 1]) |
43 | rest = chunk[nl + 1:] |
44 | self._kept = rest or None |
45 | break |
46 | else: |
47 | + size += len(chunk) |
48 | + if size > self.max_entry_size: |
49 | + raise BadRequest |
50 | line_parts.append(chunk) |
51 | return ''.join(line_parts) |
52 | |
53 | |
54 | -class BadRequest(Exception): |
55 | - """Bad request.""" |
56 | - |
57 | - |
58 | def http_method(**control): |
59 | """Decoration for handling of query arguments and content for a HTTP method. |
60 | |
61 | @@ -389,9 +394,11 @@ |
62 | class HTTPInvocationByMethodWithBody(object): |
63 | """Invoke methods on a resource.""" |
64 | |
65 | - def __init__(self, resource, environ): |
66 | + def __init__(self, resource, environ, parameters): |
67 | self.resource = resource |
68 | self.environ = environ |
69 | + self.max_request_size = parameters.max_request_size |
70 | + self.max_entry_size = parameters.max_entry_size |
71 | |
72 | def _lookup(self, method): |
73 | try: |
74 | @@ -419,7 +426,10 @@ |
75 | raise BadRequest |
76 | if content_length <= 0: |
77 | raise BadRequest |
78 | - reader = _FencedReader(self.environ['wsgi.input'], content_length) |
79 | + if content_length > self.max_request_size: |
80 | + raise BadRequest |
81 | + reader = _FencedReader(self.environ['wsgi.input'], content_length, |
82 | + self.max_entry_size) |
83 | content_type = self.environ.get('CONTENT_TYPE') |
84 | if content_type == 'application/json': |
85 | meth = self._lookup(method) |
86 | @@ -453,6 +463,11 @@ |
87 | |
88 | class HTTPApp(object): |
89 | |
90 | + # maximum allowed request body size |
91 | + max_request_size = 15 * 1024 * 1024 # 15Mb |
92 | + # maximum allowed entry/line size in request body |
93 | + max_entry_size = 10 * 1024 * 1024 # 10Mb |
94 | + |
95 | def __init__(self, state): |
96 | self.state = state |
97 | |
98 | @@ -468,7 +483,7 @@ |
99 | self.request_begin(environ) |
100 | try: |
101 | resource = self._lookup_resource(environ, responder) |
102 | - HTTPInvocationByMethodWithBody(resource, environ)() |
103 | + HTTPInvocationByMethodWithBody(resource, environ, self)() |
104 | except errors.U1DBError, e: |
105 | self.request_u1db_error(environ, e) |
106 | status = http_errors.wire_description_to_status.get( |
107 | |
108 | === modified file 'u1db/tests/test_http_app.py' |
109 | --- u1db/tests/test_http_app.py 2012-04-24 13:40:36 +0000 |
110 | +++ u1db/tests/test_http_app.py 2012-04-30 15:54:31 +0000 |
111 | @@ -36,12 +36,12 @@ |
112 | class TestFencedReader(tests.TestCase): |
113 | |
114 | def test_init(self): |
115 | - reader = http_app._FencedReader(StringIO.StringIO(""), 25) |
116 | + reader = http_app._FencedReader(StringIO.StringIO(""), 25, 100) |
117 | self.assertEqual(25, reader.remaining) |
118 | |
119 | def test_read_chunk(self): |
120 | inp = StringIO.StringIO("abcdef") |
121 | - reader = http_app._FencedReader(inp, 5) |
122 | + reader = http_app._FencedReader(inp, 5, 10) |
123 | data = reader.read_chunk(2) |
124 | self.assertEqual("ab", data) |
125 | self.assertEqual(2, inp.tell()) |
126 | @@ -49,7 +49,7 @@ |
127 | |
128 | def test_read_chunk_remaining(self): |
129 | inp = StringIO.StringIO("abcdef") |
130 | - reader = http_app._FencedReader(inp, 4) |
131 | + reader = http_app._FencedReader(inp, 4, 10) |
132 | data = reader.read_chunk(9999) |
133 | self.assertEqual("abcd", data) |
134 | self.assertEqual(4, inp.tell()) |
135 | @@ -57,7 +57,7 @@ |
136 | |
137 | def test_read_chunk_nothing_left(self): |
138 | inp = StringIO.StringIO("abc") |
139 | - reader = http_app._FencedReader(inp, 2) |
140 | + reader = http_app._FencedReader(inp, 2, 10) |
141 | reader.read_chunk(2) |
142 | self.assertEqual(2, inp.tell()) |
143 | self.assertEqual(0, reader.remaining) |
144 | @@ -68,7 +68,7 @@ |
145 | |
146 | def test_read_chunk_kept(self): |
147 | inp = StringIO.StringIO("abcde") |
148 | - reader = http_app._FencedReader(inp, 4) |
149 | + reader = http_app._FencedReader(inp, 4, 10) |
150 | reader._kept = "xyz" |
151 | data = reader.read_chunk(2) # atmost ignored |
152 | self.assertEqual("xyz", data) |
153 | @@ -78,7 +78,7 @@ |
154 | |
155 | def test_getline(self): |
156 | inp = StringIO.StringIO("abc\r\nde") |
157 | - reader = http_app._FencedReader(inp, 6) |
158 | + reader = http_app._FencedReader(inp, 6, 10) |
159 | reader.MAXCHUNK = 6 |
160 | line = reader.getline() |
161 | self.assertEqual("abc\r\n", line) |
162 | @@ -86,7 +86,7 @@ |
163 | |
164 | def test_getline_exact(self): |
165 | inp = StringIO.StringIO("abcd\r\nef") |
166 | - reader = http_app._FencedReader(inp, 6) |
167 | + reader = http_app._FencedReader(inp, 6, 10) |
168 | reader.MAXCHUNK = 6 |
169 | line = reader.getline() |
170 | self.assertEqual("abcd\r\n", line) |
171 | @@ -94,14 +94,14 @@ |
172 | |
173 | def test_getline_no_newline(self): |
174 | inp = StringIO.StringIO("abcd") |
175 | - reader = http_app._FencedReader(inp, 4) |
176 | + reader = http_app._FencedReader(inp, 4, 10) |
177 | reader.MAXCHUNK = 6 |
178 | line = reader.getline() |
179 | self.assertEqual("abcd", line) |
180 | |
181 | def test_getline_many_chunks(self): |
182 | inp = StringIO.StringIO("abcde\r\nf") |
183 | - reader = http_app._FencedReader(inp, 8) |
184 | + reader = http_app._FencedReader(inp, 8, 10) |
185 | reader.MAXCHUNK = 4 |
186 | line = reader.getline() |
187 | self.assertEqual("abcde\r\n", line) |
188 | @@ -111,7 +111,7 @@ |
189 | |
190 | def test_getline_empty(self): |
191 | inp = StringIO.StringIO("") |
192 | - reader = http_app._FencedReader(inp, 0) |
193 | + reader = http_app._FencedReader(inp, 0, 10) |
194 | reader.MAXCHUNK = 4 |
195 | line = reader.getline() |
196 | self.assertEqual("", line) |
197 | @@ -120,13 +120,25 @@ |
198 | |
199 | def test_getline_just_newline(self): |
200 | inp = StringIO.StringIO("\r\n") |
201 | - reader = http_app._FencedReader(inp, 2) |
202 | + reader = http_app._FencedReader(inp, 2, 10) |
203 | reader.MAXCHUNK = 4 |
204 | line = reader.getline() |
205 | self.assertEqual("\r\n", line) |
206 | line = reader.getline() |
207 | self.assertEqual("", line) |
208 | |
209 | + def test_getline_too_large(self): |
210 | + inp = StringIO.StringIO("x" * 50) |
211 | + reader = http_app._FencedReader(inp, 50, 25) |
212 | + reader.MAXCHUNK = 4 |
213 | + self.assertRaises(http_app.BadRequest, reader.getline) |
214 | + |
215 | + def test_getline_too_large_complete(self): |
216 | + inp = StringIO.StringIO("x" * 25 + "\r\n") |
217 | + reader = http_app._FencedReader(inp, 50, 25) |
218 | + reader.MAXCHUNK = 4 |
219 | + self.assertRaises(http_app.BadRequest, reader.getline) |
220 | + |
221 | |
222 | class TestHTTPMethodDecorator(tests.TestCase): |
223 | |
224 | @@ -231,12 +243,18 @@ |
225 | return "Put/end" |
226 | |
227 | |
228 | +class parameters: |
229 | + max_request_size = 200000 |
230 | + max_entry_size = 100000 |
231 | + |
232 | + |
233 | class TestHTTPInvocationByMethodWithBody(tests.TestCase): |
234 | |
235 | def test_get(self): |
236 | resource = TestResource() |
237 | environ = {'QUERY_STRING': 'a=1&b=2', 'REQUEST_METHOD': 'GET'} |
238 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
239 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
240 | + parameters) |
241 | res = invoke() |
242 | self.assertEqual('Get', res) |
243 | self.assertEqual({'a': '1', 'b': '2'}, resource.args) |
244 | @@ -248,7 +266,8 @@ |
245 | 'wsgi.input': StringIO.StringIO(body), |
246 | 'CONTENT_LENGTH': str(len(body)), |
247 | 'CONTENT_TYPE': 'application/json'} |
248 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
249 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
250 | + parameters) |
251 | res = invoke() |
252 | self.assertEqual('Put', res) |
253 | self.assertEqual({'a': '1'}, resource.args) |
254 | @@ -267,7 +286,8 @@ |
255 | 'wsgi.input': StringIO.StringIO(body), |
256 | 'CONTENT_LENGTH': str(len(body)), |
257 | 'CONTENT_TYPE': 'application/x-u1db-sync-stream'} |
258 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
259 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
260 | + parameters) |
261 | res = invoke() |
262 | self.assertEqual('Put/end', res) |
263 | self.assertEqual({'a': '1', 'b': 2}, resource.args) |
264 | @@ -280,7 +300,8 @@ |
265 | 'wsgi.input': StringIO.StringIO(body), |
266 | 'CONTENT_LENGTH': str(len(body)), |
267 | 'CONTENT_TYPE': 'application/x-u1db-sync-stream'} |
268 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
269 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
270 | + parameters) |
271 | res = invoke() |
272 | |
273 | def test_put_sync_stream_wrong_start(self): |
274 | @@ -320,7 +341,8 @@ |
275 | 'wsgi.input': StringIO.StringIO('{}'), |
276 | 'CONTENT_LENGTH': '2', |
277 | 'CONTENT_TYPE': 'application/json'} |
278 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
279 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
280 | + parameters) |
281 | self.assertRaises(http_app.BadRequest, invoke) |
282 | |
283 | def test_bad_request_unsupported_content_type(self): |
284 | @@ -329,7 +351,21 @@ |
285 | 'wsgi.input': StringIO.StringIO('{}'), |
286 | 'CONTENT_LENGTH': '2', |
287 | 'CONTENT_TYPE': 'text/plain'} |
288 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
289 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
290 | + parameters) |
291 | + self.assertRaises(http_app.BadRequest, invoke) |
292 | + |
293 | + def test_bad_request_content_length_too_large(self): |
294 | + resource = TestResource() |
295 | + environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', |
296 | + 'wsgi.input': StringIO.StringIO('{}'), |
297 | + 'CONTENT_LENGTH': '10000', |
298 | + 'CONTENT_TYPE': 'text/plain'} |
299 | + class params: |
300 | + max_request_size = 5000 |
301 | + max_entry_size = sys.maxint # we don't get to use this |
302 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
303 | + params) |
304 | self.assertRaises(http_app.BadRequest, invoke) |
305 | |
306 | def test_bad_request_no_content_length(self): |
307 | @@ -337,7 +373,8 @@ |
308 | environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'PUT', |
309 | 'wsgi.input': StringIO.StringIO('a'), |
310 | 'CONTENT_TYPE': 'application/json'} |
311 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
312 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
313 | + parameters) |
314 | self.assertRaises(http_app.BadRequest, invoke) |
315 | |
316 | def test_bad_request_invalid_content_length(self): |
317 | @@ -346,7 +383,8 @@ |
318 | 'wsgi.input': StringIO.StringIO('abc'), |
319 | 'CONTENT_LENGTH': '1unk', |
320 | 'CONTENT_TYPE': 'application/json'} |
321 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
322 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
323 | + parameters) |
324 | self.assertRaises(http_app.BadRequest, invoke) |
325 | |
326 | def test_bad_request_empty_body(self): |
327 | @@ -355,12 +393,14 @@ |
328 | 'wsgi.input': StringIO.StringIO(''), |
329 | 'CONTENT_LENGTH': '0', |
330 | 'CONTENT_TYPE': 'application/json'} |
331 | - invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ) |
332 | + invoke = http_app.HTTPInvocationByMethodWithBody(resource, environ, |
333 | + parameters) |
334 | self.assertRaises(http_app.BadRequest, invoke) |
335 | |
336 | def test_bad_request_unsupported_method_get_like(self): |
337 | environ = {'QUERY_STRING': '', 'REQUEST_METHOD': 'DELETE'} |
338 | - invoke = http_app.HTTPInvocationByMethodWithBody(None, environ) |
339 | + invoke = http_app.HTTPInvocationByMethodWithBody(None, environ, |
340 | + parameters) |
341 | self.assertRaises(http_app.BadRequest, invoke) |
342 | |
343 | def test_bad_request_unsupported_method_put_like(self): |
344 | @@ -368,7 +408,8 @@ |
345 | 'wsgi.input': StringIO.StringIO('{}'), |
346 | 'CONTENT_LENGTH': '2', |
347 | 'CONTENT_TYPE': 'application/json'} |
348 | - invoke = http_app.HTTPInvocationByMethodWithBody(None, environ) |
349 | + invoke = http_app.HTTPInvocationByMethodWithBody(None, environ, |
350 | + parameters) |
351 | self.assertRaises(http_app.BadRequest, invoke) |
352 | |
353 | def test_bad_request_unsupported_method_put_like_multi_json(self): |
354 | @@ -377,7 +418,8 @@ |
355 | 'wsgi.input': StringIO.StringIO(body), |
356 | 'CONTENT_LENGTH': str(len(body)), |
357 | 'CONTENT_TYPE': 'application/x-u1db-multi-json'} |
358 | - invoke = http_app.HTTPInvocationByMethodWithBody(None, environ) |
359 | + invoke = http_app.HTTPInvocationByMethodWithBody(None, environ, |
360 | + parameters) |
361 | self.assertRaises(http_app.BadRequest, invoke) |
362 | |
363 | |
364 | @@ -457,8 +499,8 @@ |
365 | def setUp(self): |
366 | super(TestHTTPApp, self).setUp() |
367 | self.state = tests.ServerStateForTests() |
368 | - application = http_app.HTTPApp(self.state) |
369 | - self.app = paste.fixture.TestApp(application) |
370 | + self.http_app = http_app.HTTPApp(self.state) |
371 | + self.app = paste.fixture.TestApp(self.http_app) |
372 | self.db0 = self.state._create_database('db0') |
373 | |
374 | def test_bad_request_broken(self): |
375 | @@ -545,6 +587,15 @@ |
376 | self.assertEqual('application/json', resp.header('content-type')) |
377 | self.assertEqual({'rev': doc.rev}, simplejson.loads(resp.body)) |
378 | |
379 | + def test_put_doc_too_large(self): |
380 | + self.http_app.max_request_size = 15000 |
381 | + doc = self.db0.create_doc('{"x": 1}', doc_id='doc1') |
382 | + resp = self.app.put('/db0/doc/doc1?old_rev=%s' % doc.rev, |
383 | + params='{"%s": 2}' % ('z' * 16000), |
384 | + headers={'content-type': 'application/json'}, |
385 | + expect_errors=True) |
386 | + self.assertEqual(400, resp.status) |
387 | + |
388 | def test_delete_doc(self): |
389 | doc = self.db0.create_doc('{"x": 1}', doc_id='doc1') |
390 | resp = self.app.delete('/db0/doc/doc1?old_rev=%s' % doc.rev) |
391 | @@ -646,6 +697,25 @@ |
392 | self.assertEqual('[\r\n{"new_generation": 2}\r\n]\r\n', resp.body) |
393 | self.assertEqual([('replica', 10), ('replica', 11)], gens) |
394 | |
395 | + def test_sync_exchange_send_entry_too_large(self): |
396 | + self.http_app.max_request_size = 20000 |
397 | + self.http_app.max_entry_size = 10000 |
398 | + entries = { |
399 | + 10: {'id': 'doc-here', 'rev': 'replica:1', 'content': |
400 | + '{"value": "%s"}' % ('H' * 11000), 'gen': 10}, |
401 | + } |
402 | + args = dict(last_known_generation=0) |
403 | + body = ("[\r\n" + |
404 | + "%s,\r\n" % simplejson.dumps(args) + |
405 | + "%s\r\n" % simplejson.dumps(entries[10]) + |
406 | + "]\r\n") |
407 | + resp = self.app.post('/db0/sync-from/replica', |
408 | + params=body, |
409 | + headers={'content-type': |
410 | + 'application/x-u1db-sync-stream'}, |
411 | + expect_errors=True) |
412 | + self.assertEqual(400, resp.status) |
413 | + |
414 | def test_sync_exchange_receive(self): |
415 | doc = self.db0.create_doc('{"value": "there"}') |
416 | doc2 = self.db0.create_doc('{"value": "there2"}') |
cool cool cool