about summary refs log tree commit homepage
diff options
context:
space:
mode:
-rw-r--r--lib/rainbows.rb12
-rw-r--r--lib/rainbows/base.rb1
-rw-r--r--lib/rainbows/const.rb2
-rw-r--r--lib/rainbows/ev_core.rb42
-rw-r--r--lib/rainbows/http_server.rb12
-rw-r--r--lib/rainbows/max_body.rb90
-rwxr-xr-xt/t0103-rack-input-limit.sh60
-rwxr-xr-xt/t0104-rack-input-limit-tiny.sh62
-rwxr-xr-xt/t0105-rack-input-limit-bigger.sh105
-rw-r--r--t/test-lib.sh1
10 files changed, 386 insertions, 1 deletions
diff --git a/lib/rainbows.rb b/lib/rainbows.rb
index ccf211e..ad4e564 100644
--- a/lib/rainbows.rb
+++ b/lib/rainbows.rb
@@ -31,6 +31,7 @@ module Rainbows
   require 'rainbows/base'
   autoload :AppPool, 'rainbows/app_pool'
   autoload :DevFdResponse, 'rainbows/dev_fd_response'
+  autoload :MaxBody, 'rainbows/max_body'
 
   class << self
 
@@ -81,6 +82,12 @@ module Rainbows
       io.respond_to?(:peeraddr) ?
                         io.peeraddr.last : Unicorn::HttpRequest::LOCALHOST
     end
+
+    # the default max body size is 1 megabyte (1024 * 1024 bytes)
+    @@max_bytes = 1024 * 1024
+
+    def max_bytes; @@max_bytes; end
+    def max_bytes=(nr); @@max_bytes = nr; end
   end
 
   # configures \Rainbows! with a given concurrency model to +use+ and
@@ -91,6 +98,7 @@ module Rainbows
   #     use :Revactor # this may also be :ThreadSpawn or :ThreadPool
   #     worker_connections 400
   #     keepalive_timeout 0 # zero disables keepalives entirely
+  #     client_max_body_size 5*1024*1024 # 5 megabytes
   #   end
   #
   #   # the rest of the Unicorn configuration
@@ -107,6 +115,10 @@ module Rainbows
   # start retrieving extra elements for.  Increasing this beyond 5
   # seconds is not recommended.  Zero disables keepalive entirely
   # (but pipelining fully-formed requests is still works).
+  #
+  # The default +client_max_body_size+ is 1 megabyte (1024 * 1024 bytes),
+  # setting this to +nil+ will disable body size checks and allow any
+  # size to be specified.
   def Rainbows!(&block)
     block_given? or raise ArgumentError, "Rainbows! requires a block"
     HttpServer.setup(block)
diff --git a/lib/rainbows/base.rb b/lib/rainbows/base.rb
index 0cbc711..864b847 100644
--- a/lib/rainbows/base.rb
+++ b/lib/rainbows/base.rb
@@ -12,6 +12,7 @@ module Rainbows
 
     def init_worker_process(worker)
       super(worker)
+      MaxBody.setup
       G.tmp = worker.tmp
 
       # avoid spurious wakeups and blocking-accept() with 1.8 green threads
diff --git a/lib/rainbows/const.rb b/lib/rainbows/const.rb
index 08c4821..42906d3 100644
--- a/lib/rainbows/const.rb
+++ b/lib/rainbows/const.rb
@@ -24,5 +24,7 @@ module Rainbows
     # of the official spec, but for now it is "hack.io"
     CLIENT_IO = "hack.io".freeze
 
+    ERROR_413_RESPONSE = "HTTP/1.1 413 Request Entity Too Large\r\n\r\n"
+
   end
 end
diff --git a/lib/rainbows/ev_core.rb b/lib/rainbows/ev_core.rb
index 682bdd6..d4ad040 100644
--- a/lib/rainbows/ev_core.rb
+++ b/lib/rainbows/ev_core.rb
@@ -49,7 +49,7 @@ module Rainbows
             write(EXPECT_100_RESPONSE)
             @env.delete(HTTP_EXPECT)
           end
-          @input = len && len <= MAX_BODY ? StringIO.new("") : Util.tmpio
+          @input = CapInput.new(len, self)
           @hp.filter_body(@buf2 = "", @buf)
           @input << @buf2
           on_read("")
@@ -73,5 +73,45 @@ module Rainbows
         handle_error(e)
     end
 
+    class CapInput < Struct.new(:io, :client, :bytes_left)
+      MAX_BODY = Unicorn::Const::MAX_BODY
+      Util = Unicorn::Util
+
+      def self.err(client, msg)
+        client.write(Const::ERROR_413_RESPONSE)
+        client.quit
+
+        # zip back up the stack
+        raise IOError, msg, []
+      end
+
+      def self.new(len, client)
+        max = Rainbows.max_bytes
+        if len
+          if max && (len > max)
+            err(client, "Content-Length too big: #{len} > #{max}")
+          end
+          len <= MAX_BODY ? StringIO.new("") : Util.tmpio
+        else
+          max ? super(Util.tmpio, client, max) : Util.tmpio
+        end
+      end
+
+      def <<(buf)
+        if (self.bytes_left -= buf.size) < 0
+          io.close
+          CapInput.err(client, "chunked request body too big")
+        end
+        io << buf
+      end
+
+      def gets; io.gets; end
+      def each(&block); io.each(&block); end
+      def size; io.size; end
+      def rewind; io.rewind; end
+      def read(*args); io.read(*args); end
+
+    end
+
   end
 end
