diff options
author | Eric Wong <normalperson@yhbt.net> | 2009-06-05 18:03:46 -0700 |
---|---|---|
committer | Eric Wong <normalperson@yhbt.net> | 2009-06-05 22:16:16 -0700 |
commit | 6945342a1f0a4caaa918f2b0b1efef88824439e0 (patch) | |
tree | 69100796a793ae26c20968ea79f24c924f362eae /test/unit/test_chunked_reader.rb | |
parent | a48695449f49e6900819fed472f23408c62b5501 (diff) | |
download | unicorn-6945342a1f0a4caaa918f2b0b1efef88824439e0.tar.gz |
This adds support for handling POST/PUT request bodies sent with chunked transfer encodings ("Transfer-Encoding: chunked"). Attention has been paid to ensure that a client cannot OOM us by sending an extremely large chunk. This implementation is pure Ruby as the Ragel-based implementation in rfuzz didn't offer a streaming interface. It should be reasonably close to RFC-compliant but please test it in an attempt to break it. The more interesting part is the ability to stream data to the hosted Rack application as it is being transferred to the server. This can be done regardless if the input is chunked or not, enabling the streaming of POST/PUT bodies can allow the hosted Rack application to process input as it receives it. See examples/echo.ru for an example echo server over HTTP. Enabling streaming also allows Rack applications to support upload progress monitoring previously supported by Mongrel handlers. Since Rack specifies that the input needs to be rewindable, this input is written to a temporary file (a la tee(1)) as it is streamed to the application the first time. Subsequent rewinded reads will read from the temporary file instead of the socket. Streaming input to the application is disabled by default since applications may not necessarily read the entire input body before returning. Since this is a completely new feature we've never seen in any Ruby HTTP application server before, we're taking the safe route by leaving it disabled by default. Enabling this can only be done globally by changing the Unicorn HttpRequest::DEFAULTS hash: Unicorn::HttpRequest::DEFAULTS["unicorn.stream_input"] = true Similarly, a Rack application can check if streaming input is enabled by checking the value of the "unicorn.stream_input" key in the environment hashed passed to it. All of this code has only been lightly tested and test coverage is lacking at the moment. [1] - http://tools.ietf.org/html/rfc2616#section-3.6.1
Diffstat (limited to 'test/unit/test_chunked_reader.rb')
-rw-r--r-- | test/unit/test_chunked_reader.rb | 145 |
1 files changed, 145 insertions, 0 deletions
diff --git a/test/unit/test_chunked_reader.rb b/test/unit/test_chunked_reader.rb new file mode 100644 index 0000000..d9fc56f --- /dev/null +++ b/test/unit/test_chunked_reader.rb @@ -0,0 +1,145 @@ +require 'test/unit' +require 'unicorn' +require 'tempfile' +require 'io/nonblock' +require 'digest/sha1' + +class TestChunkedReader < Test::Unit::TestCase + + def setup + @cr = Unicorn::ChunkedReader.new + @rd, @wr = IO.pipe + @rd.binmode + @wr.binmode + @rd.sync = @wr.sync = true + @start_pid = $$ + end + + def teardown + return if $$ != @start_pid + @rd.close rescue nil + @wr.close rescue nil + end + + def test_eof1 + @cr.reopen(@rd, "0\r\n") + assert_raises(EOFError) { @cr.readpartial(8192) } + end + + def test_eof2 + @cr.reopen(@rd, "0\r\n\r\n") + assert_raises(EOFError) { @cr.readpartial(8192) } + end + + def test_readpartial1 + @cr.reopen(@rd, "4\r\nasdf\r\n0\r\n") + assert_equal 'asdf', @cr.readpartial(8192) + assert_raises(EOFError) { @cr.readpartial(8192) } + end + + def test_gets1 + @cr.reopen(@rd, "4\r\nasdf\r\n0\r\n") + STDOUT.sync = true + assert_equal 'asdf', @cr.gets + assert_raises(EOFError) { @cr.readpartial(8192) } + end + + def test_gets2 + @cr.reopen(@rd, "4\r\nasd\n\r\n0\r\n\r\n") + assert_equal "asd\n", @cr.gets + assert_nil @cr.gets + end + + def test_gets3 + max = Unicorn::Const::CHUNK_SIZE * 2 + str = ('a' * max).freeze + first = 5 + last = str.size - first + @cr.reopen(@rd, + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + assert_equal str, @cr.gets + assert_nil @cr.gets + end + + def test_readpartial_gets_mixed1 + max = Unicorn::Const::CHUNK_SIZE * 2 + str = ('a' * max).freeze + first = 5 + last = str.size - first + @cr.reopen(@rd, + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + partial = @cr.readpartial(16384) + assert String === partial + + len = max - partial.size + assert_equal(str[-len, len], @cr.gets) + assert_raises(EOFError) { @cr.readpartial(1) } + assert_nil @cr.gets + end + + def test_gets_mixed_readpartial + max = 10 + str = ("z\n" * max).freeze + first = 5 + last = str.size - first + @cr.reopen(@rd, + "#{'%x' % first}\r\n#{str[0, first]}\r\n" \ + "#{'%x' % last}\r\n#{str[-last, last]}\r\n" \ + "0\r\n") + assert_equal("z\n", @cr.gets) + assert_equal("z\n", @cr.gets) + end + + def test_dd + @cr.reopen(@rd, "6\r\nhello\n\r\n") + tmp = Tempfile.new('test_dd') + tmp.sync = true + + pid = fork { + crd, cwr = IO.pipe + crd.binmode + cwr.binmode + crd.sync = cwr.sync = true + + pid = fork { + STDOUT.reopen(cwr) + crd.close + cwr.close + exec('dd', 'if=/dev/urandom', 'bs=93390', 'count=16') + } + cwr.close + begin + buf = crd.readpartial(16384) + tmp.write(buf) + @wr.write("#{'%x' % buf.size}\r\n#{buf}\r\n") + rescue EOFError + @wr.write("0\r\n\r\n") + Process.waitpid(pid) + exit 0 + end while true + } + assert_equal "hello\n", @cr.gets + sha1 = Digest::SHA1.new + buf = '' + begin + @cr.readpartial(16384, buf) + sha1.update(buf) + rescue EOFError + break + end while true + + assert_nothing_raised { Process.waitpid(pid) } + sha1_file = Digest::SHA1.new + File.open(tmp.path, 'rb') { |fp| + while fp.read(16384, buf) + sha1_file.update(buf) + end + } + assert_equal sha1_file.hexdigest, sha1.hexdigest + end + +end |