From b73d91e6235f2c83cb768268a6661b88cdc96d4c Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Thu, 24 Oct 2013 23:25:47 +0000 Subject: implement + test Expect: 100-continue handling This should speed up out-of-the-box performance with curl as well as allow input_buffering :lazy/true users to reject requests they don't like. --- lib/yahns/http_client.rb | 13 +++++- lib/yahns/http_response.rb | 12 +++++ test/server_helper.rb | 8 ++++ test/test_client_max_body_size.rb | 8 ---- test/test_expect_100.rb | 92 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 test/test_expect_100.rb diff --git a/lib/yahns/http_client.rb b/lib/yahns/http_client.rb index ce55525..641dc65 100644 --- a/lib/yahns/http_client.rb +++ b/lib/yahns/http_client.rb @@ -56,6 +56,10 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc: end @state = :body @input = k.tmpio_for(len) + + # FIXME: we must buffer and not block here even if it's only a few bytes + http_100_response(@hs.env) + rbuf = Thread.current[:yahns_rbuf] @hs.filter_body(rbuf, @hs.buf) @input.write(rbuf) @@ -158,11 +162,16 @@ class Yahns::HttpClient < Kgio::Socket # :nodoc: end # run the rack app - response = k.app.call(env.merge!(k.app_defaults)) + status, headers, body = k.app.call(env.merge!(k.app_defaults)) return :ignore if env.include?(RACK_HIJACK_IO) + if status.to_i == 100 + # FIXME: we must buffer and not wait here even if it's only a few bytes + http_100_response(env) + status, headers, body = k.app.call(env) + end # this returns :wait_readable, :wait_writable, :ignore, or nil: - http_response_write(*response) + http_response_write(status, headers, body) end def hijack_proc(env) diff --git a/lib/yahns/http_response.rb b/lib/yahns/http_response.rb index 3ee4e21..a34832a 100644 --- a/lib/yahns/http_response.rb +++ b/lib/yahns/http_response.rb @@ -19,11 +19,23 @@ module Yahns::HttpResponse # :nodoc: CONN_CLOSE = "Connection: close\r\n\r\n" Z = "" RESPONSE_START = "HTTP/1.1 " + R100_RAW = "HTTP/1.1 100 Continue\r\n\r\n" + R100_CCC = "100 Continue\r\n\r\nHTTP/1.1 " + HTTP_EXPECT = "HTTP_EXPECT" def response_start @response_start_sent ? Z : RESPONSE_START end + # only used if input_buffering is true (not :lazy or false) + # :lazy/false gives control to the app + def http_100_response(env) + if env[HTTP_EXPECT] =~ /\A100-continue\z/i + kgio_write(@response_start_sent ? R100_CCC : R100_RAW) + env.delete(HTTP_EXPECT) + end + end + def response_wait_write(rv) # call the kgio_wait_readable or kgio_wait_writable method ok = __send__("kgio_#{rv}") and return ok diff --git a/test/server_helper.rb b/test/server_helper.rb index 7a64618..aeb0dc9 100644 --- a/test/server_helper.rb +++ b/test/server_helper.rb @@ -70,4 +70,12 @@ module ServerHelper @err = tmpfile(%w(srv .err)) @ru = nil end + + def mkserver(cfg) + fork do + srv = Yahns::Server.new(cfg) + ENV["YAHNS_FD"] = @srv.fileno.to_s + srv.start.join + end + end end diff --git a/test/test_client_max_body_size.rb b/test/test_client_max_body_size.rb index cdf8ce9..5688f2b 100644 --- a/test/test_client_max_body_size.rb +++ b/test/test_client_max_body_size.rb @@ -27,14 +27,6 @@ class TestClientMaxBodySize < Testcase "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 diff --git a/test/test_expect_100.rb b/test/test_expect_100.rb new file mode 100644 index 0000000..484f44d --- /dev/null +++ b/test/test_expect_100.rb @@ -0,0 +1,92 @@ +# Copyright (C) 2013, Eric Wong and all contributors +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require_relative 'server_helper' + +class TestExpect100 < Testcase + ENV["N"].to_i > 1 and parallelize_me! + include ServerHelper + alias setup server_helper_setup + alias teardown server_helper_teardown + + APP = lambda do |env| + h = [ %w(Content-Length 0), %w(Content-Type text/plain) ] + if env["HTTP_EXPECT"] =~ /100-continue/ + code = env["HTTP_X_FORCE_RCODE"] || 100 + [ code, h, [] ] + else + [ 201, h, [] ] + end + end + + def test_buffer_noccc; _test_expect_100(true, false); end + def test_nobuffer_noccc; _test_expect_100(false, false); end + def test_lazybuffer_noccc; _test_expect_100(:lazy, false); end + def test_buffer_ccc; _test_expect_100(true, true); end + def test_nobuffer_ccc; _test_expect_100(false, true); end + def test_lazybuffer_ccc; _test_expect_100(:lazy, true); end + + def _test_expect_100(btype, ccc) + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + stderr_path err.path + GTL.synchronize { + app(:rack, APP) { + listen "#{host}:#{port}" + input_buffering btype + check_client_connection ccc + } + } + end + pid = mkserver(cfg) + c = get_tcp_client(host, port) + r = "PUT / HTTP/1.0\r\nExpect: 100-continue\r\n\r\n" + c.write(r) + assert c.wait(10), "timed out" + buf = c.read + assert_match(%r{\AHTTP/1\.1 100 Continue\r\n\r\nHTTP/1\.1 201}, buf) + + rc = system("curl -sSf -T- http://#{host}:#{port}/", in: "/dev/null") + assert $?.success?, $?.inspect + assert rc + ensure + quit_wait(pid) + end + + def _test_reject(btype, ccc) + err = @err + cfg = Yahns::Config.new + host, port = @srv.addr[3], @srv.addr[1] + cfg.instance_eval do + stderr_path err.path + GTL.synchronize { + app(:rack, APP) { + listen "#{host}:#{port}" + input_buffering btype + check_client_connection ccc + } + } + end + pid = mkserver(cfg) + c = get_tcp_client(host, port) + r = "PUT / HTTP/1.0\r\nExpect: 100-continue\r\nX-Force-RCODE: 666\r\n\r\n" + c.write(r) + assert c.wait(10), "timed out" + buf = c.read + assert_match(%r{\AHTTP/1\.1 666\r\n}, buf) + + opts = { in: "/dev/null" } + url = "http://#{host}:#{port}/" + rc = system("curl -sf -T- -HX-Force-Rcode:666 #{url}", in: "/dev/null") + refute $?.success?, $?.inspect + refute rc + ensure + quit_wait(pid) + end + + def test_reject_lazy_noccc; _test_reject(:lazy, false); end + def test_reject_true_noccc; _test_reject(false, false); end + def test_reject_lazy_ccc; _test_reject(:lazy, true); end + def test_reject_true_ccc; _test_reject(false, true); end +end -- cgit v1.2.3-24-ge0c7