diff --git a/lib/rainbows/http_server.rb b/lib/rainbows/http_server.rb
index ea2e23f..50231ff 100644
--- a/lib/rainbows/http_server.rb
+++ b/lib/rainbows/http_server.rb
@@ -85,6 +85,18 @@ module Rainbows
         raise ArgumentError, "keepalive must be a non-negative Integer"
       G.kato = nr
     end
+
+    def client_max_body_size(nr)
+      err = "client_max_body_size must be nil or a non-negative Integer"
+      case nr
+      when nil
+      when Integer
+        nr >= 0 or raise ArgumentError, err
+      else
+        raise ArgumentError, err
+      end
+      Rainbows.max_bytes = nr
+    end
   end
 
 end
diff --git a/lib/rainbows/max_body.rb b/lib/rainbows/max_body.rb
new file mode 100644
index 0000000..7450b2a
--- /dev/null
+++ b/lib/rainbows/max_body.rb
@@ -0,0 +1,90 @@
+# -*- encoding: binary -*-
+module Rainbows
+
+# middleware used to enforce client_max_body_size for TeeInput users,
+# there is no need to configure this middleware manually, it will
+# automatically be configured for you based on the client_max_body_size
+# setting
+class MaxBody < Struct.new(:app)
+
+  # this is meant to be included in Unicorn::TeeInput (and derived
+  # classes) to limit body sizes
+  module Limit
+    Util = Unicorn::Util
+
+    def initialize(socket, req, parser, buf)
+      self.len = parser.content_length
+
+      max = Rainbows.max_bytes # never nil, see MaxBody.setup
+      if len && len > max
+        socket.write(Const::ERROR_413_RESPONSE)
+        socket.close
+        raise IOError, "Content-Length too big: #{len} > #{max}", []
+      end
+
+      self.req = req
+      self.parser = parser
+      self.buf = buf
+      self.socket = socket
+      self.buf2 = ""
+      if buf.size > 0
+        parser.filter_body(buf2, buf) and finalize_input
+        buf2.size > max and raise IOError, "chunked request body too big", []
+      end
+      self.tmp = len && len < Const::MAX_BODY ? StringIO.new("") : Util.tmpio
+      if buf2.size > 0
+        tmp.write(buf2)
+        tmp.seek(0)
+        max -= buf2.size
+      end
+      @max_body = max
+    end
+
+    def tee(length, dst)
+      rv = _tee(length, dst)
+      if rv && ((@max_body -= rv.size) < 0)
+        $stderr.puts "#@max_body  TOO SMALL"
+        # make HttpParser#keepalive? => false to force an immediate disconnect
+        # after we write
+        parser.reset
+        throw :rainbows_EFBIG
+      end
+      rv
+    end
+
+  end
+
+  # this is called after forking, so it won't ever affect the master
+  # if it's reconfigured
+  def self.setup
+    Rainbows.max_bytes or return
+    case G.server.use
+    when :Rev, :EventMachine, :NeverBlock
+      return
+    when :Revactor
+      Rainbows::Revactor::TeeInput
+    else
+      Unicorn::TeeInput
+    end.class_eval do
+      alias _tee tee # can't use super here :<
+      remove_method :tee
+      remove_method :initialize if G.server.use != :Revactor # FIXME CODE SMELL
+      include Limit
+    end
+
+    # force ourselves to the outermost middleware layer
+    G.server.app = MaxBody.new(G.server.app)
+  end
+
+  # Rack response returned when there's an error
+  def err(env)
+    [ 413, [ %w(Content-Length 0), %w(Content-Type text/plain) ], [] ]
+  end
+
+  # our main Rack middleware endpoint
+  def call(env)
+    catch(:rainbows_EFBIG) { app.call(env) } || err(env)
+  end
+
+end # class
+end # module
diff --git a/t/t0103-rack-input-limit.sh b/t/t0103-rack-input-limit.sh
new file mode 100755
index 0000000..38dbd4c
--- /dev/null
+++ b/t/t0103-rack-input-limit.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+. ./test-lib.sh
+test -r random_blob || die "random_blob required, run with 'make $0'"
+
+t_plan 6 "rack.input client_max_body_size default"
+
+t_begin "setup and startup" && {
+        rtmpfiles curl_out curl_err cmbs_config
+        rainbows_setup $model
+        grep -v client_max_body_size < $unicorn_config > $cmbs_config
+        rainbows -D sha1-random-size.ru -c $cmbs_config
+        rainbows_wait_start
+}
+
+t_begin "regular request" && {
+        rm -f $ok
+        curl -vsSf -T random_blob -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "chunked request" && {
+        rm -f $ok
+        curl -vsSf -T- < random_blob -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "default size sha1 chunked" && {
+        blob_sha1=3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3
+        rm -f $ok
+        > $r_err
+        dd if=/dev/zero bs=1048576 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        test "$(cat $curl_out)" = $blob_sha1
+        dbgcat curl_err
+        dbgcat curl_out
+}
+
+t_begin "default size sha1 content-length" && {
+        blob_sha1=3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3
+        rm -f $ok
+        dd if=/dev/zero bs=1048576 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        test "$(cat $curl_out)" = $blob_sha1
+        dbgcat curl_err
+        dbgcat curl_out
+}
+
+t_begin "shutdown" && {
+        kill $rainbows_pid
+}
+
+t_done
diff --git a/t/t0104-rack-input-limit-tiny.sh b/t/t0104-rack-input-limit-tiny.sh
new file mode 100755
index 0000000..e68bc53
--- /dev/null
+++ b/t/t0104-rack-input-limit-tiny.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+. ./test-lib.sh
+test -r random_blob || die "random_blob required, run with 'make $0'"
+
+t_plan 6 "rack.input client_max_body_size tiny"
+
+t_begin "setup and startup" && {
+        rtmpfiles curl_out curl_err cmbs_config
+        rainbows_setup $model
+        sed -e 's/client_max_body_size.*/client_max_body_size 256/' \
+          < $unicorn_config > $cmbs_config
+        rainbows -D sha1-random-size.ru -c $cmbs_config
+        rainbows_wait_start
+}
+
+t_begin "stops a regular request" && {
+        rm -f $ok
+        dd if=/dev/zero bs=257 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "stops a large chunked request" && {
+        rm -f $ok
+        dd if=/dev/zero bs=257 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "small size sha1 chunked ok" && {
+        blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91
+        rm -f $ok
+        dd if=/dev/zero bs=256 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "small size sha1 content-length ok" && {
+        blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91
+        rm -f $ok
+        dd if=/dev/zero bs=256 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "shutdown" && {
+        kill $rainbows_pid
+}
+
+t_done
diff --git a/t/t0105-rack-input-limit-bigger.sh b/t/t0105-rack-input-limit-bigger.sh
new file mode 100755
index 0000000..6b58291
--- /dev/null
+++ b/t/t0105-rack-input-limit-bigger.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+. ./test-lib.sh
+
+t_plan 10 "rack.input client_max_body_size bigger"
+
+t_begin "setup and startup" && {
+        rtmpfiles curl_out curl_err cmbs_config
+        rainbows_setup $model
+        sed -e 's/client_max_body_size.*/client_max_body_size 10485760/' \
+          < $unicorn_config > $cmbs_config
+        rainbows -D sha1-random-size.ru -c $cmbs_config
+        rainbows_wait_start
+}
+
+t_begin "stops a regular request" && {
+        rm -f $ok
+        dd if=/dev/zero bs=102485761 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "stops a large chunked request" && {
+        rm -f $ok
+        dd if=/dev/zero bs=102485761 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err || > $ok
+        dbgcat curl_err
+        dbgcat curl_out
+        test -e $ok
+}
+
+t_begin "small size sha1 chunked ok" && {
+        blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91
+        rm -f $ok
+        dd if=/dev/zero bs=256 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "small size sha1 content-length ok" && {
+        blob_sha1=b376885ac8452b6cbf9ced81b1080bfd570d9b91
+        rm -f $ok
+        dd if=/dev/zero bs=256 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "right size sha1 chunked ok" && {
+        blob_sha1=8c206a1a87599f532ce68675536f0b1546900d7a
+        rm -f $ok
+        dd if=/dev/zero bs=10485760 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "right size sha1 content-length ok" && {
+        blob_sha1=8c206a1a87599f532ce68675536f0b1546900d7a
+        rm -f $ok
+        dd if=/dev/zero bs=10485760 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "default size sha1 chunked ok" && {
+        blob_sha1=3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3
+        rm -f $ok
+        dd if=/dev/zero bs=1048576 count=1 | \
+          curl -vsSf -T- -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "default size sha1 content-length ok" && {
+        blob_sha1=3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3
+        rm -f $ok
+        dd if=/dev/zero bs=1048576 count=1 of=$tmp
+        curl -vsSf -T $tmp -H Expect: \
+          http://$listen/ > $curl_out 2> $curl_err
+        dbgcat curl_err
+        dbgcat curl_out
+        test "$(cat $curl_out)" = $blob_sha1
+}
+
+t_begin "shutdown" && {
+        kill $rainbows_pid
+}
+
+t_done
diff --git a/t/test-lib.sh b/t/test-lib.sh
index 5aa75b7..04ebeb1 100644
--- a/t/test-lib.sh
+++ b/t/test-lib.sh
@@ -113,6 +113,7 @@ EOF
                 # boxes and sometimes sleep 1s in tests
                 kato=5
                 echo 'Rainbows! do'
+                echo "  client_max_body_size nil"
                 if test $# -ge 1
                 then
                         echo "  use :$1"