1 # -*- encoding: binary -*-
3 # Copyright (c) 2005 Zed A. Shaw
4 # You can redistribute it and/or modify it under the same terms as Ruby.
6 # Additional work donated by contributors. See http://mongrel.rubyforge.org/attributions.html
7 # for more information.
9 require 'test/test_helper'
13 class HttpParserTest < Test::Unit::TestCase
16 parser = HttpParser.new
18 http = "GET / HTTP/1.1\r\n\r\n"
19 assert_equal req, parser.headers(req, http)
22 assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
23 assert_equal '/', req['REQUEST_PATH']
24 assert_equal 'HTTP/1.1', req['HTTP_VERSION']
25 assert_equal '/', req['REQUEST_URI']
26 assert_equal 'GET', req['REQUEST_METHOD']
27 assert_nil req['FRAGMENT']
28 assert_equal '', req['QUERY_STRING']
30 assert parser.keepalive?
35 assert_nil parser.headers(req, http)
36 assert_equal "G", http
39 # try parsing again to ensure we were reset correctly
40 http = "GET /hello-world HTTP/1.1\r\n\r\n"
41 assert parser.headers(req, http)
43 assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL']
44 assert_equal '/hello-world', req['REQUEST_PATH']
45 assert_equal 'HTTP/1.1', req['HTTP_VERSION']
46 assert_equal '/hello-world', req['REQUEST_URI']
47 assert_equal 'GET', req['REQUEST_METHOD']
48 assert_nil req['FRAGMENT']
49 assert_equal '', req['QUERY_STRING']
51 assert parser.keepalive?
55 parser = HttpParser.new
57 tmp = "GET / HTTP/1.1\r\nHost:\tfoo.bar\r\n\r\n"
58 assert_equal req.object_id, parser.headers(req, tmp).object_id
59 assert_equal "foo.bar", req['HTTP_HOST']
62 def test_connection_close_no_ka
63 parser = HttpParser.new
65 tmp = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
66 assert_equal req.object_id, parser.headers(req, tmp).object_id
67 assert_equal "GET", req['REQUEST_METHOD']
68 assert ! parser.keepalive?
71 def test_connection_keep_alive_ka
72 parser = HttpParser.new
74 tmp = "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
75 assert_equal req.object_id, parser.headers(req, tmp).object_id
76 assert parser.keepalive?
79 def test_connection_keep_alive_ka_bad_method
80 parser = HttpParser.new
82 tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"
83 assert_equal req.object_id, parser.headers(req, tmp).object_id
84 assert ! parser.keepalive?
87 def test_connection_keep_alive_ka_bad_version
88 parser = HttpParser.new
90 tmp = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n"
91 assert_equal req.object_id, parser.headers(req, tmp).object_id
92 assert parser.keepalive?
95 def test_parse_server_host_default_port
96 parser = HttpParser.new
98 tmp = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n"
99 assert_equal req, parser.headers(req, tmp)
100 assert_equal 'foo', req['SERVER_NAME']
101 assert_equal '80', req['SERVER_PORT']
103 assert parser.keepalive?
106 def test_parse_server_host_alt_port
107 parser = HttpParser.new
109 tmp = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n"
110 assert_equal req, parser.headers(req, tmp)
111 assert_equal 'foo', req['SERVER_NAME']
112 assert_equal '999', req['SERVER_PORT']
114 assert parser.keepalive?
117 def test_parse_server_host_empty_port
118 parser = HttpParser.new
120 tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n"
121 assert_equal req, parser.headers(req, tmp)
122 assert_equal 'foo', req['SERVER_NAME']
123 assert_equal '80', req['SERVER_PORT']
125 assert parser.keepalive?
128 def test_parse_server_host_xfp_https
129 parser = HttpParser.new
131 tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n" \
132 "X-Forwarded-Proto: https\r\n\r\n"
133 assert_equal req, parser.headers(req, tmp)
134 assert_equal 'foo', req['SERVER_NAME']
135 assert_equal '443', req['SERVER_PORT']
137 assert parser.keepalive?
140 def test_parse_strange_headers
141 parser = HttpParser.new
143 should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n"
144 assert_equal req, parser.headers(req, should_be_good)
145 assert_equal '', should_be_good
146 assert parser.keepalive?
149 # legacy test case from Mongrel that we never supported before...
150 # I still consider Pound irrelevant, unfortunately stupid clients that
151 # send extremely big headers do exist and they've managed to find Unicorn...
152 def test_nasty_pound_header
153 parser = HttpParser.new
154 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"
156 buf = nasty_pound_header.dup
158 assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m
160 expect.gsub!(/\r\n\t/, ' ')
161 assert_equal req, parser.headers(req, buf)
163 assert_equal expect, req['HTTP_X_SSL_BULLSHIT']
166 def test_continuation_eats_leading_spaces
167 parser = HttpParser.new
168 header = "GET / HTTP/1.1\r\n" \
174 assert_equal req, parser.headers(req, header)
175 assert_equal '', header
176 assert_equal 'ASDF', req['HTTP_X_ASDF']
179 def test_continuation_eats_scattered_leading_spaces
180 parser = HttpParser.new
181 header = "GET / HTTP/1.1\r\n" \
188 assert_equal req, parser.headers(req, header)
189 assert_equal '', header
190 assert_equal 'hi y x ASDF', req['HTTP_X_ASDF']
193 def test_continuation_with_absolute_uri_and_ignored_host_header
194 parser = HttpParser.new
195 header = "GET http://example.com/ HTTP/1.1\r\n" \
200 assert_equal req, parser.headers(req, header)
201 assert_equal 'example.com', req['HTTP_HOST']
204 # this may seem to be testing more of an implementation detail, but
205 # it also helps ensure we're safe in the presence of multiple parsers
206 # in case we ever go multithreaded/evented...
207 def test_resumable_continuations
210 header = "GET / HTTP/1.1\r\n" \
215 parser = HttpParser.new
216 assert parser.headers(req, "#{header} #{i}\r\n").nil?
217 asdf = req['HTTP_X_ASDF']
218 assert_equal "hello #{i}", asdf
219 tmp << [ parser, asdf ]
222 tmp.each_with_index { |(parser, asdf), i|
223 assert_equal req, parser.headers(req, "#{header} #{i}\r\n .\r\n\r\n")
224 assert_equal "hello #{i} .", asdf
228 def test_invalid_continuation
229 parser = HttpParser.new
230 header = "GET / HTTP/1.1\r\n" \
235 assert_raises(HttpParserError) { parser.headers(req, header) }
238 def test_parse_ie6_urls
239 %w(/some/random/path"
242 /we/love/you/ie6?q=<"">
246 parser = HttpParser.new
248 sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n)
249 assert_equal req, parser.headers(req, sorta_safe)
250 assert_equal path, req['REQUEST_URI']
251 assert_equal '', sorta_safe
252 assert parser.keepalive?
257 parser = HttpParser.new
259 bad_http = "GET / SsUTF/1.1"
261 assert_raises(HttpParserError) { parser.headers(req, bad_http) }
263 # make sure we can recover
266 assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n")
267 assert ! parser.keepalive?
271 parser = HttpParser.new
274 assert_nil parser.headers(req, http)
275 assert_nil parser.headers(req, http)
276 assert_nil parser.headers(req, http << " / HTTP/1.0")
277 assert_equal '/', req['REQUEST_PATH']
278 assert_equal '/', req['REQUEST_URI']
279 assert_equal 'GET', req['REQUEST_METHOD']
280 assert_nil parser.headers(req, http << "\r\n")
281 assert_equal 'HTTP/1.0', req['HTTP_VERSION']
282 assert_nil parser.headers(req, http << "\r")
283 assert_equal req, parser.headers(req, http << "\n")
284 assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
285 assert_nil req['FRAGMENT']
286 assert_equal '', req['QUERY_STRING']
287 assert_equal "", http
288 assert ! parser.keepalive?
291 # not common, but underscores do appear in practice
292 def test_absolute_uri_underscores
293 parser = HttpParser.new
295 http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n"
296 assert_equal req, parser.headers(req, http)
297 assert_equal 'http', req['rack.url_scheme']
298 assert_equal '/foo?q=bar', req['REQUEST_URI']
299 assert_equal '/foo', req['REQUEST_PATH']
300 assert_equal 'q=bar', req['QUERY_STRING']
302 assert_equal 'under_score.example.com', req['HTTP_HOST']
303 assert_equal 'under_score.example.com', req['SERVER_NAME']
304 assert_equal '80', req['SERVER_PORT']
305 assert_equal "", http
306 assert ! parser.keepalive?
309 # some dumb clients add users because they're stupid
310 def test_absolute_uri_w_user
311 parser = HttpParser.new
313 http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n"
314 assert_equal req, parser.headers(req, http)
315 assert_equal 'http', req['rack.url_scheme']
316 assert_equal '/foo?q=bar', req['REQUEST_URI']
317 assert_equal '/foo', req['REQUEST_PATH']
318 assert_equal 'q=bar', req['QUERY_STRING']
320 assert_equal 'example.com', req['HTTP_HOST']
321 assert_equal 'example.com', req['SERVER_NAME']
322 assert_equal '80', req['SERVER_PORT']
323 assert_equal "", http
324 assert ! parser.keepalive?
327 # since Mongrel supported anything URI.parse supported, we're stuck
328 # supporting everything URI.parse supports
329 def test_absolute_uri_uri_parse
330 "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char|
331 parser = HttpParser.new
333 http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n"
334 assert_equal req, parser.headers(req, http)
335 assert_equal 'http', req['rack.url_scheme']
336 assert_equal '/', req['REQUEST_URI']
337 assert_equal '/', req['REQUEST_PATH']
338 assert_equal '', req['QUERY_STRING']
340 assert_equal 'example.com', req['HTTP_HOST']
341 assert_equal 'example.com', req['SERVER_NAME']
342 assert_equal '80', req['SERVER_PORT']
343 assert_equal "", http
344 assert ! parser.keepalive?
348 def test_absolute_uri
349 parser = HttpParser.new
351 http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n"
352 assert_equal req, parser.headers(req, http)
353 assert_equal 'http', req['rack.url_scheme']
354 assert_equal '/foo?q=bar', req['REQUEST_URI']
355 assert_equal '/foo', req['REQUEST_PATH']
356 assert_equal 'q=bar', req['QUERY_STRING']
358 assert_equal 'example.com', req['HTTP_HOST']
359 assert_equal 'example.com', req['SERVER_NAME']
360 assert_equal '80', req['SERVER_PORT']
361 assert_equal "", http
362 assert ! parser.keepalive?
365 # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however...
366 def test_absolute_uri_https
367 parser = HttpParser.new
369 http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \
370 "X-Forwarded-Proto: http\r\n\r\n"
371 assert_equal req, parser.headers(req, http)
372 assert_equal 'https', req['rack.url_scheme']
373 assert_equal '/foo?q=bar', req['REQUEST_URI']
374 assert_equal '/foo', req['REQUEST_PATH']
375 assert_equal 'q=bar', req['QUERY_STRING']
377 assert_equal 'example.com', req['HTTP_HOST']
378 assert_equal 'example.com', req['SERVER_NAME']
379 assert_equal '443', req['SERVER_PORT']
380 assert_equal "", http
381 assert parser.keepalive?
384 # Host: header should be ignored for absolute URIs
385 def test_absolute_uri_with_port
386 parser = HttpParser.new
388 http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \
389 "Host: bad.example.com\r\n\r\n"
390 assert_equal req, parser.headers(req, http)
391 assert_equal 'http', req['rack.url_scheme']
392 assert_equal '/foo?q=bar', req['REQUEST_URI']
393 assert_equal '/foo', req['REQUEST_PATH']
394 assert_equal 'q=bar', req['QUERY_STRING']
396 assert_equal 'example.com:8080', req['HTTP_HOST']
397 assert_equal 'example.com', req['SERVER_NAME']
398 assert_equal '8080', req['SERVER_PORT']
399 assert_equal "", http
400 assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
403 def test_absolute_uri_with_empty_port
404 parser = HttpParser.new
406 http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \
407 "Host: bad.example.com\r\n\r\n"
408 assert_equal req, parser.headers(req, http)
409 assert_equal 'https', req['rack.url_scheme']
410 assert_equal '/foo?q=bar', req['REQUEST_URI']
411 assert_equal '/foo', req['REQUEST_PATH']
412 assert_equal 'q=bar', req['QUERY_STRING']
414 assert_equal 'example.com:', req['HTTP_HOST']
415 assert_equal 'example.com', req['SERVER_NAME']
416 assert_equal '443', req['SERVER_PORT']
417 assert_equal "", http
418 assert parser.keepalive? # TODO: read HTTP/1.2 when it's final
421 def test_put_body_oneshot
422 parser = HttpParser.new
424 http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde"
425 assert_equal req, parser.headers(req, http)
426 assert_equal '/', req['REQUEST_PATH']
427 assert_equal '/', req['REQUEST_URI']
428 assert_equal 'PUT', req['REQUEST_METHOD']
429 assert_equal 'HTTP/1.0', req['HTTP_VERSION']
430 assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
431 assert_equal "abcde", http
432 assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
435 def test_put_body_later
436 parser = HttpParser.new
438 http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
439 assert_equal req, parser.headers(req, http)
440 assert_equal '/l', req['REQUEST_PATH']
441 assert_equal '/l', req['REQUEST_URI']
442 assert_equal 'PUT', req['REQUEST_METHOD']
443 assert_equal 'HTTP/1.0', req['HTTP_VERSION']
444 assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL']
445 assert_equal "", http
446 assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
449 def test_unknown_methods
450 %w(GETT HEADR XGET XHEAD).each { |m|
451 parser = HttpParser.new
453 s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
455 assert_nothing_raised do
456 ok = parser.headers(req, s)
459 assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
460 assert_equal 'posts-17408', req['FRAGMENT']
461 assert_equal 'page=1', req['QUERY_STRING']
463 assert_equal m, req['REQUEST_METHOD']
464 assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final
468 def test_fragment_in_uri
469 parser = HttpParser.new
471 get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n"
473 assert_nothing_raised do
474 ok = parser.headers(req, get)
477 assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI']
478 assert_equal 'posts-17408', req['FRAGMENT']
479 assert_equal 'page=1', req['QUERY_STRING']
481 assert parser.keepalive?
484 # lame random garbage maker
485 def rand_data(min, max, readable=true)
486 count = min + ((rand(max)+1) *10).to_i
487 res = count.to_s + "/"
490 res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40)
492 res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20)
499 def test_horrible_queries
500 parser = HttpParser.new
502 # then that large header names are caught
504 get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n"
505 assert_raises Unicorn::HttpParserError do
506 parser.headers({}, get)
511 # then that large mangled field values are caught
513 get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
514 assert_raises Unicorn::HttpParserError do
515 parser.headers({}, get)
520 # then large headers are rejected too
521 get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n"
522 get << "X-Test: test\r\n" * (80 * 1024)
523 assert_raises Unicorn::HttpParserError do
524 parser.headers({}, get)
528 # finally just that random garbage gets blocked all the time
530 get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n"
531 assert_raises Unicorn::HttpParserError do
532 parser.headers({}, get)
539 # so we don't care about the portability of this test
540 # if it doesn't leak on Linux, it won't leak anywhere else
541 # unless your C compiler or platform is otherwise broken
542 LINUX_PROC_PID_STATUS = "/proc/self/status"
544 match_rss = /^VmRSS:\s+(\d+)/
545 if File.read(LINUX_PROC_PID_STATUS) =~ match_rss
547 1000000.times { Unicorn::HttpParser.new }
548 File.read(LINUX_PROC_PID_STATUS) =~ match_rss
550 diff = after - before
551 assert(diff < 10000, "memory grew more than 10M: #{diff}")
553 end if RUBY_PLATFORM =~ /linux/ &&
554 File.readable?(LINUX_PROC_PID_STATUS) &&
555 !defined?(RUBY_ENGINE)