tee_input: update documentation for Rack 1.2
[unicorn.git] / test / unit / test_http_parser_ng.rb
blobcb30f32478ab9ef92509ebe35c9f0685ca8a1719
1 # -*- encoding: binary -*-
3 require 'test/test_helper'
4 require 'digest/md5'
6 include Unicorn
8 class HttpParserNgTest < Test::Unit::TestCase
10   def setup
11     @parser = HttpParser.new
12   end
14   def test_identity_byte_headers
15     req = {}
16     str = "PUT / HTTP/1.1\r\n"
17     str << "Content-Length: 123\r\n"
18     str << "\r"
19     hdr = ""
20     str.each_byte { |byte|
21       assert_nil @parser.headers(req, hdr << byte.chr)
22     }
23     hdr << "\n"
24     assert_equal req.object_id, @parser.headers(req, hdr).object_id
25     assert_equal '123', req['CONTENT_LENGTH']
26     assert_equal 0, hdr.size
27     assert ! @parser.keepalive?
28     assert @parser.headers?
29     assert_equal 123, @parser.content_length
30   end
32   def test_identity_step_headers
33     req = {}
34     str = "PUT / HTTP/1.1\r\n"
35     assert ! @parser.headers(req, str)
36     str << "Content-Length: 123\r\n"
37     assert ! @parser.headers(req, str)
38     str << "\r\n"
39     assert_equal req.object_id, @parser.headers(req, str).object_id
40     assert_equal '123', req['CONTENT_LENGTH']
41     assert_equal 0, str.size
42     assert ! @parser.keepalive?
43     assert @parser.headers?
44   end
46   def test_identity_oneshot_header
47     req = {}
48     str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n"
49     assert_equal req.object_id, @parser.headers(req, str).object_id
50     assert_equal '123', req['CONTENT_LENGTH']
51     assert_equal 0, str.size
52     assert ! @parser.keepalive?
53   end
55   def test_identity_oneshot_header_with_body
56     body = ('a' * 123).freeze
57     req = {}
58     str = "PUT / HTTP/1.1\r\n" \
59           "Content-Length: #{body.length}\r\n" \
60           "\r\n#{body}"
61     assert_equal req.object_id, @parser.headers(req, str).object_id
62     assert_equal '123', req['CONTENT_LENGTH']
63     assert_equal 123, str.size
64     assert_equal body, str
65     tmp = ''
66     assert_nil @parser.filter_body(tmp, str)
67     assert_equal 0, str.size
68     assert_equal tmp, body
69     assert_equal "", @parser.filter_body(tmp, str)
70     assert ! @parser.keepalive?
71   end
73   def test_identity_oneshot_header_with_body_partial
74     str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na"
75     assert_equal Hash, @parser.headers({}, str).class
76     assert_equal 1, str.size
77     assert_equal 'a', str
78     tmp = ''
79     assert_nil @parser.filter_body(tmp, str)
80     assert_equal "", str
81     assert_equal "a", tmp
82     str << ' ' * 122
83     rv = @parser.filter_body(tmp, str)
84     assert_equal 122, tmp.size
85     assert_nil rv
86     assert_equal "", str
87     assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
88     assert ! @parser.keepalive?
89   end
91   def test_identity_oneshot_header_with_body_slop
92     str = "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG"
93     assert_equal Hash, @parser.headers({}, str).class
94     assert_equal 2, str.size
95     assert_equal 'aG', str
96     tmp = ''
97     assert_nil @parser.filter_body(tmp, str)
98     assert_equal "G", str
99     assert_equal "G", @parser.filter_body(tmp, str)
100     assert_equal 1, tmp.size
101     assert_equal "a", tmp
102     assert ! @parser.keepalive?
103   end
105   def test_chunked
106     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
107     req = {}
108     assert_equal req, @parser.headers(req, str)
109     assert_equal 0, str.size
110     tmp = ""
111     assert_nil @parser.filter_body(tmp, "6")
112     assert_equal 0, tmp.size
113     assert_nil @parser.filter_body(tmp, rv = "\r\n")
114     assert_equal 0, rv.size
115     assert_equal 0, tmp.size
116     tmp = ""
117     assert_nil @parser.filter_body(tmp, "..")
118     assert_equal "..", tmp
119     assert_nil @parser.filter_body(tmp, "abcd\r\n0\r\n")
120     assert_equal "abcd", tmp
121     rv = "PUT"
122     assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id
123     assert_equal "PUT", rv
124     assert ! @parser.keepalive?
125   end
127   def test_two_chunks
128     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
129     req = {}
130     assert_equal req, @parser.headers(req, str)
131     assert_equal 0, str.size
132     tmp = ""
133     assert_nil @parser.filter_body(tmp, "6")
134     assert_equal 0, tmp.size
135     assert_nil @parser.filter_body(tmp, rv = "\r\n")
136     assert_equal "", rv
137     assert_equal 0, tmp.size
138     tmp = ""
139     assert_nil @parser.filter_body(tmp, "..")
140     assert_equal 2, tmp.size
141     assert_equal "..", tmp
142     assert_nil @parser.filter_body(tmp, "abcd\r\n1")
143     assert_equal "abcd", tmp
144     assert_nil @parser.filter_body(tmp, "\r")
145     assert_equal "", tmp
146     assert_nil @parser.filter_body(tmp, "\n")
147     assert_equal "", tmp
148     assert_nil @parser.filter_body(tmp, "z")
149     assert_equal "z", tmp
150     assert_nil @parser.filter_body(tmp, "\r\n")
151     assert_nil @parser.filter_body(tmp, "0")
152     assert_nil @parser.filter_body(tmp, "\r")
153     rv = @parser.filter_body(tmp, buf = "\nGET")
154     assert_equal "GET", rv
155     assert_equal buf.object_id, rv.object_id
156     assert ! @parser.keepalive?
157   end
159   def test_big_chunk
160     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
161           "4000\r\nabcd"
162     req = {}
163     assert_equal req, @parser.headers(req, str)
164     tmp = ''
165     assert_nil @parser.filter_body(tmp, str)
166     assert_equal '', str
167     str = ' ' * 16300
168     assert_nil @parser.filter_body(tmp, str)
169     assert_equal '', str
170     str = ' ' * 80
171     assert_nil @parser.filter_body(tmp, str)
172     assert_equal '', str
173     assert ! @parser.body_eof?
174     assert_equal "", @parser.filter_body(tmp, "\r\n0\r\n")
175     assert_equal "", tmp
176     assert @parser.body_eof?
177     assert_equal req, @parser.trailers(req, moo = "\r\n")
178     assert_equal "", moo
179     assert @parser.body_eof?
180     assert ! @parser.keepalive?
181   end
183   def test_two_chunks_oneshot
184     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
185           "1\r\na\r\n2\r\n..\r\n0\r\n"
186     req = {}
187     assert_equal req, @parser.headers(req, str)
188     tmp = ''
189     assert_nil @parser.filter_body(tmp, str)
190     assert_equal 'a..', tmp
191     rv = @parser.filter_body(tmp, str)
192     assert_equal rv.object_id, str.object_id
193     assert ! @parser.keepalive?
194   end
196   def test_chunks_bytewise
197     chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n"
198     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n#{chunked}"
199     req = {}
200     assert_equal req, @parser.headers(req, str)
201     assert_equal chunked, str
202     tmp = ''
203     buf = ''
204     body = ''
205     str = str[0..-2]
206     str.each_byte { |byte|
207       assert_nil @parser.filter_body(tmp, buf << byte.chr)
208       body << tmp
209     }
210     assert_equal 'abcdefghijklmnop0123456789abcdefg', body
211     rv = @parser.filter_body(tmp, buf << "\n")
212     assert_equal rv.object_id, buf.object_id
213     assert ! @parser.keepalive?
214   end
216   def test_trailers
217     str = "PUT / HTTP/1.1\r\n" \
218           "Trailer: Content-MD5\r\n" \
219           "transfer-Encoding: chunked\r\n\r\n" \
220           "1\r\na\r\n2\r\n..\r\n0\r\n"
221     req = {}
222     assert_equal req, @parser.headers(req, str)
223     assert_equal 'Content-MD5', req['HTTP_TRAILER']
224     assert_nil req['HTTP_CONTENT_MD5']
225     tmp = ''
226     assert_nil @parser.filter_body(tmp, str)
227     assert_equal 'a..', tmp
228     md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
229     rv = @parser.filter_body(tmp, str)
230     assert_equal rv.object_id, str.object_id
231     assert_equal '', str
232     md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
233     str << md5_hdr
234     assert_nil @parser.trailers(req, str)
235     assert_equal md5_b64, req['HTTP_CONTENT_MD5']
236     assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
237     assert_nil @parser.trailers(req, str << "\r")
238     assert_equal req, @parser.trailers(req, str << "\nGET / ")
239     assert_equal "GET / ", str
240     assert ! @parser.keepalive?
241   end
243   def test_trailers_slowly
244     str = "PUT / HTTP/1.1\r\n" \
245           "Trailer: Content-MD5\r\n" \
246           "transfer-Encoding: chunked\r\n\r\n" \
247           "1\r\na\r\n2\r\n..\r\n0\r\n"
248     req = {}
249     assert_equal req, @parser.headers(req, str)
250     assert_equal 'Content-MD5', req['HTTP_TRAILER']
251     assert_nil req['HTTP_CONTENT_MD5']
252     tmp = ''
253     assert_nil @parser.filter_body(tmp, str)
254     assert_equal 'a..', tmp
255     md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
256     rv = @parser.filter_body(tmp, str)
257     assert_equal rv.object_id, str.object_id
258     assert_equal '', str
259     assert_nil @parser.trailers(req, str)
260     md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
261     md5_hdr.each_byte { |byte|
262       str << byte.chr
263       assert_nil @parser.trailers(req, str)
264     }
265     assert_equal md5_b64, req['HTTP_CONTENT_MD5']
266     assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
267     assert_nil @parser.trailers(req, str << "\r")
268     assert_equal req, @parser.trailers(req, str << "\n")
269   end
271   def test_max_chunk
272     str = "PUT / HTTP/1.1\r\n" \
273           "transfer-Encoding: chunked\r\n\r\n" \
274           "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
275     req = {}
276     assert_equal req, @parser.headers(req, str)
277     assert_nil @parser.content_length
278     assert_nothing_raised { @parser.filter_body('', str) }
279     assert ! @parser.keepalive?
280   end
282   def test_max_body
283     n = HttpParser::LENGTH_MAX
284     str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
285     req = {}
286     assert_nothing_raised { @parser.headers(req, str) }
287     assert_equal n, req['CONTENT_LENGTH'].to_i
288     assert ! @parser.keepalive?
289   end
291   def test_overflow_chunk
292     n = HttpParser::CHUNK_MAX + 1
293     str = "PUT / HTTP/1.1\r\n" \
294           "transfer-Encoding: chunked\r\n\r\n" \
295           "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
296     req = {}
297     assert_equal req, @parser.headers(req, str)
298     assert_nil @parser.content_length
299     assert_raise(HttpParserError) { @parser.filter_body('', str) }
300     assert ! @parser.keepalive?
301   end
303   def test_overflow_content_length
304     n = HttpParser::LENGTH_MAX + 1
305     str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
306     assert_raise(HttpParserError) { @parser.headers({}, str) }
307     assert ! @parser.keepalive?
308   end
310   def test_bad_chunk
311     str = "PUT / HTTP/1.1\r\n" \
312           "transfer-Encoding: chunked\r\n\r\n" \
313           "#zzz\r\na\r\n2\r\n..\r\n0\r\n"
314     req = {}
315     assert_equal req, @parser.headers(req, str)
316     assert_nil @parser.content_length
317     assert_raise(HttpParserError) { @parser.filter_body('', str) }
318     assert ! @parser.keepalive?
319   end
321   def test_bad_content_length
322     str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
323     assert_raise(HttpParserError) { @parser.headers({}, str) }
324     assert ! @parser.keepalive?
325   end
327   def test_bad_trailers
328     str = "PUT / HTTP/1.1\r\n" \
329           "Trailer: Transfer-Encoding\r\n" \
330           "transfer-Encoding: chunked\r\n\r\n" \
331           "1\r\na\r\n2\r\n..\r\n0\r\n"
332     req = {}
333     assert_equal req, @parser.headers(req, str)
334     assert_equal 'Transfer-Encoding', req['HTTP_TRAILER']
335     tmp = ''
336     assert_nil @parser.filter_body(tmp, str)
337     assert_equal 'a..', tmp
338     assert_equal '', str
339     str << "Transfer-Encoding: identity\r\n\r\n"
340     assert_raise(HttpParserError) { @parser.trailers(req, str) }
341     assert ! @parser.keepalive?
342   end
344   def test_repeat_headers
345     str = "PUT / HTTP/1.1\r\n" \
346           "Trailer: Content-MD5\r\n" \
347           "Trailer: Content-SHA1\r\n" \
348           "transfer-Encoding: chunked\r\n\r\n" \
349           "1\r\na\r\n2\r\n..\r\n0\r\n"
350     req = {}
351     assert_equal req, @parser.headers(req, str)
352     assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER']
353     assert ! @parser.keepalive?
354   end
356   def test_parse_simple_request
357     parser = HttpParser.new
358     req = {}
359     http = "GET /read-rfc1945-if-you-dont-believe-me\r\n"
360     assert_equal req, parser.headers(req, http)
361     assert_equal '', http
362     expect = {
363       "SERVER_NAME"=>"localhost",
364       "rack.url_scheme"=>"http",
365       "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me",
366       "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me",
367       "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me",
368       "SERVER_PORT"=>"80",
369       "SERVER_PROTOCOL"=>"HTTP/0.9",
370       "REQUEST_METHOD"=>"GET",
371       "QUERY_STRING"=>""
372     }
373     assert_equal expect, req
374     assert ! parser.headers?
375   end
377   def test_path_info_semicolon
378     qs = "QUERY_STRING"
379     pi = "PATH_INFO"
380     req = {}
381     str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n"
382     {
383       "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
384       "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
385       "/1;a=b" => { qs => "", pi => "/1;a=b" },
386       "/1;a=b?" => { qs => "", pi => "/1;a=b" },
387       "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
388       "*" => { qs => "", pi => "" },
389     }.each do |uri,expect|
390       assert_equal req, @parser.headers(req.clear, str % [ uri ])
391       @parser.reset
392       assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
393       assert_equal expect[qs], req[qs], "#{qs} mismatch"
394       assert_equal expect[pi], req[pi], "#{pi} mismatch"
395       next if uri == "*"
396       uri = URI.parse("http://example.com#{uri}")
397       assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees"
398       assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees"
399     end
400   end
402   def test_path_info_semicolon_absolute
403     qs = "QUERY_STRING"
404     pi = "PATH_INFO"
405     req = {}
406     str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
407     {
408       "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" },
409       "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" },
410       "/1;a=b" => { qs => "", pi => "/1;a=b" },
411       "/1;a=b?" => { qs => "", pi => "/1;a=b" },
412       "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" },
413     }.each do |uri,expect|
414       assert_equal req, @parser.headers(req.clear, str % [ uri ])
415       @parser.reset
416       assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch"
417       assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch"
418       assert_equal expect[qs], req[qs], "#{qs} mismatch"
419       assert_equal expect[pi], req[pi], "#{pi} mismatch"
420     end
421   end
423   def test_negative_content_length
424     req = {}
425     str = "PUT / HTTP/1.1\r\n" \
426           "Content-Length: -1\r\n" \
427           "\r\n"
428     assert_raises(HttpParserError) do
429       @parser.headers(req, str)
430     end
431   end
433   def test_invalid_content_length
434     req = {}
435     str = "PUT / HTTP/1.1\r\n" \
436           "Content-Length: zzzzz\r\n" \
437           "\r\n"
438     assert_raises(HttpParserError) do
439       @parser.headers(req, str)
440     end
441   end
443   def test_ignore_version_header
444     http = "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n"
445     req = {}
446     assert_equal req, @parser.headers(req, http)
447     assert_equal '', http
448     expect = {
449       "SERVER_NAME" => "localhost",
450       "rack.url_scheme" => "http",
451       "REQUEST_PATH" => "/",
452       "SERVER_PROTOCOL" => "HTTP/1.1",
453       "PATH_INFO" => "/",
454       "HTTP_VERSION" => "HTTP/1.1",
455       "REQUEST_URI" => "/",
456       "SERVER_PORT" => "80",
457       "REQUEST_METHOD" => "GET",
458       "QUERY_STRING" => ""
459     }
460     assert_equal expect, req
461   end