about summary refs log tree commit homepage
path: root/test
diff options
context:
space:
mode:
authorEric Wong <e@80x24.org>2013-10-19 01:50:42 +0000
committerEric Wong <normalperson@yhbt.net>2013-10-19 01:51:29 +0000
commit2377d5a1cafa518313b0b597e4c3af65bb57f887 (patch)
treea1d26cf33782e580f0d98641d9a3889975434ba6 /test
parentf89ee896e14bfa97179f3773d303dd0a1bdcf971 (diff)
downloadyahns-2377d5a1cafa518313b0b597e4c3af65bb57f887.tar.gz
This is mostly code imported from Rainbows! (so GPLv2+).  This should
implement everything necessary to prevent clients from DoS-ing us with
overly large bodies.  The default is 1M (same as Rainbows! and nginx).

Yahns::MaxBody may become part of the public API (as the equivalent is
in Rainbows!), since it makes more sense in the rackup (config.ru) file
(since it's endpoint-specific).  However, that's confusing as
Yahns::MaxBody only works when input_buffering is :lazy or false, and
not when it is true (preread).
Diffstat (limited to 'test')
-rw-r--r--test/test_client_max_body_size.rb163
1 files changed, 163 insertions, 0 deletions
diff --git a/test/test_client_max_body_size.rb b/test/test_client_max_body_size.rb
new file mode 100644
index 0000000..b50e2de
--- /dev/null
+++ b/test/test_client_max_body_size.rb
@@ -0,0 +1,163 @@
+# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
+# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
+require_relative 'server_helper'
+
+class TestClientMaxBodySize < Testcase
+  parallelize_me!
+  include ServerHelper
+  alias setup server_helper_setup
+  alias teardown server_helper_teardown
+  DEFMBS = 1024 * 1024
+
+  DRAINER = lambda do |e|
+    input = e["rack.input"]
+    buf = ""
+    nr = 0
+    while rv = input.read(16384, buf)
+      nr += rv.size
+    end
+    body = nr.to_s
+    h = { "Content-Length" => body.size.to_s, "Content-Type" => 'text/plain' }
+    [ 200, h, [body] ]
+  end
+
+  def identity_req(bytes, body = true)
+    body_bytes = body ? bytes : 0
+    "PUT / HTTP/1.1\r\nConnection: close\r\nHost: example.com\r\n" \
+    "Content-Length: #{bytes}\r\n\r\n#{'*' * body_bytes}"
+  end
+
+  def mkserver(cfg)
+    fork do
+      srv = Yahns::Server.new(cfg)
+      ENV["YAHNS_FD"] = @srv.fileno.to_s
+      srv.start.join
+    end
+  end
+
+  def test_0_lazy; cmbs_test_0(:lazy); end
+  def test_0_true; cmbs_test_0(true); end
+  def test_0_false; cmbs_test_0(false); end
+
+  def cmbs_test_0(btype)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      GTL.synchronize {
+        app(:rack, DRAINER) {
+          listen "#{host}:#{port}"
+          input_buffering btype
+          client_max_body_size 0
+        }
+      }
+      logger(Logger.new(err.path))
+    end
+    pid = mkserver(cfg)
+    default_identity_checks(host, port, 0)
+    default_chunked_checks(host, port, 0)
+  ensure
+    quit_wait(pid)
+  end
+
+  def test_cmbs_lazy; cmbs_test(:lazy); end
+  def test_cmbs_true; cmbs_test(true); end
+  def test_cmbs_false; cmbs_test(false); end
+
+  def cmbs_test(btype)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      GTL.synchronize {
+        app(:rack, DRAINER) {
+          listen "#{host}:#{port}"
+          input_buffering btype
+        }
+      }
+      logger(Logger.new(err.path))
+    end
+    pid = mkserver(cfg)
+    default_identity_checks(host, port)
+    default_chunked_checks(host, port)
+  ensure
+    quit_wait(pid)
+  end
+
+  def test_inf_false; big_test(false); end
+  def test_inf_true; big_test(true); end
+  def test_inf_lazy; big_test(:lazy); end
+
+  def big_test(btype)
+    err = @err
+    cfg = Yahns::Config.new
+    host, port = @srv.addr[3], @srv.addr[1]
+    cfg.instance_eval do
+      GTL.synchronize {
+        app(:rack, DRAINER) {
+          listen "#{host}:#{port}"
+          input_buffering btype
+          client_max_body_size nil
+        }
+      }
+      logger(Logger.new(err.path))
+    end
+    pid = mkserver(cfg)
+
+    bytes = 10 * 1024 * 1024
+    r = `dd if=/dev/zero bs=#{bytes} count=1 2>/dev/null | \
+         curl -sSf -HExpect: -T- http://#{host}:#{port}/`
+    assert $?.success?, $?.inspect
+    assert_equal bytes.to_s, r
+
+    r = `dd if=/dev/zero bs=#{bytes} count=1 2>/dev/null | \
+         curl -sSf -HExpect: -HContent-Length:#{bytes} -HTransfer-Encoding: \
+         -T- http://#{host}:#{port}/`
+    assert $?.success?, $?.inspect
+    assert_equal bytes.to_s, r
+  ensure
+    quit_wait(pid)
+  end
+
+  def default_chunked_checks(host, port, defmax = DEFMBS)
+    r = `curl -sSf -HExpect: -T- </dev/null http://#{host}:#{port}/`
+    assert $?.success?, $?.inspect
+    assert_equal "0", r
+
+    r = `dd if=/dev/zero bs=#{defmax} count=1 2>/dev/null | \
+         curl -sSf -HExpect: -T- http://#{host}:#{port}/`
+    assert $?.success?, $?.inspect
+    assert_equal "#{defmax}", r
+
+    r = `dd if=/dev/zero bs=#{defmax + 1} count=1 2>/dev/null | \
+         curl -sf -HExpect: -T- --write-out %{http_code} \
+         http://#{host}:#{port}/ 2>&1`
+    refute $?.success?, $?.inspect
+    assert_equal "413", r
+  end
+
+  def default_identity_checks(host, port, defmax = DEFMBS)
+    if defmax >= 666
+      c = TCPSocket.new(host, port)
+      c.write(identity_req(666))
+      assert_equal "666", c.read.split(/\r\n\r\n/)[1]
+      c.close
+    end
+
+    c = TCPSocket.new(host, port)
+    c.write(identity_req(0))
+    assert_equal "0", c.read.split(/\r\n\r\n/)[1]
+    c.close
+
+    c = TCPSocket.new(host, port)
+    c.write(identity_req(defmax))
+    assert_equal "#{defmax}", c.read.split(/\r\n\r\n/)[1]
+    c.close
+
+    toobig = defmax + 1
+    c = TCPSocket.new(host, port)
+    c.write(identity_req(toobig, false))
+    assert_match(%r{\AHTTP/1\.[01] 413 }, Timeout.timeout(10) { c.read })
+    c.close
+  end
+end