diff options
Diffstat (limited to 'test/unit')
-rw-r--r-- | test/unit/test_configurator.rb | 65 | ||||
-rw-r--r-- | test/unit/test_http_parser.rb | 304 | ||||
-rw-r--r-- | test/unit/test_http_parser_ng.rb | 420 | ||||
-rw-r--r-- | test/unit/test_request.rb | 43 | ||||
-rw-r--r-- | test/unit/test_response.rb | 13 | ||||
-rw-r--r-- | test/unit/test_server.rb | 132 | ||||
-rw-r--r-- | test/unit/test_signals.rb | 31 | ||||
-rw-r--r-- | test/unit/test_socket_helper.rb | 16 | ||||
-rw-r--r-- | test/unit/test_tee_input.rb | 229 | ||||
-rw-r--r-- | test/unit/test_upload.rb | 236 | ||||
-rw-r--r-- | test/unit/test_util.rb | 7 |
11 files changed, 1313 insertions, 183 deletions
diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb index 98f2db6..ac1efa8 100644 --- a/test/unit/test_configurator.rb +++ b/test/unit/test_configurator.rb @@ -1,7 +1,11 @@ +# -*- encoding: binary -*- + require 'test/unit' require 'tempfile' -require 'unicorn/configurator' +require 'unicorn' +TestStruct = Struct.new( + *(Unicorn::Configurator::DEFAULTS.keys + %w(listener_opts listeners))) class TestConfigurator < Test::Unit::TestCase def test_config_init @@ -28,8 +32,10 @@ class TestConfigurator < Test::Unit::TestCase assert_equal "0.0.0.0:2007", meth.call('*:2007') assert_equal "0.0.0.0:2007", meth.call('2007') assert_equal "0.0.0.0:2007", meth.call(2007) - assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007') - assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007') + + # the next two aren't portable, consider them unsupported for now + # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('1:2007') + # assert_match %r{\A\d+\.\d+\.\d+\.\d+:2007\z}, meth.call('2:2007') end def test_config_invalid @@ -51,22 +57,23 @@ class TestConfigurator < Test::Unit::TestCase def test_config_defaults cfg = Unicorn::Configurator.new(:use_defaults => true) - assert_nothing_raised { cfg.commit!(self) } + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } Unicorn::Configurator::DEFAULTS.each do |key,value| - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end end def test_config_defaults_skip cfg = Unicorn::Configurator.new(:use_defaults => true) skip = [ :logger ] - assert_nothing_raised { cfg.commit!(self, :skip => skip) } - @logger = nil + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct, :skip => skip) } Unicorn::Configurator::DEFAULTS.each do |key,value| next if skip.include?(key) - assert_equal value, instance_variable_get("@#{key.to_s}") + assert_equal value, test_struct.__send__(key) end - assert_nil @logger + assert_nil test_struct.logger end def test_listen_options @@ -78,8 +85,9 @@ class TestConfigurator < Test::Unit::TestCase assert_nothing_raised do cfg = Unicorn::Configurator.new(:config_file => tmp.path) end - assert_nothing_raised { cfg.commit!(self) } - assert(listener_opts = instance_variable_get("@listener_opts")) + test_struct = TestStruct.new + assert_nothing_raised { cfg.commit!(test_struct) } + assert(listener_opts = test_struct.listener_opts) assert_equal expect, listener_opts[listener] end @@ -93,10 +101,41 @@ class TestConfigurator < Test::Unit::TestCase end end + def test_listen_option_bad_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => "five" } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_raises(ArgumentError) do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + + def test_listen_option_float_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => 0.5 } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_nothing_raised do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + + def test_listen_option_int_delay + tmp = Tempfile.new('unicorn_config') + expect = { :delay => 5 } + listener = "127.0.0.1:12345" + tmp.syswrite("listen '#{listener}', #{expect.inspect}\n") + assert_nothing_raised do + Unicorn::Configurator.new(:config_file => tmp.path) + end + end + def test_after_fork_proc + test_struct = TestStruct.new [ proc { |a,b| }, Proc.new { |a,b| }, lambda { |a,b| } ].each do |my_proc| - Unicorn::Configurator.new(:after_fork => my_proc).commit!(self) - assert_equal my_proc, @after_fork + Unicorn::Configurator.new(:after_fork => my_proc).commit!(test_struct) + assert_equal my_proc, test_struct.after_fork end end diff --git a/test/unit/test_http_parser.rb b/test/unit/test_http_parser.rb index a158ebb..0443b46 100644 --- a/test/unit/test_http_parser.rb +++ b/test/unit/test_http_parser.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -9,12 +11,13 @@ require 'test/test_helper' include Unicorn class HttpParserTest < Test::Unit::TestCase - + def test_parse_simple parser = HttpParser.new req = {} http = "GET / HTTP/1.1\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) + assert_equal '', http assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/', req['REQUEST_PATH'] @@ -24,15 +27,18 @@ class HttpParserTest < Test::Unit::TestCase assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert parser.keepalive? parser.reset req.clear - assert ! parser.execute(req, "G") + http = "G" + assert_nil parser.headers(req, http) + assert_equal "G", http assert req.empty? # try parsing again to ensure we were reset correctly http = "GET /hello-world HTTP/1.1\r\n\r\n" - assert parser.execute(req, http) + assert parser.headers(req, http) assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] assert_equal '/hello-world', req['REQUEST_PATH'] @@ -41,55 +47,184 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'GET', req['REQUEST_METHOD'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_equal '', http + assert parser.keepalive? + end + + def test_connection_close_no_ka + parser = HttpParser.new + req = {} + tmp = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert_equal "GET", req['REQUEST_METHOD'] + assert ! parser.keepalive? + end + + def test_connection_keep_alive_ka + parser = HttpParser.new + req = {} + tmp = "HEAD / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert parser.keepalive? + end + + def test_connection_keep_alive_ka_bad_method + parser = HttpParser.new + req = {} + tmp = "POST / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert ! parser.keepalive? + end + + def test_connection_keep_alive_ka_bad_version + parser = HttpParser.new + req = {} + tmp = "GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n" + assert_equal req.object_id, parser.headers(req, tmp).object_id + assert parser.keepalive? end def test_parse_server_host_default_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_alt_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:999\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '999', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_empty_port parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_server_host_xfp_https parser = HttpParser.new req = {} - assert parser.execute(req, "GET / HTTP/1.1\r\nHost: foo:\r\n" \ - "X-Forwarded-Proto: https\r\n\r\n") + tmp = "GET / HTTP/1.1\r\nHost: foo:\r\n" \ + "X-Forwarded-Proto: https\r\n\r\n" + assert_equal req, parser.headers(req, tmp) assert_equal 'foo', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal '', tmp + assert parser.keepalive? end def test_parse_strange_headers parser = HttpParser.new req = {} should_be_good = "GET / HTTP/1.1\r\naaaaaaaaaaaaa:++++++++++\r\n\r\n" - assert parser.execute(req, should_be_good) + assert_equal req, parser.headers(req, should_be_good) + assert_equal '', should_be_good + assert parser.keepalive? + end + + # legacy test case from Mongrel that we never supported before... + # I still consider Pound irrelevant, unfortunately stupid clients that + # send extremely big headers do exist and they've managed to find Unicorn... + def test_nasty_pound_header + parser = HttpParser.new + 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" + req = {} + buf = nasty_pound_header.dup + + assert nasty_pound_header =~ /(-----BEGIN .*--END CERTIFICATE-----)/m + expect = $1.dup + expect.gsub!(/\r\n\t/, ' ') + assert_equal req, parser.headers(req, buf) + assert_equal '', buf + assert_equal expect, req['HTTP_X_SSL_BULLSHIT'] + end + + def test_continuation_eats_leading_spaces + parser = HttpParser.new + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + "\t\r\n" \ + " \r\n" \ + " ASDF\r\n\r\n" + req = {} + assert_equal req, parser.headers(req, header) + assert_equal '', header + assert_equal 'ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_eats_scattered_leading_spaces + parser = HttpParser.new + 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 = {} + assert_equal req, parser.headers(req, header) + assert_equal '', header + assert_equal 'hi y x ASDF', req['HTTP_X_ASDF'] + end + + def test_continuation_with_absolute_uri_and_ignored_host_header + parser = HttpParser.new + header = "GET http://example.com/ HTTP/1.1\r\n" \ + "Host: \r\n" \ + " YHBT.net\r\n" \ + "\r\n" + req = {} + assert_equal req, parser.headers(req, header) + assert_equal 'example.com', req['HTTP_HOST'] + end - # ref: http://thread.gmane.org/gmane.comp.lang.ruby.mongrel.devel/37/focus=45 - # (note we got 'pen' mixed up with 'pound' in that thread, - # but the gist of it is still relevant: these nasty headers are irrelevant - # - # 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" - # parser = HttpParser.new - # req = {} - # assert parser.execute(req, nasty_pound_header, 0) + # 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 + req = {} + header = "GET / HTTP/1.1\r\n" \ + "X-ASDF: \r\n" \ + " hello\r\n" + tmp = [] + nr.times { |i| + parser = HttpParser.new + assert parser.headers(req, "#{header} #{i}\r\n").nil? + asdf = req['HTTP_X_ASDF'] + assert_equal "hello #{i}", asdf + tmp << [ parser, asdf ] + req.clear + } + tmp.each_with_index { |(parser, asdf), i| + assert_equal req, parser.headers(req, "#{header} #{i}\r\n .\r\n\r\n") + assert_equal "hello #{i} .", asdf + } + end + + def test_invalid_continuation + parser = HttpParser.new + header = "GET / HTTP/1.1\r\n" \ + " y\r\n" \ + "Host: hello\r\n" \ + "\r\n" + req = {} + assert_raises(HttpParserError) { parser.headers(req, header) } end def test_parse_ie6_urls @@ -103,7 +238,10 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} sorta_safe = %(GET #{path} HTTP/1.1\r\n\r\n) - assert parser.execute(req, sorta_safe) + assert_equal req, parser.headers(req, sorta_safe) + assert_equal path, req['REQUEST_URI'] + assert_equal '', sorta_safe + assert parser.keepalive? end end @@ -112,28 +250,34 @@ class HttpParserTest < Test::Unit::TestCase req = {} bad_http = "GET / SsUTF/1.1" - assert_raises(HttpParserError) { parser.execute(req, bad_http) } + assert_raises(HttpParserError) { parser.headers(req, bad_http) } + + # make sure we can recover parser.reset - assert(parser.execute({}, "GET / HTTP/1.0\r\n\r\n")) + req.clear + assert_equal req, parser.headers(req, "GET / HTTP/1.0\r\n\r\n") + assert ! parser.keepalive? end def test_piecemeal parser = HttpParser.new req = {} http = "GET" - assert ! parser.execute(req, http) - assert_raises(HttpParserError) { parser.execute(req, http) } - assert ! parser.execute(req, http << " / HTTP/1.0") + assert_nil parser.headers(req, http) + assert_nil parser.headers(req, http) + assert_nil parser.headers(req, http << " / HTTP/1.0") assert_equal '/', req['REQUEST_PATH'] assert_equal '/', req['REQUEST_URI'] assert_equal 'GET', req['REQUEST_METHOD'] - assert ! parser.execute(req, http << "\r\n") + assert_nil parser.headers(req, http << "\r\n") assert_equal 'HTTP/1.0', req['HTTP_VERSION'] - assert ! parser.execute(req, http << "\r") - assert parser.execute(req, http << "\n") - assert_equal 'HTTP/1.1', req['SERVER_PROTOCOL'] + assert_nil parser.headers(req, http << "\r") + assert_equal req, parser.headers(req, http << "\n") + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] assert_nil req['FRAGMENT'] assert_equal '', req['QUERY_STRING'] + assert_equal "", http + assert ! parser.keepalive? end # not common, but underscores do appear in practice @@ -141,7 +285,7 @@ class HttpParserTest < Test::Unit::TestCase parser = HttpParser.new req = {} http = "GET http://under_score.example.com/foo?q=bar HTTP/1.0\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -150,13 +294,54 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'under_score.example.com', req['HTTP_HOST'] assert_equal 'under_score.example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? + end + + # some dumb clients add users because they're stupid + def test_absolute_uri_w_user + parser = HttpParser.new + req = {} + http = "GET http://user%20space@example.com/foo?q=bar HTTP/1.0\r\n\r\n" + assert_equal req, parser.headers(req, http) + 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 ! parser.keepalive? + end + + # since Mongrel supported anything URI.parse supported, we're stuck + # supporting everything URI.parse supports + def test_absolute_uri_uri_parse + "#{URI::REGEXP::PATTERN::UNRESERVED};:&=+$,".split(//).each do |char| + parser = HttpParser.new + req = {} + http = "GET http://#{char}@example.com/ HTTP/1.0\r\n\r\n" + assert_equal req, parser.headers(req, 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 ! parser.keepalive? + end end def test_absolute_uri parser = HttpParser.new req = {} http = "GET http://example.com/foo?q=bar HTTP/1.0\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -165,6 +350,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '80', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? end # X-Forwarded-Proto is not in rfc2616, absolute URIs are, however... @@ -173,7 +360,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET https://example.com/foo?q=bar HTTP/1.1\r\n" \ "X-Forwarded-Proto: http\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -182,6 +369,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert parser.keepalive? end # Host: header should be ignored for absolute URIs @@ -190,7 +379,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET http://example.com:8080/foo?q=bar HTTP/1.2\r\n" \ "Host: bad.example.com\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'http', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -199,6 +388,8 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com:8080', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '8080', req['SERVER_PORT'] + assert_equal "", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_absolute_uri_with_empty_port @@ -206,7 +397,7 @@ class HttpParserTest < Test::Unit::TestCase req = {} http = "GET https://example.com:/foo?q=bar HTTP/1.1\r\n" \ "Host: bad.example.com\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) assert_equal 'https', req['rack.url_scheme'] assert_equal '/foo?q=bar', req['REQUEST_URI'] assert_equal '/foo', req['REQUEST_PATH'] @@ -215,32 +406,55 @@ class HttpParserTest < Test::Unit::TestCase assert_equal 'example.com:', req['HTTP_HOST'] assert_equal 'example.com', req['SERVER_NAME'] assert_equal '443', req['SERVER_PORT'] + assert_equal "", http + assert parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_put_body_oneshot parser = HttpParser.new req = {} http = "PUT / HTTP/1.0\r\nContent-Length: 5\r\n\r\nabcde" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) 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.1', req['SERVER_PROTOCOL'] - assert_equal "abcde", req[:http_body] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "abcde", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final end def test_put_body_later parser = HttpParser.new req = {} http = "PUT /l HTTP/1.0\r\nContent-Length: 5\r\n\r\n" - assert parser.execute(req, http) + assert_equal req, parser.headers(req, http) 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.1', req['SERVER_PROTOCOL'] - assert_equal "", req[:http_body] + assert_equal 'HTTP/1.0', req['SERVER_PROTOCOL'] + assert_equal "", http + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final + end + + def test_unknown_methods + %w(GETT HEADR XGET XHEAD).each { |m| + parser = HttpParser.new + req = {} + s = "#{m} /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" + ok = false + assert_nothing_raised do + ok = parser.headers(req, s) + end + assert ok + assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] + assert_equal 'posts-17408', req['FRAGMENT'] + assert_equal 'page=1', req['QUERY_STRING'] + assert_equal "", s + assert_equal m, req['REQUEST_METHOD'] + assert ! parser.keepalive? # TODO: read HTTP/1.2 when it's final + } end def test_fragment_in_uri @@ -249,12 +463,14 @@ class HttpParserTest < Test::Unit::TestCase get = "GET /forums/1/topics/2375?page=1#posts-17408 HTTP/1.1\r\n\r\n" ok = false assert_nothing_raised do - ok = parser.execute(req, get) + ok = parser.headers(req, get) end assert ok assert_equal '/forums/1/topics/2375?page=1', req['REQUEST_URI'] assert_equal 'posts-17408', req['FRAGMENT'] assert_equal 'page=1', req['QUERY_STRING'] + assert_equal '', get + assert parser.keepalive? end # lame random garbage maker @@ -279,7 +495,7 @@ class HttpParserTest < Test::Unit::TestCase 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 Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end @@ -288,7 +504,7 @@ class HttpParserTest < Test::Unit::TestCase 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 Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end @@ -297,7 +513,7 @@ class HttpParserTest < Test::Unit::TestCase get = "GET /#{rand_data(10,120)} HTTP/1.1\r\n" get << "X-Test: test\r\n" * (80 * 1024) assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end @@ -305,7 +521,7 @@ class HttpParserTest < Test::Unit::TestCase 10.times do |c| get = "GET #{rand_data(1024, 1024+(c*1024), false)} #{rand_data(1024, 1024+(c*1024), false)}\r\n\r\n" assert_raises Unicorn::HttpParserError do - parser.execute({}, get) + parser.headers({}, get) parser.reset end end diff --git a/test/unit/test_http_parser_ng.rb b/test/unit/test_http_parser_ng.rb new file mode 100644 index 0000000..bb61e7f --- /dev/null +++ b/test/unit/test_http_parser_ng.rb @@ -0,0 +1,420 @@ +# -*- encoding: binary -*- + +# coding: binary +require 'test/test_helper' +require 'digest/md5' + +include Unicorn + +class HttpParserNgTest < Test::Unit::TestCase + + def setup + @parser = HttpParser.new + end + + def test_identity_byte_headers + req = {} + str = "PUT / HTTP/1.1\r\n" + str << "Content-Length: 123\r\n" + str << "\r" + hdr = "" + str.each_byte { |byte| + assert_nil @parser.headers(req, hdr << byte.chr) + } + hdr << "\n" + assert_equal req.object_id, @parser.headers(req, hdr).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, hdr.size + assert ! @parser.keepalive? + assert @parser.headers? + assert 123, @parser.content_length + end + + def test_identity_step_headers + req = {} + str = "PUT / HTTP/1.1\r\n" + assert ! @parser.headers(req, str) + str << "Content-Length: 123\r\n" + assert ! @parser.headers(req, str) + str << "\r\n" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, str.size + assert ! @parser.keepalive? + assert @parser.headers? + end + + def test_identity_oneshot_header + req = {} + str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\n" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 0, str.size + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body + body = ('a' * 123).freeze + req = {} + str = "PUT / HTTP/1.1\r\n" \ + "Content-Length: #{body.length}\r\n" \ + "\r\n#{body}" + assert_equal req.object_id, @parser.headers(req, str).object_id + assert_equal '123', req['CONTENT_LENGTH'] + assert_equal 123, str.size + assert_equal body, str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 0, str.size + assert_equal tmp, body + assert_equal "", @parser.filter_body(tmp, str) + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body_partial + str = "PUT / HTTP/1.1\r\nContent-Length: 123\r\n\r\na" + assert_equal Hash, @parser.headers({}, str).class + assert_equal 1, str.size + assert_equal 'a', str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal "", str + assert_equal "a", tmp + str << ' ' * 122 + rv = @parser.filter_body(tmp, str) + assert_equal 122, tmp.size + assert_nil rv + assert_equal "", str + assert_equal str.object_id, @parser.filter_body(tmp, str).object_id + assert ! @parser.keepalive? + end + + def test_identity_oneshot_header_with_body_slop + str = "PUT / HTTP/1.1\r\nContent-Length: 1\r\n\r\naG" + assert_equal Hash, @parser.headers({}, str).class + assert_equal 2, str.size + assert_equal 'aG', str + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal "G", str + assert_equal "G", @parser.filter_body(tmp, str) + assert_equal 1, tmp.size + assert_equal "a", tmp + assert ! @parser.keepalive? + end + + def test_chunked + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 0, str.size + tmp = "" + assert_nil @parser.filter_body(tmp, "6") + assert_equal 0, tmp.size + assert_nil @parser.filter_body(tmp, rv = "\r\n") + assert_equal 0, rv.size + assert_equal 0, tmp.size + tmp = "" + assert_nil @parser.filter_body(tmp, "..") + assert_equal "..", tmp + assert_nil @parser.filter_body(tmp, "abcd\r\n0\r\n") + assert_equal "abcd", tmp + rv = "PUT" + assert_equal rv.object_id, @parser.filter_body(tmp, rv).object_id + assert_equal "PUT", rv + assert ! @parser.keepalive? + end + + def test_two_chunks + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 0, str.size + tmp = "" + assert_nil @parser.filter_body(tmp, "6") + assert_equal 0, tmp.size + assert_nil @parser.filter_body(tmp, rv = "\r\n") + assert_equal "", rv + assert_equal 0, tmp.size + tmp = "" + assert_nil @parser.filter_body(tmp, "..") + assert_equal 2, tmp.size + assert_equal "..", tmp + assert_nil @parser.filter_body(tmp, "abcd\r\n1") + assert_equal "abcd", tmp + assert_nil @parser.filter_body(tmp, "\r") + assert_equal "", tmp + assert_nil @parser.filter_body(tmp, "\n") + assert_equal "", tmp + assert_nil @parser.filter_body(tmp, "z") + assert_equal "z", tmp + assert_nil @parser.filter_body(tmp, "\r\n") + assert_nil @parser.filter_body(tmp, "0") + assert_nil @parser.filter_body(tmp, "\r") + rv = @parser.filter_body(tmp, buf = "\nGET") + assert_equal "GET", rv + assert_equal buf.object_id, rv.object_id + assert ! @parser.keepalive? + end + + def test_big_chunk + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ + "4000\r\nabcd" + req = {} + assert_equal req, @parser.headers(req, str) + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + str = ' ' * 16300 + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + str = ' ' * 80 + assert_nil @parser.filter_body(tmp, str) + assert_equal '', str + assert ! @parser.body_eof? + assert_equal "", @parser.filter_body(tmp, "\r\n0\r\n") + assert @parser.body_eof? + assert ! @parser.keepalive? + end + + def test_two_chunks_oneshot + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert ! @parser.keepalive? + end + + def test_chunks_bytewise + chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n" + str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n#{chunked}" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal chunked, str + tmp = '' + buf = '' + body = '' + str = str[0..-2] + str.each_byte { |byte| + assert_nil @parser.filter_body(tmp, buf << byte.chr) + body << tmp + } + assert_equal 'abcdefghijklmnop0123456789abcdefg', body + rv = @parser.filter_body(tmp, buf << "\n") + assert_equal rv.object_id, buf.object_id + assert ! @parser.keepalive? + end + + def test_trailers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5', req['HTTP_TRAILER'] + assert_nil req['HTTP_CONTENT_MD5'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert_equal '', str + md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze + str << md5_hdr + assert_nil @parser.trailers(req, str) + assert_equal md5_b64, req['HTTP_CONTENT_MD5'] + assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str + assert_nil @parser.trailers(req, str << "\r") + assert_equal req, @parser.trailers(req, str << "\nGET / ") + assert_equal "GET / ", str + assert ! @parser.keepalive? + end + + def test_trailers_slowly + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Content-MD5\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5', req['HTTP_TRAILER'] + assert_nil req['HTTP_CONTENT_MD5'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze + rv = @parser.filter_body(tmp, str) + assert_equal rv.object_id, str.object_id + assert_equal '', str + assert_nil @parser.trailers(req, str) + md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze + md5_hdr.each_byte { |byte| + str << byte.chr + assert_nil @parser.trailers(req, str) + } + assert_equal md5_b64, req['HTTP_CONTENT_MD5'] + assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str + assert_nil @parser.trailers(req, str << "\r") + assert_equal req, @parser.trailers(req, str << "\n") + end + + def test_max_chunk + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#{HttpParser::CHUNK_MAX.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_nothing_raised { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_max_body + n = HttpParser::LENGTH_MAX + str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" + req = {} + assert_nothing_raised { @parser.headers(req, str) } + assert_equal n, req['CONTENT_LENGTH'].to_i + assert ! @parser.keepalive? + end + + def test_overflow_chunk + n = HttpParser::CHUNK_MAX + 1 + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#{n.to_s(16)}\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_raise(HttpParserError) { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_overflow_content_length + n = HttpParser::LENGTH_MAX + 1 + str = "PUT / HTTP/1.1\r\nContent-Length: #{n}\r\n\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + assert ! @parser.keepalive? + end + + def test_bad_chunk + str = "PUT / HTTP/1.1\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "#zzz\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_nil @parser.content_length + assert_raise(HttpParserError) { @parser.filter_body('', str) } + assert ! @parser.keepalive? + end + + def test_bad_content_length + str = "PUT / HTTP/1.1\r\nContent-Length: 7ff\r\n\r\n" + assert_raise(HttpParserError) { @parser.headers({}, str) } + assert ! @parser.keepalive? + end + + def test_bad_trailers + str = "PUT / HTTP/1.1\r\n" \ + "Trailer: Transfer-Encoding\r\n" \ + "transfer-Encoding: chunked\r\n\r\n" \ + "1\r\na\r\n2\r\n..\r\n0\r\n" + req = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Transfer-Encoding', req['HTTP_TRAILER'] + tmp = '' + assert_nil @parser.filter_body(tmp, str) + assert_equal 'a..', tmp + assert_equal '', str + str << "Transfer-Encoding: identity\r\n\r\n" + assert_raise(HttpParserError) { @parser.trailers(req, str) } + assert ! @parser.keepalive? + 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 = {} + assert_equal req, @parser.headers(req, str) + assert_equal 'Content-MD5,Content-SHA1', req['HTTP_TRAILER'] + assert ! @parser.keepalive? + end + + def test_parse_simple_request + parser = HttpParser.new + req = {} + http = "GET /read-rfc1945-if-you-dont-believe-me\r\n" + assert_equal req, parser.headers(req, http) + assert_equal '', http + expect = { + "SERVER_NAME"=>"localhost", + "rack.url_scheme"=>"http", + "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_PORT"=>"80", + "SERVER_PROTOCOL"=>"HTTP/0.9", + "REQUEST_METHOD"=>"GET", + "QUERY_STRING"=>"" + } + assert_equal expect, req + assert ! parser.headers? + 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| + assert_equal req, @parser.headers(req.clear, str % [ uri ]) + @parser.reset + 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" + req = {} + 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| + assert_equal req, @parser.headers(req.clear, str % [ uri ]) + @parser.reset + 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 + +end diff --git a/test/unit/test_request.rb b/test/unit/test_request.rb index 0bfff7d..1896300 100644 --- a/test/unit/test_request.rb +++ b/test/unit/test_request.rb @@ -1,14 +1,9 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby. require 'test/test_helper' -begin - require 'rack' - require 'rack/lint' -rescue LoadError - warn "Unable to load rack, skipping test" - exit 0 -end include Unicorn @@ -16,10 +11,11 @@ class RequestTest < Test::Unit::TestCase class MockRequest < StringIO alias_method :readpartial, :sysread + alias_method :read_nonblock, :sysread end def setup - @request = HttpRequest.new(Logger.new($stderr)) + @request = HttpRequest.new @app = lambda do |env| [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ] end @@ -119,6 +115,31 @@ class RequestTest < Test::Unit::TestCase assert_nothing_raised { res = @lint.call(env) } end + def test_no_content_stringio + client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal StringIO, env['rack.input'].class + end + + def test_zero_content_stringio + client = MockRequest.new("PUT / HTTP/1.1\r\n" \ + "Content-Length: 0\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal StringIO, env['rack.input'].class + end + + def test_real_content_not_stringio + client = MockRequest.new("PUT / HTTP/1.1\r\n" \ + "Content-Length: 1\r\n" \ + "Host: foo\r\n\r\n") + res = env = nil + assert_nothing_raised { env = @request.read(client) } + assert_equal Unicorn::TeeInput, env['rack.input'].class + end + def test_rack_lint_put client = MockRequest.new( "PUT / HTTP/1.1\r\n" \ @@ -149,7 +170,11 @@ class RequestTest < Test::Unit::TestCase assert_nothing_raised { env = @request.read(client) } assert ! env.include?(:http_body) assert_equal length, env['rack.input'].size - count.times { assert_equal buf, env['rack.input'].read(bs) } + count.times { + tmp = env['rack.input'].read(bs) + tmp << env['rack.input'].read(bs - tmp.size) if tmp.size != bs + assert_equal buf, tmp + } assert_nil env['rack.input'].read(bs) assert_nothing_raised { env['rack.input'].rewind } assert_nothing_raised { res = @lint.call(env) } diff --git a/test/unit/test_response.rb b/test/unit/test_response.rb index 66c2b54..f9eda8e 100644 --- a/test/unit/test_response.rb +++ b/test/unit/test_response.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -94,4 +96,15 @@ class ResponseTest < Test::Unit::TestCase assert_match(expect_body, out.string.split(/\r\n/).last) end + def test_unknown_status_pass_through + out = StringIO.new + HttpResponse.write(out,["666 I AM THE BEAST", {}, [] ]) + assert out.closed? + headers = out.string.split(/\r\n\r\n/).first.split(/\r\n/) + assert %r{\AHTTP/\d\.\d 666 I AM THE BEAST\z}.match(headers[0]) + status = headers.grep(/\AStatus:/i).first + assert status + assert_equal "Status: 666 I AM THE BEAST", status + end + end diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb index 742b240..00705d0 100644 --- a/test/unit/test_server.rb +++ b/test/unit/test_server.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2005 Zed A. Shaw # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -10,9 +12,13 @@ include Unicorn class TestHandler - def call(env) - # response.socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nhello!\n") + def call(env) + while env['rack.input'].read(4096) + end [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']] + rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e + $stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n") + raise e end end @@ -31,6 +37,8 @@ class WebServerTest < Test::Unit::TestCase def teardown redirect_test_io do + wait_workers_ready("test_stderr.#$$.log", 1) + File.truncate("test_stderr.#$$.log", 0) @server.stop(true) end end @@ -51,8 +59,10 @@ class WebServerTest < Test::Unit::TestCase end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i + assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i + assert loader_pid != 0 assert_equal worker_pid, loader_pid teardown @@ -63,6 +73,7 @@ class WebServerTest < Test::Unit::TestCase end results = hit(["http://localhost:#@port/"]) worker_pid = results[0].to_i + assert worker_pid != 0 tmp.sysseek(0) loader_pid = tmp.sysread(4096).to_i assert_equal $$, loader_pid @@ -94,6 +105,92 @@ class WebServerTest < Test::Unit::TestCase assert_equal 'hello!\n', results[0], "Handler didn't really run" end + def test_client_shutdown_writes + sock = nil + buf = nil + bs = 15609315 * rand + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("PUT /hello HTTP/1.1\r\n") + sock.syswrite("Host: example.com\r\n") + sock.syswrite("Transfer-Encoding: chunked\r\n") + sock.syswrite("Trailer: X-Foo\r\n") + sock.syswrite("\r\n") + sock.syswrite("%x\r\n" % [ bs ]) + sock.syswrite("F" * bs) + sock.syswrite("\r\n0\r\nX-") + "Foo: bar\r\n\r\n".each_byte do |x| + sock.syswrite x.chr + sleep 0.05 + end + # we wrote the entire request before shutting down, server should + # continue to process our request and never hit EOFError on our sock + sock.shutdown(Socket::SHUT_WR) + buf = sock.read + end + assert_equal 'hello!\n', buf.split(/\r\n\r\n/).last + next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) + assert_equal 'hello!\n', next_client + lines = File.readlines("test_stderr.#$$.log") + assert lines.grep(/^Unicorn::ClientShutdown: /).empty? + assert_nothing_raised { sock.close } + end + + def test_client_shutdown_write_truncates + sock = nil + buf = nil + bs = 15609315 * rand + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("PUT /hello HTTP/1.1\r\n") + sock.syswrite("Host: example.com\r\n") + sock.syswrite("Transfer-Encoding: chunked\r\n") + sock.syswrite("Trailer: X-Foo\r\n") + sock.syswrite("\r\n") + sock.syswrite("%x\r\n" % [ bs ]) + sock.syswrite("F" * (bs / 2.0)) + + # shutdown prematurely, this will force the server to abort + # processing on us even during app dispatch + sock.shutdown(Socket::SHUT_WR) + IO.select([sock], nil, nil, 60) or raise "Timed out" + buf = sock.read + end + assert_equal "", buf + next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) + assert_equal 'hello!\n', next_client + lines = File.readlines("test_stderr.#$$.log") + lines = lines.grep(/^Unicorn::ClientShutdown: bytes_read=\d+/) + assert_equal 1, lines.size + assert_match %r{\AUnicorn::ClientShutdown: bytes_read=\d+ true$}, lines[0] + assert_nothing_raised { sock.close } + end + + def test_client_malformed_body + sock = nil + buf = nil + bs = 15653984 + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("PUT /hello HTTP/1.1\r\n") + sock.syswrite("Host: example.com\r\n") + sock.syswrite("Transfer-Encoding: chunked\r\n") + sock.syswrite("Trailer: X-Foo\r\n") + sock.syswrite("\r\n") + sock.syswrite("%x\r\n" % [ bs ]) + sock.syswrite("F" * bs) + end + begin + File.open("/dev/urandom", "rb") { |fp| sock.syswrite(fp.sysread(16384)) } + rescue + end + assert_nothing_raised { sock.close } + next_client = Net::HTTP.get(URI.parse("http://127.0.0.1:#@port/")) + assert_equal 'hello!\n', next_client + lines = File.readlines("test_stderr.#$$.log") + lines = lines.grep(/^Unicorn::HttpParserError: .* true$/) + assert_equal 1, lines.size + end def do_test(string, chunk, close_after=nil, shutdown_delay=0) # Do not use instance variables here, because it needs to be thread safe @@ -131,6 +228,16 @@ class WebServerTest < Test::Unit::TestCase end end + def test_logger_set + assert_equal @server.logger, Unicorn::HttpRequest::DEFAULTS["rack.logger"] + end + + def test_logger_changed + tmp = Logger.new($stdout) + @server.logger = tmp + assert_equal tmp, Unicorn::HttpRequest::DEFAULTS["rack.logger"] + end + def test_bad_client_400 sock = nil assert_nothing_raised do @@ -141,6 +248,16 @@ class WebServerTest < Test::Unit::TestCase assert_nothing_raised { sock.close } end + def test_http_0_9 + sock = nil + assert_nothing_raised do + sock = TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET /hello\r\n") + end + assert_match 'hello!\n', sock.sysread(4096) + assert_nothing_raised { sock.close } + end + def test_header_is_too_long redirect_test_io do long = "GET /test HTTP/1.1\r\n" + ("X-Big: stuff\r\n" * 15000) + "\r\n" @@ -152,9 +269,18 @@ class WebServerTest < Test::Unit::TestCase def test_file_streamed_request body = "a" * (Unicorn::Const::MAX_BODY * 2) - long = "GET /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body + long = "PUT /test HTTP/1.1\r\nContent-length: #{body.length}\r\n\r\n" + body do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) end + def test_file_streamed_request_bad_body + body = "a" * (Unicorn::Const::MAX_BODY * 2) + long = "GET /test HTTP/1.1\r\nContent-ength: #{body.length}\r\n\r\n" + body + assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, + Errno::EBADF) { + do_test(long, Unicorn::Const::CHUNK_SIZE * 2 -400) + } + end + end diff --git a/test/unit/test_signals.rb b/test/unit/test_signals.rb index ef66ed6..eb2af0b 100644 --- a/test/unit/test_signals.rb +++ b/test/unit/test_signals.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong # You can redistribute it and/or modify it under the same terms as Ruby. # @@ -24,14 +26,15 @@ class SignalsTest < Test::Unit::TestCase @bs = 1 * 1024 * 1024 @count = 100 @port = unused_port - tmp = @tmp = Tempfile.new('unicorn.sock') + @sock = Tempfile.new('unicorn.sock') + @tmp = Tempfile.new('unicorn.write') + @tmp.sync = true + File.unlink(@sock.path) File.unlink(@tmp.path) - n = 0 - tmp.chmod(0) @server_opts = { - :listeners => [ "127.0.0.1:#@port", @tmp.path ], + :listeners => [ "127.0.0.1:#@port", @sock.path ], :after_fork => lambda { |server,worker| - trap(:HUP) { tmp.chmod(n += 1) } + trap(:HUP) { @tmp.syswrite('.') } }, } @server = nil @@ -53,8 +56,10 @@ class SignalsTest < Test::Unit::TestCase buf =~ /\bX-Pid: (\d+)\b/ or raise Exception child = $1.to_i wait_master_ready("test_stderr.#{pid}.log") + wait_workers_ready("test_stderr.#{pid}.log", 1) Process.kill(:KILL, pid) Process.waitpid(pid) + File.unlink("test_stderr.#{pid}.log", "test_stdout.#{pid}.log") t0 = Time.now end assert child @@ -137,8 +142,9 @@ class SignalsTest < Test::Unit::TestCase pid = buf[/\r\nX-Pid: (\d+)\r\n/, 1].to_i header_len = buf[/\A(.+?\r\n\r\n)/m, 1].size end + assert pid > 0, "pid not positive: #{pid.inspect}" read = buf.size - mode_before = @tmp.stat.mode + size_before = @tmp.stat.size assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL, Errno::EBADF) do loop do @@ -151,13 +157,17 @@ class SignalsTest < Test::Unit::TestCase redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged - assert mode_before < @tmp.stat.mode - assert_equal(read - header_len, @bs * @count) + assert size_before < @tmp.stat.size + got = read - header_len + expect = @bs * @count + assert_equal(expect, got, "expect=#{expect} got=#{got}") assert_nothing_raised { sock.close } end def test_request_read app = lambda { |env| + while env['rack.input'].read(4096) + end [ 200, {'Content-Type'=>'text/plain', 'X-Pid'=>Process.pid.to_s}, [] ] } redirect_test_io { @server = HttpServer.new(app, @server_opts).start } @@ -171,11 +181,12 @@ class SignalsTest < Test::Unit::TestCase sock.close end + assert pid > 0, "pid not positive: #{pid.inspect}" sock = TCPSocket.new('127.0.0.1', @port) sock.syswrite("PUT / HTTP/1.0\r\n") sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n") 1000.times { Process.kill(:HUP, pid) } - mode_before = @tmp.stat.mode + size_before = @tmp.stat.size killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } } buf = ' ' * @bs @count.times { sock.syswrite(buf) } @@ -183,7 +194,7 @@ class SignalsTest < Test::Unit::TestCase Process.waitpid2(killer) redirect_test_io { @server.stop(true) } # can't check for == since pending signals get merged - assert mode_before < @tmp.stat.mode + assert size_before < @tmp.stat.size assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i sock.close end diff --git a/test/unit/test_socket_helper.rb b/test/unit/test_socket_helper.rb index 75d9f7b..c35b0c2 100644 --- a/test/unit/test_socket_helper.rb +++ b/test/unit/test_socket_helper.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'test/test_helper' require 'tempfile' @@ -61,6 +63,20 @@ class TestSocketHelper < Test::Unit::TestCase File.umask(old_umask) end + def test_bind_listen_unix_umask + old_umask = File.umask(0777) + tmp = Tempfile.new 'unix.sock' + @unix_listener_path = tmp.path + File.unlink(@unix_listener_path) + @unix_listener = bind_listen(@unix_listener_path, :umask => 077) + assert UNIXServer === @unix_listener + assert_equal @unix_listener_path, sock_name(@unix_listener) + assert_equal 0140700, File.stat(@unix_listener_path).mode + assert_equal 0777, File.umask + ensure + File.umask(old_umask) + end + def test_bind_listen_unix_idempotent test_bind_listen_unix a = bind_listen(@unix_listener) diff --git a/test/unit/test_tee_input.rb b/test/unit/test_tee_input.rb new file mode 100644 index 0000000..403f698 --- /dev/null +++ b/test/unit/test_tee_input.rb @@ -0,0 +1,229 @@ +# -*- encoding: binary -*- + +require 'test/unit' +require 'digest/sha1' +require 'unicorn' + +class TestTeeInput < Test::Unit::TestCase + + def setup + @rs = $/ + @env = {} + @rd, @wr = IO.pipe + @rd.sync = @wr.sync = true + @start_pid = $$ + end + + def teardown + return if $$ != @start_pid + $/ = @rs + @rd.close rescue nil + @wr.close rescue nil + begin + Process.wait + rescue Errno::ECHILD + break + end while true + end + + def test_gets_long + init_parser("hello", 5 + (4096 * 4 * 3) + "#$/foo#$/".size) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + status = line = nil + pid = fork { + @rd.close + 3.times { @wr.write("ffff" * 4096) } + @wr.write "#$/foo#$/" + @wr.close + } + @wr.close + assert_nothing_raised { line = ti.gets } + assert_equal(4096 * 4 * 3 + 5 + $/.size, line.size) + assert_equal("hello" << ("ffff" * 4096 * 3) << "#$/", line) + assert_nothing_raised { line = ti.gets } + assert_equal "foo#$/", line + assert_nil ti.gets + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_gets_short + init_parser("hello", 5 + "#$/foo".size) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + status = line = nil + pid = fork { + @rd.close + @wr.write "#$/foo" + @wr.close + } + @wr.close + assert_nothing_raised { line = ti.gets } + assert_equal("hello#$/", line) + assert_nothing_raised { line = ti.gets } + assert_equal "foo", line + assert_nil ti.gets + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_small_body + init_parser('hello') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal 0, @parser.content_length + assert @parser.body_eof? + assert_equal StringIO, ti.instance_eval { @tmp.class } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 5, ti.size + assert_equal 'hello', ti.read + assert_equal '', ti.read + assert_nil ti.read(4096) + end + + def test_read_with_buffer + init_parser('hello') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + buf = '' + rv = ti.read(4, buf) + assert_equal 'hell', rv + assert_equal 'hell', buf + assert_equal rv.object_id, buf.object_id + assert_equal 'o', ti.read + assert_equal nil, ti.read(5, buf) + assert_equal 0, ti.rewind + assert_equal 'hello', ti.read(5, buf) + assert_equal 'hello', buf + end + + def test_big_body + init_parser('.' * Unicorn::Const::MAX_BODY << 'a') + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal 0, @parser.content_length + assert @parser.body_eof? + assert_kind_of File, ti.instance_eval { @tmp } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + end + + def test_read_in_full_if_content_length + a, b = 300, 3 + init_parser('.' * b, 300) + assert_equal 300, @parser.content_length + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + pid = fork { + @wr.write('.' * 197) + sleep 1 # still a *potential* race here that would make the test moot... + @wr.write('.' * 100) + } + assert_equal a, ti.read(a).size + _, status = Process.waitpid2(pid) + assert status.success? + @wr.close + end + + def test_big_body_multi + init_parser('.', Unicorn::Const::MAX_BODY + 1) + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_equal Unicorn::Const::MAX_BODY, @parser.content_length + assert ! @parser.body_eof? + assert_kind_of File, ti.instance_eval { @tmp } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 1, ti.instance_eval { @tmp.size } + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + nr = Unicorn::Const::MAX_BODY / 4 + pid = fork { + @rd.close + nr.times { @wr.write('....') } + @wr.close + } + @wr.close + assert_equal '.', ti.read(1) + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + nr.times { + assert_equal '....', ti.read(4) + assert_equal Unicorn::Const::MAX_BODY + 1, ti.size + } + assert_nil ti.read(1) + status = nil + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_chunked + @parser = Unicorn::HttpParser.new + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal "", @buf + + pid = fork { + @rd.close + 5.times { @wr.write("5\r\nabcde\r\n") } + @wr.write("0\r\n") + } + @wr.close + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_nil @parser.content_length + assert_nil ti.instance_eval { @size } + assert ! @parser.body_eof? + assert_equal 25, ti.size + assert @parser.body_eof? + assert_equal 25, ti.instance_eval { @size } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_nothing_raised { ti.rewind } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_equal 'abcdeabcdeabcdeabcde', ti.read(20) + assert_equal 20, ti.instance_eval { @tmp.pos } + assert_nothing_raised { ti.rewind } + assert_equal 0, ti.instance_eval { @tmp.pos } + assert_kind_of File, ti.instance_eval { @tmp } + status = nil + assert_nothing_raised { pid, status = Process.waitpid2(pid) } + assert status.success? + end + + def test_chunked_ping_pong + @parser = Unicorn::HttpParser.new + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "\r\n" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal "", @buf + chunks = %w(aa bbb cccc dddd eeee) + rd, wr = IO.pipe + + pid = fork { + chunks.each do |chunk| + rd.read(1) == "." and + @wr.write("#{'%x' % [ chunk.size]}\r\n#{chunk}\r\n") + end + @wr.write("0\r\n") + } + ti = Unicorn::TeeInput.new(@rd, @env, @parser, @buf) + assert_nil @parser.content_length + assert_nil ti.instance_eval { @size } + assert ! @parser.body_eof? + chunks.each do |chunk| + wr.write('.') + assert_equal chunk, ti.read(16384) + end + _, status = Process.waitpid2(pid) + assert status.success? + end + +private + + def init_parser(body, size = nil) + @parser = Unicorn::HttpParser.new + body = body.to_s.freeze + @buf = "POST / HTTP/1.1\r\n" \ + "Host: localhost\r\n" \ + "Content-Length: #{size || body.size}\r\n" \ + "\r\n#{body}" + assert_equal @env, @parser.headers(@env, @buf) + assert_equal body, @buf + end + +end diff --git a/test/unit/test_upload.rb b/test/unit/test_upload.rb index 9ef3ed7..7ac3c9e 100644 --- a/test/unit/test_upload.rb +++ b/test/unit/test_upload.rb @@ -1,5 +1,8 @@ +# -*- encoding: binary -*- + # Copyright (c) 2009 Eric Wong require 'test/test_helper' +require 'digest/md5' include Unicorn @@ -18,29 +21,33 @@ class UploadTest < Test::Unit::TestCase @sha1 = Digest::SHA1.new @sha1_app = lambda do |env| input = env['rack.input'] - resp = { :pos => input.pos, :size => input.size, :class => input.class } + resp = {} - # sysread @sha1.reset - begin - loop { @sha1.update(input.sysread(@bs)) } - rescue EOFError + while buf = input.read(@bs) + @sha1.update(buf) end resp[:sha1] = @sha1.hexdigest - # read - input.sysseek(0) if input.respond_to?(:sysseek) + # rewind and read again input.rewind @sha1.reset - loop { - buf = input.read(@bs) or break + while buf = input.read(@bs) @sha1.update(buf) - } + end if resp[:sha1] == @sha1.hexdigest resp[:sysread_read_byte_match] = true end + if expect_size = env['HTTP_X_EXPECT_SIZE'] + if expect_size.to_i == input.size + resp[:expect_size_match] = true + end + end + resp[:size] = input.size + resp[:content_md5] = env['HTTP_CONTENT_MD5'] + [ 200, @hdr.merge({'X-Resp' => resp.inspect}), [] ] end end @@ -54,7 +61,7 @@ class UploadTest < Test::Unit::TestCase start_server(@sha1_app) sock = TCPSocket.new(@addr, @port) sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times do + @count.times do |i| buf = @random.sysread(@bs) @sha1.update(buf) sock.syswrite(buf) @@ -63,10 +70,34 @@ class UploadTest < Test::Unit::TestCase assert_equal "HTTP/1.1 200 OK", read[0] resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) assert_equal length, resp[:size] - assert_equal 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] end + def test_put_content_md5 + md5 = Digest::MD5.new + start_server(@sha1_app) + sock = TCPSocket.new(@addr, @port) + sock.syswrite("PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n" \ + "Trailer: Content-MD5\r\n\r\n") + @count.times do |i| + buf = @random.sysread(@bs) + @sha1.update(buf) + md5.update(buf) + sock.syswrite("#{'%x' % buf.size}\r\n") + sock.syswrite(buf << "\r\n") + end + sock.syswrite("0\r\n") + + content_md5 = [ md5.digest! ].pack('m').strip.freeze + sock.syswrite("Content-MD5: #{content_md5}\r\n\r\n") + read = sock.read.split(/\r\n/) + assert_equal "HTTP/1.1 200 OK", read[0] + resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) + assert_equal length, resp[:size] + assert_equal @sha1.hexdigest, resp[:sha1] + assert_equal content_md5, resp[:content_md5] + end + def test_put_trickle_small @count, @bs = 2, 128 start_server(@sha1_app) @@ -85,42 +116,7 @@ class UploadTest < Test::Unit::TestCase assert_equal "HTTP/1.1 200 OK", read[0] resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) assert_equal length, resp[:size] - assert_equal 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] - assert_equal StringIO, resp[:class] - end - - def test_tempfile_unlinked - spew_path = lambda do |env| - if orig = env['HTTP_X_OLD_PATH'] - assert orig != env['rack.input'].path - end - assert_equal length, env['rack.input'].size - [ 200, @hdr.merge('X-Tempfile-Path' => env['rack.input'].path), [] ] - end - start_server(spew_path) - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(' ' * @bs) } - path = sock.read[/^X-Tempfile-Path: (\S+)/, 1] - sock.close - - # send another request to ensure we hit the next request - sock = TCPSocket.new(@addr, @port) - sock.syswrite("PUT / HTTP/1.0\r\nX-Old-Path: #{path}\r\n" \ - "Content-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(' ' * @bs) } - path2 = sock.read[/^X-Tempfile-Path: (\S+)/, 1] - sock.close - assert path != path2 - - # make sure the next request comes in so the unlink got processed - sock = TCPSocket.new(@addr, @port) - sock.syswrite("GET ?lasdf\r\n\r\n\r\n\r\n") - sock.sysread(4096) rescue nil - sock.close - - assert ! File.exist?(path) end def test_put_keepalive_truncates_small_overwrite @@ -136,75 +132,31 @@ class UploadTest < Test::Unit::TestCase sock.syswrite('12345') # write 4 bytes more than we expected @sha1.update('1') - read = sock.read.split(/\r\n/) + buf = sock.readpartial(4096) + while buf !~ /\r\n\r\n/ + buf << sock.readpartial(4096) + end + read = buf.split(/\r\n/) assert_equal "HTTP/1.1 200 OK", read[0] resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) assert_equal to_upload, resp[:size] - assert_equal 0, resp[:pos] assert_equal @sha1.hexdigest, resp[:sha1] end def test_put_excessive_overwrite_closed - start_server(lambda { |env| [ 200, @hdr, [] ] }) - sock = TCPSocket.new(@addr, @port) - buf = ' ' * @bs - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(buf) } - assert_raise(Errno::ECONNRESET, Errno::EPIPE) do - ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } - end - end - - def test_put_handler_closed_file - nr = '0' start_server(lambda { |env| - env['rack.input'].close - resp = { :nr => nr.succ! } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] + while env['rack.input'].read(65536); end + [ 200, @hdr, [] ] }) sock = TCPSocket.new(@addr, @port) buf = ' ' * @bs sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") - @count.times { sock.syswrite(buf) } - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal '1', resp[:nr] - # server still alive? - sock = TCPSocket.new(@addr, @port) - sock.syswrite("GET / HTTP/1.0\r\n\r\n") - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - assert_equal '2', resp[:nr] - end - - def test_renamed_file_not_closed - start_server(lambda { |env| - new_tmp = Tempfile.new('unicorn_test') - input = env['rack.input'] - File.rename(input.path, new_tmp.path) - resp = { - :inode => input.stat.ino, - :size => input.stat.size, - :new_tmp => new_tmp.path, - :old_tmp => input.path, - } - [ 200, @hdr.merge({ 'X-Resp' => resp.inspect}), [] ] - }) - sock = TCPSocket.new(@addr, @port) - buf = ' ' * @bs - sock.syswrite("PUT / HTTP/1.0\r\nContent-Length: #{length}\r\n\r\n") @count.times { sock.syswrite(buf) } - read = sock.read.split(/\r\n/) - assert_equal "HTTP/1.1 200 OK", read[0] - resp = eval(read.grep(/^X-Resp: /).first.sub!(/X-Resp: /, '')) - new_tmp = File.open(resp[:new_tmp]) - assert_equal resp[:inode], new_tmp.stat.ino - assert_equal length, resp[:size] - assert ! File.exist?(resp[:old_tmp]) - assert_equal resp[:size], new_tmp.stat.size + assert_raise(Errno::ECONNRESET, Errno::EPIPE) do + ::Unicorn::Const::CHUNK_SIZE.times { sock.syswrite(buf) } + end + assert_equal "HTTP/1.1 200 OK\r\n", sock.gets end # Despite reading numerous articles and inspecting the 1.9.1-p0 C @@ -233,7 +185,6 @@ class UploadTest < Test::Unit::TestCase resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` assert $?.success?, 'curl ran OK' assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/Tempfile/, resp) assert_match(/sysread_read_byte_match/, resp) # small StringIO path @@ -249,10 +200,87 @@ class UploadTest < Test::Unit::TestCase resp = `curl -isSfN -T#{tmp.path} http://#@addr:#@port/` assert $?.success?, 'curl ran OK' assert_match(%r!\b#{sha1}\b!, resp) - assert_match(/StringIO/, resp) assert_match(/sysread_read_byte_match/, resp) end + def test_chunked_upload_via_curl + # POSIX doesn't require all of these to be present on a system + which('curl') or return + which('sha1sum') or return + which('dd') or return + + start_server(@sha1_app) + + tmp = Tempfile.new('dd_dest') + assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", + "bs=#{@bs}", "count=#{@count}"), + "dd #@random to #{tmp}") + sha1_re = %r!\b([a-f0-9]{40})\b! + sha1_out = `sha1sum #{tmp.path}` + assert $?.success?, 'sha1sum ran OK' + + assert_match(sha1_re, sha1_out) + sha1 = sha1_re.match(sha1_out)[1] + cmd = "curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ + -isSf --no-buffer -T- " \ + "http://#@addr:#@port/" + resp = Tempfile.new('resp') + resp.sync = true + + rd, wr = IO.pipe + wr.sync = rd.sync = true + pid = fork { + STDIN.reopen(rd) + rd.close + wr.close + STDOUT.reopen(resp) + exec cmd + } + rd.close + + tmp.rewind + @count.times { |i| + wr.write(tmp.read(@bs)) + sleep(rand / 10) if 0 == i % 8 + } + wr.close + pid, status = Process.waitpid2(pid) + + resp.rewind + resp = resp.read + assert status.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/sysread_read_byte_match/, resp) + assert_match(/expect_size_match/, resp) + end + + def test_curl_chunked_small + # POSIX doesn't require all of these to be present on a system + which('curl') or return + which('sha1sum') or return + which('dd') or return + + start_server(@sha1_app) + + tmp = Tempfile.new('dd_dest') + # small StringIO path + assert(system("dd", "if=#{@random.path}", "of=#{tmp.path}", + "bs=1024", "count=1"), + "dd #@random to #{tmp}") + sha1_re = %r!\b([a-f0-9]{40})\b! + sha1_out = `sha1sum #{tmp.path}` + assert $?.success?, 'sha1sum ran OK' + + assert_match(sha1_re, sha1_out) + sha1 = sha1_re.match(sha1_out)[1] + resp = `curl -H 'X-Expect-Size: #{tmp.size}' --tcp-nodelay \ + -isSf --no-buffer -T- http://#@addr:#@port/ < #{tmp.path}` + assert $?.success?, 'curl ran OK' + assert_match(%r!\b#{sha1}\b!, resp) + assert_match(/sysread_read_byte_match/, resp) + assert_match(/expect_size_match/, resp) + end + private def length diff --git a/test/unit/test_util.rb b/test/unit/test_util.rb index 032f0be..4a1e21f 100644 --- a/test/unit/test_util.rb +++ b/test/unit/test_util.rb @@ -1,3 +1,5 @@ +# -*- encoding: binary -*- + require 'test/test_helper' require 'tempfile' @@ -15,6 +17,7 @@ class TestUtil < Test::Unit::TestCase assert_equal before, File.stat(tmp.path).inspect assert_equal ext, (tmp.external_encoding rescue nil) assert_equal int, (tmp.internal_encoding rescue nil) + assert_nothing_raised { tmp.close! } end def test_reopen_logs_renamed @@ -37,6 +40,8 @@ class TestUtil < Test::Unit::TestCase assert_equal int, (tmp.internal_encoding rescue nil) assert_equal(EXPECT_FLAGS, EXPECT_FLAGS & tmp.fcntl(Fcntl::F_GETFL)) assert tmp.sync + assert_nothing_raised { tmp.close! } + assert_nothing_raised { to.close! } end def test_reopen_logs_renamed_with_encoding @@ -59,6 +64,7 @@ class TestUtil < Test::Unit::TestCase assert fp.sync } } + assert_nothing_raised { tmp.close! } end if STDIN.respond_to?(:external_encoding) def test_reopen_logs_renamed_with_internal_encoding @@ -84,6 +90,7 @@ class TestUtil < Test::Unit::TestCase } } } + assert_nothing_raised { tmp.close! } end if STDIN.respond_to?(:external_encoding) end |