1 # -*- encoding: binary -*-
2 # frozen_string_literal: false
4 require './test/test_helper'
9 class HttpParserNgTest < Test::Unit::TestCase
12 @parser = HttpParser.new
15 # RFC 7230 allows gzip/deflate/compress Transfer-Encoding,
16 # but "chunked" must be last if used
18 [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x|
19 assert_raise(HttpParserError) { HttpParser.is_chunked?(x) }
21 [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x|
22 assert HttpParser.is_chunked?(x)
24 [ 'gzip', 'xhunked', 'xchunked' ].each do |x|
25 assert !HttpParser.is_chunked?(x)
29 def test_parser_max_len
30 assert_raises(RangeError) do
31 HttpParser.max_header_len = 0xffffffff + 1
36 r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
39 @parser.response_start_sent = true
40 assert @parser.keepalive?
42 assert @parser.response_start_sent
44 # persistent client makes another request:
47 assert @parser.keepalive?
49 assert_equal false, @parser.response_start_sent
52 def test_response_start_sent
53 assert_equal false, @parser.response_start_sent, "default is false"
54 @parser.response_start_sent = true
55 assert_equal true, @parser.response_start_sent
56 @parser.response_start_sent = false
57 assert_equal false, @parser.response_start_sent
58 @parser.response_start_sent = true
60 assert_equal false, @parser.response_start_sent
63 def test_connection_TE
64 @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: TE\r\n"
65 @parser.buf << "TE: trailers\r\n\r\n"
67 assert @parser.keepalive?
71 def test_keepalive_requests_with_next?
72 req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".freeze
74 "SERVER_NAME" => "example.com",
75 "HTTP_HOST" => "example.com",
76 "rack.url_scheme" => "http",
77 "REQUEST_PATH" => "/",
78 "SERVER_PROTOCOL" => "HTTP/1.1",
80 "HTTP_VERSION" => "HTTP/1.1",
82 "SERVER_PORT" => "80",
83 "REQUEST_METHOD" => "GET",
88 assert_equal expect, @parser.parse
93 def test_default_keepalive_is_off
94 assert ! @parser.keepalive?
95 assert ! @parser.next?
96 @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
98 assert @parser.keepalive?
100 assert ! @parser.keepalive?
101 assert ! @parser.next?
104 def test_identity_byte_headers
106 str = "PUT / HTTP/1.1\r\n"
107 str << "Content-Length: 123\r\n"
110 str.each_byte { |byte|
112 assert_nil @parser.parse
115 assert_equal req.object_id, @parser.parse.object_id
116 assert_equal '123', req['CONTENT_LENGTH']
117 assert_equal 0, hdr.size
118 assert ! @parser.keepalive?
119 assert @parser.headers?
120 assert_equal 123, @parser.content_length
123 @parser.filter_body(dst, buf)
124 assert_equal '.' * 123, dst
126 assert @parser.keepalive?
129 def test_identity_step_headers
132 str << "PUT / HTTP/1.1\r\n"
133 assert ! @parser.parse
134 str << "Content-Length: 123\r\n"
135 assert ! @parser.parse
137 assert_equal req.object_id, @parser.parse.object_id
138 assert_equal '123', req['CONTENT_LENGTH']
139 assert_equal 0, str.size
140 assert ! @parser.keepalive?
141 assert @parser.headers?
144 @parser.filter_body(dst, buf)
145 assert_equal '.' * 123, dst
147 assert @parser.keepalive?
150 def test_identity_oneshot_header
153 str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n"
154 assert_equal req.object_id, @parser.parse.object_id
155 assert_equal '123', req['CONTENT_LENGTH']
156 assert_equal 0, str.size
157 assert ! @parser.keepalive?
158 assert @parser.headers?
161 @parser.filter_body(dst, buf)
162 assert_equal '.' * 123, dst
166 def test_identity_oneshot_header_with_body
167 body = ('a' * 123).freeze
170 str << "PUT / HTTP/1.1\r\n" \
171 "Content-Length: #{body.length}\r\n" \
173 assert_equal req.object_id, @parser.parse.object_id
174 assert_equal '123', req['CONTENT_LENGTH']
175 assert_equal 123, str.size
176 assert_equal body, str
178 assert_nil @parser.filter_body(tmp, str)
179 assert_equal 0, str.size
180 assert_equal tmp, body
181 assert_equal "", @parser.filter_body(tmp, str)
182 assert @parser.keepalive?
185 def test_identity_oneshot_header_with_body_partial
187 str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na"
188 assert_equal Hash, @parser.parse.class
189 assert_equal 1, str.size
190 assert_equal 'a', str
192 assert_nil @parser.filter_body(tmp, str)
194 assert_equal "a", tmp
196 rv = @parser.filter_body(tmp, str)
197 assert_equal 122, tmp.size
200 assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
201 assert @parser.keepalive?
204 def test_identity_oneshot_header_with_body_slop
206 str << "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG"
207 assert_equal Hash, @parser.parse.class
208 assert_equal 2, str.size
209 assert_equal 'aG', str
211 assert_nil @parser.filter_body(tmp, str)
212 assert_equal "G", str
213 assert_equal "G", @parser.filter_body(tmp, str)
214 assert_equal 1, tmp.size
215 assert_equal "a", tmp
216 assert @parser.keepalive?
222 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
223 assert_equal req, @parser.parse, "msg=#{str}"
224 assert_equal 0, str.size
226 assert_nil @parser.filter_body(tmp, str << "6")
227 assert_equal 0, tmp.size
228 assert_nil @parser.filter_body(tmp, str << "\r\n")
229 assert_equal 0, str.size
230 assert_equal 0, tmp.size
232 assert_nil @parser.filter_body(tmp, str << "..")
233 assert_equal "..", tmp
234 assert_nil @parser.filter_body(tmp, str << "abcd\r\n0\r\n")
235 assert_equal "abcd", tmp
236 assert_equal str.object_id, @parser.filter_body(tmp, str << "PUT").object_id
237 assert_equal "PUT", str
238 assert ! @parser.keepalive?
239 str << "TY: FOO\r\n\r\n"
240 assert_equal req, @parser.parse
241 assert_equal "FOO", req["HTTP_PUTTY"]
242 assert @parser.keepalive?
245 def test_chunked_empty
248 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
249 assert_equal req, @parser.parse, "msg=#{str}"
250 assert_equal 0, str.size
252 assert_equal str, @parser.filter_body(tmp, str << "0\r\n\r\n")
258 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
260 assert_equal req, @parser.parse
261 assert_equal 0, str.size
263 assert_nil @parser.filter_body(tmp, str << "6")
264 assert_equal 0, tmp.size
265 assert_nil @parser.filter_body(tmp, str << "\r\n")
267 assert_equal 0, tmp.size
269 assert_nil @parser.filter_body(tmp, str << "..")
270 assert_equal 2, tmp.size
271 assert_equal "..", tmp
272 assert_nil @parser.filter_body(tmp, str << "abcd\r\n1")
273 assert_equal "abcd", tmp
274 assert_nil @parser.filter_body(tmp, str << "\r")
276 assert_nil @parser.filter_body(tmp, str << "\n")
278 assert_nil @parser.filter_body(tmp, str << "z")
279 assert_equal "z", tmp
280 assert_nil @parser.filter_body(tmp, str << "\r\n")
281 assert_nil @parser.filter_body(tmp, str << "0")
282 assert_nil @parser.filter_body(tmp, str << "\r")
283 rv = @parser.filter_body(tmp, str << "\nGET")
284 assert_equal "GET", rv
285 assert_equal str.object_id, rv.object_id
286 assert ! @parser.keepalive?
291 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
294 assert_equal req, @parser.parse
296 assert_nil @parser.filter_body(tmp, str)
299 assert_nil @parser.filter_body(tmp, str)
302 assert_nil @parser.filter_body(tmp, str)
304 assert ! @parser.body_eof?
305 assert_equal "", @parser.filter_body(tmp, str << "\r\n0\r\n")
307 assert @parser.body_eof?
309 assert_equal req, @parser.parse
311 assert @parser.body_eof?
312 assert @parser.keepalive?
315 def test_two_chunks_oneshot
318 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
319 "1\r\na\r\n2\r\n..\r\n0\r\n"
320 assert_equal req, @parser.parse
322 assert_nil @parser.filter_body(tmp, str)
323 assert_equal 'a..', tmp
324 rv = @parser.filter_body(tmp, str)
325 assert_equal rv.object_id, str.object_id
326 assert ! @parser.keepalive?
329 def test_chunks_bytewise
330 chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n"
331 str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
335 assert_equal req, @parser.parse
340 str.each_byte { |byte|
341 assert_nil @parser.filter_body(tmp, buf << byte.chr)
344 assert_equal 'abcdefghijklmnop0123456789abcdefg', body
345 rv = @parser.filter_body(tmp, buf<< "\n")
346 assert_equal rv.object_id, buf.object_id
347 assert ! @parser.keepalive?
353 str << "PUT / HTTP/1.1\r\n" \
354 "Trailer: Content-MD5\r\n" \
355 "transfer-Encoding: chunked\r\n\r\n" \
356 "1\r\na\r\n2\r\n..\r\n0\r\n"
357 assert_equal req, @parser.parse
358 assert_equal 'Content-MD5', req['HTTP_TRAILER']
359 assert_nil req['HTTP_CONTENT_MD5']
361 assert_nil @parser.filter_body(tmp, str)
362 assert_equal 'a..', tmp
363 md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
364 rv = @parser.filter_body(tmp, str)
365 assert_equal rv.object_id, str.object_id
367 md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
369 assert_nil @parser.trailers(req, str)
370 assert_equal md5_b64, req['HTTP_CONTENT_MD5']
371 assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
373 assert_nil @parser.parse
375 assert_equal req, @parser.parse
376 assert_equal "GET / ", str
377 assert @parser.keepalive?
380 def test_trailers_slowly
382 str << "PUT / HTTP/1.1\r\n" \
383 "Trailer: Content-MD5\r\n" \
384 "transfer-Encoding: chunked\r\n\r\n" \
385 "1\r\na\r\n2\r\n..\r\n0\r\n"
387 assert_equal req, @parser.parse
388 assert_equal 'Content-MD5', req['HTTP_TRAILER']
389 assert_nil req['HTTP_CONTENT_MD5']
391 assert_nil @parser.filter_body(tmp, str)
392 assert_equal 'a..', tmp
393 md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
394 rv = @parser.filter_body(tmp, str)
395 assert_equal rv.object_id, str.object_id
397 assert_nil @parser.trailers(req, str)
398 md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
399 md5_hdr.each_byte { |byte|
401 assert_nil @parser.trailers(req, str)
403 assert_equal md5_b64, req['HTTP_CONTENT_MD5']
404 assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
406 assert_nil @parser.parse
408 assert_equal req, @parser.parse
413 str << "PUT / HTTP/1.1\r\n" \
414 "transfer-Encoding: chunked\r\n\r\n" \
415 "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
417 assert_equal req, @parser.parse
418 assert_nil @parser.content_length
419 @parser.filter_body('', str)
420 assert ! @parser.keepalive?
424 n = HttpParser::LENGTH_MAX
425 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
427 @parser.headers(req, @parser.buf)
428 assert_equal n, req['CONTENT_LENGTH'].to_i
429 assert ! @parser.keepalive?
432 def test_overflow_chunk
433 n = HttpParser::CHUNK_MAX + 1
436 str << "PUT / HTTP/1.1\r\n" \
437 "transfer-Encoding: chunked\r\n\r\n" \
438 "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
439 assert_equal req, @parser.parse
440 assert_nil @parser.content_length
441 assert_raise(HttpParserError) { @parser.filter_body('', str) }
444 def test_overflow_content_length
445 n = HttpParser::LENGTH_MAX + 1
446 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
447 assert_raise(HttpParserError) { @parser.parse }
451 @parser.buf << "PUT / HTTP/1.1\r\n" \
452 "transfer-Encoding: chunked\r\n\r\n" \
453 "#zzz\r\na\r\n2\r\n..\r\n0\r\n"
455 assert_equal req, @parser.parse
456 assert_nil @parser.content_length
457 assert_raise(HttpParserError) { @parser.filter_body("", @parser.buf) }
460 def test_bad_content_length
461 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
462 assert_raise(HttpParserError) { @parser.parse }
465 def test_bad_trailers
468 str << "PUT / HTTP/1.1\r\n" \
469 "Trailer: Transfer-Encoding\r\n" \
470 "transfer-Encoding: chunked\r\n\r\n" \
471 "1\r\na\r\n2\r\n..\r\n0\r\n"
472 assert_equal req, @parser.parse
473 assert_equal 'Transfer-Encoding', req['HTTP_TRAILER']
475 assert_nil @parser.filter_body(tmp, str)
476 assert_equal 'a..', tmp
478 str << "Transfer-Encoding: identity\r\n\r\n"
479 assert_raise(HttpParserError) { @parser.parse }
482 def test_repeat_headers
483 str = "PUT / HTTP/1.1\r\n" \
484 "Trailer: Content-MD5\r\n" \
485 "Trailer: Content-SHA1\r\n" \
486 "transfer-Encoding: chunked\r\n\r\n" \
487 "1\r\na\r\n2\r\n..\r\n0\r\n"
490 assert_equal req, @parser.parse
491 assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER']
492 assert ! @parser.keepalive?
495 def test_parse_simple_request
496 parser = HttpParser.new
498 parser.buf << "GET /read-rfc1945-if-you-dont-believe-me\r\n"
499 assert_equal req, parser.parse
500 assert_equal '', parser.buf
502 "SERVER_NAME"=>"localhost",
503 "rack.url_scheme"=>"http",
504 "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me",
505 "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me",
506 "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me",
508 "SERVER_PROTOCOL"=>"HTTP/0.9",
509 "REQUEST_METHOD"=>"GET",
512 assert_equal expect, req
513 assert ! parser.headers?
516 def test_path_info_semicolon
520 str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n"
522 "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
523 "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
524 "/1;a=b" => { qs => "", pi => "/1;a=b" },
525 "/1;a=b?" => { qs => "", pi => "/1;a=b" },
526 "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
527 "*" => { qs => "", pi => "" },
528 }.each do |uri,expect|
529 assert_equal req, @parser.headers(req.clear, str % [ uri ])
532 assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
533 assert_equal expect[qs], req[qs], "#{qs} mismatch"
534 assert_equal expect[pi], req[pi], "#{pi} mismatch"
536 uri = URI.parse("http://example.com#{uri}")
537 assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees"
538 assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees"
542 def test_path_info_semicolon_absolute
546 str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
548 "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
549 "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
550 "/1;a=b" => { qs => "", pi => "/1;a=b" },
551 "/1;a=b?" => { qs => "", pi => "/1;a=b" },
552 "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
553 }.each do |uri,expect|
554 assert_equal req, @parser.headers(req.clear, str % [ uri ])
557 assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
558 assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch"
559 assert_equal expect[qs], req[qs], "#{qs} mismatch"
560 assert_equal expect[pi], req[pi], "#{pi} mismatch"
564 def test_negative_content_length
566 str = "PUT / HTTP/1.1\r\n" \
567 "Content-Length: -1\r\n" \
569 assert_raises(HttpParserError) do
570 @parser.headers(req, str)
574 def test_invalid_content_length
576 str = "PUT / HTTP/1.1\r\n" \
577 "Content-Length: zzzzz\r\n" \
579 assert_raises(HttpParserError) do
580 @parser.headers(req, str)
584 def test_duplicate_content_length
585 str = "PUT / HTTP/1.1\r\n" \
586 "Content-Length: 1\r\n" \
587 "Content-Length: 9\r\n" \
589 assert_raises(HttpParserError) { @parser.headers({}, str) }
592 def test_chunked_overrides_content_length
593 order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ]
595 str = "PUT /#{x} HTTP/1.1\r\n" \
596 "#{order.join("\r\n")}" \
597 "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n"
599 env = @parser.headers({}, str)
600 assert_nil @parser.content_length
601 assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING']
602 assert_equal '666', env['CONTENT_LENGTH'],
603 'Content-Length logged so the app can log a possible client bug/attack'
604 @parser.filter_body(dst = '', str)
605 assert_equal 'helloworld', dst
606 @parser.parse # handle the non-existent trailer
611 def test_chunked_order_good
612 str = "PUT /x HTTP/1.1\r\n" \
613 "Transfer-Encoding: gzip\r\n" \
614 "Transfer-Encoding: chunked\r\n" \
616 env = @parser.headers({}, str)
617 assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING']
618 assert_nil @parser.content_length
621 str = "PUT /x HTTP/1.1\r\n" \
622 "Transfer-Encoding: gzip, chunked\r\n" \
624 env = @parser.headers({}, str)
625 assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING']
626 assert_nil @parser.content_length
629 def test_chunked_order_bad
630 str = "PUT /x HTTP/1.1\r\n" \
631 "Transfer-Encoding: chunked\r\n" \
632 "Transfer-Encoding: gzip\r\n" \
634 assert_raise(HttpParserError) { @parser.headers({}, str) }
637 def test_double_chunked
638 str = "PUT /x HTTP/1.1\r\n" \
639 "Transfer-Encoding: chunked\r\n" \
640 "Transfer-Encoding: chunked\r\n" \
642 assert_raise(HttpParserError) { @parser.headers({}, str) }
645 str = "PUT /x HTTP/1.1\r\n" \
646 "Transfer-Encoding: chunked,chunked\r\n" \
648 assert_raise(HttpParserError) { @parser.headers({}, str) }
651 def test_backtrace_is_empty
653 @parser.headers({}, "AAADFSFDSFD\r\n\r\n")
654 assert false, "should never get here line:#{__LINE__}"
655 rescue HttpParserError => e
656 assert_equal [], e.backtrace
659 assert false, "should never get here line:#{__LINE__}"
662 def test_ignore_version_header
663 @parser.buf << "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n"
665 assert_equal req, @parser.parse
666 assert_equal '', @parser.buf
668 "SERVER_NAME" => "localhost",
669 "rack.url_scheme" => "http",
670 "REQUEST_PATH" => "/",
671 "SERVER_PROTOCOL" => "HTTP/1.1",
673 "HTTP_VERSION" => "HTTP/1.1",
674 "REQUEST_URI" => "/",
675 "SERVER_PORT" => "80",
676 "REQUEST_METHOD" => "GET",
679 assert_equal expect, req
682 def test_pipelined_requests
686 "SERVER_NAME" => host,
687 "REQUEST_PATH" => "/",
688 "rack.url_scheme" => "http",
689 "SERVER_PROTOCOL" => "HTTP/1.1",
691 "HTTP_VERSION" => "HTTP/1.1",
692 "REQUEST_URI" => "/",
693 "SERVER_PORT" => "80",
694 "REQUEST_METHOD" => "GET",
697 req1 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
698 req2 = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
699 @parser.buf << (req1 + req2)
700 env1 = @parser.parse.dup
701 assert_equal expect, env1
702 assert_equal req2, @parser.buf
703 assert ! @parser.env.empty?
705 assert @parser.keepalive?
706 assert @parser.headers?
707 assert_equal expect, @parser.env
708 env2 = @parser.parse.dup
709 host.replace "www.example.com"
710 assert_equal "www.example.com", expect["HTTP_HOST"]
711 assert_equal "www.example.com", expect["SERVER_NAME"]
712 assert_equal expect, env2
713 assert_equal "", @parser.buf