Skip to content

Commit a35f3d7

Browse files
popravichasvetlov
authored andcommitted
[3.1] Make LineTooLong exception more detailed about actual data size (GH-2863)
(cherry picked from commit 6a60358) Co-authored-by: Alexey Popravka <popravich@users.noreply.github.com>
1 parent db7dcb8 commit a35f3d7

File tree

4 files changed

+136
-41
lines changed

4 files changed

+136
-41
lines changed

aiohttp/_http_parser.pyx

+6-5
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ cdef int cb_on_url(cparser.http_parser* parser,
370370
try:
371371
if length > pyparser._max_line_size:
372372
raise LineTooLong(
373-
'Status line is too long', pyparser._max_line_size)
373+
'Status line is too long', pyparser._max_line_size, length)
374374
pyparser._buf.extend(at[:length])
375375
except BaseException as ex:
376376
pyparser._last_error = ex
@@ -386,7 +386,7 @@ cdef int cb_on_status(cparser.http_parser* parser,
386386
try:
387387
if length > pyparser._max_line_size:
388388
raise LineTooLong(
389-
'Status line is too long', pyparser._max_line_size)
389+
'Status line is too long', pyparser._max_line_size, length)
390390
pyparser._buf.extend(at[:length])
391391
except BaseException as ex:
392392
pyparser._last_error = ex
@@ -402,7 +402,7 @@ cdef int cb_on_header_field(cparser.http_parser* parser,
402402
pyparser._on_status_complete()
403403
if length > pyparser._max_field_size:
404404
raise LineTooLong(
405-
'Header name is too long', pyparser._max_field_size)
405+
'Header name is too long', pyparser._max_field_size, length)
406406
pyparser._on_header_field(
407407
at[:length].decode('utf-8', 'surrogateescape'), at[:length])
408408
except BaseException as ex:
@@ -419,10 +419,11 @@ cdef int cb_on_header_value(cparser.http_parser* parser,
419419
if pyparser._header_value is not None:
420420
if len(pyparser._header_value) + length > pyparser._max_field_size:
421421
raise LineTooLong(
422-
'Header value is too long', pyparser._max_field_size)
422+
'Header value is too long', pyparser._max_field_size,
423+
len(pyparser._header_value) + length)
423424
elif length > pyparser._max_field_size:
424425
raise LineTooLong(
425-
'Header value is too long', pyparser._max_field_size)
426+
'Header value is too long', pyparser._max_field_size, length)
426427
pyparser._on_header_value(
427428
at[:length].decode('utf-8', 'surrogateescape'), at[:length])
428429
except BaseException as ex:

aiohttp/http_exceptions.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ class ContentLengthError(PayloadEncodingError):
5959

6060
class LineTooLong(BadHttpMessage):
6161

62-
def __init__(self, line, limit='Unknown'):
62+
def __init__(self, line, limit='Unknown', actual_size='Unknown'):
6363
super().__init__(
64-
"Got more than %s bytes when reading %s." % (limit, line))
64+
"Got more than %s bytes (%s) when reading %s." % (
65+
limit, actual_size, line))
6566

6667

6768
class InvalidHeader(BadHttpMessage):

aiohttp/http_parser.py

+27-17
Original file line numberDiff line numberDiff line change
@@ -256,17 +256,24 @@ def parse_headers(self, lines):
256256
line_count = len(lines)
257257

258258
while line:
259-
header_length = len(line)
260-
261259
# Parse initial header name : value pair.
262260
try:
263261
bname, bvalue = line.split(b':', 1)
264262
except ValueError:
265263
raise InvalidHeader(line) from None
266264

267265
bname = bname.strip(b' \t')
266+
bvalue = bvalue.lstrip()
268267
if HDRRE.search(bname):
269268
raise InvalidHeader(bname)
269+
if len(bname) > self.max_field_size:
270+
raise LineTooLong(
271+
"request header name {}".format(
272+
bname.decode("utf8", "xmlcharrefreplace")),
273+
self.max_field_size,
274+
len(bname))
275+
276+
header_length = len(bvalue)
270277

271278
# next line
272279
lines_idx += 1
@@ -283,7 +290,8 @@ def parse_headers(self, lines):
283290
raise LineTooLong(
284291
'request header field {}'.format(
285292
bname.decode("utf8", "xmlcharrefreplace")),
286-
self.max_field_size)
293+
self.max_field_size,
294+
header_length)
287295
bvalue.append(line)
288296

289297
# next line
@@ -301,7 +309,8 @@ def parse_headers(self, lines):
301309
raise LineTooLong(
302310
'request header field {}'.format(
303311
bname.decode("utf8", "xmlcharrefreplace")),
304-
self.max_field_size)
312+
self.max_field_size,
313+
header_length)
305314

