From 994a1ce10e66aa957af62227fa8c93b9c290f5bc Mon Sep 17 00:00:00 2001 From: Wojciech Mitros Date: Fri, 31 Jul 2020 16:34:02 +0200 Subject: [PATCH 1/2] http: add "Expect: 100-continue" handling This patch enables sending a response with status code "100 Continue" before reading the requests body if the request contains a "Expect: 100-continue" header. Signed-off-by: Wojciech Mitros --- include/seastar/http/reply.hh | 1 + src/http/httpd.cc | 28 +++++++++++++++++++++++----- src/http/reply.cc | 3 +++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/include/seastar/http/reply.hh b/include/seastar/http/reply.hh index f31487e520..ad0ab1b5dd 100644 --- a/include/seastar/http/reply.hh +++ b/include/seastar/http/reply.hh @@ -53,6 +53,7 @@ struct reply { * The status of the reply. */ enum class status_type { + continue_ = 100, //!< continue ok = 200, //!< ok created = 201, //!< created accepted = 202, //!< accepted diff --git a/src/http/httpd.cc b/src/http/httpd.cc index acf04744a9..3c7bac2435 100644 --- a/src/http/httpd.cc +++ b/src/http/httpd.cc @@ -236,11 +236,29 @@ future<> connection::read_one() { generate_error_reply_and_close(std::move(req), reply::status_type::payload_too_large, std::move(msg)); return make_ready_future<>(); } - return read_request_body(_read_buf, std::move(req)).then([this] (std::unique_ptr req) { - return _replies.not_full().then([req = std::move(req), this] () mutable { - return generate_reply(std::move(req)); - }).then([this](bool done) { - _done = done; + + auto maybe_reply_continue = [this, req = std::move(req)] () mutable { + if (req->_version == "1.1" && req->get_header("Expect") == "100-continue") { + auto continue_reply = std::make_unique(); + set_headers(*continue_reply); + continue_reply->set_version(req->_version); + continue_reply->set_status(reply::status_type::continue_).done(); + return _replies.not_full().then([continue_reply = std::move(continue_reply), req = std::move(req), this] () mutable { + this->_replies.push(std::move(continue_reply)); + return make_ready_future>(std::move(req)); + }); + } else { + return make_ready_future>(std::move(req)); + } + }; + + return maybe_reply_continue().then([this] (std::unique_ptr req) { + return read_request_body(_read_buf, std::move(req)).then([this] (std::unique_ptr req) { + return _replies.not_full().then([req = std::move(req), this] () mutable { + return generate_reply(std::move(req)); + }).then([this](bool done) { + _done = done; + }); }); }); }); diff --git a/src/http/reply.cc b/src/http/reply.cc index cdec471501..924d10baba 100644 --- a/src/http/reply.cc +++ b/src/http/reply.cc @@ -38,6 +38,7 @@ namespace httpd { namespace status_strings { +const sstring continue_ = " 100 Continue\r\n"; const sstring ok = " 200 OK\r\n"; const sstring created = " 201 Created\r\n"; const sstring accepted = " 202 Accepted\r\n"; @@ -59,6 +60,8 @@ const sstring service_unavailable = " 503 Service Unavailable\r\n"; static const sstring& to_string(reply::status_type status) { switch (status) { + case reply::status_type::continue_: + return continue_; case reply::status_type::ok: return ok; case reply::status_type::created: From 6e8eb0c14fbb89695d8b770249e1a2680196b25e Mon Sep 17 00:00:00 2001 From: Wojciech Mitros Date: Tue, 4 Aug 2020 17:46:51 +0200 Subject: [PATCH 2/2] tests: httpd: add test for the 100 Continue status code The test checks if the 100 Continue response is sent when the request contained Expect: 100-continue header and its HTTP version was 1.1, and confirms that the response isn't sent otherwise. Signed-off-by: Wojciech Mitros --- tests/unit/httpd_test.cc | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/httpd_test.cc b/tests/unit/httpd_test.cc index ef072921e7..660f066db6 100644 --- a/tests/unit/httpd_test.cc +++ b/tests/unit/httpd_test.cc @@ -750,6 +750,75 @@ SEASTAR_TEST_CASE(content_length_limit) { }); } +SEASTAR_TEST_CASE(test_100_continue) { + return seastar::async([] { + loopback_connection_factory lcf; + http_server server("test"); + server.set_content_length_limit(11); + loopback_socket_impl lsi(lcf); + httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); + future<> client = seastar::async([&lsi, &server] { + connected_socket c_socket = std::get(lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get()); + input_stream input(c_socket.input()); + output_stream output(c_socket.output()); + + for (auto version : {sstring("1.0"), sstring("1.1")}) { + for (auto content : {sstring(""), sstring("xxxxxxxxxxx")}) { + for (auto expect : {sstring(""), sstring("Expect: 100-continue\r\n")}) { + auto content_len = content.empty() ? sstring("") : (sstring("Content-Length: ") + to_sstring(content.length()) + sstring("\r\n")); + sstring req = sstring("GET /test HTTP/") + version + sstring("\r\nHost: test\r\nConnection: Keep-Alive\r\n") + content_len + expect + sstring("\r\n"); + output.write(req).get(); + output.flush().get(); + bool already_ok = false; + if (version == "1.1" && expect.length()) { + auto resp = input.read().get0(); + BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); + already_ok = content.empty() && std::string(resp.get(), resp.size()).find("200 OK") != std::string::npos; + } + if (!already_ok) { + //If the body is empty, the final response might have already been read + output.write(content).get(); + output.flush().get(); + auto resp = input.read().get0(); + BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); + BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("200 OK"), std::string::npos); + } + } + } + } + output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\nContent-Length: 17\r\nExpect: 100-continue\r\n\r\n")).get(); + output.flush().get(); + auto resp = input.read().get0(); + BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); + BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("413 Payload Too Large"), std::string::npos); + + input.close().get(); + output.close().get(); + }); + + future<> writer = seastar::async([&server] { + class test_handler : public handler_base { + std::function(output_stream &&)> _write_func; + public: + test_handler(http_server& server, std::function(output_stream &&)>&& write_func) : _write_func(write_func) { + } + future> handle(const sstring& path, + std::unique_ptr req, std::unique_ptr rep) override { + rep->write_body("json", _write_func); + return make_ready_future>(std::move(rep)); + } + }; + auto handler = new test_handler(server, json::stream_object("hello")); + server._routes.put(GET, "/test", handler); + server.do_accepts(0).get(); + }); + + client.get(); + writer.get(); + server.stop().get(); + }); +} + SEASTAR_TEST_CASE(case_insensitive_header) { std::unique_ptr req = std::make_unique(); req->_headers["conTEnt-LengtH"] = "17";