http: allow headers/trailers to be written byte-wise
[unicorn.git] / test / unit / test_http_parser_ng.rb
blob0167f8f83247a911aee12eaa1273660ae88bf007
1 # -*- encoding: binary -*-
3 # coding: binary
4 require 'test/test_helper'
5 require 'digest/md5'
7 include Unicorn
9 class HttpParserNgTest < Test::Unit::TestCase
11   def setup
12     @parser = HttpParser.new
13   end
15   def test_identity_byte_headers
16     req = {}
17     str = "PUT / HTTP/1.1\r\n"
18     str << "Content-Length: 123\r\n"
19     str << "\r"
20     hdr = ""
21     str.each_byte { |byte|
22       assert_nil @parser.headers(req, hdr << byte.chr)
23     }
24     hdr << "\n"
25     assert_equal req.object_id, @parser.headers(req, hdr).object_id
26     assert_equal '123', req['CONTENT_LENGTH']
27     assert_equal 0, hdr.size
28     assert ! @parser.keepalive?
29     assert @parser.headers?
30     assert 123, @parser.content_length
31   end
33   def test_identity_step_headers
34     req = {}
35     str = "PUT / HTTP/1.1\r\n"
36     assert ! @parser.headers(req, str)
37     str << "Content-Length: 123\r\n"
38     assert ! @parser.headers(req, str)
39     str << "\r\n"
40     assert_equal req.object_id, @parser.headers(req, str).object_id
41     assert_equal '123', req['CONTENT_LENGTH']
42     assert_equal 0, str.size
43     assert ! @parser.keepalive?
44     assert @parser.headers?
45   end
47   def test_identity_oneshot_header
48     req = {}
49     str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n"
50     assert_equal req.object_id, @parser.headers(req, str).object_id
51     assert_equal '123', req['CONTENT_LENGTH']
52     assert_equal 0, str.size
53     assert ! @parser.keepalive?
54   end
56   def test_identity_oneshot_header_with_body
57     body = ('a' * 123).freeze
58     req = {}
59     str = "PUT / HTTP/1.1\r\n" \
60           "Content-Length: #{body.length}\r\n" \
61           "\r\n#{body}"
62     assert_equal req.object_id, @parser.headers(req, str).object_id
63     assert_equal '123', req['CONTENT_LENGTH']
64     assert_equal 123, str.size
65     assert_equal body, str
66     tmp = ''
67     assert_nil @parser.filter_body(tmp, str)
68     assert_equal 0, str.size
69     assert_equal tmp, body
70     assert_equal "", @parser.filter_body(tmp, str)
71     assert ! @parser.keepalive?
72   end
74   def test_identity_oneshot_header_with_body_partial
75     str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na"
76     assert_equal Hash, @parser.headers({}, str).class
77     assert_equal 1, str.size
78     assert_equal 'a', str
79     tmp = ''
80     assert_nil @parser.filter_body(tmp, str)
81     assert_equal "", str
82     assert_equal "a", tmp
83     str << ' ' * 122
84     rv = @parser.filter_body(tmp, str)
85     assert_equal 122, tmp.size
86     assert_nil rv
87     assert_equal "", str
88     assert_equal str.object_id, @parser.filter_body(tmp, str).object_id
89     assert ! @parser.keepalive?
90   end
92   def test_identity_oneshot_header_with_body_slop
93     str = "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG"
94     assert_equal Hash, @parser.headers({}, str).class
95     assert_equal 2, str.size
96     assert_equal 'aG', str
97     tmp = ''
98     assert_nil @parser.filter_body(tmp, str)
99     assert_equal "G", str
100     assert_equal "G", @parser.filter_body(tmp, str)
101     assert_equal 1, tmp.size
102     assert_equal "a", tmp
103     assert ! @parser.keepalive?
104   end
106   def test_chunked
107     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
108     req = {}
109     assert_equal req, @parser.headers(req, str)
110     assert_equal 0, str.size
111     tmp = ""
112     assert_nil @parser.filter_body(tmp, "6")
113     assert_equal 0, tmp.size
114     assert_nil @parser.filter_body(tmp, rv = "\r\n")
115     assert_equal 0, rv.size
116     assert_equal 0, tmp.size
117     tmp = ""
118     assert_nil @parser.filter_body(tmp, "..")
119     assert_equal "..", tmp
120     assert_nil @parser.filter_body(tmp, "abcd\r\n0\r\n")
121     assert_equal "abcd", tmp
122     rv = "PUT"
123     assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id
124     assert_equal "PUT", rv
125     assert ! @parser.keepalive?
126   end
128   def test_two_chunks
129     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n"
130     req = {}
131     assert_equal req, @parser.headers(req, str)
132     assert_equal 0, str.size
133     tmp = ""
134     assert_nil @parser.filter_body(tmp, "6")
135     assert_equal 0, tmp.size
136     assert_nil @parser.filter_body(tmp, rv = "\r\n")
137     assert_equal "", rv
138     assert_equal 0, tmp.size
139     tmp = ""
140     assert_nil @parser.filter_body(tmp, "..")
141     assert_equal 2, tmp.size
142     assert_equal "..", tmp
143     assert_nil @parser.filter_body(tmp, "abcd\r\n1")
144     assert_equal "abcd", tmp
145     assert_nil @parser.filter_body(tmp, "\r")
146     assert_equal "", tmp
147     assert_nil @parser.filter_body(tmp, "\n")
148     assert_equal "", tmp
149     assert_nil @parser.filter_body(tmp, "z")
150     assert_equal "z", tmp
151     assert_nil @parser.filter_body(tmp, "\r\n")
152     assert_nil @parser.filter_body(tmp, "0")
153     assert_nil @parser.filter_body(tmp, "\r")
154     rv = @parser.filter_body(tmp, buf = "\nGET")
155     assert_equal "GET", rv
156     assert_equal buf.object_id, rv.object_id
157     assert ! @parser.keepalive?
158   end
160   def test_big_chunk
161     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
162           "4000\r\nabcd"
163     req = {}
164     assert_equal req, @parser.headers(req, str)
165     tmp = ''
166     assert_nil @parser.filter_body(tmp, str)
167     assert_equal '', str
168     str = ' ' * 16300
169     assert_nil @parser.filter_body(tmp, str)
170     assert_equal '', str
171     str = ' ' * 80
172     assert_nil @parser.filter_body(tmp, str)
173     assert_equal '', str
174     assert ! @parser.body_eof?
175     assert_equal "", @parser.filter_body(tmp, "\r\n0\r\n")
176     assert @parser.body_eof?
177     assert ! @parser.keepalive?
178   end
180   def test_two_chunks_oneshot
181     str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \
182           "1\r\na\r\n2\r\n..\r\n0\r\n"
183     req = {}
184     assert_equal req, @parser.headers(req, str)
185     tmp = ''
186     assert_nil @parser.filter_body(tmp, str)
187     assert_equal 'a..', tmp
188     rv = @parser.filter_body(tmp, str)
189     assert_equal rv.object_id, str.object_id
190     assert ! @parser.keepalive?
191   end
193   def test_trailers
194     str = "PUT / HTTP/1.1\r\n" \
195           "Trailer: Content-MD5\r\n" \
196           "transfer-Encoding: chunked\r\n\r\n" \
197           "1\r\na\r\n2\r\n..\r\n0\r\n"
198     req = {}
199     assert_equal req, @parser.headers(req, str)
200     assert_equal 'Content-MD5', req['HTTP_TRAILER']
201     assert_nil req['HTTP_CONTENT_MD5']
202     tmp = ''
203     assert_nil @parser.filter_body(tmp, str)
204     assert_equal 'a..', tmp
205     md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
206     rv = @parser.filter_body(tmp, str)
207     assert_equal rv.object_id, str.object_id
208     assert_equal '', str
209     md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
210     str << md5_hdr
211     assert_nil @parser.trailers(req, str)
212     assert_equal md5_b64, req['HTTP_CONTENT_MD5']
213     assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
214     assert_nil @parser.trailers(req, str << "\r")
215     assert_equal req, @parser.trailers(req, str << "\nGET / ")
216     assert_equal "GET / ", str
217     assert ! @parser.keepalive?
218   end
220   def test_trailers_slowly
221     str = "PUT / HTTP/1.1\r\n" \
222           "Trailer: Content-MD5\r\n" \
223           "transfer-Encoding: chunked\r\n\r\n" \
224           "1\r\na\r\n2\r\n..\r\n0\r\n"
225     req = {}
226     assert_equal req, @parser.headers(req, str)
227     assert_equal 'Content-MD5', req['HTTP_TRAILER']
228     assert_nil req['HTTP_CONTENT_MD5']
229     tmp = ''
230     assert_nil @parser.filter_body(tmp, str)
231     assert_equal 'a..', tmp
232     md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
233     rv = @parser.filter_body(tmp, str)
234     assert_equal rv.object_id, str.object_id
235     assert_equal '', str
236     assert_nil @parser.trailers(req, str)
237     md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
238     md5_hdr.each_byte { |byte|
239       str << byte.chr
240       assert_nil @parser.trailers(req, str)
241     }
242     assert_equal md5_b64, req['HTTP_CONTENT_MD5']
243     assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
244     assert_nil @parser.trailers(req, str << "\r")
245     assert_equal req, @parser.trailers(req, str << "\n")
246   end
248   def test_max_chunk
249     str = "PUT / HTTP/1.1\r\n" \
250           "transfer-Encoding: chunked\r\n\r\n" \
251           "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
252     req = {}
253     assert_equal req, @parser.headers(req, str)
254     assert_nil @parser.content_length
255     assert_nothing_raised { @parser.filter_body('', str) }
256     assert ! @parser.keepalive?
257   end
259   def test_max_body
260     n = HttpParser::LENGTH_MAX
261     str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
262     req = {}
263     assert_nothing_raised { @parser.headers(req, str) }
264     assert_equal n, req['CONTENT_LENGTH'].to_i
265     assert ! @parser.keepalive?
266   end
268   def test_overflow_chunk
269     n = HttpParser::CHUNK_MAX + 1
270     str = "PUT / HTTP/1.1\r\n" \
271           "transfer-Encoding: chunked\r\n\r\n" \
272           "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n"
273     req = {}
274     assert_equal req, @parser.headers(req, str)
275     assert_nil @parser.content_length
276     assert_raise(HttpParserError) { @parser.filter_body('', str) }
277     assert ! @parser.keepalive?
278   end
280   def test_overflow_content_length
281     n = HttpParser::LENGTH_MAX + 1
282     str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n"
283     assert_raise(HttpParserError) { @parser.headers({}, str) }
284     assert ! @parser.keepalive?
285   end
287   def test_bad_chunk
288     str = "PUT / HTTP/1.1\r\n" \
289           "transfer-Encoding: chunked\r\n\r\n" \
290           "#zzz\r\na\r\n2\r\n..\r\n0\r\n"
291     req = {}
292     assert_equal req, @parser.headers(req, str)
293     assert_nil @parser.content_length
294     assert_raise(HttpParserError) { @parser.filter_body('', str) }
295     assert ! @parser.keepalive?
296   end
298   def test_bad_content_length
299     str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n"
300     assert_raise(HttpParserError) { @parser.headers({}, str) }
301     assert ! @parser.keepalive?
302   end
304   def test_bad_trailers
305     str = "PUT / HTTP/1.1\r\n" \
306           "Trailer: Transfer-Encoding\r\n" \
307           "transfer-Encoding: chunked\r\n\r\n" \
308           "1\r\na\r\n2\r\n..\r\n0\r\n"
309     req = {}
310     assert_equal req, @parser.headers(req, str)
311     assert_equal 'Transfer-Encoding', req['HTTP_TRAILER']
312     tmp = ''
313     assert_nil @parser.filter_body(tmp, str)
314     assert_equal 'a..', tmp
315     assert_equal '', str
316     str << "Transfer-Encoding: identity\r\n\r\n"
317     assert_raise(HttpParserError) { @parser.trailers(req, str) }
318     assert ! @parser.keepalive?
319   end
321   def test_repeat_headers
322     str = "PUT / HTTP/1.1\r\n" \
323           "Trailer: Content-MD5\r\n" \
324           "Trailer: Content-SHA1\r\n" \
325           "transfer-Encoding: chunked\r\n\r\n" \
326           "1\r\na\r\n2\r\n..\r\n0\r\n"
327     req = {}
328     assert_equal req, @parser.headers(req, str)
329     assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER']
330     assert ! @parser.keepalive?
331   end
333   def test_parse_simple_request
334     parser = HttpParser.new
335     req = {}
336     http = "GET /read-rfc1945-if-you-dont-believe-me\r\n"
337     assert_equal req, parser.headers(req, http)
338     assert_equal '', http
339     expect = {
340       "SERVER_NAME"=>"localhost",
341       "rack.url_scheme"=>"http",
342       "REQUEST_PATH"=>"/read-rfc1945-if-you-dont-believe-me",
343       "PATH_INFO"=>"/read-rfc1945-if-you-dont-believe-me",
344       "REQUEST_URI"=>"/read-rfc1945-if-you-dont-believe-me",
345       "SERVER_PORT"=>"80",
346       "SERVER_PROTOCOL"=>"HTTP/0.9",
347       "REQUEST_METHOD"=>"GET",
348       "QUERY_STRING"=>""
349     }
350     assert_equal expect, req
351     assert ! parser.headers?
352   end