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'
11 class HttpParserTest < Test::Unit::TestCase
14 parser = HttpParser.new
16 http = "GET / HTTP/1.1\r\n\r\n"
17 assert_equal req, parser.headers(req, 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?
33 assert_nil parser.headers(req, http)
34 assert_equal "G", http
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']
49 assert parser.keepalive?
52 def test_connection_close_no_ka
53 parser = HttpParser.new
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?
61 def test_connection_keep_alive_ka
62 parser = HttpParser.new
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?
69 def test_connection_keep_alive_ka_bad_method
70 parser = HttpParser.new
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?
77 def test_connection_keep_alive_ka_bad_version
78 parser = HttpParser.new
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?
85 def test_parse_server_host_default_port
86 parser = HttpParser.new
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']
93 assert parser.keepalive?
96 def test_parse_server_host_alt_port
97 parser = HttpParser.new
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']
104 assert parser.keepalive?
107 def test_parse_server_host_empty_port
108 parser = HttpParser.new
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']
115 assert parser.keepalive?
118 def test_parse_server_host_xfp_https
119 parser = HttpParser.new
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']
127 assert parser.keepalive?
130 def test_parse_strange_headers
131 parser = HttpParser.new
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?
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"
146 buf = nasty_pound_header.dup
148 assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m
150 expect.gsub!(/\r\n\t/, ' ')
151 assert_equal req, parser.headers(req, buf)
153 assert_equal expect, req['HTTP_X_SSL_BULLSHIT']
156 def test_continuation_eats_leading_spaces
157 parser = HttpParser.new
158 header = "GET / HTTP/1.1\r\n" \
164 assert_equal req, parser.headers(req, header)
165 assert_equal '', header
166 assert_equal 'ASDF', req['HTTP_X_ASDF']
169 def test_continuation_eats_scattered_leading_spaces
170 parser = HttpParser.new
171 header = "GET / HTTP/1.1\r\n" \
178 assert_equal req, parser.headers(req, header)
179 assert_equal '', header
180 assert_equal 'hi y x ASDF', req['HTTP_X_ASDF']
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
189 header = "GET / HTTP/1.1\r\n" \
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 ]
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
207 def test_invalid_continuation
208 parser = HttpParser.new
209 header = "GET / HTTP/1.1\r\n" \
214 assert_raises(HttpParserError) { parser.headers(req, header) }
217 def test_parse_ie6_urls
218 %w(/some/random/path"
221 /we/love/you/ie6?q=<"">
225 parser = HttpParser.new
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?
236 parser = HttpParser.new
238 bad_http = "GET / SsUTF/1.1"
240 assert_raises(HttpParserError) { parser.headers(req, bad_http) }
242 # make sure we can recover
245 assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n")
246 assert ! parser.keepalive?
250 parser = HttpParser.new
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?
270 # not common, but underscores do appear in practice
271 def test_absolute_uri_underscores
272 parser = HttpParser.new
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?
288 def test_absolute_uri
289 parser = HttpParser.new
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?
305 # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
306 def test_absolute_uri_https
307 parser = HttpParser.new
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?
324 # Host: header should be ignored for absolute URIs
325 def test_absolute_uri_with_port
326 parser = HttpParser.new
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
343 def test_absolute_uri_with_empty_port
344 parser = HttpParser.new
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
361 def test_put_body_oneshot
362 parser = HttpParser.new
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
375 def test_put_body_later
376 parser = HttpParser.new
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
389 def test_unknown_methods
390 %w(GETT HEADR XGET XHEAD).each { |m|
391 parser = HttpParser.new
393 s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
395 assert_nothing_raised do
396 ok = parser.headers(req, s)
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']
403 assert_equal m, req['REQUEST_METHOD']
404 assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
408 def test_fragment_in_uri
409 parser = HttpParser.new
411 get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
413 assert_nothing_raised do
414 ok = parser.headers(req, get)
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']
421 assert parser.keepalive?
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 + "/"
430 res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
432 res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
439 def test_horrible_queries
440 parser = HttpParser.new
442 # then that large header names are caught
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)
451 # then that large mangled field values are caught
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)
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)
468 # finally just that random garbage gets blocked all the time
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)