306315
bvalue = bvalue.strip()
307316
name = bname.decode('utf-8', 'surrogateescape')
@@ -349,17 +358,17 @@ class HttpRequestParserPy(HttpParser):
349358
"""
350359

351360
def parse_message(self, lines):
352-
if len(lines[0]) > self.max_line_size:
353-
raise LineTooLong(
354-
'Status line is too long', self.max_line_size)
355-
356361
# request line
357362
line = lines[0].decode('utf-8', 'surrogateescape')
358363
try:
359364
method, path, version = line.split(None, 2)
360365
except ValueError:
361366
raise BadStatusLine(line) from None
362367

368+
if len(path) > self.max_line_size:
369+
raise LineTooLong(
370+
'Status line is too long', self.max_line_size, len(path))
371+
363372
# method
364373
method = method.upper()
365374
if not METHRE.match(method):
@@ -397,20 +406,21 @@ class HttpResponseParserPy(HttpParser):
397406
Returns RawResponseMessage"""
398407

399408
def parse_message(self, lines):
400-
if len(lines[0]) > self.max_line_size:
401-
raise LineTooLong(
402-
'Status line is too long', self.max_line_size)
403-
404409
line = lines[0].decode('utf-8', 'surrogateescape')
405410
try:
406411
version, status = line.split(None, 1)
407412
except ValueError:
408413
raise BadStatusLine(line) from None
409-
else:
410-
try:
411-
status, reason = status.split(None, 1)
412-
except ValueError:
413-
reason = ''
414+
415+
try:
416+
status, reason = status.split(None, 1)
417+
except ValueError:
418+
reason = ''
419+
420+
if len(reason) > self.max_line_size:
421+
raise LineTooLong(
422+
'Status line is too long', self.max_line_size,
423+
len(reason))
414424

415425
# version
416426
match = VERSRE.match(version)

tests/test_http_parser.py

+100-17
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ def protocol():
3838
@pytest.fixture(params=REQUEST_PARSERS)
3939
def parser(loop, protocol, request):
4040
"""Parser implementations"""
41-
return request.param(protocol, loop, 8190, 32768, 8190)
41+
return request.param(protocol, loop,
42+
max_line_size=8190,
43+
max_headers=32768,
44+
max_field_size=8190)
4245

4346

4447
@pytest.fixture(params=REQUEST_PARSERS)
@@ -50,7 +53,10 @@ def request_cls(request):
5053
@pytest.fixture(params=RESPONSE_PARSERS)
5154
def response(loop, protocol, request):
5255
"""Parser implementations"""
53-
return request.param(protocol, loop, 8190, 32768, 8190)
56+
return request.param(protocol, loop,
57+
max_line_size=8190,
58+
max_headers=32768,
59+
max_field_size=8190)
5460

5561

5662
@pytest.fixture(params=RESPONSE_PARSERS)
@@ -358,32 +364,82 @@ def test_invalid_name(parser):
358364
parser.feed_data(text)
359365

360366

361-
def test_max_header_field_size(parser):
362-
name = b'test' * 10 * 1024
367+
@pytest.mark.parametrize('size', [40960, 8191])
368+
def test_max_header_field_size(parser, size):
369+
name = b't' * size
363370
text = (b'GET /test HTTP/1.1\r\n' + name + b':data\r\n\r\n')
364371

365-
with pytest.raises(http_exceptions.LineTooLong):
372+
match = ("400, message='Got more than 8190 bytes \({}\) when reading"
373+
.format(size))
374+
with pytest.raises(http_exceptions.LineTooLong, match=match):
366375
parser.feed_data(text)
367376

368377

369-
def test_max_header_value_size(parser):
370-
name = b'test' * 10 * 1024
378+
def test_max_header_field_size_under_limit(parser):
379+
name = b't' * 8190
380+
text = (b'GET /test HTTP/1.1\r\n' + name + b':data\r\n\r\n')
381+
382+
messages, upgrade, tail = parser.feed_data(text)
383+
msg = messages[0][0]
384+
assert msg == (
385+
'GET', '/test', (1, 1),
386+
CIMultiDict({name.decode(): 'data'}),
387+
((name, b'data'),),
388+
False, None, False, False, URL('/test'))
389+
390+
391+
@pytest.mark.parametrize('size', [40960, 8191])
392+
def test_max_header_value_size(parser, size):
393+
name = b't' * size
371394
text = (b'GET /test HTTP/1.1\r\n'
372395
b'data:' + name + b'\r\n\r\n')
373396

374-
with pytest.raises(http_exceptions.LineTooLong):
397+
match = ("400, message='Got more than 8190 bytes \({}\) when reading"
398+
.format(size))
399+
with pytest.raises(http_exceptions.LineTooLong, match=match):
375400
parser.feed_data(text)
376401

377402

