http: SERVER_PROTOCOL matches HTTP_VERSION
[unicorn.git] / test / unit / test_http_parser.rb
blob5ba023981d0cf9141f10025c8a33dec3d7aaa438
1 # Copyright (c) 2005 Zed A. Shaw 
2 # You can redistribute it and/or modify it under the same terms as Ruby.
4 # Additional work donated by contributors.  See http://mongrel.rubyforge.org/attributions.html
5 # for more information.
7 require 'test/test_helper'
9 include Unicorn
11 class HttpParserTest < Test::Unit::TestCase
13   def test_parse_simple
14     parser = HttpParser.new
15     req = {}
16     http = "GET / HTTP/1.1\r\n\r\n"
17     assert_equal req, parser.headers(req, http)
18     assert_equal '', http
20     assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
21     assert_equal '/', req['REQUEST_PATH']
22     assert_equal 'HTTP/1.1', req['HTTP_VERSION']
23     assert_equal '/', req['REQUEST_URI']
24     assert_equal 'GET', req['REQUEST_METHOD']
25     assert_nil req['FRAGMENT']
26     assert_equal '', req['QUERY_STRING']
28     assert parser.keepalive?
29     parser.reset
30     req.clear
32     http = "G"
33     assert_nil parser.headers(req, http)
34     assert_equal "G", http
35     assert req.empty?
37     # try parsing again to ensure we were reset correctly
38     http = "GET /hello-world HTTP/1.1\r\n\r\n"
39     assert parser.headers(req, http)
41     assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
42     assert_equal '/hello-world', req['REQUEST_PATH']
43     assert_equal 'HTTP/1.1', req['HTTP_VERSION']
44     assert_equal '/hello-world', req['REQUEST_URI']
45     assert_equal 'GET', req['REQUEST_METHOD']
46     assert_nil req['FRAGMENT']
47     assert_equal '', req['QUERY_STRING']
48     assert_equal '', http
49     assert parser.keepalive?
50   end
52   def test_connection_close_no_ka
53     parser = HttpParser.new
54     req = {}
55     tmp = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
56     assert_equal req.object_id, parser.headers(req, tmp).object_id
57     assert_equal "GET", req['REQUEST_METHOD']
58     assert ! parser.keepalive?
59   end
61   def test_connection_keep_alive_ka
62     parser = HttpParser.new
63     req = {}
64     tmp = "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
65     assert_equal req.object_id, parser.headers(req, tmp).object_id
66     assert parser.keepalive?
67   end
69   def test_connection_keep_alive_ka_bad_method
70     parser = HttpParser.new
71     req = {}
72     tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
73     assert_equal req.object_id, parser.headers(req, tmp).object_id
74     assert ! parser.keepalive?
75   end
77   def test_connection_keep_alive_ka_bad_version
78     parser = HttpParser.new
79     req = {}
80     tmp = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
81     assert_equal req.object_id, parser.headers(req, tmp).object_id
82     assert parser.keepalive?
83   end
85   def test_parse_server_host_default_port
86     parser = HttpParser.new
87     req = {}
88     tmp = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n"
89     assert_equal req, parser.headers(req, tmp)
90     assert_equal 'foo', req['SERVER_NAME']
91     assert_equal '80', req['SERVER_PORT']
92     assert_equal '', tmp
93     assert parser.keepalive?
94   end
96   def test_parse_server_host_alt_port
97     parser = HttpParser.new
98     req = {}
99     tmp = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n"
100     assert_equal req, parser.headers(req, tmp)
101     assert_equal 'foo', req['SERVER_NAME']
102     assert_equal '999', req['SERVER_PORT']
103     assert_equal '', tmp
104     assert parser.keepalive?
105   end
107   def test_parse_server_host_empty_port
108     parser = HttpParser.new
109     req = {}
110     tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n"
111     assert_equal req, parser.headers(req, tmp)
112     assert_equal 'foo', req['SERVER_NAME']
113     assert_equal '80', req['SERVER_PORT']
114     assert_equal '', tmp
115     assert parser.keepalive?
116   end
118   def test_parse_server_host_xfp_https
119     parser = HttpParser.new
120     req = {}
121     tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n" \
122           "X-Forwarded-Proto: https\r\n\r\n"
123     assert_equal req, parser.headers(req, tmp)
124     assert_equal 'foo', req['SERVER_NAME']
125     assert_equal '443', req['SERVER_PORT']
126     assert_equal '', tmp
127     assert parser.keepalive?
128   end
130   def test_parse_strange_headers
131     parser = HttpParser.new
132     req = {}
133     should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
134     assert_equal req, parser.headers(req, should_be_good)
135     assert_equal '', should_be_good
136     assert parser.keepalive?
137   end
139   # legacy test case from Mongrel that we never supported before...
140   # I still consider Pound irrelevant, unfortunately stupid clients that
141   # send extremely big headers do exist and they've managed to find Unicorn...
142   def test_nasty_pound_header
143     parser = HttpParser.new
144     nasty_pound_header = "GET / HTTP/1.1\r\nX-SSL-Bullshit:   -----BEGIN CERTIFICATE-----\r\n\tMIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n\tETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n\tAkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n\tdWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n\tSzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n\tBAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n\tBQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n\tW51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n\tgW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n\t0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n\tu2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n\twgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n\tA1UdEwEB/wQCMAAwEQYJYIZIAYb4QgEBBAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n\tBglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n\tVR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n\tloCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n\taWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n\t9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n\tIjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n\tBgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n\tcHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4QgEDBDAWLmh0\r\n\tdHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC5jcmwwPwYD\r\n\tVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n\tY3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n\tXCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n\tUO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n\thTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n\twTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n\tYhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n\tRA==\r\n\t-----END CERTIFICATE-----\r\n\r\n"
145     req = {}
146     buf = nasty_pound_header.dup
148     assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m
149     expect = $1.dup
150     expect.gsub!(/\r\n\t/, ' ')
151     assert_equal req, parser.headers(req, buf)
152     assert_equal '', buf
153     assert_equal expect, req['HTTP_X_SSL_BULLSHIT']
154   end
156   def test_continuation_eats_leading_spaces
157     parser = HttpParser.new
158     header = "GET / HTTP/1.1\r\n" \
159              "X-ASDF:      \r\n" \
160              "\t\r\n" \
161              "    \r\n" \
162              "  ASDF\r\n\r\n"
163     req = {}
164     assert_equal req, parser.headers(req, header)
165     assert_equal '', header
166     assert_equal 'ASDF', req['HTTP_X_ASDF']
167   end
169   def test_continuation_eats_scattered_leading_spaces
170     parser = HttpParser.new
171     header = "GET / HTTP/1.1\r\n" \
172              "X-ASDF:   hi\r\n" \
173              "    y\r\n" \
174              "\t\r\n" \
175              "       x\r\n" \
176              "  ASDF\r\n\r\n"
177     req = {}
178     assert_equal req, parser.headers(req, header)
179     assert_equal '', header
180     assert_equal 'hi y x ASDF', req['HTTP_X_ASDF']
181   end
183   # this may seem to be testing more of an implementation detail, but
184   # it also helps ensure we're safe in the presence of multiple parsers
185   # in case we ever go multithreaded/evented...
186   def test_resumable_continuations
187     nr = 1000
188     req = {}
189     header = "GET / HTTP/1.1\r\n" \
190              "X-ASDF:      \r\n" \
191              "  hello\r\n"
192     tmp = []
193     nr.times { |i|
194       parser = HttpParser.new
195       assert parser.headers(req, "#{header} #{i}\r\n").nil?
196       asdf = req['HTTP_X_ASDF']
197       assert_equal "hello #{i}", asdf
198       tmp << [ parser, asdf ]
199       req.clear
200     }
201     tmp.each_with_index { |(parser, asdf), i|
202       assert_equal req, parser.headers(req, "#{header} #{i}\r\n .\r\n\r\n")
203       assert_equal "hello #{i} .", asdf
204     }
205   end
207   def test_invalid_continuation
208     parser = HttpParser.new
209     header = "GET / HTTP/1.1\r\n" \
210              "    y\r\n" \
211              "Host: hello\r\n" \
212              "\r\n"
213     req = {}
214     assert_raises(HttpParserError) { parser.headers(req, header) }
215   end
217   def test_parse_ie6_urls
218     %w(/some/random/path"
219        /some/random/path>
220        /some/random/path<
221        /we/love/you/ie6?q=<"">
222        /url?<="&>="
223        /mal"formed"?
224     ).each do |path|
225       parser = HttpParser.new
226       req = {}
227       sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
228       assert_equal req, parser.headers(req, sorta_safe)
229       assert_equal path, req['REQUEST_URI']
230       assert_equal '', sorta_safe
231       assert parser.keepalive?
232     end
233   end
234   
235   def test_parse_error
236     parser = HttpParser.new
237     req = {}
238     bad_http = "GET / SsUTF/1.1"
240     assert_raises(HttpParserError) { parser.headers(req, bad_http) }
242     # make sure we can recover
243     parser.reset
244     req.clear
245     assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n")
246     assert ! parser.keepalive?
247   end
249   def test_piecemeal
250     parser = HttpParser.new
251     req = {}
252     http = "GET"
253     assert_nil parser.headers(req, http)
254     assert_nil parser.headers(req, http)
255     assert_nil parser.headers(req, http << " / HTTP/1.0")
256     assert_equal '/', req['REQUEST_PATH']
257     assert_equal '/', req['REQUEST_URI']
258     assert_equal 'GET', req['REQUEST_METHOD']
259     assert_nil parser.headers(req, http << "\r\n")
260     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
261     assert_nil parser.headers(req, http << "\r")
262     assert_equal req, parser.headers(req, http << "\n")
263     assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
264     assert_nil req['FRAGMENT']
265     assert_equal '', req['QUERY_STRING']
266     assert_equal "", http
267     assert ! parser.keepalive?
268   end
270   # not common, but underscores do appear in practice
271   def test_absolute_uri_underscores
272     parser = HttpParser.new
273     req = {}
274     http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n"
275     assert_equal req, parser.headers(req, http)
276     assert_equal 'http', req['rack.url_scheme']
277     assert_equal '/foo?q=bar', req['REQUEST_URI']
278     assert_equal '/foo', req['REQUEST_PATH']
279     assert_equal 'q=bar', req['QUERY_STRING']
281     assert_equal 'under_score.example.com', req['HTTP_HOST']
282     assert_equal 'under_score.example.com', req['SERVER_NAME']
283     assert_equal '80', req['SERVER_PORT']
284     assert_equal "", http
285     assert ! parser.keepalive?
286   end
288   def test_absolute_uri
289     parser = HttpParser.new
290     req = {}
291     http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n"
292     assert_equal req, parser.headers(req, http)
293     assert_equal 'http', req['rack.url_scheme']
294     assert_equal '/foo?q=bar', req['REQUEST_URI']
295     assert_equal '/foo', req['REQUEST_PATH']
296     assert_equal 'q=bar', req['QUERY_STRING']
298     assert_equal 'example.com', req['HTTP_HOST']
299     assert_equal 'example.com', req['SERVER_NAME']
300     assert_equal '80', req['SERVER_PORT']
301     assert_equal "", http
302     assert ! parser.keepalive?
303   end
305   # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
306   def test_absolute_uri_https
307     parser = HttpParser.new
308     req = {}
309     http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \
310            "X-Forwarded-Proto: http\r\n\r\n"
311     assert_equal req, parser.headers(req, http)
312     assert_equal 'https', req['rack.url_scheme']
313     assert_equal '/foo?q=bar', req['REQUEST_URI']
314     assert_equal '/foo', req['REQUEST_PATH']
315     assert_equal 'q=bar', req['QUERY_STRING']
317     assert_equal 'example.com', req['HTTP_HOST']
318     assert_equal 'example.com', req['SERVER_NAME']
319     assert_equal '443', req['SERVER_PORT']
320     assert_equal "", http
321     assert parser.keepalive?
322   end
324   # Host: header should be ignored for absolute URIs
325   def test_absolute_uri_with_port
326     parser = HttpParser.new
327     req = {}
328     http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \
329            "Host: bad.example.com\r\n\r\n"
330     assert_equal req, parser.headers(req, http)
331     assert_equal 'http', req['rack.url_scheme']
332     assert_equal '/foo?q=bar', req['REQUEST_URI']
333     assert_equal '/foo', req['REQUEST_PATH']
334     assert_equal 'q=bar', req['QUERY_STRING']
336     assert_equal 'example.com:8080', req['HTTP_HOST']
337     assert_equal 'example.com', req['SERVER_NAME']
338     assert_equal '8080', req['SERVER_PORT']
339     assert_equal "", http
340     assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
341   end
343   def test_absolute_uri_with_empty_port
344     parser = HttpParser.new
345     req = {}
346     http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \
347            "Host: bad.example.com\r\n\r\n"
348     assert_equal req, parser.headers(req, http)
349     assert_equal 'https', req['rack.url_scheme']
350     assert_equal '/foo?q=bar', req['REQUEST_URI']
351     assert_equal '/foo', req['REQUEST_PATH']
352     assert_equal 'q=bar', req['QUERY_STRING']
354     assert_equal 'example.com:', req['HTTP_HOST']
355     assert_equal 'example.com', req['SERVER_NAME']
356     assert_equal '443', req['SERVER_PORT']
357     assert_equal "", http
358     assert parser.keepalive? # TODO: read HTTP/1.2 when it's final
359   end
361   def test_put_body_oneshot
362     parser = HttpParser.new
363     req = {}
364     http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde"
365     assert_equal req, parser.headers(req, http)
366     assert_equal '/', req['REQUEST_PATH']
367     assert_equal '/', req['REQUEST_URI']
368     assert_equal 'PUT', req['REQUEST_METHOD']
369     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
370     assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
371     assert_equal "abcde", http
372     assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
373   end
375   def test_put_body_later
376     parser = HttpParser.new
377     req = {}
378     http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
379     assert_equal req, parser.headers(req, http)
380     assert_equal '/l', req['REQUEST_PATH']
381     assert_equal '/l', req['REQUEST_URI']
382     assert_equal 'PUT', req['REQUEST_METHOD']
383     assert_equal 'HTTP/1.0', req['HTTP_VERSION']
384     assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
385     assert_equal "", http
386     assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
387   end
389   def test_unknown_methods
390     %w(GETT HEADR XGET XHEAD).each { |m|
391       parser = HttpParser.new
392       req = {}
393       s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
394       ok = false
395       assert_nothing_raised do
396         ok = parser.headers(req, s)
397       end
398       assert ok
399       assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
400       assert_equal 'posts-17408', req['FRAGMENT']
401       assert_equal 'page=1', req['QUERY_STRING']
402       assert_equal "", s
403       assert_equal m, req['REQUEST_METHOD']
404       assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
405     }
406   end
408   def test_fragment_in_uri
409     parser = HttpParser.new
410     req = {}
411     get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
412     ok = false
413     assert_nothing_raised do
414       ok = parser.headers(req, get)
415     end
416     assert ok
417     assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
418     assert_equal 'posts-17408', req['FRAGMENT']
419     assert_equal 'page=1', req['QUERY_STRING']
420     assert_equal '', get
421     assert parser.keepalive?
422   end
424   # lame random garbage maker
425   def rand_data(min, max, readable=true)
426     count = min + ((rand(max)+1) *10).to_i
427     res = count.to_s + "/"
428     
429     if readable
430       res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
431     else
432       res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
433     end
435     return res
436   end
437   
439   def test_horrible_queries
440     parser = HttpParser.new
442     # then that large header names are caught
443     10.times do |c|
444       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
445       assert_raises Unicorn::HttpParserError do
446         parser.headers({}, get)
447         parser.reset
448       end
449     end
451     # then that large mangled field values are caught
452     10.times do |c|
453       get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
454       assert_raises Unicorn::HttpParserError do
455         parser.headers({}, get)
456         parser.reset
457       end
458     end
460     # then large headers are rejected too
461     get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
462     get << "X-Test: test\r\n" * (80 * 1024)
463     assert_raises Unicorn::HttpParserError do
464       parser.headers({}, get)
465       parser.reset
466     end
468     # finally just that random garbage gets blocked all the time
469     10.times do |c|
470       get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
471       assert_raises Unicorn::HttpParserError do
472         parser.headers({}, get)
473         parser.reset
474       end
475     end
477   end