diff options
Diffstat (limited to 'src/seastar/tests/unit/httpd_test.cc')
-rw-r--r-- | src/seastar/tests/unit/httpd_test.cc | 889 |
1 files changed, 889 insertions, 0 deletions
diff --git a/src/seastar/tests/unit/httpd_test.cc b/src/seastar/tests/unit/httpd_test.cc new file mode 100644 index 000000000..2428f038d --- /dev/null +++ b/src/seastar/tests/unit/httpd_test.cc @@ -0,0 +1,889 @@ +/* + * Copyright 2015 Cloudius Systems + */ + +#include <seastar/http/httpd.hh> +#include <seastar/http/handlers.hh> +#include <seastar/http/matcher.hh> +#include <seastar/http/matchrules.hh> +#include <seastar/json/formatter.hh> +#include <seastar/http/routes.hh> +#include <seastar/http/exception.hh> +#include <seastar/http/transformers.hh> +#include <seastar/core/do_with.hh> +#include <seastar/core/loop.hh> +#include <seastar/core/when_all.hh> +#include <seastar/testing/test_case.hh> +#include <seastar/testing/thread_test_case.hh> +#include "loopback_socket.hh" +#include <boost/algorithm/string.hpp> +#include <seastar/core/thread.hh> +#include <seastar/util/noncopyable_function.hh> +#include <seastar/http/json_path.hh> +#include <sstream> + +using namespace seastar; +using namespace httpd; + +class handl : public httpd::handler_base { +public: + virtual future<std::unique_ptr<reply> > handle(const sstring& path, + std::unique_ptr<request> req, std::unique_ptr<reply> rep) { + rep->done("html"); + return make_ready_future<std::unique_ptr<reply>>(std::move(rep)); + } +}; + +SEASTAR_TEST_CASE(test_reply) +{ + reply r; + r.set_content_type("txt"); + BOOST_REQUIRE_EQUAL(r._headers["Content-Type"], sstring("text/plain")); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_str_matcher) +{ + + str_matcher m("/hello"); + parameters param; + BOOST_REQUIRE_EQUAL(m.match("/abc/hello", 4, param), 10u); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_param_matcher) +{ + + param_matcher m("param"); + parameters param; + BOOST_REQUIRE_EQUAL(m.match("/abc/hello", 4, param), 10u); + BOOST_REQUIRE_EQUAL(param.path("param"), "/hello"); + BOOST_REQUIRE_EQUAL(param["param"], "hello"); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_match_rule) +{ + + parameters param; + handl* h = new handl(); + match_rule mr(h); + mr.add_str("/hello").add_param("param"); + httpd::handler_base* res = mr.get("/hello/val1", param); + BOOST_REQUIRE_EQUAL(res, h); + BOOST_REQUIRE_EQUAL(param["param"], "val1"); + res = mr.get("/hell/val1", param); + httpd::handler_base* nl = nullptr; + BOOST_REQUIRE_EQUAL(res, nl); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_match_rule_order) +{ + parameters param; + routes route; + + handl* h1 = new handl(); + route.add(operation_type::GET, url("/hello"), h1); + + handl* h2 = new handl(); + route.add(operation_type::GET, url("/hello"), h2); + + auto rh = route.get_handler(GET, "/hello", param); + BOOST_REQUIRE_EQUAL(rh, h1); + + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_put_drop_rule) +{ + routes rts; + auto h = std::make_unique<handl>(); + parameters params; + + { + auto reg = handler_registration(rts, *h, "/hello", operation_type::GET); + auto res = rts.get_handler(operation_type::GET, "/hello", params); + BOOST_REQUIRE_EQUAL(res, h.get()); + } + + auto res = rts.get_handler(operation_type::GET, "/hello", params); + httpd::handler_base* nl = nullptr; + BOOST_REQUIRE_EQUAL(res, nl); + return make_ready_future<>(); +} + +// Putting a duplicated exact rule would result +// in a memory leak due to the fact that rules are implemented +// as raw pointers. In order to prevent such leaks, +// an exception is thrown if somebody tries to put +// a duplicated rule without removing the old one first. +// The interface demands that the callee allocates the handle, +// so it should also expect the callee to free it before +// overwriting. +SEASTAR_TEST_CASE(test_duplicated_exact_rule) +{ + parameters param; + routes route; + + handl* h1 = new handl; + route.put(operation_type::GET, "/hello", h1); + + handl* h2 = new handl; + BOOST_REQUIRE_THROW(route.put(operation_type::GET, "/hello", h2), std::runtime_error); + + delete route.drop(operation_type::GET, "/hello"); + route.put(operation_type::GET, "/hello", h2); + + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_add_del_cookie) +{ + routes rts; + handl* h = new handl(); + match_rule mr(h); + mr.add_str("/hello"); + parameters params; + + { + auto reg = rule_registration(rts, mr, operation_type::GET); + auto res = rts.get_handler(operation_type::GET, "/hello", params); + BOOST_REQUIRE_EQUAL(res, h); + } + + auto res = rts.get_handler(operation_type::GET, "/hello", params); + httpd::handler_base* nl = nullptr; + BOOST_REQUIRE_EQUAL(res, nl); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_formatter) +{ + BOOST_REQUIRE_EQUAL(json::formatter::to_json(true), "true"); + BOOST_REQUIRE_EQUAL(json::formatter::to_json(false), "false"); + BOOST_REQUIRE_EQUAL(json::formatter::to_json(1), "1"); + const char* txt = "efg"; + BOOST_REQUIRE_EQUAL(json::formatter::to_json(txt), "\"efg\""); + sstring str = "abc"; + BOOST_REQUIRE_EQUAL(json::formatter::to_json(str), "\"abc\""); + float f = 1; + BOOST_REQUIRE_EQUAL(json::formatter::to_json(f), "1"); + f = 1.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(f), std::out_of_range); + f = -1.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(f), std::out_of_range); + f = 0.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(f), std::invalid_argument); + double d = -1; + BOOST_REQUIRE_EQUAL(json::formatter::to_json(d), "-1"); + d = 1.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(d), std::out_of_range); + d = -1.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(d), std::out_of_range); + d = 0.0/0.0; + BOOST_CHECK_THROW(json::formatter::to_json(d), std::invalid_argument); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_decode_url) { + request req; + req._url = "/a?q=%23%24%23"; + sstring url = http_server::connection::set_query_param(req); + BOOST_REQUIRE_EQUAL(url, "/a"); + BOOST_REQUIRE_EQUAL(req.get_query_param("q"), "#$#"); + req._url = "/a?a=%23%24%23&b=%22%26%22"; + http_server::connection::set_query_param(req); + BOOST_REQUIRE_EQUAL(req.get_query_param("a"), "#$#"); + BOOST_REQUIRE_EQUAL(req.get_query_param("b"), "\"&\""); + return make_ready_future<>(); +} + +SEASTAR_TEST_CASE(test_routes) { + handl* h1 = new handl(); + handl* h2 = new handl(); + routes route; + route.add(operation_type::GET, url("/api").remainder("path"), h1); + route.add(operation_type::GET, url("/"), h2); + std::unique_ptr<request> req = std::make_unique<request>(); + std::unique_ptr<reply> rep = std::make_unique<reply>(); + + auto f1 = + route.handle("/api", std::move(req), std::move(rep)).then( + [] (std::unique_ptr<reply> rep) { + BOOST_REQUIRE_EQUAL((int )rep->_status, (int )reply::status_type::ok); + }); + req.reset(new request); + rep.reset(new reply); + + auto f2 = + route.handle("/", std::move(req), std::move(rep)).then( + [] (std::unique_ptr<reply> rep) { + BOOST_REQUIRE_EQUAL((int )rep->_status, (int )reply::status_type::ok); + }); + req.reset(new request); + rep.reset(new reply); + auto f3 = + route.handle("/api/abc", std::move(req), std::move(rep)).then( + [] (std::unique_ptr<reply> rep) { + }); + req.reset(new request); + rep.reset(new reply); + auto f4 = + route.handle("/ap", std::move(req), std::move(rep)).then( + [] (std::unique_ptr<reply> rep) { + BOOST_REQUIRE_EQUAL((int )rep->_status, + (int )reply::status_type::not_found); + }); + return when_all(std::move(f1), std::move(f2), std::move(f3), std::move(f4)) + .then([] (std::tuple<future<>, future<>, future<>, future<>> fs) { + std::get<0>(fs).get(); + std::get<1>(fs).get(); + std::get<2>(fs).get(); + std::get<3>(fs).get(); + }); +} + +SEASTAR_TEST_CASE(test_json_path) { + shared_ptr<bool> res1 = make_shared<bool>(false); + shared_ptr<bool> res2 = make_shared<bool>(false); + shared_ptr<bool> res3 = make_shared<bool>(false); + shared_ptr<routes> route = make_shared<routes>(); + path_description path1("/my/path",GET,"path1", + {{"param1", path_description::url_component_type::PARAM} + ,{"/text", path_description::url_component_type::FIXED_STRING}},{}); + path_description path2("/my/path",GET,"path2", + {{"param1", path_description::url_component_type::PARAM} + ,{"param2", path_description::url_component_type::PARAM}},{}); + path_description path3("/my/path",GET,"path3", + {{"param1", path_description::url_component_type::PARAM} + ,{"param2", path_description::url_component_type::PARAM_UNTIL_END_OF_PATH}},{}); + + path1.set(*route, [res1] (const_req req) { + (*res1) = true; + BOOST_REQUIRE_EQUAL(req.param["param1"], "value1"); + return ""; + }); + + path2.set(*route, [res2] (const_req req) { + (*res2) = true; + BOOST_REQUIRE_EQUAL(req.param["param1"], "value2"); + BOOST_REQUIRE_EQUAL(req.param["param2"], "text1"); + return ""; + }); + + path3.set(*route, [res3] (const_req req) { + (*res3) = true; + BOOST_REQUIRE_EQUAL(req.param["param1"], "value3"); + BOOST_REQUIRE_EQUAL(req.param["param2"], "text2/text3"); + return ""; + }); + + auto f1 = route->handle("/my/path/value1/text", std::make_unique<request>(), std::make_unique<reply>()).then([res1, route] (auto f) { + BOOST_REQUIRE_EQUAL(*res1, true); + }); + + auto f2 = route->handle("/my/path/value2/text1", std::make_unique<request>(), std::make_unique<reply>()).then([res2, route] (auto f) { + BOOST_REQUIRE_EQUAL(*res2, true); + }); + + auto f3 = route->handle("/my/path/value3/text2/text3", std::make_unique<request>(), std::make_unique<reply>()).then([res3, route] (auto f) { + BOOST_REQUIRE_EQUAL(*res3, true); + }); + + return when_all(std::move(f1), std::move(f2), std::move(f3)) + .then([] (std::tuple<future<>, future<>, future<>> fs) { + std::get<0>(fs).get(); + std::get<1>(fs).get(); + std::get<2>(fs).get(); + }); +} + +/*! + * \brief a helper data sink that stores everything it gets in a stringstream + */ +class memory_data_sink_impl : public data_sink_impl { + std::stringstream& _ss; +public: + memory_data_sink_impl(std::stringstream& ss) : _ss(ss) { + } + virtual future<> put(net::packet data) override { + abort(); + return make_ready_future<>(); + } + virtual future<> put(temporary_buffer<char> buf) override { + _ss.write(buf.get(), buf.size()); + return make_ready_future<>(); + } + virtual future<> flush() override { + return make_ready_future<>(); + } + + virtual future<> close() override { + return make_ready_future<>(); + } +}; + +class memory_data_sink : public data_sink { +public: + memory_data_sink(std::stringstream& ss) + : data_sink(std::make_unique<memory_data_sink_impl>(ss)) {} +}; + +future<> test_transformer_stream(std::stringstream& ss, content_replace& cr, std::vector<sstring>&& buffer_parts) { + std::unique_ptr<seastar::httpd::request> req = std::make_unique<seastar::httpd::request>(); + ss.str(""); + req->_headers["Host"] = "localhost"; + return do_with(output_stream<char>(cr.transform(std::move(req), "json", output_stream<char>(memory_data_sink(ss), 32000, true))), + std::vector<sstring>(std::move(buffer_parts)), [] (output_stream<char>& os, std::vector<sstring>& parts) { + return do_for_each(parts, [&os](auto& p) { + return os.write(p); + }).then([&os] { + return os.close(); + }); + }); +} + +SEASTAR_TEST_CASE(test_transformer) { + return do_with(std::stringstream(), content_replace("json"), [] (std::stringstream& ss, content_replace& cr) { + return do_with(output_stream<char>(cr.transform(std::make_unique<seastar::httpd::request>(), "html", output_stream<char>(memory_data_sink(ss), 32000, true))), + [] (output_stream<char>& os) { + return os.write(sstring("hello-{{Protocol}}-xyz-{{Host}}")).then([&os] { + return os.close(); + }); + }).then([&ss, &cr] () { + BOOST_REQUIRE_EQUAL(ss.str(), "hello-{{Protocol}}-xyz-{{Host}}"); + return test_transformer_stream(ss, cr, {"hell", "o-{", "{Pro", "tocol}}-xyz-{{Ho", "st}}{{Pr"}).then([&ss, &cr] { + BOOST_REQUIRE_EQUAL(ss.str(), "hello-http-xyz-localhost{{Pr"); + return test_transformer_stream(ss, cr, {"hell", "o-{{", "Pro", "tocol}}{{Protocol}}-{{Protoxyz-{{Ho", "st}}{{Pr"}).then([&ss, &cr] { + BOOST_REQUIRE_EQUAL(ss.str(), "hello-httphttp-{{Protoxyz-localhost{{Pr"); + return test_transformer_stream(ss, cr, {"hell", "o-{{Pro", "t{{Protocol}}ocol}}", "{{Host}}"}).then([&ss] { + BOOST_REQUIRE_EQUAL(ss.str(), "hello-{{Prothttpocol}}localhost"); + }); + }); + }); + }); + }); +} + +struct http_consumer { + std::map<sstring, std::string> _headers; + std::string _body; + uint32_t _remain = 0; + std::string _current; + char last = '\0'; + uint32_t _size = 0; + bool _concat = true; + + enum class status_type { + READING_HEADERS, + CHUNK_SIZE, + CHUNK_BODY, + CHUNK_END, + READING_BODY_BY_SIZE, + DONE + }; + status_type status = status_type::READING_HEADERS; + + bool read(const temporary_buffer<char>& b) { + for (auto c : b) { + if (last =='\r' && c == '\n') { + if (_current == "") { + if (status == status_type::READING_HEADERS || (status == status_type::CHUNK_BODY && _remain == 0)) { + if (status == status_type::READING_HEADERS && _headers.find("Content-Length") != _headers.end()) { + _remain = stoi(_headers["Content-Length"], nullptr, 16); + if (_remain == 0) { + status = status_type::DONE; + break; + } + status = status_type::READING_BODY_BY_SIZE; + } else { + status = status_type::CHUNK_SIZE; + } + } else if (status == status_type::CHUNK_END) { + status = status_type::DONE; + break; + } + } else { + switch (status) { + case status_type::READING_HEADERS: add_header(_current); + break; + case status_type::CHUNK_SIZE: set_chunk(_current); + break; + default: + break; + } + _current = ""; + } + last = '\0'; + } else { + if (last != '\0') { + if (status == status_type::CHUNK_BODY || status == status_type::READING_BODY_BY_SIZE) { + if (_concat) { + _body = _body + last; + } + _size++; + _remain--; + if (_remain <= 1 && status == status_type::READING_BODY_BY_SIZE) { + if (_concat) { + _body = _body + c; + } + _size++; + status = status_type::DONE; + break; + } + } else { + _current = _current + last; + } + + } + last = c; + } + } + return status == status_type::DONE; + } + + void set_chunk(const std::string& s) { + _remain = stoi(s, nullptr, 16); + if (_remain == 0) { + status = status_type::CHUNK_END; + } else { + status = status_type::CHUNK_BODY; + } + } + + void add_header(const std::string& s) { + std::vector<std::string> strs; + boost::split(strs, s, boost::is_any_of(":")); + if (strs.size() > 1) { + _headers[strs[0]] = strs[1]; + } + } +}; + +class test_client_server { +public: + static future<> write_request(output_stream<char>& output) { + return output.write(sstring("GET /test HTTP/1.1\r\nHost: myhost.org\r\n\r\n")).then([&output]{ + return output.flush(); + }); + } + + static future<> run_test(std::function<future<>(output_stream<char> &&)>&& write_func, std::function<bool(size_t, http_consumer&)> reader) { + return do_with(loopback_connection_factory(), foreign_ptr<shared_ptr<http_server>>(make_shared<http_server>("test")), + [reader, &write_func] (loopback_connection_factory& lcf, auto& server) { + return do_with(loopback_socket_impl(lcf), [&server, &lcf, reader, &write_func](loopback_socket_impl& lsi) { + httpd::http_server_tester::listeners(*server).emplace_back(lcf.get_server_socket()); + + auto client = seastar::async([&lsi, reader] { + connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); + input_stream<char> input(c_socket.input()); + output_stream<char> output(c_socket.output()); + bool more = true; + size_t count = 0; + while (more) { + http_consumer htp; + htp._concat = false; + + write_request(output).get(); + repeat([&input, &htp] { + return input.read().then([&htp](const temporary_buffer<char>& b) mutable { + return (b.size() == 0 || htp.read(b)) ? make_ready_future<stop_iteration>(stop_iteration::yes) : + make_ready_future<stop_iteration>(stop_iteration::no); + }); + }).get(); + std::cout << htp._body << std::endl; + more = reader(count, htp); + count++; + } + if (input.eof()) { + input.close().get(); + } + }); + + auto server_setup = seastar::async([&server, &write_func] { + class test_handler : public handler_base { + size_t count = 0; + http_server& _server; + std::function<future<>(output_stream<char> &&)> _write_func; + promise<> _all_message_sent; + public: + test_handler(http_server& server, std::function<future<>(output_stream<char> &&)>&& write_func) : _server(server), _write_func(write_func) { + } + future<std::unique_ptr<reply>> handle(const sstring& path, + std::unique_ptr<request> req, std::unique_ptr<reply> rep) override { + rep->write_body("json", std::move(_write_func)); + count++; + _all_message_sent.set_value(); + return make_ready_future<std::unique_ptr<reply>>(std::move(rep)); + } + future<> wait_for_message() { + return _all_message_sent.get_future(); + } + }; + auto handler = new test_handler(*server, std::move(write_func)); + server->_routes.put(GET, "/test", handler); + when_all(server->do_accepts(0), handler->wait_for_message()).get(); + }); + return when_all(std::move(client), std::move(server_setup)); + }).discard_result().then_wrapped([&server] (auto f) { + f.ignore_ready_future(); + return server->stop(); + }); + }); + } + static future<> run(std::vector<std::tuple<bool, size_t>> tests) { + return do_with(loopback_connection_factory(), foreign_ptr<shared_ptr<http_server>>(make_shared<http_server>("test")), + [tests] (loopback_connection_factory& lcf, auto& server) { + return do_with(loopback_socket_impl(lcf), [&server, &lcf, tests](loopback_socket_impl& lsi) { + httpd::http_server_tester::listeners(*server).emplace_back(lcf.get_server_socket()); + + auto client = seastar::async([&lsi, tests] { + connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); + input_stream<char> input(c_socket.input()); + output_stream<char> output(c_socket.output()); + bool more = true; + size_t count = 0; + while (more) { + http_consumer htp; + write_request(output).get(); + repeat([&input, &htp] { + return input.read().then([&htp](const temporary_buffer<char>& b) mutable { + return (b.size() == 0 || htp.read(b)) ? make_ready_future<stop_iteration>(stop_iteration::yes) : + make_ready_future<stop_iteration>(stop_iteration::no); + }); + }).get(); + if (std::get<bool>(tests[count])) { + BOOST_REQUIRE_EQUAL(htp._body.length(), std::get<size_t>(tests[count])); + } else { + BOOST_REQUIRE_EQUAL(input.eof(), true); + more = false; + } + count++; + if (count == tests.size()) { + more = false; + } + } + if (input.eof()) { + input.close().get(); + } + }); + + auto server_setup = seastar::async([&server, tests] { + class test_handler : public handler_base { + size_t count = 0; + http_server& _server; + std::vector<std::tuple<bool, size_t>> _tests; + promise<> _all_message_sent; + public: + test_handler(http_server& server, const std::vector<std::tuple<bool, size_t>>& tests) : _server(server), _tests(tests) { + } + future<std::unique_ptr<reply>> handle(const sstring& path, + std::unique_ptr<request> req, std::unique_ptr<reply> rep) override { + rep->write_body("txt", make_writer(std::get<size_t>(_tests[count]), std::get<bool>(_tests[count]))); + count++; + if (count == _tests.size()) { + _all_message_sent.set_value(); + } + return make_ready_future<std::unique_ptr<reply>>(std::move(rep)); + } + future<> wait_for_message() { + return _all_message_sent.get_future(); + } + }; + auto handler = new test_handler(*server, tests); + server->_routes.put(GET, "/test", handler); + when_all(server->do_accepts(0), handler->wait_for_message()).get(); + }); + return when_all(std::move(client), std::move(server_setup)); + }).discard_result().then_wrapped([&server] (auto f) { + f.ignore_ready_future(); + return server->stop(); + }); + }); + } + + static noncopyable_function<future<>(output_stream<char>&& o_stream)> make_writer(size_t len, bool success) { + return [len, success] (output_stream<char>&& o_stream) mutable { + return do_with(output_stream<char>(std::move(o_stream)), uint32_t(len/10), [success](output_stream<char>& str, uint32_t& remain) { + if (remain == 0) { + if (success) { + return str.close(); + } else { + throw std::runtime_error("Throwing exception before writing"); + } + } + return repeat([&str, &remain] () mutable { + return str.write("1234567890").then([&remain]() mutable { + remain--; + return (remain == 0)? make_ready_future<stop_iteration>(stop_iteration::yes) : make_ready_future<stop_iteration>(stop_iteration::no); + }); + }).then([&str, success] { + if (!success) { + return str.flush(); + } + return make_ready_future<>(); + }).then([&str, success] { + if (success) { + return str.close(); + } else { + throw std::runtime_error("Throwing exception after writing"); + } + }); + }); + }; + } +}; + +SEASTAR_TEST_CASE(test_message_with_error_non_empty_body) { + std::vector<std::tuple<bool, size_t>> tests = { + std::make_tuple(true, 100), + std::make_tuple(false, 10000)}; + return test_client_server::run(tests); +} + +SEASTAR_TEST_CASE(test_simple_chunked) { + std::vector<std::tuple<bool, size_t>> tests = { + std::make_tuple(true, 100000), + std::make_tuple(true, 100)}; + return test_client_server::run(tests); +} + +SEASTAR_TEST_CASE(test_http_client_server_full) { + std::vector<std::tuple<bool, size_t>> tests = { + std::make_tuple(true, 100), + std::make_tuple(true, 10000), + std::make_tuple(true, 100), + std::make_tuple(true, 0), + std::make_tuple(true, 5000), + std::make_tuple(true, 10000), + std::make_tuple(true, 9000), + std::make_tuple(true, 10000)}; + return test_client_server::run(tests); +} + +/* + * return string in the given size + * The string size takes the quotes into consideration. + */ +std::string get_value(int size) { + std::stringstream res; + for (auto i = 0; i < size - 2; i++) { + res << "a"; + } + return res.str(); +} + +/* + * A helper object that map to a big json string + * in the format of: + * {"valu": "aaa....aa", "valu": "aaa....aa", "valu": "aaa....aa"...} + * + * The object can have an arbitrary size in multiplication of 10000 bytes + * */ +struct extra_big_object : public json::json_base { + json::json_element<sstring>* value; + extra_big_object(size_t size) { + value = new json::json_element<sstring>; + // size = brackets + (name + ": " + get_value) * n + ", " * (n-1) + // size = 2 + (name + 6 + get_value) * n - 2 + value->_name = "valu"; + *value = get_value(9990); + for (size_t i = 0; i < size/10000; i++) { + _elements.emplace_back(value); + } + } + + virtual ~extra_big_object() { + delete value; + } + + extra_big_object(const extra_big_object& o) { + value = new json::json_element<sstring>; + value->_name = o.value->_name; + *value = (*o.value)(); + for (size_t i = 0; i < o._elements.size(); i++) { + _elements.emplace_back(value); + } + } +}; + +SEASTAR_TEST_CASE(json_stream) { + std::vector<extra_big_object> vec; + size_t num_objects = 1000; + size_t total_size = num_objects * 1000001 + 1; + for (size_t i = 0; i < num_objects; i++) { + vec.emplace_back(1000000); + } + return test_client_server::run_test(json::stream_object(vec), [total_size](size_t s, http_consumer& h) { + BOOST_REQUIRE_EQUAL(h._size, total_size); + return false; + }); +} + +class json_test_handler : public handler_base { + std::function<future<>(output_stream<char> &&)> _write_func; +public: + json_test_handler(std::function<future<>(output_stream<char> &&)>&& write_func) : _write_func(write_func) { + } + future<std::unique_ptr<reply>> handle(const sstring& path, + std::unique_ptr<request> req, std::unique_ptr<reply> rep) override { + rep->write_body("json", _write_func); + return make_ready_future<std::unique_ptr<reply>>(std::move(rep)); + } +}; + +SEASTAR_TEST_CASE(content_length_limit) { + 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] { + connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); + input_stream<char> input(c_socket.input()); + output_stream<char> output(c_socket.output()); + + output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\n\r\n")).get(); + output.flush().get(); + auto resp = input.read().get0(); + 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: 11\r\n\r\nxxxxxxxxxxx")).get(); + output.flush().get(); + resp = input.read().get0(); + 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\n\r\nxxxxxxxxxxxxxxxx")).get(); + output.flush().get(); + resp = input.read().get0(); + BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("200 OK"), 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(); + }); + + auto handler = new json_test_handler(json::stream_object("hello")); + server._routes.put(GET, "/test", handler); + server.do_accepts(0).get(); + + client.get(); + server.stop().get(); + }); +} + +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] { + connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); + input_stream<char> input(c_socket.input()); + output_stream<char> 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"), 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(); + }); + + auto handler = new json_test_handler(json::stream_object("hello")); + server._routes.put(GET, "/test", handler); + server.do_accepts(0).get(); + + client.get(); + server.stop().get(); + }); +} + + +SEASTAR_TEST_CASE(test_unparsable_request) { + // Test if a message that cannot be parsed as a http request is being replied with a 400 Bad Request response + return seastar::async([] { + loopback_connection_factory lcf; + http_server server("test"); + loopback_socket_impl lsi(lcf); + httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); + future<> client = seastar::async([&lsi] { + connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); + input_stream<char> input(c_socket.input()); + output_stream<char> output(c_socket.output()); + + output.write(sstring("GET /test HTTP/1.1\r\nhello\r\nContent-Length: 17\r\nExpect: 100-continue\r\n\r\n")).get(); + output.flush().get(); + auto resp = input.read().get0(); + BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("400 Bad Request"), std::string::npos); + BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("Can't parse the request"), std::string::npos); + + input.close().get(); + output.close().get(); + }); + + auto handler = new json_test_handler(json::stream_object("hello")); + server._routes.put(GET, "/test", handler); + server.do_accepts(0).get(); + + client.get(); + server.stop().get(); + }); +} + +SEASTAR_TEST_CASE(case_insensitive_header) { + std::unique_ptr<seastar::httpd::request> req = std::make_unique<seastar::httpd::request>(); + req->_headers["conTEnt-LengtH"] = "17"; + BOOST_REQUIRE_EQUAL(req->get_header("content-length"), "17"); + BOOST_REQUIRE_EQUAL(req->get_header("Content-Length"), "17"); + BOOST_REQUIRE_EQUAL(req->get_header("cOnTeNT-lEnGTh"), "17"); + return make_ready_future<>(); +} + +SEASTAR_THREAD_TEST_CASE(multiple_connections) { + loopback_connection_factory lcf; + http_server server("test"); + httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); + socket_address addr{ipv4_addr()}; + + std::vector<connected_socket> socks; + // Make sure one shard has two connections pending. + for (unsigned i = 0; i <= smp::count; ++i) { + socks.push_back(loopback_socket_impl(lcf).connect(addr, addr).get0()); + } + + server.do_accepts(0).get(); + server.stop().get(); + lcf.destroy_all_shards().get(); +} |