]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | /* |
2 | * Copyright 2015 Cloudius Systems | |
3 | */ | |
4 | ||
5 | #include <seastar/http/httpd.hh> | |
6 | #include <seastar/http/handlers.hh> | |
7 | #include <seastar/http/matcher.hh> | |
8 | #include <seastar/http/matchrules.hh> | |
9 | #include <seastar/json/formatter.hh> | |
10 | #include <seastar/http/routes.hh> | |
11 | #include <seastar/http/exception.hh> | |
12 | #include <seastar/http/transformers.hh> | |
f67539c2 TL |
13 | #include <seastar/core/do_with.hh> |
14 | #include <seastar/core/loop.hh> | |
15 | #include <seastar/core/when_all.hh> | |
11fdf7f2 | 16 | #include <seastar/testing/test_case.hh> |
f67539c2 | 17 | #include <seastar/testing/thread_test_case.hh> |
11fdf7f2 TL |
18 | #include "loopback_socket.hh" |
19 | #include <boost/algorithm/string.hpp> | |
20 | #include <seastar/core/thread.hh> | |
21 | #include <seastar/util/noncopyable_function.hh> | |
22 | #include <seastar/http/json_path.hh> | |
20effc67 | 23 | #include <seastar/http/response_parser.hh> |
11fdf7f2 | 24 | #include <sstream> |
20effc67 | 25 | #include <seastar/core/shared_future.hh> |
1e59de90 TL |
26 | #include <seastar/http/client.hh> |
27 | #include <seastar/http/url.hh> | |
20effc67 | 28 | #include <seastar/util/later.hh> |
1e59de90 | 29 | #include <seastar/util/short_streams.hh> |
11fdf7f2 TL |
30 | |
31 | using namespace seastar; | |
32 | using namespace httpd; | |
33 | ||
34 | class handl : public httpd::handler_base { | |
35 | public: | |
1e59de90 TL |
36 | virtual future<std::unique_ptr<http::reply> > handle(const sstring& path, |
37 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) { | |
11fdf7f2 | 38 | rep->done("html"); |
1e59de90 | 39 | return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep)); |
11fdf7f2 TL |
40 | } |
41 | }; | |
42 | ||
43 | SEASTAR_TEST_CASE(test_reply) | |
44 | { | |
1e59de90 | 45 | http::reply r; |
11fdf7f2 TL |
46 | r.set_content_type("txt"); |
47 | BOOST_REQUIRE_EQUAL(r._headers["Content-Type"], sstring("text/plain")); | |
48 | return make_ready_future<>(); | |
49 | } | |
50 | ||
51 | SEASTAR_TEST_CASE(test_str_matcher) | |
52 | { | |
53 | ||
54 | str_matcher m("/hello"); | |
55 | parameters param; | |
56 | BOOST_REQUIRE_EQUAL(m.match("/abc/hello", 4, param), 10u); | |
57 | return make_ready_future<>(); | |
58 | } | |
59 | ||
60 | SEASTAR_TEST_CASE(test_param_matcher) | |
61 | { | |
62 | ||
63 | param_matcher m("param"); | |
64 | parameters param; | |
65 | BOOST_REQUIRE_EQUAL(m.match("/abc/hello", 4, param), 10u); | |
66 | BOOST_REQUIRE_EQUAL(param.path("param"), "/hello"); | |
67 | BOOST_REQUIRE_EQUAL(param["param"], "hello"); | |
68 | return make_ready_future<>(); | |
69 | } | |
70 | ||
71 | SEASTAR_TEST_CASE(test_match_rule) | |
72 | { | |
73 | ||
74 | parameters param; | |
75 | handl* h = new handl(); | |
76 | match_rule mr(h); | |
77 | mr.add_str("/hello").add_param("param"); | |
78 | httpd::handler_base* res = mr.get("/hello/val1", param); | |
79 | BOOST_REQUIRE_EQUAL(res, h); | |
80 | BOOST_REQUIRE_EQUAL(param["param"], "val1"); | |
81 | res = mr.get("/hell/val1", param); | |
82 | httpd::handler_base* nl = nullptr; | |
83 | BOOST_REQUIRE_EQUAL(res, nl); | |
84 | return make_ready_future<>(); | |
85 | } | |
86 | ||
f67539c2 TL |
87 | SEASTAR_TEST_CASE(test_match_rule_order) |
88 | { | |
89 | parameters param; | |
90 | routes route; | |
91 | ||
92 | handl* h1 = new handl(); | |
93 | route.add(operation_type::GET, url("/hello"), h1); | |
94 | ||
95 | handl* h2 = new handl(); | |
96 | route.add(operation_type::GET, url("/hello"), h2); | |
97 | ||
98 | auto rh = route.get_handler(GET, "/hello", param); | |
99 | BOOST_REQUIRE_EQUAL(rh, h1); | |
100 | ||
101 | return make_ready_future<>(); | |
102 | } | |
103 | ||
104 | SEASTAR_TEST_CASE(test_put_drop_rule) | |
105 | { | |
106 | routes rts; | |
107 | auto h = std::make_unique<handl>(); | |
108 | parameters params; | |
109 | ||
110 | { | |
111 | auto reg = handler_registration(rts, *h, "/hello", operation_type::GET); | |
112 | auto res = rts.get_handler(operation_type::GET, "/hello", params); | |
113 | BOOST_REQUIRE_EQUAL(res, h.get()); | |
114 | } | |
115 | ||
116 | auto res = rts.get_handler(operation_type::GET, "/hello", params); | |
117 | httpd::handler_base* nl = nullptr; | |
118 | BOOST_REQUIRE_EQUAL(res, nl); | |
119 | return make_ready_future<>(); | |
120 | } | |
121 | ||
122 | // Putting a duplicated exact rule would result | |
123 | // in a memory leak due to the fact that rules are implemented | |
124 | // as raw pointers. In order to prevent such leaks, | |
125 | // an exception is thrown if somebody tries to put | |
126 | // a duplicated rule without removing the old one first. | |
127 | // The interface demands that the callee allocates the handle, | |
128 | // so it should also expect the callee to free it before | |
129 | // overwriting. | |
130 | SEASTAR_TEST_CASE(test_duplicated_exact_rule) | |
131 | { | |
132 | parameters param; | |
133 | routes route; | |
134 | ||
135 | handl* h1 = new handl; | |
136 | route.put(operation_type::GET, "/hello", h1); | |
137 | ||
138 | handl* h2 = new handl; | |
139 | BOOST_REQUIRE_THROW(route.put(operation_type::GET, "/hello", h2), std::runtime_error); | |
140 | ||
141 | delete route.drop(operation_type::GET, "/hello"); | |
142 | route.put(operation_type::GET, "/hello", h2); | |
143 | ||
144 | return make_ready_future<>(); | |
145 | } | |
146 | ||
147 | SEASTAR_TEST_CASE(test_add_del_cookie) | |
148 | { | |
149 | routes rts; | |
150 | handl* h = new handl(); | |
151 | match_rule mr(h); | |
152 | mr.add_str("/hello"); | |
153 | parameters params; | |
154 | ||
155 | { | |
156 | auto reg = rule_registration(rts, mr, operation_type::GET); | |
157 | auto res = rts.get_handler(operation_type::GET, "/hello", params); | |
158 | BOOST_REQUIRE_EQUAL(res, h); | |
159 | } | |
160 | ||
161 | auto res = rts.get_handler(operation_type::GET, "/hello", params); | |
162 | httpd::handler_base* nl = nullptr; | |
163 | BOOST_REQUIRE_EQUAL(res, nl); | |
164 | return make_ready_future<>(); | |
165 | } | |
166 | ||
11fdf7f2 TL |
167 | SEASTAR_TEST_CASE(test_formatter) |
168 | { | |
169 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(true), "true"); | |
170 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(false), "false"); | |
171 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(1), "1"); | |
172 | const char* txt = "efg"; | |
173 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(txt), "\"efg\""); | |
174 | sstring str = "abc"; | |
175 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(str), "\"abc\""); | |
176 | float f = 1; | |
177 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(f), "1"); | |
178 | f = 1.0/0.0; | |
179 | BOOST_CHECK_THROW(json::formatter::to_json(f), std::out_of_range); | |
180 | f = -1.0/0.0; | |
181 | BOOST_CHECK_THROW(json::formatter::to_json(f), std::out_of_range); | |
182 | f = 0.0/0.0; | |
183 | BOOST_CHECK_THROW(json::formatter::to_json(f), std::invalid_argument); | |
184 | double d = -1; | |
185 | BOOST_REQUIRE_EQUAL(json::formatter::to_json(d), "-1"); | |
186 | d = 1.0/0.0; | |
187 | BOOST_CHECK_THROW(json::formatter::to_json(d), std::out_of_range); | |
188 | d = -1.0/0.0; | |
189 | BOOST_CHECK_THROW(json::formatter::to_json(d), std::out_of_range); | |
190 | d = 0.0/0.0; | |
191 | BOOST_CHECK_THROW(json::formatter::to_json(d), std::invalid_argument); | |
192 | return make_ready_future<>(); | |
193 | } | |
194 | ||
195 | SEASTAR_TEST_CASE(test_decode_url) { | |
1e59de90 | 196 | http::request req; |
11fdf7f2 | 197 | req._url = "/a?q=%23%24%23"; |
1e59de90 | 198 | sstring url = req.parse_query_param(); |
11fdf7f2 TL |
199 | BOOST_REQUIRE_EQUAL(url, "/a"); |
200 | BOOST_REQUIRE_EQUAL(req.get_query_param("q"), "#$#"); | |
201 | req._url = "/a?a=%23%24%23&b=%22%26%22"; | |
1e59de90 | 202 | req.parse_query_param(); |
11fdf7f2 TL |
203 | BOOST_REQUIRE_EQUAL(req.get_query_param("a"), "#$#"); |
204 | BOOST_REQUIRE_EQUAL(req.get_query_param("b"), "\"&\""); | |
205 | return make_ready_future<>(); | |
206 | } | |
207 | ||
208 | SEASTAR_TEST_CASE(test_routes) { | |
209 | handl* h1 = new handl(); | |
210 | handl* h2 = new handl(); | |
211 | routes route; | |
212 | route.add(operation_type::GET, url("/api").remainder("path"), h1); | |
213 | route.add(operation_type::GET, url("/"), h2); | |
1e59de90 TL |
214 | std::unique_ptr<http::request> req = std::make_unique<http::request>(); |
215 | std::unique_ptr<http::reply> rep = std::make_unique<http::reply>(); | |
11fdf7f2 TL |
216 | |
217 | auto f1 = | |
218 | route.handle("/api", std::move(req), std::move(rep)).then( | |
1e59de90 TL |
219 | [] (std::unique_ptr<http::reply> rep) { |
220 | BOOST_REQUIRE_EQUAL((int )rep->_status, (int )http::reply::status_type::ok); | |
11fdf7f2 | 221 | }); |
1e59de90 TL |
222 | req.reset(new http::request); |
223 | rep.reset(new http::reply); | |
11fdf7f2 TL |
224 | |
225 | auto f2 = | |
226 | route.handle("/", std::move(req), std::move(rep)).then( | |
1e59de90 TL |
227 | [] (std::unique_ptr<http::reply> rep) { |
228 | BOOST_REQUIRE_EQUAL((int )rep->_status, (int )http::reply::status_type::ok); | |
11fdf7f2 | 229 | }); |
1e59de90 TL |
230 | req.reset(new http::request); |
231 | rep.reset(new http::reply); | |
11fdf7f2 TL |
232 | auto f3 = |
233 | route.handle("/api/abc", std::move(req), std::move(rep)).then( | |
1e59de90 | 234 | [] (std::unique_ptr<http::reply> rep) { |
11fdf7f2 | 235 | }); |
1e59de90 TL |
236 | req.reset(new http::request); |
237 | rep.reset(new http::reply); | |
11fdf7f2 TL |
238 | auto f4 = |
239 | route.handle("/ap", std::move(req), std::move(rep)).then( | |
1e59de90 | 240 | [] (std::unique_ptr<http::reply> rep) { |
11fdf7f2 | 241 | BOOST_REQUIRE_EQUAL((int )rep->_status, |
1e59de90 | 242 | (int )http::reply::status_type::not_found); |
11fdf7f2 TL |
243 | }); |
244 | return when_all(std::move(f1), std::move(f2), std::move(f3), std::move(f4)) | |
245 | .then([] (std::tuple<future<>, future<>, future<>, future<>> fs) { | |
246 | std::get<0>(fs).get(); | |
247 | std::get<1>(fs).get(); | |
248 | std::get<2>(fs).get(); | |
249 | std::get<3>(fs).get(); | |
250 | }); | |
251 | } | |
252 | ||
253 | SEASTAR_TEST_CASE(test_json_path) { | |
254 | shared_ptr<bool> res1 = make_shared<bool>(false); | |
255 | shared_ptr<bool> res2 = make_shared<bool>(false); | |
256 | shared_ptr<bool> res3 = make_shared<bool>(false); | |
257 | shared_ptr<routes> route = make_shared<routes>(); | |
258 | path_description path1("/my/path",GET,"path1", | |
259 | {{"param1", path_description::url_component_type::PARAM} | |
260 | ,{"/text", path_description::url_component_type::FIXED_STRING}},{}); | |
261 | path_description path2("/my/path",GET,"path2", | |
262 | {{"param1", path_description::url_component_type::PARAM} | |
263 | ,{"param2", path_description::url_component_type::PARAM}},{}); | |
264 | path_description path3("/my/path",GET,"path3", | |
265 | {{"param1", path_description::url_component_type::PARAM} | |
266 | ,{"param2", path_description::url_component_type::PARAM_UNTIL_END_OF_PATH}},{}); | |
267 | ||
268 | path1.set(*route, [res1] (const_req req) { | |
269 | (*res1) = true; | |
270 | BOOST_REQUIRE_EQUAL(req.param["param1"], "value1"); | |
271 | return ""; | |
272 | }); | |
273 | ||
274 | path2.set(*route, [res2] (const_req req) { | |
275 | (*res2) = true; | |
276 | BOOST_REQUIRE_EQUAL(req.param["param1"], "value2"); | |
277 | BOOST_REQUIRE_EQUAL(req.param["param2"], "text1"); | |
278 | return ""; | |
279 | }); | |
280 | ||
281 | path3.set(*route, [res3] (const_req req) { | |
282 | (*res3) = true; | |
283 | BOOST_REQUIRE_EQUAL(req.param["param1"], "value3"); | |
284 | BOOST_REQUIRE_EQUAL(req.param["param2"], "text2/text3"); | |
285 | return ""; | |
286 | }); | |
287 | ||
1e59de90 | 288 | auto f1 = route->handle("/my/path/value1/text", std::make_unique<http::request>(), std::make_unique<http::reply>()).then([res1, route] (auto f) { |
11fdf7f2 TL |
289 | BOOST_REQUIRE_EQUAL(*res1, true); |
290 | }); | |
291 | ||
1e59de90 | 292 | auto f2 = route->handle("/my/path/value2/text1", std::make_unique<http::request>(), std::make_unique<http::reply>()).then([res2, route] (auto f) { |
11fdf7f2 TL |
293 | BOOST_REQUIRE_EQUAL(*res2, true); |
294 | }); | |
295 | ||
1e59de90 | 296 | auto f3 = route->handle("/my/path/value3/text2/text3", std::make_unique<http::request>(), std::make_unique<http::reply>()).then([res3, route] (auto f) { |
11fdf7f2 TL |
297 | BOOST_REQUIRE_EQUAL(*res3, true); |
298 | }); | |
299 | ||
300 | return when_all(std::move(f1), std::move(f2), std::move(f3)) | |
301 | .then([] (std::tuple<future<>, future<>, future<>> fs) { | |
302 | std::get<0>(fs).get(); | |
303 | std::get<1>(fs).get(); | |
304 | std::get<2>(fs).get(); | |
305 | }); | |
306 | } | |
307 | ||
308 | /*! | |
309 | * \brief a helper data sink that stores everything it gets in a stringstream | |
310 | */ | |
311 | class memory_data_sink_impl : public data_sink_impl { | |
312 | std::stringstream& _ss; | |
313 | public: | |
314 | memory_data_sink_impl(std::stringstream& ss) : _ss(ss) { | |
315 | } | |
316 | virtual future<> put(net::packet data) override { | |
317 | abort(); | |
318 | return make_ready_future<>(); | |
319 | } | |
320 | virtual future<> put(temporary_buffer<char> buf) override { | |
321 | _ss.write(buf.get(), buf.size()); | |
322 | return make_ready_future<>(); | |
323 | } | |
324 | virtual future<> flush() override { | |
325 | return make_ready_future<>(); | |
326 | } | |
327 | ||
328 | virtual future<> close() override { | |
329 | return make_ready_future<>(); | |
330 | } | |
331 | }; | |
332 | ||
333 | class memory_data_sink : public data_sink { | |
334 | public: | |
335 | memory_data_sink(std::stringstream& ss) | |
336 | : data_sink(std::make_unique<memory_data_sink_impl>(ss)) {} | |
337 | }; | |
338 | ||
339 | future<> test_transformer_stream(std::stringstream& ss, content_replace& cr, std::vector<sstring>&& buffer_parts) { | |
1e59de90 | 340 | std::unique_ptr<seastar::http::request> req = std::make_unique<seastar::http::request>(); |
11fdf7f2 TL |
341 | ss.str(""); |
342 | req->_headers["Host"] = "localhost"; | |
20effc67 TL |
343 | output_stream_options opts; |
344 | opts.trim_to_size = true; | |
345 | return do_with(output_stream<char>(cr.transform(std::move(req), "json", output_stream<char>(memory_data_sink(ss), 32000, opts))), | |
9f95a23c | 346 | std::vector<sstring>(std::move(buffer_parts)), [] (output_stream<char>& os, std::vector<sstring>& parts) { |
11fdf7f2 TL |
347 | return do_for_each(parts, [&os](auto& p) { |
348 | return os.write(p); | |
9f95a23c | 349 | }).then([&os] { |
11fdf7f2 TL |
350 | return os.close(); |
351 | }); | |
352 | }); | |
353 | } | |
354 | ||
355 | SEASTAR_TEST_CASE(test_transformer) { | |
356 | return do_with(std::stringstream(), content_replace("json"), [] (std::stringstream& ss, content_replace& cr) { | |
20effc67 TL |
357 | output_stream_options opts; |
358 | opts.trim_to_size = true; | |
1e59de90 | 359 | return do_with(output_stream<char>(cr.transform(std::make_unique<seastar::http::request>(), "html", output_stream<char>(memory_data_sink(ss), 32000, opts))), |
9f95a23c | 360 | [] (output_stream<char>& os) { |
11fdf7f2 TL |
361 | return os.write(sstring("hello-{{Protocol}}-xyz-{{Host}}")).then([&os] { |
362 | return os.close(); | |
363 | }); | |
364 | }).then([&ss, &cr] () { | |
365 | BOOST_REQUIRE_EQUAL(ss.str(), "hello-{{Protocol}}-xyz-{{Host}}"); | |
366 | return test_transformer_stream(ss, cr, {"hell", "o-{", "{Pro", "tocol}}-xyz-{{Ho", "st}}{{Pr"}).then([&ss, &cr] { | |
367 | BOOST_REQUIRE_EQUAL(ss.str(), "hello-http-xyz-localhost{{Pr"); | |
368 | return test_transformer_stream(ss, cr, {"hell", "o-{{", "Pro", "tocol}}{{Protocol}}-{{Protoxyz-{{Ho", "st}}{{Pr"}).then([&ss, &cr] { | |
369 | BOOST_REQUIRE_EQUAL(ss.str(), "hello-httphttp-{{Protoxyz-localhost{{Pr"); | |
9f95a23c | 370 | return test_transformer_stream(ss, cr, {"hell", "o-{{Pro", "t{{Protocol}}ocol}}", "{{Host}}"}).then([&ss] { |
11fdf7f2 TL |
371 | BOOST_REQUIRE_EQUAL(ss.str(), "hello-{{Prothttpocol}}localhost"); |
372 | }); | |
373 | }); | |
374 | }); | |
375 | }); | |
376 | }); | |
377 | } | |
378 | ||
379 | struct http_consumer { | |
380 | std::map<sstring, std::string> _headers; | |
381 | std::string _body; | |
382 | uint32_t _remain = 0; | |
383 | std::string _current; | |
384 | char last = '\0'; | |
385 | uint32_t _size = 0; | |
386 | bool _concat = true; | |
387 | ||
388 | enum class status_type { | |
389 | READING_HEADERS, | |
390 | CHUNK_SIZE, | |
391 | CHUNK_BODY, | |
392 | CHUNK_END, | |
393 | READING_BODY_BY_SIZE, | |
394 | DONE | |
395 | }; | |
396 | status_type status = status_type::READING_HEADERS; | |
397 | ||
398 | bool read(const temporary_buffer<char>& b) { | |
399 | for (auto c : b) { | |
400 | if (last =='\r' && c == '\n') { | |
401 | if (_current == "") { | |
402 | if (status == status_type::READING_HEADERS || (status == status_type::CHUNK_BODY && _remain == 0)) { | |
403 | if (status == status_type::READING_HEADERS && _headers.find("Content-Length") != _headers.end()) { | |
404 | _remain = stoi(_headers["Content-Length"], nullptr, 16); | |
405 | if (_remain == 0) { | |
406 | status = status_type::DONE; | |
407 | break; | |
408 | } | |
409 | status = status_type::READING_BODY_BY_SIZE; | |
410 | } else { | |
411 | status = status_type::CHUNK_SIZE; | |
412 | } | |
413 | } else if (status == status_type::CHUNK_END) { | |
414 | status = status_type::DONE; | |
415 | break; | |
416 | } | |
417 | } else { | |
418 | switch (status) { | |
419 | case status_type::READING_HEADERS: add_header(_current); | |
420 | break; | |
421 | case status_type::CHUNK_SIZE: set_chunk(_current); | |
422 | break; | |
423 | default: | |
424 | break; | |
425 | } | |
426 | _current = ""; | |
427 | } | |
428 | last = '\0'; | |
429 | } else { | |
430 | if (last != '\0') { | |
431 | if (status == status_type::CHUNK_BODY || status == status_type::READING_BODY_BY_SIZE) { | |
432 | if (_concat) { | |
433 | _body = _body + last; | |
434 | } | |
435 | _size++; | |
436 | _remain--; | |
437 | if (_remain <= 1 && status == status_type::READING_BODY_BY_SIZE) { | |
438 | if (_concat) { | |
439 | _body = _body + c; | |
440 | } | |
441 | _size++; | |
442 | status = status_type::DONE; | |
443 | break; | |
444 | } | |
445 | } else { | |
446 | _current = _current + last; | |
447 | } | |
448 | ||
449 | } | |
450 | last = c; | |
451 | } | |
452 | } | |
453 | return status == status_type::DONE; | |
454 | } | |
455 | ||
456 | void set_chunk(const std::string& s) { | |
457 | _remain = stoi(s, nullptr, 16); | |
458 | if (_remain == 0) { | |
459 | status = status_type::CHUNK_END; | |
460 | } else { | |
461 | status = status_type::CHUNK_BODY; | |
462 | } | |
463 | } | |
464 | ||
465 | void add_header(const std::string& s) { | |
466 | std::vector<std::string> strs; | |
467 | boost::split(strs, s, boost::is_any_of(":")); | |
468 | if (strs.size() > 1) { | |
469 | _headers[strs[0]] = strs[1]; | |
470 | } | |
471 | } | |
472 | }; | |
473 | ||
474 | class test_client_server { | |
475 | public: | |
476 | static future<> write_request(output_stream<char>& output) { | |
477 | return output.write(sstring("GET /test HTTP/1.1\r\nHost: myhost.org\r\n\r\n")).then([&output]{ | |
478 | return output.flush(); | |
479 | }); | |
480 | } | |
481 | ||
482 | static future<> run_test(std::function<future<>(output_stream<char> &&)>&& write_func, std::function<bool(size_t, http_consumer&)> reader) { | |
1e59de90 | 483 | return do_with(loopback_connection_factory(1), foreign_ptr<shared_ptr<http_server>>(make_shared<http_server>("test")), |
11fdf7f2 TL |
484 | [reader, &write_func] (loopback_connection_factory& lcf, auto& server) { |
485 | return do_with(loopback_socket_impl(lcf), [&server, &lcf, reader, &write_func](loopback_socket_impl& lsi) { | |
486 | httpd::http_server_tester::listeners(*server).emplace_back(lcf.get_server_socket()); | |
487 | ||
488 | auto client = seastar::async([&lsi, reader] { | |
f67539c2 | 489 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); |
9f95a23c TL |
490 | input_stream<char> input(c_socket.input()); |
491 | output_stream<char> output(c_socket.output()); | |
11fdf7f2 TL |
492 | bool more = true; |
493 | size_t count = 0; | |
494 | while (more) { | |
495 | http_consumer htp; | |
496 | htp._concat = false; | |
497 | ||
498 | write_request(output).get(); | |
9f95a23c TL |
499 | repeat([&input, &htp] { |
500 | return input.read().then([&htp](const temporary_buffer<char>& b) mutable { | |
11fdf7f2 TL |
501 | return (b.size() == 0 || htp.read(b)) ? make_ready_future<stop_iteration>(stop_iteration::yes) : |
502 | make_ready_future<stop_iteration>(stop_iteration::no); | |
503 | }); | |
504 | }).get(); | |
505 | std::cout << htp._body << std::endl; | |
506 | more = reader(count, htp); | |
507 | count++; | |
508 | } | |
509 | if (input.eof()) { | |
510 | input.close().get(); | |
511 | } | |
512 | }); | |
513 | ||
f67539c2 | 514 | auto server_setup = seastar::async([&server, &write_func] { |
11fdf7f2 TL |
515 | class test_handler : public handler_base { |
516 | size_t count = 0; | |
517 | http_server& _server; | |
518 | std::function<future<>(output_stream<char> &&)> _write_func; | |
519 | promise<> _all_message_sent; | |
520 | public: | |
521 | test_handler(http_server& server, std::function<future<>(output_stream<char> &&)>&& write_func) : _server(server), _write_func(write_func) { | |
522 | } | |
1e59de90 TL |
523 | future<std::unique_ptr<http::reply>> handle(const sstring& path, |
524 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) override { | |
11fdf7f2 TL |
525 | rep->write_body("json", std::move(_write_func)); |
526 | count++; | |
527 | _all_message_sent.set_value(); | |
1e59de90 | 528 | return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep)); |
11fdf7f2 TL |
529 | } |
530 | future<> wait_for_message() { | |
531 | return _all_message_sent.get_future(); | |
532 | } | |
533 | }; | |
534 | auto handler = new test_handler(*server, std::move(write_func)); | |
535 | server->_routes.put(GET, "/test", handler); | |
536 | when_all(server->do_accepts(0), handler->wait_for_message()).get(); | |
537 | }); | |
f67539c2 | 538 | return when_all(std::move(client), std::move(server_setup)); |
11fdf7f2 TL |
539 | }).discard_result().then_wrapped([&server] (auto f) { |
540 | f.ignore_ready_future(); | |
541 | return server->stop(); | |
542 | }); | |
543 | }); | |
544 | } | |
545 | static future<> run(std::vector<std::tuple<bool, size_t>> tests) { | |
1e59de90 | 546 | return do_with(loopback_connection_factory(1), foreign_ptr<shared_ptr<http_server>>(make_shared<http_server>("test")), |
11fdf7f2 TL |
547 | [tests] (loopback_connection_factory& lcf, auto& server) { |
548 | return do_with(loopback_socket_impl(lcf), [&server, &lcf, tests](loopback_socket_impl& lsi) { | |
549 | httpd::http_server_tester::listeners(*server).emplace_back(lcf.get_server_socket()); | |
550 | ||
551 | auto client = seastar::async([&lsi, tests] { | |
f67539c2 | 552 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); |
9f95a23c TL |
553 | input_stream<char> input(c_socket.input()); |
554 | output_stream<char> output(c_socket.output()); | |
11fdf7f2 TL |
555 | bool more = true; |
556 | size_t count = 0; | |
557 | while (more) { | |
558 | http_consumer htp; | |
559 | write_request(output).get(); | |
9f95a23c TL |
560 | repeat([&input, &htp] { |
561 | return input.read().then([&htp](const temporary_buffer<char>& b) mutable { | |
11fdf7f2 TL |
562 | return (b.size() == 0 || htp.read(b)) ? make_ready_future<stop_iteration>(stop_iteration::yes) : |
563 | make_ready_future<stop_iteration>(stop_iteration::no); | |
564 | }); | |
565 | }).get(); | |
566 | if (std::get<bool>(tests[count])) { | |
567 | BOOST_REQUIRE_EQUAL(htp._body.length(), std::get<size_t>(tests[count])); | |
568 | } else { | |
569 | BOOST_REQUIRE_EQUAL(input.eof(), true); | |
570 | more = false; | |
571 | } | |
572 | count++; | |
573 | if (count == tests.size()) { | |
574 | more = false; | |
575 | } | |
576 | } | |
577 | if (input.eof()) { | |
578 | input.close().get(); | |
579 | } | |
580 | }); | |
581 | ||
f67539c2 | 582 | auto server_setup = seastar::async([&server, tests] { |
11fdf7f2 TL |
583 | class test_handler : public handler_base { |
584 | size_t count = 0; | |
585 | http_server& _server; | |
586 | std::vector<std::tuple<bool, size_t>> _tests; | |
587 | promise<> _all_message_sent; | |
588 | public: | |
589 | test_handler(http_server& server, const std::vector<std::tuple<bool, size_t>>& tests) : _server(server), _tests(tests) { | |
590 | } | |
1e59de90 TL |
591 | future<std::unique_ptr<http::reply>> handle(const sstring& path, |
592 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) override { | |
11fdf7f2 TL |
593 | rep->write_body("txt", make_writer(std::get<size_t>(_tests[count]), std::get<bool>(_tests[count]))); |
594 | count++; | |
595 | if (count == _tests.size()) { | |
596 | _all_message_sent.set_value(); | |
597 | } | |
1e59de90 | 598 | return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep)); |
11fdf7f2 TL |
599 | } |
600 | future<> wait_for_message() { | |
601 | return _all_message_sent.get_future(); | |
602 | } | |
603 | }; | |
604 | auto handler = new test_handler(*server, tests); | |
605 | server->_routes.put(GET, "/test", handler); | |
606 | when_all(server->do_accepts(0), handler->wait_for_message()).get(); | |
607 | }); | |
f67539c2 | 608 | return when_all(std::move(client), std::move(server_setup)); |
11fdf7f2 TL |
609 | }).discard_result().then_wrapped([&server] (auto f) { |
610 | f.ignore_ready_future(); | |
611 | return server->stop(); | |
612 | }); | |
613 | }); | |
614 | } | |
615 | ||
616 | static noncopyable_function<future<>(output_stream<char>&& o_stream)> make_writer(size_t len, bool success) { | |
617 | return [len, success] (output_stream<char>&& o_stream) mutable { | |
618 | return do_with(output_stream<char>(std::move(o_stream)), uint32_t(len/10), [success](output_stream<char>& str, uint32_t& remain) { | |
619 | if (remain == 0) { | |
620 | if (success) { | |
621 | return str.close(); | |
622 | } else { | |
623 | throw std::runtime_error("Throwing exception before writing"); | |
624 | } | |
625 | } | |
9f95a23c | 626 | return repeat([&str, &remain] () mutable { |
11fdf7f2 TL |
627 | return str.write("1234567890").then([&remain]() mutable { |
628 | remain--; | |
629 | return (remain == 0)? make_ready_future<stop_iteration>(stop_iteration::yes) : make_ready_future<stop_iteration>(stop_iteration::no); | |
630 | }); | |
631 | }).then([&str, success] { | |
632 | if (!success) { | |
633 | return str.flush(); | |
634 | } | |
635 | return make_ready_future<>(); | |
636 | }).then([&str, success] { | |
637 | if (success) { | |
638 | return str.close(); | |
639 | } else { | |
640 | throw std::runtime_error("Throwing exception after writing"); | |
641 | } | |
642 | }); | |
643 | }); | |
644 | }; | |
645 | } | |
646 | }; | |
647 | ||
648 | SEASTAR_TEST_CASE(test_message_with_error_non_empty_body) { | |
649 | std::vector<std::tuple<bool, size_t>> tests = { | |
650 | std::make_tuple(true, 100), | |
651 | std::make_tuple(false, 10000)}; | |
652 | return test_client_server::run(tests); | |
653 | } | |
654 | ||
655 | SEASTAR_TEST_CASE(test_simple_chunked) { | |
656 | std::vector<std::tuple<bool, size_t>> tests = { | |
657 | std::make_tuple(true, 100000), | |
658 | std::make_tuple(true, 100)}; | |
659 | return test_client_server::run(tests); | |
660 | } | |
661 | ||
662 | SEASTAR_TEST_CASE(test_http_client_server_full) { | |
663 | std::vector<std::tuple<bool, size_t>> tests = { | |
664 | std::make_tuple(true, 100), | |
665 | std::make_tuple(true, 10000), | |
666 | std::make_tuple(true, 100), | |
667 | std::make_tuple(true, 0), | |
668 | std::make_tuple(true, 5000), | |
669 | std::make_tuple(true, 10000), | |
670 | std::make_tuple(true, 9000), | |
671 | std::make_tuple(true, 10000)}; | |
672 | return test_client_server::run(tests); | |
673 | } | |
674 | ||
675 | /* | |
676 | * return string in the given size | |
677 | * The string size takes the quotes into consideration. | |
678 | */ | |
679 | std::string get_value(int size) { | |
680 | std::stringstream res; | |
681 | for (auto i = 0; i < size - 2; i++) { | |
682 | res << "a"; | |
683 | } | |
684 | return res.str(); | |
685 | } | |
686 | ||
687 | /* | |
688 | * A helper object that map to a big json string | |
689 | * in the format of: | |
690 | * {"valu": "aaa....aa", "valu": "aaa....aa", "valu": "aaa....aa"...} | |
691 | * | |
692 | * The object can have an arbitrary size in multiplication of 10000 bytes | |
693 | * */ | |
694 | struct extra_big_object : public json::json_base { | |
695 | json::json_element<sstring>* value; | |
696 | extra_big_object(size_t size) { | |
697 | value = new json::json_element<sstring>; | |
698 | // size = brackets + (name + ": " + get_value) * n + ", " * (n-1) | |
699 | // size = 2 + (name + 6 + get_value) * n - 2 | |
700 | value->_name = "valu"; | |
701 | *value = get_value(9990); | |
702 | for (size_t i = 0; i < size/10000; i++) { | |
703 | _elements.emplace_back(value); | |
704 | } | |
705 | } | |
706 | ||
707 | virtual ~extra_big_object() { | |
708 | delete value; | |
709 | } | |
710 | ||
711 | extra_big_object(const extra_big_object& o) { | |
712 | value = new json::json_element<sstring>; | |
713 | value->_name = o.value->_name; | |
714 | *value = (*o.value)(); | |
715 | for (size_t i = 0; i < o._elements.size(); i++) { | |
716 | _elements.emplace_back(value); | |
717 | } | |
718 | } | |
11fdf7f2 TL |
719 | }; |
720 | ||
721 | SEASTAR_TEST_CASE(json_stream) { | |
722 | std::vector<extra_big_object> vec; | |
723 | size_t num_objects = 1000; | |
724 | size_t total_size = num_objects * 1000001 + 1; | |
725 | for (size_t i = 0; i < num_objects; i++) { | |
9f95a23c | 726 | vec.emplace_back(1000000); |
11fdf7f2 TL |
727 | } |
728 | return test_client_server::run_test(json::stream_object(vec), [total_size](size_t s, http_consumer& h) { | |
729 | BOOST_REQUIRE_EQUAL(h._size, total_size); | |
730 | return false; | |
731 | }); | |
732 | } | |
9f95a23c | 733 | |
f67539c2 TL |
734 | class json_test_handler : public handler_base { |
735 | std::function<future<>(output_stream<char> &&)> _write_func; | |
736 | public: | |
737 | json_test_handler(std::function<future<>(output_stream<char> &&)>&& write_func) : _write_func(write_func) { | |
738 | } | |
1e59de90 TL |
739 | future<std::unique_ptr<http::reply>> handle(const sstring& path, |
740 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) override { | |
f67539c2 | 741 | rep->write_body("json", _write_func); |
1e59de90 | 742 | return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep)); |
f67539c2 TL |
743 | } |
744 | }; | |
745 | ||
9f95a23c TL |
746 | SEASTAR_TEST_CASE(content_length_limit) { |
747 | return seastar::async([] { | |
1e59de90 | 748 | loopback_connection_factory lcf(1); |
9f95a23c TL |
749 | http_server server("test"); |
750 | server.set_content_length_limit(11); | |
751 | loopback_socket_impl lsi(lcf); | |
752 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
753 | ||
754 | future<> client = seastar::async([&lsi] { | |
f67539c2 | 755 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); |
9f95a23c TL |
756 | input_stream<char> input(c_socket.input()); |
757 | output_stream<char> output(c_socket.output()); | |
758 | ||
759 | output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\n\r\n")).get(); | |
760 | output.flush().get(); | |
761 | auto resp = input.read().get0(); | |
762 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("200 OK"), std::string::npos); | |
763 | ||
764 | output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\nContent-Length: 11\r\n\r\nxxxxxxxxxxx")).get(); | |
765 | output.flush().get(); | |
766 | resp = input.read().get0(); | |
767 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("200 OK"), std::string::npos); | |
768 | ||
769 | output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\nContent-Length: 17\r\n\r\nxxxxxxxxxxxxxxxx")).get(); | |
770 | output.flush().get(); | |
771 | resp = input.read().get0(); | |
772 | BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("200 OK"), std::string::npos); | |
773 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("413 Payload Too Large"), std::string::npos); | |
774 | ||
775 | input.close().get(); | |
776 | output.close().get(); | |
777 | }); | |
778 | ||
f67539c2 TL |
779 | auto handler = new json_test_handler(json::stream_object("hello")); |
780 | server._routes.put(GET, "/test", handler); | |
781 | server.do_accepts(0).get(); | |
782 | ||
783 | client.get(); | |
784 | server.stop().get(); | |
785 | }); | |
786 | } | |
787 | ||
788 | SEASTAR_TEST_CASE(test_100_continue) { | |
789 | return seastar::async([] { | |
1e59de90 | 790 | loopback_connection_factory lcf(1); |
f67539c2 TL |
791 | http_server server("test"); |
792 | server.set_content_length_limit(11); | |
793 | loopback_socket_impl lsi(lcf); | |
794 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
795 | future<> client = seastar::async([&lsi] { | |
796 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); | |
797 | input_stream<char> input(c_socket.input()); | |
798 | output_stream<char> output(c_socket.output()); | |
799 | ||
800 | for (auto version : {sstring("1.0"), sstring("1.1")}) { | |
801 | for (auto content : {sstring(""), sstring("xxxxxxxxxxx")}) { | |
802 | for (auto expect : {sstring(""), sstring("Expect: 100-continue\r\n"), sstring("Expect: 100-cOnTInUE\r\n")}) { | |
803 | auto content_len = content.empty() ? sstring("") : (sstring("Content-Length: ") + to_sstring(content.length()) + sstring("\r\n")); | |
804 | sstring req = sstring("GET /test HTTP/") + version + sstring("\r\nHost: test\r\nConnection: Keep-Alive\r\n") + content_len + expect + sstring("\r\n"); | |
805 | output.write(req).get(); | |
806 | output.flush().get(); | |
807 | bool already_ok = false; | |
808 | if (version == "1.1" && expect.length()) { | |
809 | auto resp = input.read().get0(); | |
810 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); | |
811 | already_ok = content.empty() && std::string(resp.get(), resp.size()).find("200 OK") != std::string::npos; | |
812 | } | |
813 | if (!already_ok) { | |
814 | //If the body is empty, the final response might have already been read | |
815 | output.write(content).get(); | |
816 | output.flush().get(); | |
817 | auto resp = input.read().get0(); | |
818 | BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); | |
819 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("200 OK"), std::string::npos); | |
820 | } | |
821 | } | |
9f95a23c | 822 | } |
f67539c2 TL |
823 | } |
824 | output.write(sstring("GET /test HTTP/1.1\r\nHost: test\r\nContent-Length: 17\r\nExpect: 100-continue\r\n\r\n")).get(); | |
825 | output.flush().get(); | |
826 | auto resp = input.read().get0(); | |
827 | BOOST_REQUIRE_EQUAL(std::string(resp.get(), resp.size()).find("100 Continue"), std::string::npos); | |
828 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("413 Payload Too Large"), std::string::npos); | |
829 | ||
830 | input.close().get(); | |
831 | output.close().get(); | |
9f95a23c TL |
832 | }); |
833 | ||
f67539c2 TL |
834 | auto handler = new json_test_handler(json::stream_object("hello")); |
835 | server._routes.put(GET, "/test", handler); | |
836 | server.do_accepts(0).get(); | |
837 | ||
9f95a23c | 838 | client.get(); |
9f95a23c TL |
839 | server.stop().get(); |
840 | }); | |
841 | } | |
f67539c2 TL |
842 | |
843 | ||
844 | SEASTAR_TEST_CASE(test_unparsable_request) { | |
845 | // Test if a message that cannot be parsed as a http request is being replied with a 400 Bad Request response | |
846 | return seastar::async([] { | |
1e59de90 | 847 | loopback_connection_factory lcf(1); |
f67539c2 TL |
848 | http_server server("test"); |
849 | loopback_socket_impl lsi(lcf); | |
850 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
851 | future<> client = seastar::async([&lsi] { | |
852 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); | |
853 | input_stream<char> input(c_socket.input()); | |
854 | output_stream<char> output(c_socket.output()); | |
855 | ||
856 | output.write(sstring("GET /test HTTP/1.1\r\nhello\r\nContent-Length: 17\r\nExpect: 100-continue\r\n\r\n")).get(); | |
857 | output.flush().get(); | |
858 | auto resp = input.read().get0(); | |
859 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("400 Bad Request"), std::string::npos); | |
860 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find("Can't parse the request"), std::string::npos); | |
861 | ||
862 | input.close().get(); | |
863 | output.close().get(); | |
864 | }); | |
865 | ||
866 | auto handler = new json_test_handler(json::stream_object("hello")); | |
867 | server._routes.put(GET, "/test", handler); | |
868 | server.do_accepts(0).get(); | |
869 | ||
870 | client.get(); | |
871 | server.stop().get(); | |
872 | }); | |
873 | } | |
874 | ||
1e59de90 TL |
875 | struct echo_handler : public handler_base { |
876 | bool chunked_reply; | |
877 | echo_handler(bool chunked_reply_) : handler_base(), chunked_reply(chunked_reply_) {} | |
878 | ||
879 | future<std::unique_ptr<http::reply>> do_handle(std::unique_ptr<http::request>& req, std::unique_ptr<http::reply>& rep, sstring& content) { | |
880 | for (auto it : req->chunk_extensions) { | |
881 | content += it.first; | |
882 | if (it.second != "") { | |
883 | content += to_sstring("=") + it.second; | |
884 | } | |
885 | } | |
886 | for (auto it : req->trailing_headers) { | |
887 | content += it.first; | |
888 | if (it.second != "") { | |
889 | content += to_sstring(": ") + it.second; | |
890 | } | |
891 | } | |
892 | if (!chunked_reply) { | |
893 | rep->write_body("txt", content); | |
894 | } else { | |
895 | rep->write_body("txt", [ c = content ] (output_stream<char>&& out) { | |
896 | return do_with(std::move(out), [ c = std::move(c) ] (output_stream<char>& out) { | |
897 | return out.write(std::move(c)).then([&out] { | |
898 | return out.flush().then([&out] { | |
899 | return out.close(); | |
900 | }); | |
901 | }); | |
902 | }); | |
903 | }); | |
904 | } | |
905 | return make_ready_future<std::unique_ptr<http::reply>>(std::move(rep)); | |
906 | } | |
907 | }; | |
908 | ||
20effc67 TL |
909 | /* |
910 | * A request handler that responds with the same body that was used in the request using the requests content_stream | |
911 | * */ | |
1e59de90 TL |
912 | struct echo_stream_handler : public echo_handler { |
913 | echo_stream_handler(bool chunked_reply = false) : echo_handler(chunked_reply) {} | |
914 | future<std::unique_ptr<http::reply>> handle(const sstring& path, | |
915 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) override { | |
916 | return do_with(std::move(req), std::move(rep), sstring(), [this] (std::unique_ptr<http::request>& req, std::unique_ptr<http::reply>& rep, sstring& rep_content) { | |
20effc67 TL |
917 | return do_until([&req] { return req->content_stream->eof(); }, [&req, &rep_content] { |
918 | return req->content_stream->read().then([&rep_content] (temporary_buffer<char> tmp) { | |
919 | rep_content += to_sstring(std::move(tmp)); | |
920 | }); | |
1e59de90 TL |
921 | }).then([&req, &rep, &rep_content, this] { |
922 | return this->do_handle(req, rep, rep_content); | |
20effc67 TL |
923 | }); |
924 | }); | |
925 | } | |
926 | }; | |
927 | ||
928 | /* | |
929 | * Same handler as above, but without using streams | |
930 | * */ | |
1e59de90 TL |
931 | struct echo_string_handler : public echo_handler { |
932 | echo_string_handler(bool chunked_reply = false) : echo_handler(chunked_reply) {} | |
933 | future<std::unique_ptr<http::reply>> handle(const sstring& path, | |
934 | std::unique_ptr<http::request> req, std::unique_ptr<http::reply> rep) override { | |
935 | return this->do_handle(req, rep, req->content); | |
20effc67 TL |
936 | } |
937 | }; | |
938 | ||
939 | /* | |
940 | * Checks if the server responds to the request equivalent to the concatenation of all req_parts with a reply containing | |
941 | * the resp_parts strings, assuming that the content streaming is set to stream and the /test route is handled by handl | |
942 | * */ | |
943 | future<> check_http_reply (std::vector<sstring>&& req_parts, std::vector<std::string>&& resp_parts, bool stream, handler_base* handl) { | |
944 | return seastar::async([req_parts = std::move(req_parts), resp_parts = std::move(resp_parts), stream, handl] { | |
1e59de90 | 945 | loopback_connection_factory lcf(1); |
20effc67 TL |
946 | http_server server("test"); |
947 | server.set_content_streaming(stream); | |
948 | loopback_socket_impl lsi(lcf); | |
949 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
950 | future<> client = seastar::async([req_parts = std::move(req_parts), resp_parts = std::move(resp_parts), &lsi] { | |
951 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); | |
952 | input_stream<char> input(c_socket.input()); | |
953 | output_stream<char> output(c_socket.output()); | |
954 | ||
955 | for (auto& str : req_parts) { | |
956 | output.write(std::move(str)).get(); | |
957 | output.flush().get(); | |
958 | } | |
959 | auto resp = input.read().get0(); | |
960 | for (auto& str : resp_parts) { | |
961 | BOOST_REQUIRE_NE(std::string(resp.get(), resp.size()).find(std::move(str)), std::string::npos); | |
962 | } | |
963 | ||
964 | input.close().get(); | |
965 | output.close().get(); | |
966 | }); | |
967 | ||
968 | server._routes.put(GET, "/test", handl); | |
969 | server.do_accepts(0).get(); | |
970 | ||
971 | client.get(); | |
972 | server.stop().get(); | |
973 | }); | |
974 | }; | |
975 | ||
1e59de90 TL |
976 | static future<> test_basic_content(bool streamed, bool chunked_reply) { |
977 | return seastar::async([streamed, chunked_reply] { | |
978 | loopback_connection_factory lcf(1); | |
20effc67 | 979 | http_server server("test"); |
1e59de90 TL |
980 | if (streamed) { |
981 | server.set_content_streaming(true); | |
982 | } | |
20effc67 TL |
983 | loopback_socket_impl lsi(lcf); |
984 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
1e59de90 | 985 | future<> client = seastar::async([&lsi, chunked_reply] { |
20effc67 | 986 | connected_socket c_socket = lsi.connect(socket_address(ipv4_addr()), socket_address(ipv4_addr())).get0(); |
1e59de90 TL |
987 | http::experimental::connection conn(std::move(c_socket)); |
988 | ||
989 | { | |
990 | fmt::print("Simple request test\n"); | |
991 | auto req = http::request::make("GET", "test", "/test"); | |
992 | auto resp = conn.make_request(std::move(req)).get0(); | |
993 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
994 | if (!chunked_reply) { | |
995 | BOOST_REQUIRE_EQUAL(resp.content_length, 0); | |
996 | } else { | |
997 | // If response is chunked it will contain the single termination | |
998 | // zero-sized chunk that still needs to be read out | |
999 | auto in = conn.in(resp); | |
1000 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1001 | BOOST_REQUIRE_EQUAL(body, ""); | |
1002 | } | |
1003 | } | |
20effc67 | 1004 | |
1e59de90 TL |
1005 | { |
1006 | fmt::print("Request with body test\n"); | |
1007 | auto req = http::request::make("GET", "test", "/test"); | |
1008 | req.write_body("txt", sstring("12345 78901\t34521345")); | |
1009 | auto resp = conn.make_request(std::move(req)).get0(); | |
1010 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
1011 | if (!chunked_reply) { | |
1012 | BOOST_REQUIRE_EQUAL(resp.content_length, 20); | |
1013 | } | |
1014 | auto in = conn.in(resp); | |
1015 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1016 | BOOST_REQUIRE_EQUAL(body, sstring("12345 78901\t34521345")); | |
1017 | } | |
20effc67 | 1018 | |
1e59de90 TL |
1019 | { |
1020 | fmt::print("Request with content-length body\n"); | |
1021 | auto req = http::request::make("GET", "test", "/test"); | |
1022 | req.write_body("txt", 12, [] (output_stream<char>&& out) { | |
1023 | return seastar::async([out = std::move(out)] () mutable { | |
1024 | out.write(sstring("1234567890")).get(); | |
1025 | out.write(sstring("AB")).get(); | |
1026 | out.flush().get(); | |
1027 | out.close().get(); | |
1028 | }); | |
1029 | }); | |
1030 | auto resp = conn.make_request(std::move(req)).get0(); | |
1031 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
1032 | if (!chunked_reply) { | |
1033 | BOOST_REQUIRE_EQUAL(resp.content_length, 12); | |
1034 | } | |
1035 | auto in = conn.in(resp); | |
1036 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1037 | BOOST_REQUIRE_EQUAL(body, sstring("1234567890AB")); | |
1038 | } | |
20effc67 | 1039 | |
1e59de90 TL |
1040 | { |
1041 | const size_t size = 128*1024; | |
1042 | fmt::print("Request with {}-kbytes content-length body\n", size >> 10); | |
1043 | temporary_buffer<char> jumbo(size); | |
1044 | temporary_buffer<char> jumbo_copy(size); | |
1045 | for (size_t i = 0; i < size; i++) { | |
1046 | jumbo.get_write()[i] = 'a' + i % ('z' - 'a'); | |
1047 | jumbo_copy.get_write()[i] = jumbo[i]; | |
1048 | } | |
1049 | auto req = http::request::make("GET", "test", "/test"); | |
1050 | req.write_body("txt", size, [jumbo = std::move(jumbo)] (output_stream<char>&& out) mutable { | |
1051 | return seastar::async([out = std::move(out), jumbo = std::move(jumbo)] () mutable { | |
1052 | out.write(jumbo.get(), jumbo.size()).get(); | |
1053 | out.flush().get(); | |
1054 | out.close().get(); | |
1055 | }); | |
1056 | }); | |
1057 | auto resp = conn.make_request(std::move(req)).get0(); | |
1058 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
1059 | if (!chunked_reply) { | |
1060 | BOOST_REQUIRE_EQUAL(resp.content_length, size); | |
1061 | } | |
1062 | auto in = conn.in(resp); | |
1063 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1064 | BOOST_REQUIRE_EQUAL(body, to_sstring(std::move(jumbo_copy))); | |
1065 | } | |
20effc67 | 1066 | |
1e59de90 TL |
1067 | { |
1068 | fmt::print("Request with chunked body\n"); | |
1069 | auto req = http::request::make("GET", "test", "/test"); | |
1070 | req.write_body("txt", [] (auto&& out) -> future<> { | |
1071 | return seastar::async([out = std::move(out)] () mutable { | |
1072 | out.write(sstring("req")).get(); | |
1073 | out.write(sstring("1234\r\n7890")).get(); | |
1074 | out.flush().get(); | |
1075 | out.close().get(); | |
1076 | }); | |
1077 | }); | |
1078 | auto resp = conn.make_request(std::move(req)).get0(); | |
1079 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
1080 | if (!chunked_reply) { | |
1081 | BOOST_REQUIRE_EQUAL(resp.content_length, 13); | |
1082 | } | |
1083 | auto in = conn.in(resp); | |
1084 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1085 | BOOST_REQUIRE_EQUAL(body, sstring("req1234\r\n7890")); | |
20effc67 | 1086 | } |
20effc67 | 1087 | |
1e59de90 TL |
1088 | { |
1089 | fmt::print("Request with expect-continue\n"); | |
1090 | auto req = http::request::make("GET", "test", "/test"); | |
1091 | req.write_body("txt", sstring("foobar")); | |
1092 | req.set_expects_continue(); | |
1093 | auto resp = conn.make_request(std::move(req)).get0(); | |
1094 | BOOST_REQUIRE_EQUAL(resp._status, http::reply::status_type::ok); | |
1095 | if (!chunked_reply) { | |
1096 | BOOST_REQUIRE_EQUAL(resp.content_length, 6); | |
1097 | } | |
1098 | auto in = conn.in(resp); | |
1099 | sstring body = util::read_entire_stream_contiguous(in).get0(); | |
1100 | BOOST_REQUIRE_EQUAL(body, sstring("foobar")); | |
1101 | } | |
20effc67 | 1102 | |
1e59de90 TL |
1103 | { |
1104 | fmt::print("Request with incomplete content-length body\n"); | |
1105 | auto req = http::request::make("GET", "test", "/test"); | |
1106 | req.write_body("txt", 12, [] (output_stream<char>&& out) { | |
1107 | return seastar::async([out = std::move(out)] () mutable { | |
1108 | out.write(sstring("1234567890A")).get(); | |
1109 | out.flush().get(); | |
1110 | out.close().get(); | |
1111 | }); | |
1112 | }); | |
1113 | BOOST_REQUIRE_THROW(conn.make_request(std::move(req)).get0(), std::runtime_error); | |
1114 | } | |
20effc67 | 1115 | |
1e59de90 TL |
1116 | { |
1117 | bool callback_completed = false; | |
1118 | fmt::print("Request with too large content-length body\n"); | |
1119 | auto req = http::request::make("GET", "test", "/test"); | |
1120 | req.write_body("txt", 12, [&callback_completed] (output_stream<char>&& out) { | |
1121 | return seastar::async([out = std::move(out), &callback_completed] () mutable { | |
1122 | out.write(sstring("1234567890ABC")).get(); | |
1123 | out.flush().get(); | |
1124 | out.close().get(); | |
1125 | callback_completed = true; | |
1126 | }); | |
1127 | }); | |
1128 | BOOST_REQUIRE_NE(callback_completed, true); // should throw early | |
1129 | BOOST_REQUIRE_THROW(conn.make_request(std::move(req)).get0(), std::runtime_error); | |
1130 | } | |
1131 | ||
1132 | conn.close().get(); | |
20effc67 TL |
1133 | }); |
1134 | ||
1e59de90 TL |
1135 | handler_base* handler; |
1136 | if (streamed) { | |
1137 | handler = new echo_stream_handler(chunked_reply); | |
1138 | } else { | |
1139 | handler = new echo_string_handler(chunked_reply); | |
1140 | } | |
20effc67 TL |
1141 | server._routes.put(GET, "/test", handler); |
1142 | server.do_accepts(0).get(); | |
1143 | ||
1144 | client.get(); | |
1145 | server.stop().get(); | |
1146 | }); | |
1147 | } | |
1148 | ||
1e59de90 TL |
1149 | SEASTAR_TEST_CASE(test_string_content) { |
1150 | return test_basic_content(false, false); | |
1151 | } | |
1152 | ||
1153 | SEASTAR_TEST_CASE(test_string_content_chunked) { | |
1154 | return test_basic_content(false, true); | |
1155 | } | |
1156 | ||
1157 | SEASTAR_TEST_CASE(test_stream_content) { | |
1158 | return test_basic_content(true, false); | |
1159 | } | |
1160 | ||
1161 | SEASTAR_TEST_CASE(test_stream_content_chunked) { | |
1162 | return test_basic_content(true, true); | |
1163 | } | |
1164 | ||
20effc67 TL |
1165 | SEASTAR_TEST_CASE(test_not_implemented_encoding) { |
1166 | return check_http_reply({ | |
1167 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: gzip, chunked\r\n\r\n", | |
1168 | "a\r\n1234567890\r\n", | |
1169 | "a\r\n1234521345\r\n", | |
1170 | "0\r\n\r\n" | |
1171 | }, {"501 Not Implemented", "Encodings other than \"chunked\" are not implemented (received encoding: \"gzip, chunked\")"}, false, new echo_string_handler()); | |
1172 | } | |
1173 | ||
20effc67 TL |
1174 | SEASTAR_TEST_CASE(test_full_chunk_format) { |
1175 | return check_http_reply({ | |
1176 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", | |
1177 | "a;abc-def;hello=world;aaaa\r\n1234567890\r\n", | |
1178 | "a;a0-!#$%&'*+.^_`|~=\"quoted string obstext\x80\x81\xff quoted_pair: \\a\"\r\n1234521345\r\n", | |
1179 | "0\r\na:b\r\n~|`_^.+*'&%$#!-0a: ~!@#$%^&*()_+\x80\x81\xff\r\n obs fold \r\n\r\n" | |
1180 | }, {"12345678901234521345", "abc-def", "hello=world", "aaaa", "a0-!#$%&'*+.^_`|~=quoted string obstext\x80\x81\xff quoted_pair: a", | |
1181 | "a: b", "~|`_^.+*'&%$#!-0a: ~!@#$%^&*()_+\x80\x81\xff obs fold" | |
1182 | }, false, new echo_string_handler()); | |
1183 | } | |
1184 | ||
1185 | SEASTAR_TEST_CASE(test_chunk_extension_parser_fail) { | |
1186 | return check_http_reply({ | |
1187 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", | |
1188 | "7; \r\nnoparse\r\n", | |
1189 | "0\r\n\r\n" | |
1190 | }, {"400 Bad Request", "Can't parse chunk size and extensions"}, false, new echo_string_handler()); | |
1191 | } | |
1192 | ||
1193 | SEASTAR_TEST_CASE(test_trailer_part_parser_fail) { | |
1194 | return check_http_reply({ | |
1195 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", | |
1196 | "8\r\nparsable\r\n", | |
1197 | "0\r\ngood:header\r\nbad=header\r\n\r\n" | |
1198 | }, {"400 Bad Request", "Can't parse chunked request trailer"}, false, new echo_string_handler()); | |
1199 | } | |
1200 | ||
1201 | SEASTAR_TEST_CASE(test_too_long_chunk) { | |
1202 | return check_http_reply({ | |
1203 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", | |
1204 | "a\r\n1234567890\r\n", | |
1205 | "a\r\n1234521345X\r\n", | |
1206 | "0\r\n\r\n" | |
1207 | }, {"400 Bad Request", "The actual chunk length exceeds the specified length"}, true, new echo_stream_handler()); | |
1208 | } | |
1209 | ||
1210 | SEASTAR_TEST_CASE(test_bad_chunk_length) { | |
1211 | return check_http_reply({ | |
1212 | "GET /test HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n", | |
1213 | "a\r\n1234567890\r\n", | |
1214 | "aX\r\n1234521345\r\n", | |
1215 | "0\r\n\r\n" | |
1216 | }, {"400 Bad Request", "Can't parse chunk size and extensions"}, true, new echo_stream_handler()); | |
1217 | } | |
1218 | ||
f67539c2 | 1219 | SEASTAR_TEST_CASE(case_insensitive_header) { |
1e59de90 | 1220 | std::unique_ptr<seastar::http::request> req = std::make_unique<seastar::http::request>(); |
f67539c2 TL |
1221 | req->_headers["conTEnt-LengtH"] = "17"; |
1222 | BOOST_REQUIRE_EQUAL(req->get_header("content-length"), "17"); | |
1223 | BOOST_REQUIRE_EQUAL(req->get_header("Content-Length"), "17"); | |
1224 | BOOST_REQUIRE_EQUAL(req->get_header("cOnTeNT-lEnGTh"), "17"); | |
1225 | return make_ready_future<>(); | |
1226 | } | |
1227 | ||
1228 | SEASTAR_THREAD_TEST_CASE(multiple_connections) { | |
1e59de90 | 1229 | loopback_connection_factory lcf(1); |
f67539c2 TL |
1230 | http_server server("test"); |
1231 | httpd::http_server_tester::listeners(server).emplace_back(lcf.get_server_socket()); | |
1232 | socket_address addr{ipv4_addr()}; | |
1233 | ||
1234 | std::vector<connected_socket> socks; | |
1235 | // Make sure one shard has two connections pending. | |
1236 | for (unsigned i = 0; i <= smp::count; ++i) { | |
1237 | socks.push_back(loopback_socket_impl(lcf).connect(addr, addr).get0()); | |
1238 | } | |
1239 | ||
1240 | server.do_accepts(0).get(); | |
1241 | server.stop().get(); | |
1242 | lcf.destroy_all_shards().get(); | |
1243 | } | |
20effc67 TL |
1244 | |
1245 | SEASTAR_TEST_CASE(http_parse_response_status) { | |
1246 | http_response_parser parser; | |
1247 | parser.init(); | |
1248 | char r101[] = "HTTP/1.1 101 Switching Protocols\r\n\r\n"; | |
1249 | char r200[] = "HTTP/1.1 200 OK\r\nHost: localhost\r\nhello\r\n"; | |
1250 | ||
1251 | parser.parse(r101, r101 + sizeof(r101), r101 + sizeof(r101)); | |
1252 | auto response = parser.get_parsed_response(); | |
1253 | BOOST_REQUIRE_EQUAL(response->_status_code, 101); | |
1254 | ||
1255 | parser.init(); | |
1256 | parser.parse(r200, r200 + sizeof(r200), r200 + sizeof(r200)); | |
1257 | response = parser.get_parsed_response(); | |
1258 | BOOST_REQUIRE_EQUAL(response->_status_code, 200); | |
1259 | return make_ready_future<>(); | |
1260 | } | |
1261 | ||
1262 | SEASTAR_TEST_CASE(test_shared_future) { | |
1263 | shared_promise<json::json_return_type> p; | |
1264 | auto fut = p.get_shared_future(); | |
1265 | ||
1e59de90 | 1266 | (void)yield().then([p = std::move(p)] () mutable { |
20effc67 TL |
1267 | p.set_value(json::json_void()); |
1268 | }); | |
1269 | ||
1270 | return std::move(fut).discard_result(); | |
1271 | } | |
1e59de90 TL |
1272 | |
1273 | SEASTAR_TEST_CASE(test_url_encode_decode) { | |
1274 | sstring encoded, decoded; | |
1275 | bool ok; | |
1276 | ||
1277 | sstring all_valid = "~abcdefghijklmnopqrstuvwhyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789."; | |
1278 | encoded = http::internal::url_encode(all_valid); | |
1279 | ok = http::internal::url_decode(encoded, decoded); | |
1280 | BOOST_REQUIRE_EQUAL(ok, true); | |
1281 | BOOST_REQUIRE_EQUAL(decoded, all_valid); | |
1282 | BOOST_REQUIRE_EQUAL(all_valid, encoded); | |
1283 | ||
1284 | sstring some_invalid = "a?/!@#$%^&*()[]=.\\ \tZ"; | |
1285 | encoded = http::internal::url_encode(some_invalid); | |
1286 | ok = http::internal::url_decode(encoded, decoded); | |
1287 | BOOST_REQUIRE_EQUAL(ok, true); | |
1288 | BOOST_REQUIRE_EQUAL(decoded, some_invalid); | |
1289 | for (size_t i = 0; i < encoded.length(); i++) { | |
1290 | if (encoded[i] != '%' && encoded[i] != '+') { | |
1291 | auto f = std::find(std::begin(all_valid), std::end(all_valid), encoded[i]); | |
1292 | BOOST_REQUIRE_NE(f, std::end(all_valid)); | |
1293 | } | |
1294 | } | |
1295 | ||
1296 | return make_ready_future<>(); | |
1297 | } | |
1298 | ||
1299 | SEASTAR_TEST_CASE(test_url_param_encode_decode) { | |
1300 | http::request to_send; | |
1301 | to_send._url = "/foo/bar"; | |
1302 | to_send.query_parameters["a"] = "a+a*a"; | |
1303 | to_send.query_parameters["b"] = "b/b\%b"; | |
1304 | ||
1305 | http::request to_recv; | |
1306 | to_recv._url = to_send.format_url(); | |
1307 | sstring url = to_recv.parse_query_param(); | |
1308 | ||
1309 | BOOST_REQUIRE_EQUAL(url, to_send._url); | |
1310 | BOOST_REQUIRE_EQUAL(to_recv.query_parameters.size(), to_send.query_parameters.size()); | |
1311 | for (const auto& p : to_send.query_parameters) { | |
1312 | auto it = to_recv.query_parameters.find(p.first); | |
1313 | BOOST_REQUIRE(it != to_recv.query_parameters.end()); | |
1314 | BOOST_REQUIRE_EQUAL(it->second, p.second); | |
1315 | } | |
1316 | ||
1317 | return make_ready_future<>(); | |
1318 | } |