Merge lp:~pedronis/u1db/enforce-max-request-sizes into lp:u1db

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
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+104137@code.launchpad.net

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
Eric Casteleijn (thisfred) wrote :

cool cool cool

review: Approve
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"}')

Subscribers

People subscribed via source and target branches