378-
def test_max_header_value_size_continuation(parser):
379-
name = b'test' * 10 * 1024
403+
def test_max_header_value_size_under_limit(parser):
404+
value = b'A' * 8190
405+
text = (b'GET /test HTTP/1.1\r\n'
406+
b'data:' + value + b'\r\n\r\n')
407+
408+
messages, upgrade, tail = parser.feed_data(text)
409+
msg = messages[0][0]
410+
assert msg == (
411+
'GET', '/test', (1, 1),
412+
CIMultiDict({'data': value.decode()}),
413+
((b'data', value),),
414+
False, None, False, False, URL('/test'))
415+
416+
417+
@pytest.mark.parametrize('size', [40965, 8191])
418+
def test_max_header_value_size_continuation(parser, size):
419+
name = b'T' * (size - 5)
380420
text = (b'GET /test HTTP/1.1\r\n'
381421
b'data: test\r\n ' + name + b'\r\n\r\n')
382422

383-
with pytest.raises(http_exceptions.LineTooLong):
423+
match = ("400, message='Got more than 8190 bytes \({}\) when reading"
424+
.format(size))
425+
with pytest.raises(http_exceptions.LineTooLong, match=match):
384426
parser.feed_data(text)
385427

386428

429+
def test_max_header_value_size_continuation_under_limit(parser):
430+
value = b'A' * 8185
431+
text = (b'GET /test HTTP/1.1\r\n'
432+
b'data: test\r\n ' + value + b'\r\n\r\n')
433+
434+
messages, upgrade, tail = parser.feed_data(text)
435+
msg = messages[0][0]
436+
assert msg == (
437+
'GET', '/test', (1, 1),
438+
CIMultiDict({'data': 'test ' + value.decode()}),
439+
((b'data', b'test ' + value),),
440+
False, None, False, False, URL('/test'))
441+
442+
387443
def test_http_request_parser(parser):
388444
text = b'GET /path HTTP/1.1\r\n\r\n'
389445
messages, upgrade, tail = parser.feed_data(text)
@@ -452,10 +508,23 @@ def test_http_request_parser_bad_version(parser):
452508
parser.feed_data(b'GET //get HT/11\r\n\r\n')
453509

454510

455-
def test_http_request_max_status_line(parser):
456-
with pytest.raises(http_exceptions.LineTooLong):
511+
@pytest.mark.parametrize('size', [40965, 8191])
512+
def test_http_request_max_status_line(parser, size):
513+
path = b't' * (size - 5)
514+
match = ("400, message='Got more than 8190 bytes \({}\) when reading"
515+
.format(size))
516+
with pytest.raises(http_exceptions.LineTooLong, match=match):
457517
parser.feed_data(
458-
b'GET /path' + b'test' * 10 * 1024 + b' HTTP/1.1\r\n\r\n')
518+
b'GET /path' + path + b' HTTP/1.1\r\n\r\n')
519+
520+
521+
def test_http_request_max_status_line_under_limit(parser):
522+
path = b't' * (8190 - 5)
523+
messages, upgraded, tail = parser.feed_data(
524+
b'GET /path' + path + b' HTTP/1.1\r\n\r\n')
525+
msg = messages[0][0]
526+
assert msg == ('GET', '/path' + path.decode(), (1, 1), CIMultiDict(), (),
527+
False, None, False, False, URL('/path' + path.decode()))
459528

460529

461530
def test_http_response_parser_utf8(response):
@@ -474,10 +543,24 @@ def test_http_response_parser_utf8(response):
474543
assert not tail
475544

476545

477-
def test_http_response_parser_bad_status_line_too_long(response):
478-
with pytest.raises(http_exceptions.LineTooLong):
546+
@pytest.mark.parametrize('size', [40962, 8191])
547+
def test_http_response_parser_bad_status_line_too_long(response, size):
548+
reason = b't' * (size - 2)
549+
match = ("400, message='Got more than 8190 bytes \({}\) when reading"
550+
.format(size))
551+
with pytest.raises(http_exceptions.LineTooLong, match=match):
479552
response.feed_data(
480-
b'HTTP/1.1 200 Ok' + b'test' * 10 * 1024 + b'\r\n\r\n')
553+
b'HTTP/1.1 200 Ok' + reason + b'\r\n\r\n')
554+
555+
556+
def test_http_response_parser_status_line_under_limit(response):
557+
reason = b'O' * 8190
558+
messages, upgraded, tail = response.feed_data(
559+
b'HTTP/1.1 200 ' + reason + b'\r\n\r\n')
560+
msg = messages[0][0]
561+
assert msg.version == (1, 1)
562+
assert msg.code == 200
563+
assert msg.reason == reason.decode()
481564

482565

483566
def test_http_response_parser_bad_version(response):

0 commit comments

Comments
 (0)