1 # -*- encoding: binary -*-
3 require './test/test_helper'
8 class HttpParserNgTest < Test::Unit::TestCase
11 @parser = HttpParser.new
14 # RFC 7230 allows gzip/deflate/compress Transfer-Encoding,
15 # but "chunked" must be last if used
17 [ 'chunked,chunked', 'chunked,gzip', 'chunked,gzip,chunked' ].each do |x|
18 assert_raise(HttpParserError) { HttpParser.is_chunked?(x) }
20 [ 'gzip, chunked', 'gzip,chunked', 'gzip ,chunked' ].each do |x|
21 assert HttpParser.is_chunked?(x)
23 [ 'gzip', 'xhunked', 'xchunked' ].each do |x|
24 assert !HttpParser.is_chunked?(x)
28 def test_parser_max_len
29 assert_raises(RangeError) do
30 HttpParser.max_header_len = 0xffffffff + 1
35 r = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
38 @parser.response_start_sent = true
39 assert @parser.keepalive?
41 assert @parser.response_start_sent
43 # persistent client makes another request:
46 assert @parser.keepalive?
48 assert_equal false, @parser.response_start_sent
51 def test_response_start_sent
52 assert_equal false, @parser.response_start_sent, "default is false"
53 @parser.response_start_sent = true
54 assert_equal true, @parser.response_start_sent
55 @parser.response_start_sent = false
56 assert_equal false, @parser.response_start_sent
57 @parser.response_start_sent = true
59 assert_equal false, @parser.response_start_sent
62 def test_connection_TE
63 @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: TE\r\n"
64 @parser.buf << "TE: trailers\r\n\r\n"
66 assert @parser.keepalive?
70 def test_keepalive_requests_with_next?
71 req = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n".freeze
73 "SERVER_NAME" => "example.com",
74 "HTTP_HOST" => "example.com",
75 "rack.url_scheme" => "http",
76 "REQUEST_PATH" => "/",
77 "SERVER_PROTOCOL" => "HTTP/1.1",
79 "HTTP_VERSION" => "HTTP/1.1",
81 "SERVER_PORT" => "80",
82 "REQUEST_METHOD" => "GET",
87 assert_equal expect, @parser.parse
92 def test_default_keepalive_is_off
93 assert ! @parser.keepalive?
94 assert ! @parser.next?
95 @parser.buf << "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
97 assert @parser.keepalive?
99 assert ! @parser.keepalive?
100 assert ! @parser.next?
103 def test_identity_byte_headers
105 str = "PUT / HTTP/1.1\r\n"
106 str << "Content-Length: 123\r\n"
109 str.each_byte { |byte|
111 assert_nil @parser.parse
114 assert_equal req.object_id, @parser.parse.object_id
115 assert_equal '123', req['CONTENT_LENGTH']
116 assert_equal 0, hdr.size
117 assert ! @parser.keepalive?
118 assert @parser.headers?
119 assert_equal 123, @parser.content_length
122 @parser.filter_body(dst, buf)
123 assert_equal '.' * 123, dst
125 assert @parser.keepalive?
128 def test_identity_step_headers
131 str << "PUT / HTTP/1.1\r\n"
132 assert ! @parser.parse
133 str << "Content-Length: 123\r\n"
134 assert ! @parser.parse
136 assert_equal req.object_id, @parser.parse.object_id
137 assert_equal '123', req['CONTENT_LENGTH']
138 assert_equal 0, str.size
139 assert ! @parser.keepalive?
140 assert @parser.headers?
143 @parser.filter_body(dst, buf)
144 assert_equal '.' * 123, dst
146 assert @parser.keepalive?
149 def test_identity_oneshot_header
152 str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n"
153 assert_equal req.object_id, @parser.parse.object_id
154 assert_equal '123', req['CONTENT_LENGTH']
155 assert_equal 0, str.size
156 assert ! @parser.keepalive?
157 assert @parser.headers?
160 @parser.filter_body(dst, buf)
161 assert_equal '.' * 123, dst
165 def test_identity_oneshot_header_with_body
166 body = ('a' * 123).freeze
169 str << "PUT / HTTP/1.1\r\n" \
170 "Content-Length: #{body.length}\r\n" \
172 assert_equal req.object_id, @parser.parse.object_id
173 assert_equal '123', req['CONTENT_LENGTH']
174 assert_equal 123, str.size
175 assert_equal body, str
177 assert_nil @parser.filter_body(tmp, str)
178 assert_equal 0, str.size
179 assert_equal tmp, body
180 assert_equal "", @parser.filter_body(tmp, str)
181 assert @parser.keepalive?
184 def test_identity_oneshot_header_with_body_partial
186 str << "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na"
187 assert_equal Hash, @parser.parse.class
188 assert_equal 1, str.size
189 assert_equal 'a', str
191 assert_nil @parser.filter_body(tmp, str)
193 assert_equal "a", tmp
195 rv = @parser.filter_body(tmp, str)
196 assert_equal 122, tmp.size
199 assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
200 assert @parser.keepalive?
203 def test_identity_oneshot_header_with_body_slop
205 str << "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG"
206 assert_equal Hash, @parser.parse.class
207 assert_equal 2, str.size
208 assert_equal 'aG', str
210 assert_nil @parser.filter_body(tmp, str)
211 assert_equal "G", str
212 assert_equal "G", @parser.filter_body(tmp, str)
213 assert_equal 1, tmp.size
214 assert_equal "a", tmp
215 assert @parser.keepalive?
221 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
222 assert_equal req, @parser.parse, "msg=#{str}"
223 assert_equal 0, str.size
225 assert_nil @parser.filter_body(tmp, str << "6")
226 assert_equal 0, tmp.size
227 assert_nil @parser.filter_body(tmp, str << "\r\n")
228 assert_equal 0, str.size
229 assert_equal 0, tmp.size
231 assert_nil @parser.filter_body(tmp, str << "..")
232 assert_equal "..", tmp
233 assert_nil @parser.filter_body(tmp, str << "abcd\r\n0\r\n")
234 assert_equal "abcd", tmp
235 assert_equal str.object_id, @parser.filter_body(tmp, str << "PUT").object_id
236 assert_equal "PUT", str
237 assert ! @parser.keepalive?
238 str << "TY: FOO\r\n\r\n"
239 assert_equal req, @parser.parse
240 assert_equal "FOO", req["HTTP_PUTTY"]
241 assert @parser.keepalive?
244 def test_chunked_empty
247 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
248 assert_equal req, @parser.parse, "msg=#{str}"
249 assert_equal 0, str.size
251 assert_equal str, @parser.filter_body(tmp, str << "0\r\n\r\n")
257 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
259 assert_equal req, @parser.parse
260 assert_equal 0, str.size
262 assert_nil @parser.filter_body(tmp, str << "6")
263 assert_equal 0, tmp.size
264 assert_nil @parser.filter_body(tmp, str << "\r\n")
266 assert_equal 0, tmp.size
268 assert_nil @parser.filter_body(tmp, str << "..")
269 assert_equal 2, tmp.size
270 assert_equal "..", tmp
271 assert_nil @parser.filter_body(tmp, str << "abcd\r\n1")
272 assert_equal "abcd", tmp
273 assert_nil @parser.filter_body(tmp, str << "\r")
275 assert_nil @parser.filter_body(tmp, str << "\n")
277 assert_nil @parser.filter_body(tmp, str << "z")
278 assert_equal "z", tmp
279 assert_nil @parser.filter_body(tmp, str << "\r\n")
280 assert_nil @parser.filter_body(tmp, str << "0")
281 assert_nil @parser.filter_body(tmp, str << "\r")
282 rv = @parser.filter_body(tmp, str << "\nGET")
283 assert_equal "GET", rv
284 assert_equal str.object_id, rv.object_id
285 assert ! @parser.keepalive?
290 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
293 assert_equal req, @parser.parse
295 assert_nil @parser.filter_body(tmp, str)
298 assert_nil @parser.filter_body(tmp, str)
301 assert_nil @parser.filter_body(tmp, str)
303 assert ! @parser.body_eof?
304 assert_equal "", @parser.filter_body(tmp, str << "\r\n0\r\n")
306 assert @parser.body_eof?
308 assert_equal req, @parser.parse
310 assert @parser.body_eof?
311 assert @parser.keepalive?
314 def test_two_chunks_oneshot
317 str << "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
318 "1\r\na\r\n2\r\n..\r\n0\r\n"
319 assert_equal req, @parser.parse
321 assert_nil @parser.filter_body(tmp, str)
322 assert_equal 'a..', tmp
323 rv = @parser.filter_body(tmp, str)
324 assert_equal rv.object_id, str.object_id
325 assert ! @parser.keepalive?
328 def test_chunks_bytewise
329 chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n"
330 str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
334 assert_equal req, @parser.parse
339 str.each_byte { |byte|
340 assert_nil @parser.filter_body(tmp, buf << byte.chr)
343 assert_equal 'abcdefghijklmnop0123456789abcdefg', body
344 rv = @parser.filter_body(tmp, buf<< "\n")
345 assert_equal rv.object_id, buf.object_id
346 assert ! @parser.keepalive?
352 str << "PUT / HTTP/1.1\r\n" \
353 "Trailer: Content-MD5\r\n" \
354 "transfer-Encoding: chunked\r\n\r\n" \
355 "1\r\na\r\n2\r\n..\r\n0\r\n"
356 assert_equal req, @parser.parse
357 assert_equal 'Content-MD5', req['HTTP_TRAILER']
358 assert_nil req['HTTP_CONTENT_MD5']
360 assert_nil @parser.filter_body(tmp, str)
361 assert_equal 'a..', tmp
362 md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
363 rv = @parser.filter_body(tmp, str)
364 assert_equal rv.object_id, str.object_id
366 md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
368 assert_nil @parser.trailers(req, str)
369 assert_equal md5_b64, req['HTTP_CONTENT_MD5']
370 assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
372 assert_nil @parser.parse
374 assert_equal req, @parser.parse
375 assert_equal "GET / ", str
376 assert @parser.keepalive?
379 def test_trailers_slowly
381 str << "PUT / HTTP/1.1\r\n" \
382 "Trailer: Content-MD5\r\n" \
383 "transfer-Encoding: chunked\r\n\r\n" \
384 "1\r\na\r\n2\r\n..\r\n0\r\n"
386 assert_equal req, @parser.parse
387 assert_equal 'Content-MD5', req['HTTP_TRAILER']
388 assert_nil req['HTTP_CONTENT_MD5']
390 assert_nil @parser.filter_body(tmp, str)
391 assert_equal 'a..', tmp
392 md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
393 rv = @parser.filter_body(tmp, str)
394 assert_equal rv.object_id, str.object_id
396 assert_nil @parser.trailers(req, str)
397 md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
398 md5_hdr.each_byte { |byte|
400 assert_nil @parser.trailers(req, str)
402 assert_equal md5_b64, req['HTTP_CONTENT_MD5']
403 assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
405 assert_nil @parser.parse
407 assert_equal req, @parser.parse
412 str << "PUT / HTTP/1.1\r\n" \
413 "transfer-Encoding: chunked\r\n\r\n" \
414 "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
416 assert_equal req, @parser.parse
417 assert_nil @parser.content_length
418 @parser.filter_body('', str)
419 assert ! @parser.keepalive?
423 n = HttpParser::LENGTH_MAX
424 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
426 @parser.headers(req, @parser.buf)
427 assert_equal n, req['CONTENT_LENGTH'].to_i
428 assert ! @parser.keepalive?
431 def test_overflow_chunk
432 n = HttpParser::CHUNK_MAX + 1
435 str << "PUT / HTTP/1.1\r\n" \
436 "transfer-Encoding: chunked\r\n\r\n" \
437 "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
438 assert_equal req, @parser.parse
439 assert_nil @parser.content_length
440 assert_raise(HttpParserError) { @parser.filter_body('', str) }
443 def test_overflow_content_length
444 n = HttpParser::LENGTH_MAX + 1
445 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
446 assert_raise(HttpParserError) { @parser.parse }
450 @parser.buf << "PUT / HTTP/1.1\r\n" \
451 "transfer-Encoding: chunked\r\n\r\n" \
452 "#zzz\r\na\r\n2\r\n..\r\n0\r\n"
454 assert_equal req, @parser.parse
455 assert_nil @parser.content_length
456 assert_raise(HttpParserError) { @parser.filter_body("", @parser.buf) }
459 def test_bad_content_length
460 @parser.buf << "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
461 assert_raise(HttpParserError) { @parser.parse }
464 def test_bad_trailers
467 str << "PUT / HTTP/1.1\r\n" \
468 "Trailer: Transfer-Encoding\r\n" \
469 "transfer-Encoding: chunked\r\n\r\n" \
470 "1\r\na\r\n2\r\n..\r\n0\r\n"
471 assert_equal req, @parser.parse
472 assert_equal 'Transfer-Encoding', req['HTTP_TRAILER']
474 assert_nil @parser.filter_body(tmp, str)
475 assert_equal 'a..', tmp
477 str << "Transfer-Encoding: identity\r\n\r\n"
478 assert_raise(HttpParserError) { @parser.parse }
481 def test_repeat_headers
482 str = "PUT / HTTP/1.1\r\n" \
483 "Trailer: Content-MD5\r\n" \
484 "Trailer: Content-SHA1\r\n" \
485 "transfer-Encoding: chunked\r\n\r\n" \
486 "1\r\na\r\n2\r\n..\r\n0\r\n"
489 assert_equal req, @parser.parse
490 assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER']
491 assert ! @parser.keepalive?
494 def test_parse_simple_request
495 parser = HttpParser.new
497 parser.buf << "GET /read-rfc1945-if-you-dont-believe-me\r\n"
498 assert_equal req, parser.parse
499 assert_equal '', parser.buf
501 "SERVER_NAME"=>"localhost",
502 "rack.url_scheme"=>"http",
503 "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me",
504 "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me",
505 "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me",
507 "SERVER_PROTOCOL"=>"HTTP/0.9",
508 "REQUEST_METHOD"=>"GET",
511 assert_equal expect, req
512 assert ! parser.headers?
515 def test_path_info_semicolon
519 str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n"
521 "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
522 "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
523 "/1;a=b" => { qs => "", pi => "/1;a=b" },
524 "/1;a=b?" => { qs => "", pi => "/1;a=b" },
525 "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
526 "*" => { qs => "", pi => "" },
527 }.each do |uri,expect|
528 assert_equal req, @parser.headers(req.clear, str % [ uri ])
531 assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
532 assert_equal expect[qs], req[qs], "#{qs} mismatch"
533 assert_equal expect[pi], req[pi], "#{pi} mismatch"
535 uri = URI.parse("http://example.com#{uri}")
536 assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees"
537 assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees"
541 def test_path_info_semicolon_absolute
545 str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
547 "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
548 "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
549 "/1;a=b" => { qs => "", pi => "/1;a=b" },
550 "/1;a=b?" => { qs => "", pi => "/1;a=b" },
551 "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
552 }.each do |uri,expect|
553 assert_equal req, @parser.headers(req.clear, str % [ uri ])
556 assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
557 assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch"
558 assert_equal expect[qs], req[qs], "#{qs} mismatch"
559 assert_equal expect[pi], req[pi], "#{pi} mismatch"
563 def test_negative_content_length
565 str = "PUT / HTTP/1.1\r\n" \
566 "Content-Length: -1\r\n" \
568 assert_raises(HttpParserError) do
569 @parser.headers(req, str)
573 def test_invalid_content_length
575 str = "PUT / HTTP/1.1\r\n" \
576 "Content-Length: zzzzz\r\n" \
578 assert_raises(HttpParserError) do
579 @parser.headers(req, str)
583 def test_duplicate_content_length
584 str = "PUT / HTTP/1.1\r\n" \
585 "Content-Length: 1\r\n" \
586 "Content-Length: 9\r\n" \
588 assert_raises(HttpParserError) { @parser.headers({}, str) }
591 def test_chunked_overrides_content_length
592 order = [ 'Transfer-Encoding: chunked', 'Content-Length: 666' ]
594 str = "PUT /#{x} HTTP/1.1\r\n" \
595 "#{order.join("\r\n")}" \
596 "\r\n\r\na\r\nhelloworld\r\n0\r\n\r\n"
598 env = @parser.headers({}, str)
599 assert_nil @parser.content_length
600 assert_equal 'chunked', env['HTTP_TRANSFER_ENCODING']
601 assert_equal '666', env['CONTENT_LENGTH'],
602 'Content-Length logged so the app can log a possible client bug/attack'
603 @parser.filter_body(dst = '', str)
604 assert_equal 'helloworld', dst
605 @parser.parse # handle the non-existent trailer
610 def test_chunked_order_good
611 str = "PUT /x HTTP/1.1\r\n" \
612 "Transfer-Encoding: gzip\r\n" \
613 "Transfer-Encoding: chunked\r\n" \
615 env = @parser.headers({}, str)
616 assert_equal 'gzip,chunked', env['HTTP_TRANSFER_ENCODING']
617 assert_nil @parser.content_length
620 str = "PUT /x HTTP/1.1\r\n" \
621 "Transfer-Encoding: gzip, chunked\r\n" \
623 env = @parser.headers({}, str)
624 assert_equal 'gzip, chunked', env['HTTP_TRANSFER_ENCODING']
625 assert_nil @parser.content_length
628 def test_chunked_order_bad
629 str = "PUT /x HTTP/1.1\r\n" \
630 "Transfer-Encoding: chunked\r\n" \
631 "Transfer-Encoding: gzip\r\n" \
633 assert_raise(HttpParserError) { @parser.headers({}, str) }
636 def test_double_chunked
637 str = "PUT /x HTTP/1.1\r\n" \
638 "Transfer-Encoding: chunked\r\n" \
639 "Transfer-Encoding: chunked\r\n" \
641 assert_raise(HttpParserError) { @parser.headers({}, str) }
644 str = "PUT /x HTTP/1.1\r\n" \
645 "Transfer-Encoding: chunked,chunked\r\n" \
647 assert_raise(HttpParserError) { @parser.headers({}, str) }
650 def test_backtrace_is_empty
652 @parser.headers({}, "AAADFSFDSFD\r\n\r\n")
653 assert false, "should never get here line:#{__LINE__}"
654 rescue HttpParserError => e
655 assert_equal [], e.backtrace
658 assert false, "should never get here line:#{__LINE__}"
661 def test_ignore_version_header
662 @parser.buf << "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n"
664 assert_equal req, @parser.parse
665 assert_equal '', @parser.buf
667 "SERVER_NAME" => "localhost",
668 "rack.url_scheme" => "http",
669 "REQUEST_PATH" => "/",
670 "SERVER_PROTOCOL" => "HTTP/1.1",
672 "HTTP_VERSION" => "HTTP/1.1",
673 "REQUEST_URI" => "/",
674 "SERVER_PORT" => "80",
675 "REQUEST_METHOD" => "GET",
678 assert_equal expect, req
681 def test_pipelined_requests
685 "SERVER_NAME" => host,
686 "REQUEST_PATH" => "/",
687 "rack.url_scheme" => "http",
688 "SERVER_PROTOCOL" => "HTTP/1.1",
690 "HTTP_VERSION" => "HTTP/1.1",
691 "REQUEST_URI" => "/",
692 "SERVER_PORT" => "80",
693 "REQUEST_METHOD" => "GET",
696 req1 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
697 req2 = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
698 @parser.buf << (req1 + req2)
699 env1 = @parser.parse.dup
700 assert_equal expect, env1
701 assert_equal req2, @parser.buf
702 assert ! @parser.env.empty?
704 assert @parser.keepalive?
705 assert @parser.headers?
706 assert_equal expect, @parser.env
707 env2 = @parser.parse.dup
708 host.replace "www.example.com"
709 assert_equal "www.example.com", expect["HTTP_HOST"]
710 assert_equal "www.example.com", expect["SERVER_NAME"]
711 assert_equal expect, env2
712 assert_equal "", @parser.buf