diff options
Diffstat (limited to 'test/test_request_parser.rb')
-rw-r--r-- | test/test_request_parser.rb | 862 |
1 files changed, 862 insertions, 0 deletions
diff --git a/test/test_request_parser.rb b/test/test_request_parser.rb new file mode 100644 index 0000000..5e97a0e --- /dev/null +++ b/test/test_request_parser.rb @@ -0,0 +1,862 @@ +# -*- encoding: binary -*- +## Copyright (c) 2005 Zed A. Shaw +# You can redistribute it and/or modify it under the same terms as Ruby 1.8 +# or GPL-2.0+ <https://www.gnu.org/licenses/gpl-2.0.txt> +# +# Additional work donated by contributors. See git history of +# unicorn for more information: git clone https://bogomips.org/unicorn.git + +require 'test/unit' +require 'digest' +require 'kcar' +require 'uri' + +class TestRequestParser < Test::Unit::TestCase + def setup + @hp = Kcar::Parser.new + @env = {} + end + + def test_parse_oneshot_simple + buf = "GET / HTTP/1.1\r\n\r\n" + http = 'http'.freeze + @env['rack.url_scheme'] = http + env = @hp.request(@env, buf.dup) + assert_same env, @env + exp = { + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'HTTP_VERSION' => 'HTTP/1.1', + 'REQUEST_PATH' => '/', + 'PATH_INFO' => '/', + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => '', + 'rack.url_scheme' => 'http', + } + assert_equal exp, env + assert_same env['HTTP_VERSION'], env['SERVER_PROTOCOL'] + assert_predicate env['HTTP_VERSION'], :frozen? + assert_predicate env['REQUEST_METHOD'], :frozen? + assert_same http, env['rack.url_scheme'] + assert_predicate @hp, :keepalive? + + @hp.reset + + buf = "G" + assert_nil @hp.request(@env, buf) + # try parsing again to ensure we were reset correctly + buf << "ET /hello-world HTTP/1.1\r\n\r\n" + assert_same env, @hp.request(env, buf) + + assert_equal 'HTTP/1.1', env['SERVER_PROTOCOL'] + assert_equal '/hello-world', env['REQUEST_PATH'] + assert_equal 'HTTP/1.1', env['HTTP_VERSION'] + assert_equal '/hello-world', env['REQUEST_URI'] + assert_equal 'GET', env['REQUEST_METHOD'] + assert_equal '', env['QUERY_STRING'] + assert @hp.keepalive? + end + + def test_tab_lws + @hp.request(@env, "GET / HTTP/1.1\r\nHost:\tfoo.bar\r\n\r\n") + assert_equal "foo.bar", @env['HTTP_HOST'] + end + + def test_connection_close_no_ka + @hp.request(@env = {}, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n") + assert_equal 'GET', @env['REQUEST_METHOD'] + assert ! @hp.keepalive? + end + + def test_connection_keep_alive_ka + @hp.request(@env, "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n") + assert @hp.keepalive? + end + + def test_connection_keep_alive_no_body + r = @hp.request(@env, "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n") + assert_same @env, r + assert @hp.keepalive? + end + + def test_connection_keep_alive_no_body_empty + buf = "POST / HTTP/1.1\r\n" \ + "Content-Length: 0\r\n" \ + "Connection: keep-alive\r\n\r\n" + assert_same @env, @hp.request(@env, buf) + assert @hp.keepalive? + end + + def test_connection_keep_alive_ka_bad_version + @hp.request(@env, "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n") + assert @hp.keepalive? + end + + def test_parse_server_host_default_port + buf = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n" + assert_same @env, @hp.request(@env, buf) + assert_equal 'foo', @env['SERVER_NAME'] + assert_equal '80', @env['SERVER_PORT'] + assert_equal '', buf + assert @hp.keepalive? + end + + def test_parse_server_host_alt_port + buf = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n" + @hp.request(@env, buf) + assert_equal 'foo', @env['SERVER_NAME'] + assert_equal '999', @env['SERVER_PORT'] + assert_equal '', buf + assert @hp.keepalive? + end + + def test_parse_server_host_empty_port + @hp.request(@env, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n") + assert_equal 'foo', @env['SERVER_NAME'] + assert_equal '80', @env['SERVER_PORT'] + assert @hp.keepalive? + end + + def test_parse_host_cont + @hp.request(@env, "GET / HTTP/1.1\r\nHost:\r\n foo\r\n\r\n") + assert_equal 'foo', @env['SERVER_NAME'] + assert_equal '80', @env['SERVER_PORT'] + assert @hp.keepalive? + end + + def test_preserve_existing_server_vars + @env = { + 'SERVER_NAME' => 'example.com', + 'SERVER_PORT' => '1234', + 'rack.url_scheme' => 'https' + } + @hp.request(@env, "GET / HTTP/1.0\r\n\r\n") + assert_equal 'example.com', @env['SERVER_NAME'] + assert_equal '1234', @env['SERVER_PORT'] + assert_equal 'https', @env['rack.url_scheme'] + end + + def test_parse_strange_headers + should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" + req = @hp.request(@env, should_be_good) + assert_same req, @env + assert_equal '', should_be_good + assert_predicate @hp, :keepalive? + assert_equal '++++++++++', @env['HTTP_AAAAAAAAAAAAA'] + end + + # legacy test case from Mongrel + # I still consider Pound irrelevant, unfortunately stupid clients that + # send extremely big headers do exist and they've managed to find us... + def test_nasty_pound_header + nasty_pound_header = "GET / HTTP/1.1\r\n" \ +"X-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" + ok = (/(-----BEGIN .*--END CERTIFICATE-----)/m =~ nasty_pound_header) + expect = $1.dup + assert ok, 'end certificate matched' + expect.gsub!(/\r\n\t/, ' ') + req = @hp.request(@env, nasty_pound_header.dup) + assert_equal expect, req['HTTP_X_SSL_BULLSHIT'] + end + + def test_multiline_header_0d0a + req = @hp.request(@env, "GET / HTTP/1.0\r\n" \ + "X-Multiline-Header: foo bar\r\n\tcha cha\r\n\tzha zha\r\n\r\n") + assert_same req, @env + assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER'] + end + + def test_multiline_header_0a + req = @hp.request(@env, "GET / HTTP/1.0\n" \ + "X-Multiline-Header: foo bar\n\tcha cha\n\tzha zha\n\n") + assert_same req, @env + assert_equal 'foo bar cha cha zha zha', req['HTTP_X_MULTILINE_HEADER'] + end + + def test_continuation_eats_leading_spaces + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + "\t\r\n" \ + " \r\n" \ + " ASDF\r\n\r\n" + req = @hp.request(@env, header) + assert_same req, @env + assert_equal '', header + assert_equal 'ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_eats_scattered_leading_spaces + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: hi\r\n" \ + " y\r\n" \ + "\t\r\n" \ + " x\r\n" \ + " ASDF\r\n\r\n" + req = @hp.request(@env, header) + assert_same req, @env + assert_equal '', header + assert_equal 'hi y x ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_eats_trailing_spaces + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + "\t\r\n" \ + " b \r\n" \ + " ASDF\r\n\r\nZ" + req = @hp.request(@env, header) + assert_same req, @env + assert_equal 'Z', header + assert_equal 'b ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_with_absolute_uri_and_ignored_host_header + header = "GET http://example.com/ HTTP/1.1\r\n" \ + "Host: \r\n" \ + " example.org\r\n" \ + "\r\n" + req = @hp.request(@env, header) + assert_same req, @env + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/', req['PATH_INFO'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + end + + # this may seem to be testing more of an implementation detail, but + # it also helps ensure we're safe in the presence of multiple parsers + # in case we ever go multithreaded/evented... + def test_resumable_continuations + nr = 1000 + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + " hello\r\n" + tmp = [] + nr.times { |i| + hp = Kcar::Parser.new + env = {} + assert_nil hp.request(env, buf = "#{header} #{i}\r\n") + asdf = env['HTTP_X_ASDF'] + assert_equal "hello #{i}", asdf + tmp << [ hp, asdf, env, buf ] + } + tmp.each_with_index { |(hp, asdf, env, buf), i| + buf << " .\r\n\r\n" + assert_same env, hp.request(env, buf) + assert_equal "hello #{i} .", asdf + } + end + + def test_invalid_continuation + header = "GET / HTTP/1.1\r\n" \ + " y\r\n" \ + "Host: hello\r\n" \ + "\r\n" + buf = header.dup + assert_raises(Kcar::ParserError) do + @hp.request(@env, buf) + end + assert_equal header, buf, 'no modification on invalid' + end + + def test_parse_ie6_urls + %w(/some/random/path" + /some/random/path> + /some/random/path< + /we/love/you/ie6?q=<""> + /url?<="&>=" + /mal"formed"? + ).each do |path| + sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) + assert_same @env, @hp.request(@env, sorta_safe) + assert_equal path, @env['REQUEST_URI'] + assert_equal '', sorta_safe + assert @hp.keepalive? + @hp.reset + end + end + + def test_parse_error + bad_http = "GET / SsUTF/1.1" + assert_raises(Kcar::ParserError) { @hp.request(@env, bad_http) } + + # make sure we can recover + @env.clear + @hp.reset + assert_equal @env, @hp.request(@env, "GET / HTTP/1.0\r\n\r\n") + assert ! @hp.keepalive? + end + + def test_piecemeal + http = "GET" + req = @env + assert_nil @hp.request(@env, http) + assert_nil @hp.request(@env, http) + assert_nil @hp.request(@env, http << " / HTTP/1.0") + assert_equal '/', req['REQUEST_PATH'] + assert_equal '/', req['REQUEST_URI'] + assert_equal 'GET', req['REQUEST_METHOD'] + assert_nil @hp.request(req, http << "\r\n") + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert_nil @hp.request(req, http << "\r") + assert_same req, @hp.request(req, http << "\n") + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_nil req['FRAGMENT'] + assert_equal '', req['QUERY_STRING'] + assert_equal "", http + assert ! @hp.keepalive? + end + + # not common, but underscores do appear in practice + def test_absolute_uri_underscores + http = "GET https://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n" + req = @hp.request(@env, http) + assert_same req, @env + assert_equal 'https', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal 'under_score.example.com', req['HTTP_HOST'] + assert_equal 'under_score.example.com', req['SERVER_NAME'] + assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert ! @hp.keepalive? + end + + # some dumb clients add users because they're stupid + def test_absolute_uri_w_user + http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n" + req = @hp.request(@env, http) + assert_same req, @env + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! @hp.keepalive? + end + + # since Mongrel supported anything URI.parse supported, we're stuck + # supporting everything URI.parse supports + def test_absolute_uri_uri_parse + require 'uri' + "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char| + http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n" + req = @hp.request(@env, http) + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/', req['REQUEST_URI'] + assert_equal '/', req['REQUEST_PATH'] + assert_equal '', req['QUERY_STRING'] + + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! @hp.keepalive? + @hp.reset + end + end + + def test_absolute_uri + req = @hp.request(@env, "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n") + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal '80', req['SERVER_PORT'] + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert ! @hp.keepalive? + end + + def test_absolute_uri_https + http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \ + "X-Foo: bar\n\r\n" + req = @hp.request(@env, http) + assert_equal 'https', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal 'example.com', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert @hp.keepalive? + end + + # Host: header should be ignored for absolute URIs + def test_absolute_uri_with_port + req = @hp.request(@env,"GET http://example.com:8080/foo?q=bar HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n") + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal 'example.com:8080', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '8080', req['SERVER_PORT'] + assert @hp.keepalive? + end + + def test_absolute_uri_with_empty_port + req = @hp.request(@env, "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n") + assert_same req, @env + assert_equal 'https', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + assert_equal 'example.com:', req['HTTP_HOST'] + assert_equal 'example.com', req['SERVER_NAME'] + assert_equal '443', req['SERVER_PORT'] + assert @hp.keepalive? + end + + def test_absolute_ipv6_uri + url = "http://[::1]/foo?q=bar" + http = "GET #{url} HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n" + req = @hp.request(@env, http) + assert_same req, @env + assert_equal 'http', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + + uri = URI.parse(url) + assert_equal "[::1]", uri.host, + "URI.parse changed upstream for #{url}? host=#{uri.host}" + assert_equal "[::1]", req['HTTP_HOST'] + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + end + + def test_absolute_ipv6_uri_alpha + url = "http://[::a]/" + http = "GET #{url} HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n" + req = @hp.request(@env, http) + assert_equal 'http', req['rack.url_scheme'] + uri = URI.parse(url) + assert_equal "[::a]", uri.host, + "URI.parse changed upstream for #{url}? host=#{uri.host}" + assert_equal "[::a]", req['HTTP_HOST'] + assert_equal "[::a]", req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + end + + def test_absolute_ipv6_uri_alpha_2 + url = "http://[::B]/" + http = "GET #{url} HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n" + req = @hp.request(@env, http) + assert_equal 'http', req['rack.url_scheme'] + + uri = URI.parse(url) + assert_equal "[::B]", uri.host, + "URI.parse changed upstream for #{url}? host=#{uri.host}" + assert_equal "[::B]", req['HTTP_HOST'] + assert_equal "[::B]", req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + end + + def test_absolute_ipv6_uri_with_empty_port + url = "https://[::1]:/foo?q=bar" + http = "GET #{url} HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n" + req = @hp.request(@env, http) + assert_equal 'https', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + + uri = URI.parse(url) + assert_equal "[::1]", uri.host, + "URI.parse changed upstream for #{url}? host=#{uri.host}" + assert_equal "[::1]:", req['HTTP_HOST'] + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + end + + def test_absolute_ipv6_uri_with_port + url = "https://[::1]:666/foo?q=bar" + http = "GET #{url} HTTP/1.1\r\n" \ + "Host: bad.example.com\r\n\r\n" + req = @hp.request(@env, http) + assert_equal 'https', req['rack.url_scheme'] + assert_equal '/foo?q=bar', req['REQUEST_URI'] + assert_equal '/foo', req['REQUEST_PATH'] + assert_equal 'q=bar', req['QUERY_STRING'] + + uri = URI.parse(url) + assert_equal "[::1]", uri.host, + "URI.parse changed upstream for #{url}? host=#{uri.host}" + assert_equal "[::1]:666", req['HTTP_HOST'] + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '666', req['SERVER_PORT'] + assert_equal "", http + end + + def test_ipv6_host_header + buf = "GET / HTTP/1.1\r\n" \ + "Host: [::1]\r\n\r\n" + req = @hp.request(@env, buf) + assert_equal "[::1]", req['HTTP_HOST'] + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + end + + def test_ipv6_host_header_with_port + req = @hp.request(@env, "GET / HTTP/1.1\r\n" \ + "Host: [::1]:666\r\n\r\n") + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '666', req['SERVER_PORT'] + assert_equal "[::1]:666", req['HTTP_HOST'] + end + + def test_ipv6_host_header_with_empty_port + req = @hp.request(@env, "GET / HTTP/1.1\r\nHost: [::1]:\r\n\r\n") + assert_equal "[::1]", req['SERVER_NAME'] + assert_equal '80', req['SERVER_PORT'] + assert_equal "[::1]:", req['HTTP_HOST'] + end + + # XXX Highly unlikely..., just make sure we don't segfault or assert on it + def test_broken_ipv6_host_header + req = @hp.request(@env, "GET / HTTP/1.1\r\nHost: [::1:\r\n\r\n") + assert_equal "[", req['SERVER_NAME'] + assert_equal ':1:', req['SERVER_PORT'] + assert_equal "[::1:", req['HTTP_HOST'] + end + + def test_put_body_oneshot + buf = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde" + req = @hp.request(@env, buf) + assert_equal '/', req['REQUEST_PATH'] + assert_equal '/', req['REQUEST_URI'] + assert_equal 'PUT', req['REQUEST_METHOD'] + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "abcde", buf + end + + def test_put_body_later + buf = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n" + req = @hp.request(@env, buf) + assert_equal '/l', req['REQUEST_PATH'] + assert_equal '/l', req['REQUEST_URI'] + assert_equal 'PUT', req['REQUEST_METHOD'] + assert_equal 'HTTP/1.0', req['HTTP_VERSION'] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "", buf + end + + def test_unknown_methods + %w(GETT HEADR XGET XHEAD).each { |m| + s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + req = @hp.request(@env, s) + assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] + assert_nil req['FRAGMENT'] + assert_equal 'page=1', req['QUERY_STRING'] + assert_equal "", s + assert_equal m, req['REQUEST_METHOD'] + @hp.reset + } + end + + def test_fragment_in_uri + get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + req = @hp.request(@env, get) + assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] + assert_nil req['FRAGMENT'] + assert_equal 'page=1', req['QUERY_STRING'] + assert_equal '', get + end + + # lame random garbage maker + def rand_data(min, max, readable=true) + count = min + ((rand(max)+1) *10).to_i + res = count.to_s + "/" + + if readable + res << Digest::SHA1.hexdigest(rand(count * 100).to_s) * (count / 40) + else + res << Digest::SHA1.digest(rand(count * 100).to_s) * (count / 20) + end + + return res + end + + def test_horrible_queries + # then that large header names are caught + 10.times do |c| + get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-#{rand_data(1024, 1024+(c*1024))}: Test\r\n\r\n" + assert_raises(Kcar::ParserError, Kcar::RequestURITooLongError) do + @hp.request(@env, get) + @hp.clear + end + end + + # then that large mangled field values are caught + 10.times do |c| + get = "GET /#{rand_data(10,120)} HTTP/1.1\r\nX-Test: #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" + assert_raises(Kcar::ParserError,Kcar::RequestURITooLongError) do + @hp.request(@env, get) + @hp.clear + end + end + + # then large headers are rejected too FIXME not supported, yet + if false + get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" + get << "X-Test: test\r\n" * (80 * 1024) + @hp.reset + assert_raises(Kcar::ParserError,Kcar::RequestURITooLongError) do + @hp.request(@env, get) + end + end + + # finally just that random garbage gets blocked all the time + 10.times do |c| + get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" + @hp.reset + assert_raises(Kcar::ParserError,Kcar::RequestURITooLongError) do + @hp.request(@env, get) + end + end + end + + def test_leading_tab + get = "GET / HTTP/1.1\r\nHost:\texample.com\r\n\r\n" + req = @hp.request(@env, get) + assert_equal 'example.com', req['HTTP_HOST'] + end + + def test_trailing_whitespace + get = "GET / HTTP/1.1\r\nHost: example.com \r\n\r\n" + req = @hp.request(@env, get) + assert_equal 'example.com', req['HTTP_HOST'] + end + + def test_trailing_tab + get = "GET / HTTP/1.1\r\nHost: example.com\t\r\n\r\n" + req = @hp.request(@env, get) + assert_equal 'example.com', req['HTTP_HOST'] + end + + def test_trailing_multiple_linear_whitespace + get = "GET / HTTP/1.1\r\nHost: example.com\t \t \t\r\n\r\n" + req = @hp.request(@env, get) + assert_equal 'example.com', req['HTTP_HOST'] + end + + def test_embedded_linear_whitespace_ok + get = "GET / HTTP/1.1\r\nX-Space: hello\t world\t \r\n\r\n" + req = @hp.request(@env, get) + assert_equal "hello\t world", req["HTTP_X_SPACE"] + end + + def test_null_byte_header + get = "GET / HTTP/1.1\r\nHost: \0\r\n\r\n" + assert_raises(Kcar::ParserError) { @hp.request(@env, get) } + end + + def test_null_byte_in_middle + get = "GET / HTTP/1.1\r\nHost: hello\0world\r\n\r\n" + assert_raises(Kcar::ParserError) { @hp.request(@env, get) } + end + + def test_null_byte_at_end + get = "GET / HTTP/1.1\r\nHost: hello\0\r\n\r\n" + assert_raises(Kcar::ParserError) { @hp.request(@env, get) } + end + + def test_empty_header + get = "GET / HTTP/1.1\r\nHost: \r\n\r\n" + req = @hp.request(@env, get) + assert_equal '', req['HTTP_HOST'] + end + + def test_connection_TE + req = @hp.request(@env, "GET / HTTP/1.1\r\nHost: example.com\r\n" \ + "Connection: TE\r\n" \ + "TE: trailers\r\n\r\n") + assert_predicate @hp, :keepalive? + assert_equal 'TE', req['HTTP_CONNECTION'] + end + + def test_repeat_headers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "Trailer: Content-SHA1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = @hp.request(@env, str) + assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER'] + assert_equal "1\r\na\r\n2\r\n..\r\n0\r\n", str + assert @hp.keepalive? + end + + def test_http_09 + buf = "GET /read-rfc1945-if-you-dont-believe-me\r\n" + req = @hp.request(@env, buf) + assert_equal '', buf + expect = { + "REQUEST_PATH" => "/read-rfc1945-if-you-dont-believe-me", + "PATH_INFO" => "/read-rfc1945-if-you-dont-believe-me", + "REQUEST_URI" => "/read-rfc1945-if-you-dont-believe-me", + "SERVER_PROTOCOL" => "HTTP/0.9", + "HTTP_VERSION" => "HTTP/0.9", + "REQUEST_METHOD" => "GET", + "QUERY_STRING" => "" + } + assert_equal expect, req + end + + def test_path_info_semicolon + qs = "QUERY_STRING" + pi = "PATH_INFO" + req = {} + str = "GET %s HTTP/1.1\r\nHost: example.com\r\n\r\n" + { + "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, + "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, + "/1;a=b" => { qs => "", pi => "/1;a=b" }, + "/1;a=b?" => { qs => "", pi => "/1;a=b" }, + "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, + "*" => { qs => "", pi => "" }, + }.each do |uri,expect| + @env.clear + @hp.reset + buf = str % [ uri ] + req = @hp.request(@env, buf) + assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" + assert_equal expect[qs], req[qs], "#{qs} mismatch" + assert_equal expect[pi], req[pi], "#{pi} mismatch" + next if uri == "*" + uri = URI.parse("http://example.com#{uri}") + assert_equal uri.query.to_s, req[qs], "#{qs} mismatch URI.parse disagrees" + assert_equal uri.path, req[pi], "#{pi} mismatch URI.parse disagrees" + end + end + + def test_path_info_semicolon_absolute + qs = "QUERY_STRING" + pi = "PATH_INFO" + str = "GET http://example.com%s HTTP/1.1\r\nHost: www.example.com\r\n\r\n" + { + "/1;a=b?c=d&e=f" => { qs => "c=d&e=f", pi => "/1;a=b" }, + "/1?c=d&e=f" => { qs => "c=d&e=f", pi => "/1" }, + "/1;a=b" => { qs => "", pi => "/1;a=b" }, + "/1;a=b?" => { qs => "", pi => "/1;a=b" }, + "/1?a=b;c=d&e=f" => { qs => "a=b;c=d&e=f", pi => "/1" }, + }.each do |uri,expect| + @hp.reset + @env.clear + buf = str % [ uri ] + req = @hp.request(@env, buf) + assert_equal uri, req["REQUEST_URI"], "REQUEST_URI mismatch" + assert_equal "example.com", req["HTTP_HOST"], "Host: mismatch" + assert_equal expect[qs], req[qs], "#{qs} mismatch" + assert_equal expect[pi], req[pi], "#{pi} mismatch" + end + end + + def test_negative_content_length + str = "PUT / HTTP/1.1\r\n" \ + "Content-Length: -1\r\n" \ + "\r\n" + assert_raises(Kcar::ParserError) do + @hp.request(@env, str) + end + end + + def test_invalid_content_length + str = "PUT / HTTP/1.1\r\n" \ + "Content-Length: zzzzz\r\n" \ + "\r\n" + assert_raises(Kcar::ParserError) do + @hp.request(@env, str) + end + end + + def test_ignore_version_header + req = @hp.request(@env, "GET / HTTP/1.1\r\nVersion: hello\r\n\r\n") + expect = { + 'REQUEST_PATH' => '/', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'PATH_INFO' => '/', + 'HTTP_VERSION' => 'HTTP/1.1', + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => '' + } + assert_equal expect, req + end + + def test_pipelined_requests + expect = { + 'HTTP_HOST' => 'example.com', + 'SERVER_NAME' => 'example.com', + 'SERVER_PORT' => '80', + 'REQUEST_PATH' => '/', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'PATH_INFO' => '/', + 'HTTP_VERSION' => 'HTTP/1.1', + 'REQUEST_URI' => '/', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => '' + } + req1 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + req2 = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n" + buf = req1 + req2 + env1 = @hp.request(@env, buf) + assert_equal expect, env1 + assert_equal req2, buf + assert_predicate @hp, :keepalive? + @env.clear + @hp.reset + env2 = @hp.request(@env, buf) + expect['HTTP_HOST'] = expect['SERVER_NAME'] = 'www.example.com' + assert_equal expect, env2 + assert_equal '', buf + end +end |