about summary refs log tree commit homepage
path: root/lib
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2010-05-03 15:19:53 -0700
committerEric Wong <normalperson@yhbt.net>2010-05-03 15:19:53 -0700
commit9f1131f5972ba90c1c54c76cc97633447142b307 (patch)
treec5ee918bfea67ffcd77b5b90ee2191ec2a5df129 /lib
parent1f3de8f8940fc7805c54d3d27e2074632ab5a0b0 (diff)
downloadrainbows-9f1131f5972ba90c1c54c76cc97633447142b307.tar.gz
Since Rainbows! is supported when exposed directly to the
Internet, administrators may want to limit the amount of data a
user may upload in a single request body to prevent a
denial-of-service via disk space exhaustion.

This amount may be specified in bytes, the default limit being
1024*1024 bytes (1 megabyte).  To override this default, a user
may specify `client_max_body_size' in the Rainbows! block
of their server config file:

  Rainbows! do
    client_max_body_size 10 * 1024 * 1024
  end

Clients that exceed the limit will get a "413 Request Entity Too
Large" response if the request body is too large and the
connection will close.

For chunked requests, we have no choice but to interrupt during
the client upload since we have no prior knowledge of the
request body size.
Diffstat (limited to 'lib')
-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
6 files changed, 158 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