diff --git a/README.md b/README.md index 27313b8..8753642 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ int main() { // Send a HTTP GET request. auto r = session.Get("http://httpbin.org/get"); - // Print the response content data. - std::cout << r->content() << std::endl; + // Print the response data. + std::cout << r->data() << std::endl; } catch (const webcc::Error& error) { std::cout << error << std::endl; @@ -57,8 +57,8 @@ int main() { The `Get()` method is nothing but a shortcut of `Request()`. Using `Request()` directly is more complicated: ```cpp -auto r = session.Request(webcc::RequestBuilder{}.Get(). - Url("http://httpbin.org/get") +auto r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get") ()); ``` As you can see, a helper class named `RequestBuilder` is used to chain the parameters and finally build (don't miss the `()` operator) a request object. @@ -69,8 +69,8 @@ Both the shortcut and `Request()` accept URL query parameters: // Query parameters are passed using a std::vector. session.Get("http://httpbin.org/get", { "key1", "value1", "key2", "value2" }); -session.Request(webcc::RequestBuilder{}.Get(). - Url("http://httpbin.org/get"). +session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get"). Query("key1", "value1"). Query("key2", "value2") ()); @@ -82,8 +82,8 @@ session.Get("http://httpbin.org/get", {"key1", "value1", "key2", "value2"}, {"Accept", "application/json"}); // Also a std::vector -session.Request(webcc::RequestBuilder{}.Get(). - Url("http://httpbin.org/get"). +session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get"). Query("key1", "value1"). Query("key2", "value2"). Header("Accept", "application/json") @@ -100,7 +100,7 @@ Listing GitHub public events is not a big deal: ```cpp auto r = session.Get("https://api.github.com/events"); ``` -You can then parse `r->content()` to JSON object with your favorite JSON library. My choice for the examples is [jsoncpp](https://github.com/open-source-parsers/jsoncpp). But the library itself doesn't understand JSON nor require one. It's up to you to choose the most appropriate JSON library. +You can then parse `r->data()` to JSON object with your favorite JSON library. My choice for the examples is [jsoncpp](https://github.com/open-source-parsers/jsoncpp). But the library itself doesn't understand JSON nor require one. It's up to you to choose the most appropriate JSON library. ## Server API Examples @@ -119,7 +119,7 @@ class BookListView : public webcc::View { public: webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { - return Get(request->query()); + return Get(request); } if (request->method() == "POST") { @@ -131,10 +131,10 @@ public: private: // Get a list of books based on query parameters. - webcc::ResponsePtr Get(const webcc::UrlQuery& query); + webcc::ResponsePtr Get(webcc::RequestPtr request); // Create a new book. - // The new book's data is attached as request content in JSON format. + // The new book's data is attached as request data in JSON format. webcc::ResponsePtr Post(webcc::RequestPtr request); }; ``` @@ -148,15 +148,15 @@ class BookDetailView : public webcc::View { public: webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { - return Get(request->args(), request->query()); + return Get(request); } if (request->method() == "PUT") { - return Put(request, request->args()); + return Put(request); } if (request->method() == "DELETE") { - return Delete(request->args()); + return Delete(request); } return {}; @@ -164,30 +164,27 @@ public: protected: // Get the detailed information of a book. - webcc::ResponsePtr Get(const webcc::UrlArgs& args, - const webcc::UrlQuery& query); + webcc::ResponsePtr Get(webcc::RequestPtr request); // Update a book. - webcc::ResponsePtr Put(webcc::RequestPtr request, - const webcc::UrlArgs& args); + webcc::ResponsePtr Put(webcc::RequestPtr request); // Delete a book. - webcc::ResponsePtr Delete(const webcc::UrlArgs& args); + webcc::ResponsePtr Delete(webcc::RequestPtr request); }; ``` The detailed implementation is out of the scope of this README, but here is an example: ```cpp -webcc::ResponsePtr BookDetailView::Get(const webcc::UrlArgs& args, - const webcc::UrlQuery& query) { - if (args.size() != 1) { +webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { + if (request->args().size() != 1) { // Using kNotFound means the resource specified by the URL cannot be found. // kBadRequest could be another choice. return webcc::ResponseBuilder{}.NotFound()(); } - const std::string& book_id = args[0]; + const std::string& book_id = request->args()[0]; // Get the book by ID from, e.g., the database. // ... @@ -195,24 +192,35 @@ webcc::ResponsePtr BookDetailView::Get(const webcc::UrlArgs& args, if () { // There's no such book with the given ID. return webcc::ResponseBuilder{}.NotFound()(); - } else { - // Convert the book to JSON string and set as response content. - return webcc::ResponseBuilder{}.OK().Data().Json()(); } + + // Convert the book to JSON string and set as response data. + return webcc::ResponseBuilder{}.OK().Data().Json().Utf8(); } ``` Last step, route URLs to the proper views and run the server: ```cpp -webcc::Server server(8080, 2); +int main(int argc, char* argv[]) { + // ... + + try { + webcc::Server server(8080, 2); -server.Route("/books", std::make_shared(), { "GET", "POST" }); + server.Route("/books", std::make_shared(), { "GET", "POST" }); -server.Route(webcc::R("/books/(\\d+)"), std::make_shared(), - { "GET", "PUT", "DELETE" }); + server.Route(webcc::R("/books/(\\d+)"), std::make_shared(), + { "GET", "PUT", "DELETE" }); -server.Run(); + server.Run(); + + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return 1; + } + + return 0; ``` Please see [examples/rest_book_server.cc](https://github.com/sprinfall/webcc/tree/master/examples/rest_book_server.cc) for more details. diff --git a/autotest/client_autotest.cc b/autotest/client_autotest.cc index cc34ba4..d778cf2 100644 --- a/autotest/client_autotest.cc +++ b/autotest/client_autotest.cc @@ -1,3 +1,5 @@ +#include + #include "gtest/gtest.h" #include "boost/algorithm/string.hpp" @@ -26,11 +28,50 @@ static Json::Value StringToJson(const std::string& str) { // ----------------------------------------------------------------------------- +TEST(ClientTest, Head_RequestFunc) { + webcc::ClientSession session; + + try { + auto r = session.Request(webcc::RequestBuilder{}. + Head("http://httpbin.org/get"). + Query("key1", "value1"). + Query("key2", "value2"). + Header("Accept", "application/json") + ()); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + EXPECT_EQ("", r->data()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + +TEST(ClientTest, Head_Shortcut) { + webcc::ClientSession session; + + try { + auto r = session.Head("http://httpbin.org/get"); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + EXPECT_EQ("", r->data()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + +// ----------------------------------------------------------------------------- + static void AssertGet(webcc::ResponsePtr r) { EXPECT_EQ(webcc::Status::kOK, r->status()); EXPECT_EQ("OK", r->reason()); - Json::Value json = StringToJson(r->content()); + Json::Value json = StringToJson(r->data()); Json::Value args = json["args"]; @@ -54,8 +95,8 @@ TEST(ClientTest, Get_RequestFunc) { webcc::ClientSession session; try { - auto r = session.Request(webcc::RequestBuilder{}.Get(). - Url("http://httpbin.org/get"). + auto r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get"). Query("key1", "value1"). Query("key2", "value2"). Header("Accept", "application/json") @@ -89,8 +130,8 @@ TEST(ClientTest, Get_SSL) { try { // HTTPS is auto-detected from the URL scheme. - auto r = session.Request(webcc::RequestBuilder{}.Get(). - Url("https://httpbin.org/get"). + auto r = session.Request(webcc::RequestBuilder{}. + Get("https://httpbin.org/get"). Query("key1", "value1"). Query("key2", "value2"). Header("Accept", "application/json") @@ -115,7 +156,7 @@ TEST(ClientTest, Compression_Gzip) { try { auto r = session.Get("http://httpbin.org/gzip"); - Json::Value json = StringToJson(r->content()); + Json::Value json = StringToJson(r->data()); EXPECT_EQ(true, json["gzipped"].asBool()); @@ -131,7 +172,7 @@ TEST(ClientTest, Compression_Deflate) { try { auto r = session.Get("http://httpbin.org/deflate"); - Json::Value json = StringToJson(r->content()); + Json::Value json = StringToJson(r->data()); EXPECT_EQ(true, json["deflated"].asBool()); @@ -140,6 +181,27 @@ TEST(ClientTest, Compression_Deflate) { } } +// Test trying to compress the request. +// TODO +TEST(ClientTest, Compression_Request) { + webcc::ClientSession session; + + try { + const std::string data = "{'name'='Adam', 'age'=20}"; + + // This doesn't really compress the body! + auto r = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + Body(data).Json(). + Gzip() + ()); + + //Json::Value json = StringToJson(r->data()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} #endif // WEBCC_ENABLE_GZIP // ----------------------------------------------------------------------------- @@ -176,15 +238,15 @@ TEST(ClientTest, KeepAlive) { EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); // Close by using request builder. - r = session.Request(webcc::RequestBuilder{}.Get(). - Url(url).KeepAlive(false) + r = session.Request(webcc::RequestBuilder{}. + Get(url).KeepAlive(false) ()); EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Close")); // Keep-Alive explicitly by using request builder. - r = session.Request(webcc::RequestBuilder{}.Get(). - Url(url).KeepAlive(true) + r = session.Request(webcc::RequestBuilder{}. + Get(url).KeepAlive(true) ()); EXPECT_TRUE(iequals(r->GetHeader("Connection"), "Keep-alive")); @@ -210,7 +272,7 @@ TEST(ClientTest, GetImageJpeg) { // {"Accept", "image/jpeg"}); //std::ofstream ofs(path, std::ios::binary); - //ofs << r->content(); + //ofs << r->data(); // TODO: Verify the response is a valid JPEG image. @@ -221,7 +283,72 @@ TEST(ClientTest, GetImageJpeg) { // ----------------------------------------------------------------------------- -// TODO: Post requests +TEST(ClientTest, Post_RequestFunc) { + webcc::ClientSession session; + + try { + const std::string data = "{'name'='Adam', 'age'=20}"; + + auto r = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + Body(data).Json() + ()); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + Json::Value json = StringToJson(r->data()); + + EXPECT_EQ(data, json["data"].asString()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + +TEST(ClientTest, Post_Shortcut) { + webcc::ClientSession session; + + try { + const std::string data = "{'name'='Adam', 'age'=20}"; + + auto r = session.Post("http://httpbin.org/post", std::string(data), true); + + EXPECT_EQ(webcc::Status::kOK, r->status()); + EXPECT_EQ("OK", r->reason()); + + Json::Value json = StringToJson(r->data()); + + EXPECT_EQ(data, json["data"].asString()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} + +#if (WEBCC_ENABLE_GZIP && WEBCC_ENABLE_SSL) +// NOTE: Most servers don't support compressed requests! +TEST(ClientTest, Post_Gzip) { + webcc::ClientSession session; + + try { + // Use Boost.org home page as the POST data. + auto r1 = session.Get("https://www.boost.org/"); + const std::string& data = r1->data(); + + auto r2 = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + Body(data).Gzip() + ()); + + EXPECT_EQ(webcc::Status::kOK, r2->status()); + EXPECT_EQ("OK", r2->reason()); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + } +} +#endif // (WEBCC_ENABLE_GZIP && WEBCC_ENABLE_SSL) // ----------------------------------------------------------------------------- diff --git a/examples/client_basics.cc b/examples/client_basics.cc index 192c74d..8609f20 100644 --- a/examples/client_basics.cc +++ b/examples/client_basics.cc @@ -3,49 +3,34 @@ #include "webcc/client_session.h" #include "webcc/logger.h" -static void PrintSeparator() { - static const std::string s_line(80, '-'); - std::cout << s_line << std::endl; -} - int main() { WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); webcc::ClientSession session; try { - PrintSeparator(); - - // Using request builder: - auto r = session.Request(webcc::RequestBuilder{}.Get(). - Url("http://httpbin.org/get"). + auto r = session.Request(webcc::RequestBuilder{}. + Get("http://httpbin.org/get"). Query("key1", "value1"). Query("key2", "value2"). Date(). Header("Accept", "application/json") ()); - std::cout << r->content() << std::endl; - - PrintSeparator(); - - // Using shortcut: r = session.Get("http://httpbin.org/get", { "key1", "value1", "key2", "value2" }, { "Accept", "application/json" }); - std::cout << r->content() << std::endl; + r = session.Request(webcc::RequestBuilder{}. + Post("http://httpbin.org/post"). + Body("{'name'='Adam', 'age'=20}"). + Json().Utf8() + ()); #if WEBCC_ENABLE_SSL - PrintSeparator(); - - // HTTPS support. - r = session.Get("https://httpbin.org/get"); - std::cout << r->content() << std::endl; - #endif // WEBCC_ENABLE_SSL } catch (const webcc::Error& error) { diff --git a/examples/file_upload_client.cc b/examples/file_upload_client.cc index 8c67be9..0909d4f 100644 --- a/examples/file_upload_client.cc +++ b/examples/file_upload_client.cc @@ -42,8 +42,8 @@ int main(int argc, char* argv[]) { webcc::ClientSession session; try { - auto r = session.Request(webcc::RequestBuilder{}.Post(). - Url(url). + auto r = session.Request(webcc::RequestBuilder{}. + Post(url). File("file", upload_dir / "remember.txt"). Form("json", "{}", "application/json") ()); diff --git a/examples/file_upload_server.cc b/examples/file_upload_server.cc index 80cca1a..01a105c 100644 --- a/examples/file_upload_server.cc +++ b/examples/file_upload_server.cc @@ -11,18 +11,22 @@ class FileUploadView : public webcc::View { public: webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "POST") { - std::cout << "files: " << request->form_parts().size() << std::endl; + return Post(request); + } + + return {}; + } - for (auto& part : request->form_parts()) { - std::cout << "name: " << part->name() << std::endl; - std::cout << "data: " << std::endl << part->data() << std::endl; - } +private: + webcc::ResponsePtr Post(webcc::RequestPtr request) { + std::cout << "form parts: " << request->form_parts().size() << std::endl; - // TODO: media_type: webcc::media_types::kTextPlain; charset = "utf-8"; - return webcc::ResponseBuilder{}.Created().Data("OK")(); + for (auto& part : request->form_parts()) { + std::cout << "name: " << part->name() << std::endl; + std::cout << "data: " << std::endl << part->data() << std::endl; } - return webcc::ResponseBuilder{}.NotImplemented()(); + return webcc::ResponseBuilder{}.Created().Body("OK")(); } }; diff --git a/examples/github_client.cc b/examples/github_client.cc index e6954e0..23891b9 100644 --- a/examples/github_client.cc +++ b/examples/github_client.cc @@ -57,7 +57,7 @@ void ListEvents(webcc::ClientSession& session) { try { auto r = session.Get(kUrlRoot + "/events"); - PRINT_JSON_STRING(r->content()); + PRINT_JSON_STRING(r->data()); } catch (const webcc::Error& error) { std::cout << error << std::endl; @@ -71,7 +71,7 @@ void ListUserFollowers(webcc::ClientSession& session, const std::string& user) { try { auto r = session.Get(kUrlRoot + "/users/" + user + "/followers"); - PRINT_JSON_STRING(r->content()); + PRINT_JSON_STRING(r->data()); } catch (const webcc::Error& error) { std::cout << error << std::endl; @@ -85,12 +85,12 @@ void ListAuthUserFollowers(webcc::ClientSession& session, const std::string& login, const std::string& password) { try { - auto r = session.Request(webcc::RequestBuilder{}.Get(). - Url(kUrlRoot + "/user/followers"). + auto r = session.Request(webcc::RequestBuilder{}. + Get(kUrlRoot + "/user/followers"). AuthBasic(login, password) ()); - PRINT_JSON_STRING(r->content()); + PRINT_JSON_STRING(r->data()); } catch (const webcc::Error& error) { std::cout << error << std::endl; @@ -107,14 +107,14 @@ void CreateAuthorization(webcc::ClientSession& session, " 'scopes': ['public_repo', 'repo', 'repo:status', 'user']\n" "}"; - auto r = session.Request(webcc::RequestBuilder{}.Post(). - Url(kUrlRoot + "/authorizations"). - Data(std::move(data)). - Json(true). + auto r = session.Request(webcc::RequestBuilder{}. + Post(kUrlRoot + "/authorizations"). + Body(std::move(data)). + Json().Utf8(). AuthBasic(login, password) ()); - std::cout << r->content() << std::endl; + std::cout << r->data() << std::endl; } catch (const webcc::Error& error) { std::cout << error << std::endl; diff --git a/examples/rest_book_client.cc b/examples/rest_book_client.cc index 2706495..a54ca4b 100644 --- a/examples/rest_book_client.cc +++ b/examples/rest_book_client.cc @@ -62,7 +62,7 @@ public: return false; } - Json::Value rsp_json = StringToJson(r->content()); + Json::Value rsp_json = StringToJson(r->data()); if (!rsp_json.isArray()) { return false; // Should be a JSON array of books. @@ -92,7 +92,7 @@ public: return false; } - Json::Value rsp_json = StringToJson(r->content()); + Json::Value rsp_json = StringToJson(r->data()); *id = rsp_json["id"].asString(); return !id->empty(); @@ -120,7 +120,7 @@ public: return false; } - return JsonStringToBook(r->content(), book); + return JsonStringToBook(r->data(), book); } catch (const webcc::Error& error) { std::cerr << error << std::endl; @@ -211,11 +211,11 @@ int main(int argc, char* argv[]) { // Share the same session. webcc::ClientSession session; - // Session-level settings. session.set_timeout(timeout); - // TODO - //session.set_content_type("application/json"); - //session.set_charset("utf-8"); + + // If the request has body, default to this content type. + session.set_media_type("application/json"); + session.set_charset("utf-8"); BookListClient list_client(session, url); BookDetailClient detail_client(session, url); diff --git a/examples/rest_book_server.cc b/examples/rest_book_server.cc index d4b7395..219484c 100644 --- a/examples/rest_book_server.cc +++ b/examples/rest_book_server.cc @@ -41,19 +41,19 @@ public: webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { - return Get(request->query()); + return Get(request); } if (request->method() == "POST") { return Post(request); } - return{}; + return {}; } -protected: +private: // Get a list of books based on query parameters. - webcc::ResponsePtr Get(const webcc::UrlQuery& query); + webcc::ResponsePtr Get(webcc::RequestPtr request); // Create a new book. webcc::ResponsePtr Post(webcc::RequestPtr request); @@ -75,31 +75,29 @@ public: webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { - return Get(request->args(), request->query()); + return Get(request); } if (request->method() == "PUT") { - return Put(request, request->args()); + return Put(request); } if (request->method() == "DELETE") { - return Delete(request->args()); + return Delete(request); } return {}; } -protected: +private: // Get the detailed information of a book. - webcc::ResponsePtr Get(const webcc::UrlArgs& args, - const webcc::UrlQuery& query); + webcc::ResponsePtr Get(webcc::RequestPtr request); // Update a book. - webcc::ResponsePtr Put(webcc::RequestPtr request, - const webcc::UrlArgs& args); + webcc::ResponsePtr Put(webcc::RequestPtr request); // Delete a book. - webcc::ResponsePtr Delete(const webcc::UrlArgs& args); + webcc::ResponsePtr Delete(webcc::RequestPtr request); private: // Sleep some seconds before send back the response. @@ -110,7 +108,7 @@ private: // ----------------------------------------------------------------------------- // Return all books as a JSON array. -webcc::ResponsePtr BookListView::Get(const webcc::UrlQuery& /*query*/) { +webcc::ResponsePtr BookListView::Get(webcc::RequestPtr request) { Sleep(sleep_seconds_); Json::Value json(Json::arrayValue); @@ -119,22 +117,22 @@ webcc::ResponsePtr BookListView::Get(const webcc::UrlQuery& /*query*/) { json.append(BookToJson(book)); } - // TODO: charset = "utf-8" - return webcc::ResponseBuilder{}.OK().Data(JsonToString(json)).Json()(); + return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json(). + Utf8()(); } webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) { Sleep(sleep_seconds_); Book book; - if (JsonStringToBook(request->content(), &book)) { + if (JsonStringToBook(request->data(), &book)) { std::string id = g_book_store.AddBook(book); Json::Value json; json["id"] = id; - // TODO: charset = "utf-8" - return webcc::ResponseBuilder{}.Created().Data(JsonToString(json)).Json()(); + return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)). + Json().Utf8()(); } else { // Invalid JSON return webcc::ResponseBuilder{}.BadRequest()(); @@ -143,39 +141,37 @@ webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) { // ----------------------------------------------------------------------------- -webcc::ResponsePtr BookDetailView::Get(const webcc::UrlArgs& args, - const webcc::UrlQuery& query) { +webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { Sleep(sleep_seconds_); - if (args.size() != 1) { + if (request->args().size() != 1) { // Using kNotFound means the resource specified by the URL cannot be found. // kBadRequest could be another choice. return webcc::ResponseBuilder{}.NotFound()(); } - const std::string& book_id = args[0]; + const std::string& book_id = request->args()[0]; const Book& book = g_book_store.GetBook(book_id); if (book.IsNull()) { return webcc::ResponseBuilder{}.NotFound()(); } - // TODO: charset = "utf-8" - return webcc::ResponseBuilder{}.OK().Data(BookToJsonString(book)).Json()(); + return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).Json(). + Utf8()(); } -webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request, - const webcc::UrlArgs& args) { +webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) { Sleep(sleep_seconds_); - if (args.size() != 1) { + if (request->args().size() != 1) { return webcc::ResponseBuilder{}.NotFound()(); } - const std::string& book_id = args[0]; + const std::string& book_id = request->args()[0]; Book book; - if (!JsonStringToBook(request->content(), &book)) { + if (!JsonStringToBook(request->data(), &book)) { return webcc::ResponseBuilder{}.BadRequest()(); } @@ -185,14 +181,14 @@ webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request, return webcc::ResponseBuilder{}.OK()(); } -webcc::ResponsePtr BookDetailView::Delete(const webcc::UrlArgs& args) { +webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) { Sleep(sleep_seconds_); - if (args.size() != 1) { + if (request->args().size() != 1) { return webcc::ResponseBuilder{}.NotFound()(); } - const std::string& book_id = args[0]; + const std::string& book_id = request->args()[0]; if (!g_book_store.DeleteBook(book_id)) { return webcc::ResponseBuilder{}.NotFound()(); diff --git a/unittest/request_parser_unittest.cc b/unittest/request_parser_unittest.cc index 0669f31..f8e640c 100644 --- a/unittest/request_parser_unittest.cc +++ b/unittest/request_parser_unittest.cc @@ -25,7 +25,7 @@ protected: EXPECT_EQ("application/json", request_.GetHeader("Accept")); EXPECT_EQ("Close", request_.GetHeader("Connection")); - EXPECT_EQ("", request_.content()); + EXPECT_EQ("", request_.data()); EXPECT_EQ(webcc::kInvalidLength, request_.content_length()); } @@ -110,7 +110,7 @@ protected: EXPECT_EQ("application/json; charset=utf-8", request_.GetHeader("Content-Type")); EXPECT_EQ(std::to_string(data_.size()), request_.GetHeader("Content-Length")); - EXPECT_EQ(data_, request_.content()); + EXPECT_EQ(data_, request_.data()); EXPECT_EQ(data_.size(), request_.content_length()); } diff --git a/webcc/body.cc b/webcc/body.cc new file mode 100644 index 0000000..2480df2 --- /dev/null +++ b/webcc/body.cc @@ -0,0 +1,144 @@ +#include "webcc/body.h" + +#include "boost/algorithm/string.hpp" + +#include "webcc/logger.h" +#include "webcc/utility.h" + +#if WEBCC_ENABLE_GZIP +#include "webcc/gzip.h" +#endif + +namespace webcc { + +// ----------------------------------------------------------------------------- + +namespace misc_strings { + +// Literal strings can't be used because they have an extra '\0'. + +const char CRLF[] = { '\r', '\n' }; +const char DOUBLE_DASHES[] = { '-', '-' }; + +} // misc_strings + +// ----------------------------------------------------------------------------- + +#if WEBCC_ENABLE_GZIP +bool StringBody::Compress() { + if (data_.size() <= kGzipThreshold) { + return false; + } + + std::string compressed; + if (gzip::Compress(data_, &compressed)) { + data_ = std::move(compressed); + return true; + } + + LOG_WARN("Failed to compress the body data!"); + return false; +} +#endif // WEBCC_ENABLE_GZIP + +void StringBody::InitPayload() { + index_ = 0; +} + +Payload StringBody::NextPayload() { + if (index_ == 0) { + index_ = 1; + return Payload{ boost::asio::buffer(data_) }; + } + return {}; +} + +// NOTE: +// - The data will be truncated if it's too large to display. +// - Binary content will not be dumped (TODO). +void StringBody::Dump(std::ostream& os, const std::string& prefix) const { + if (data_.empty()) { + return; + } + + // Split by EOL to achieve more readability. + std::vector lines; + boost::split(lines, data_, boost::is_any_of("\n")); + + std::size_t size = 0; + + for (const std::string& line : lines) { + os << prefix; + + if (line.size() + size > kMaxDumpSize) { + os.write(line.c_str(), kMaxDumpSize - size); + os << "..." << std::endl; + break; + } else { + os << line << std::endl; + size += line.size(); + } + } +} + +// ----------------------------------------------------------------------------- + +FormBody::FormBody(const std::vector& parts, + const std::string& boundary) + : parts_(parts), boundary_(boundary) { +} + +std::size_t FormBody::GetSize() const { + std::size_t size = 0; + + for (auto& part : parts_) { + size += boundary_.size() + 4; // 4: -- and CRLF + size += part->GetSize(); + } + + size += boundary_.size() + 6; + + return size; +} + +void FormBody::Dump(std::ostream& os, const std::string& prefix) const { + // TODO +} + +void FormBody::InitPayload() { + index_ = 0; +} + +// TODO: Clear previous payload memory. +Payload FormBody::NextPayload() { + Payload payload; + + if (index_ < parts_.size()) { + auto& part = parts_[index_]; + AddBoundary(&payload); + part->Prepare(&payload); + + if (index_ + 1 == parts_.size()) { + AddBoundaryEnd(&payload); + } + } + + return payload; +} + +void FormBody::AddBoundary(Payload* payload) { + using boost::asio::buffer; + payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); + payload->push_back(buffer(boundary_)); + payload->push_back(buffer(misc_strings::CRLF)); +} + +void FormBody::AddBoundaryEnd(Payload* payload) { + using boost::asio::buffer; + payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); + payload->push_back(buffer(boundary_)); + payload->push_back(buffer(misc_strings::DOUBLE_DASHES)); + payload->push_back(buffer(misc_strings::CRLF)); +} + +} // namespace webcc diff --git a/webcc/body.h b/webcc/body.h new file mode 100644 index 0000000..c5874a0 --- /dev/null +++ b/webcc/body.h @@ -0,0 +1,125 @@ +#ifndef WEBCC_BODY_H_ +#define WEBCC_BODY_H_ + +#include +#include +#include // for move() + +#include "webcc/common.h" + +namespace webcc { + +// ----------------------------------------------------------------------------- + +class Body { +public: + virtual ~Body() = default; + + // Get the size in bytes of the body. + virtual std::size_t GetSize() const { + return 0; + } + + bool IsEmpty() const { + return GetSize() == 0; + } + +#if WEBCC_ENABLE_GZIP + // Compress with Gzip. + // If the body size <= the threshold (1400 bytes), no compression will be done + // and just return false. + virtual bool Compress() { + return false; + } +#endif // WEBCC_ENABLE_GZIP + + // Initialize the payload for iteration. + // Usage: + // InitPayload(); + // for (auto p = NextPayload(); !p.empty(); p = NextPayload()) { + // } + virtual void InitPayload() {} + + // Get the next payload. + // An empty payload returned indicates the end. + virtual Payload NextPayload() { + return {}; + } + + // Dump to output stream for logging purpose. + virtual void Dump(std::ostream& os, const std::string& prefix) const { + } +}; + +using BodyPtr = std::shared_ptr; + +// ----------------------------------------------------------------------------- + +class StringBody : public Body { +public: + explicit StringBody(const std::string& data) : data_(data) { + } + + explicit StringBody(std::string&& data) : data_(std::move(data)) { + } + + std::size_t GetSize() const override { + return data_.size(); + } + + const std::string& data() const { + return data_; + } + +#if WEBCC_ENABLE_GZIP + bool Compress() override; +#endif + + void InitPayload() override; + + Payload NextPayload() override; + + void Dump(std::ostream& os, const std::string& prefix) const override; + +private: + std::string data_; + + // Index for (not really) iterating the payload. + std::size_t index_ = 0; +}; + +// ----------------------------------------------------------------------------- + +// Multi-part form body for request. +class FormBody : public Body { +public: + FormBody(const std::vector& parts, + const std::string& boundary); + + std::size_t GetSize() const override; + + const std::vector& parts() const { + return parts_; + } + + void InitPayload() override; + + Payload NextPayload() override; + + void Dump(std::ostream& os, const std::string& prefix) const override; + +private: + void AddBoundary(Payload* payload); + void AddBoundaryEnd(Payload* payload); + +private: + std::vector parts_; + std::string boundary_; + + // Index for iterating the payload. + std::size_t index_ = 0; +}; + +} // namespace webcc + +#endif // WEBCC_BODY_H_ diff --git a/webcc/client.cc b/webcc/client.cc index 0aefda7..46a0a1e 100644 --- a/webcc/client.cc +++ b/webcc/client.cc @@ -89,7 +89,10 @@ void Client::Connect(RequestPtr request) { void Client::DoConnect(RequestPtr request, const std::string& default_port) { tcp::resolver resolver(io_context_); - std::string port = request->port(default_port); + std::string port = request->port(); + if (port.empty()) { + port = default_port; + } boost::system::error_code ec; auto endpoints = resolver.resolve(tcp::v4(), request->host(), port, ec); @@ -103,10 +106,8 @@ void Client::DoConnect(RequestPtr request, const std::string& default_port) { LOG_VERB("Connect to server..."); // Use sync API directly since we don't need timeout control. - socket_->Connect(request->host(), endpoints, &ec); - // Determine whether a connection was successfully established. - if (ec) { + if (!socket_->Connect(request->host(), endpoints, &ec)) { LOG_ERRO("Socket connect error (%s).", ec.message().c_str()); Close(); // TODO: Handshake error @@ -117,17 +118,29 @@ void Client::DoConnect(RequestPtr request, const std::string& default_port) { } void Client::WriteReqeust(RequestPtr request) { - LOG_VERB("HTTP request:\n%s", request->Dump(4, "> ").c_str()); + LOG_VERB("HTTP request:\n%s", request->Dump().c_str()); // NOTE: // It doesn't make much sense to set a timeout for socket write. // I find that it's almost impossible to simulate a situation in the server // side to test this timeout. + // Use sync API directly since we don't need timeout control. + boost::system::error_code ec; - // Use sync API directly since we don't need timeout control. - socket_->Write(*request, &ec); + if (socket_->Write(request->GetPayload(), &ec)) { + // Write request body. + if (request->body()) { + auto body = request->body(); + body->InitPayload(); + for (auto p = body->NextPayload(); !p.empty(); p = body->NextPayload()) { + if (!socket_->Write(p, &ec)) { + break; + } + } + } + } if (ec) { LOG_ERRO("Socket write error (%s).", ec.message().c_str()); @@ -147,7 +160,7 @@ void Client::ReadResponse() { DoReadResponse(); if (!error_) { - LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); + LOG_VERB("HTTP response:\n%s", response_->Dump().c_str()); } } diff --git a/webcc/client.h b/webcc/client.h index 93ab52e..a35fa32 100644 --- a/webcc/client.h +++ b/webcc/client.h @@ -18,9 +18,6 @@ namespace webcc { -class Client; -using ClientPtr = std::shared_ptr; - // Synchronous HTTP & HTTPS client. // In synchronous mode, a request won't return until the response is received // or timeout occurs. @@ -114,6 +111,8 @@ private: Error error_; }; +using ClientPtr = std::shared_ptr; + } // namespace webcc #endif // WEBCC_CLIENT_H_ diff --git a/webcc/client_session.cc b/webcc/client_session.cc index 90efed9..059a48d 100644 --- a/webcc/client_session.cc +++ b/webcc/client_session.cc @@ -7,10 +7,6 @@ namespace webcc { -ClientSession::ClientSession() { - InitHeaders(); -} - void ClientSession::Auth(const std::string& type, const std::string& credentials) { headers_.Set(headers::kAuthorization, type + " " + credentials); @@ -29,15 +25,15 @@ void ClientSession::AuthToken(const std::string& token) { ResponsePtr ClientSession::Request(RequestPtr request) { assert(request); - for (const auto& h : headers_.data()) { + for (auto& h : headers_.data()) { if (!request->HasHeader(h.first)) { request->SetHeader(h.first, h.second); } } - if (!content_type_.empty() && - !request->HasHeader(headers::kContentType)) { - request->SetContentType(content_type_, charset_); + if (!request->body()->IsEmpty() && + !media_type_.empty() && !request->HasHeader(headers::kContentType)) { + request->SetContentType(media_type_, charset_); } request->Prepare(); @@ -45,8 +41,7 @@ ResponsePtr ClientSession::Request(RequestPtr request) { return Send(request); } -static void SetHeaders(const std::vector& headers, - RequestBuilder* builder) { +static void SetHeaders(const Strings& headers, RequestBuilder* builder) { assert(headers.size() % 2 == 0); for (std::size_t i = 1; i < headers.size(); i += 2) { @@ -55,10 +50,26 @@ static void SetHeaders(const std::vector& headers, } ResponsePtr ClientSession::Get(const std::string& url, - const std::vector& parameters, - const std::vector& headers) { + const Strings& parameters, + const Strings& headers) { + RequestBuilder builder; + builder.Get(url); + + assert(parameters.size() % 2 == 0); + for (std::size_t i = 1; i < parameters.size(); i += 2) { + builder.Query(parameters[i - 1], parameters[i]); + } + + SetHeaders(headers, &builder); + + return Request(builder()); +} + +ResponsePtr ClientSession::Head(const std::string& url, + const Strings& parameters, + const Strings& headers) { RequestBuilder builder; - builder.Get().Url(url); + builder.Head(url); assert(parameters.size() % 2 == 0); for (std::size_t i = 1; i < parameters.size(); i += 2) { @@ -71,37 +82,41 @@ ResponsePtr ClientSession::Get(const std::string& url, } ResponsePtr ClientSession::Post(const std::string& url, std::string&& data, - bool json, - const std::vector& headers) { + bool json, const Strings& headers) { RequestBuilder builder; - builder.Post().Url(url); + builder.Post(url); SetHeaders(headers, &builder); - builder.Data(std::move(data)); - builder.Json(json); + builder.Body(std::move(data)); + + if (json) { + builder.Json(); + } return Request(builder()); } ResponsePtr ClientSession::Put(const std::string& url, std::string&& data, - bool json, - const std::vector& headers) { + bool json, const Strings& headers) { RequestBuilder builder; - builder.Put().Url(url); + builder.Put(url); SetHeaders(headers, &builder); - builder.Data(std::move(data)); - builder.Json(json); + builder.Body(std::move(data)); + + if (json) { + builder.Json(); + } return Request(builder()); } ResponsePtr ClientSession::Delete(const std::string& url, - const std::vector& headers) { + const Strings& headers) { RequestBuilder builder; - builder.Delete().Url(url); + builder.Delete(url); SetHeaders(headers, &builder); @@ -109,15 +124,17 @@ ResponsePtr ClientSession::Delete(const std::string& url, } ResponsePtr ClientSession::Patch(const std::string& url, std::string&& data, - bool json, - const std::vector& headers) { + bool json, const Strings& headers) { RequestBuilder builder; - builder.Patch().Url(url); + builder.Patch(url); SetHeaders(headers, &builder); - builder.Data(std::move(data)); - builder.Json(json); + builder.Body(std::move(data)); + + if (json) { + builder.Json(); + } return Request(builder()); } diff --git a/webcc/client_session.h b/webcc/client_session.h index 15e76fd..af76bc2 100644 --- a/webcc/client_session.h +++ b/webcc/client_session.h @@ -15,7 +15,9 @@ namespace webcc { // session for each thread instead. class ClientSession { public: - ClientSession(); + ClientSession() { + InitHeaders(); + } ~ClientSession() = default; @@ -37,8 +39,8 @@ public: headers_.Set(key, value); } - void set_content_type(const std::string& content_type) { - content_type_ = content_type; + void set_media_type(const std::string& media_type) { + media_type_ = media_type; } void set_charset(const std::string& charset) { @@ -59,25 +61,27 @@ public: ResponsePtr Request(RequestPtr request); // Shortcut for GET request. - ResponsePtr Get(const std::string& url, - const std::vector& parameters = {}, - const std::vector& headers = {}); + ResponsePtr Get(const std::string& url, const Strings& parameters = {}, + const Strings& headers = {}); + + // Shortcut for HEAD request. + ResponsePtr Head(const std::string& url, const Strings& parameters = {}, + const Strings& headers = {}); // Shortcut for POST request. ResponsePtr Post(const std::string& url, std::string&& data, bool json, - const std::vector& headers = {}); + const Strings& headers = {}); // Shortcut for PUT request. ResponsePtr Put(const std::string& url, std::string&& data, bool json, - const std::vector& headers = {}); + const Strings& headers = {}); // Shortcut for DELETE request. - ResponsePtr Delete(const std::string& url, - const std::vector& headers = {}); + ResponsePtr Delete(const std::string& url, const Strings& headers = {}); // Shortcut for PATCH request. ResponsePtr Patch(const std::string& url, std::string&& data, bool json, - const std::vector& headers = {}); + const Strings& headers = {}); private: void InitHeaders(); @@ -85,9 +89,11 @@ private: ResponsePtr Send(RequestPtr request); private: + // Default media type for `Content-Type` header. // E.g., "application/json". - std::string content_type_; + std::string media_type_; + // Default charset for `Content-Type` header. // E.g., "utf-8". std::string charset_; @@ -104,7 +110,7 @@ private: // Timeout in seconds for receiving response. int timeout_ = 0; - // Connection pool for keep-alive. + // Pool for Keep-Alive client connections. ClientPool pool_; }; diff --git a/webcc/common.cc b/webcc/common.cc index b6b7fa1..c567aa6 100644 --- a/webcc/common.cc +++ b/webcc/common.cc @@ -113,11 +113,14 @@ ContentType::ContentType(const std::string& str) { } void ContentType::Parse(const std::string& str) { + Reset(); + Init(str); +} + +void ContentType::Reset() { media_type_.clear(); additional_.clear(); multipart_ = false; - - Init(str); } bool ContentType::Valid() const { @@ -224,15 +227,17 @@ FormPart::FormPart(const std::string& name, std::string&& data, } void FormPart::Prepare(Payload* payload) { + using boost::asio::buffer; + + // NOTE: // The payload buffers don't own the memory. // It depends on some existing variables/objects to keep the memory. - // That's why we need |headers_|. + // That's why we need save headers to member variable. + if (headers_.empty()) { SetHeaders(); } - using boost::asio::buffer; - for (const Header& h : headers_.data()) { payload->push_back(buffer(h.first)); payload->push_back(buffer(misc_strings::HEADER_SEPARATOR)); @@ -249,6 +254,28 @@ void FormPart::Prepare(Payload* payload) { payload->push_back(buffer(misc_strings::CRLF)); } +std::size_t FormPart::GetSize() { + std::size_t size = 0; + + if (headers_.empty()) { + SetHeaders(); + } + + for (const Header& h : headers_.data()) { + size += h.first.size(); + size += sizeof(misc_strings::HEADER_SEPARATOR); + size += h.second.size(); + size += sizeof(misc_strings::CRLF); + } + size += sizeof(misc_strings::CRLF); + + size += data_.size(); + + size += sizeof(misc_strings::CRLF); + + return size; +} + void FormPart::SetHeaders() { // Header: Content-Disposition diff --git a/webcc/common.h b/webcc/common.h index 7204a9f..e9021d7 100644 --- a/webcc/common.h +++ b/webcc/common.h @@ -79,6 +79,8 @@ public: void Parse(const std::string& str); + void Reset(); + bool Valid() const; bool multipart() const { @@ -209,6 +211,10 @@ public: // API: CLIENT void Prepare(Payload* payload); + // Get the payload size. + // Used by the request to calculate content length. + std::size_t GetSize(); + private: // Generate headers from properties. void SetHeaders(); diff --git a/webcc/connection.cc b/webcc/connection.cc index 566fbd5..cb1c79b 100644 --- a/webcc/connection.cc +++ b/webcc/connection.cc @@ -91,7 +91,7 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) { return; } - LOG_VERB("HTTP request:\n%s", request_->Dump(4, "> ").c_str()); + LOG_VERB("HTTP request:\n%s", request_->Dump().c_str()); // Enqueue this connection. // Some worker thread will handle it later. @@ -99,36 +99,67 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) { } void Connection::DoWrite() { - LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); + LOG_VERB("HTTP response:\n%s", response_->Dump().c_str()); - boost::asio::async_write(socket_, response_->payload(), - std::bind(&Connection::OnWrite, shared_from_this(), - std::placeholders::_1, + // Firstly, write the headers. + boost::asio::async_write(socket_, response_->GetPayload(), + std::bind(&Connection::OnWriteHeaders, + shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } -// NOTE: -// This write handler will be called from main thread (the thread calling -// io_context.run), even though AsyncWrite() is invoked by worker threads. -// This is ensured by Asio. -void Connection::OnWrite(boost::system::error_code ec, std::size_t length) { +void Connection::OnWriteHeaders(boost::system::error_code ec, + std::size_t length) { if (ec) { - LOG_ERRO("Socket write error (%s).", ec.message().c_str()); + OnWriteError(ec); + } else { + // Write the body payload by payload. + response_->body()->InitPayload(); + DoWriteBody(); + } +} - if (ec != boost::asio::error::operation_aborted) { - pool_->Close(shared_from_this()); - } +void Connection::DoWriteBody() { + auto payload = response_->body()->NextPayload(); + + if (!payload.empty()) { + boost::asio::async_write(socket_, payload, + std::bind(&Connection::OnWriteBody, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); } else { - LOG_INFO("Response has been sent back, length: %u.", length); + // No more body payload left, we're done. + OnWriteOK(); + } +} - if (request_->IsConnectionKeepAlive()) { - LOG_INFO("The client asked for a keep-alive connection."); - LOG_INFO("Continue to read the next request..."); - Start(); - } else { - Shutdown(); - pool_->Close(shared_from_this()); - } +void Connection::OnWriteBody(boost::system::error_code ec, std::size_t length) { + if (ec) { + OnWriteError(ec); + } else { + DoWriteBody(); + } +} + +void Connection::OnWriteOK() { + LOG_INFO("Response has been sent back."); + + if (request_->IsConnectionKeepAlive()) { + LOG_INFO("The client asked for a keep-alive connection."); + LOG_INFO("Continue to read the next request..."); + Start(); + } else { + Shutdown(); + pool_->Close(shared_from_this()); + } +} + +void Connection::OnWriteError(boost::system::error_code ec) { + LOG_ERRO("Socket write error (%s).", ec.message().c_str()); + + if (ec != boost::asio::error::operation_aborted) { + pool_->Close(shared_from_this()); } } diff --git a/webcc/connection.h b/webcc/connection.h index 7e30f99..15d8862 100644 --- a/webcc/connection.h +++ b/webcc/connection.h @@ -51,7 +51,11 @@ private: void OnRead(boost::system::error_code ec, std::size_t length); void DoWrite(); - void OnWrite(boost::system::error_code ec, std::size_t length); + void OnWriteHeaders(boost::system::error_code ec, std::size_t length); + void DoWriteBody(); + void OnWriteBody(boost::system::error_code ec, std::size_t length); + void OnWriteOK(); + void OnWriteError(boost::system::error_code ec); // Shutdown the socket. void Shutdown(); diff --git a/webcc/globals.h b/webcc/globals.h index 9718e33..d854fc0 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -171,6 +171,7 @@ public: kSocketWriteError, kParseError, kFileError, + kDataError, }; public: diff --git a/webcc/message.cc b/webcc/message.cc index 80cc9aa..191ee78 100644 --- a/webcc/message.cc +++ b/webcc/message.cc @@ -22,12 +22,37 @@ const char CRLF[] = { '\r', '\n' }; // ----------------------------------------------------------------------------- -std::ostream& operator<<(std::ostream& os, const Message& message) { - message.Dump(os); - return os; +Message::Message() : body_(new Body{}), content_length_(kInvalidLength) { } -// ----------------------------------------------------------------------------- +void Message::SetBody(BodyPtr body, bool set_length) { + if (body == body_) { + return; + } + + if (!body) { + body_.reset(new Body{}); + } else { + body_ = body; + } + + if (set_length) { + content_length_ = body_->GetSize(); + SetHeader(headers::kContentLength, std::to_string(content_length_)); + } +} + +const std::string& Message::data() const { + static const std::string kEmptyData; + + auto string_body = std::dynamic_pointer_cast(body_); + + if (string_body) { + return string_body->data(); + } + + return kEmptyData; +} bool Message::IsConnectionKeepAlive() const { using headers::kConnection; @@ -51,12 +76,15 @@ ContentEncoding Message::GetContentEncoding() const { using headers::kContentEncoding; const std::string& encoding = GetHeader(kContentEncoding); + if (encoding == "gzip") { return ContentEncoding::kGzip; } + if (encoding == "deflate") { return ContentEncoding::kDeflate; } + return ContentEncoding::kUnknown; } @@ -66,7 +94,6 @@ bool Message::AcceptEncodingGzip() const { return GetHeader(kAcceptEncoding).find("gzip") != std::string::npos; } -// See: https://tools.ietf.org/html/rfc7231#section-3.1.1.1 void Message::SetContentType(const std::string& media_type, const std::string& charset) { using headers::kContentType; @@ -74,108 +101,47 @@ void Message::SetContentType(const std::string& media_type, if (charset.empty()) { SetHeader(kContentType, media_type); } else { - SetHeader(kContentType, media_type + ";charset=" + charset); - } -} - -void Message::SetContent(std::string&& content, bool set_length) { - content_ = std::move(content); - if (set_length) { - SetContentLength(content_.size()); + SetHeader(kContentType, media_type + "; charset=" + charset); } } -void Message::Prepare() { - assert(!start_line_.empty()); - +Payload Message::GetPayload() const { using boost::asio::buffer; - payload_.clear(); + Payload payload; - payload_.push_back(buffer(start_line_)); - payload_.push_back(buffer(misc_strings::CRLF)); + payload.push_back(buffer(start_line_)); + payload.push_back(buffer(misc_strings::CRLF)); for (const Header& h : headers_.data()) { - payload_.push_back(buffer(h.first)); - payload_.push_back(buffer(misc_strings::HEADER_SEPARATOR)); - payload_.push_back(buffer(h.second)); - payload_.push_back(buffer(misc_strings::CRLF)); - } - - payload_.push_back(buffer(misc_strings::CRLF)); - - if (!content_.empty()) { - payload_.push_back(buffer(content_)); + payload.push_back(buffer(h.first)); + payload.push_back(buffer(misc_strings::HEADER_SEPARATOR)); + payload.push_back(buffer(h.second)); + payload.push_back(buffer(misc_strings::CRLF)); } -} -void Message::CopyPayload(std::ostream& os) const { - for (const boost::asio::const_buffer& b : payload_) { - os.write(static_cast(b.data()), b.size()); - } -} + payload.push_back(buffer(misc_strings::CRLF)); -void Message::CopyPayload(std::string* str) const { - std::stringstream ss; - CopyPayload(ss); - *str = ss.str(); + return payload; } -void Message::Dump(std::ostream& os, std::size_t indent, - const std::string& prefix) const { - std::string indent_str; - if (indent > 0) { - indent_str.append(indent, ' '); - } - indent_str.append(prefix); +void Message::Dump(std::ostream& os) const { + const std::string prefix = " > "; - os << indent_str << start_line_ << std::endl; + os << prefix << start_line_ << std::endl; for (const Header& h : headers_.data()) { - os << indent_str << h.first << ": " << h.second << std::endl; + os << prefix << h.first << ": " << h.second << std::endl; } - os << indent_str << std::endl; - - // NOTE: - // - The content will be truncated if it's too large to display. - // - Binary content will not be dumped (TODO). - - if (!content_.empty()) { - if (indent == 0) { - if (content_.size() > kMaxDumpSize) { - os.write(content_.c_str(), kMaxDumpSize); - os << "..." << std::endl; - } else { - os << content_ << std::endl; - } - } else { - // Split by EOL to achieve more readability. - std::vector lines; - boost::split(lines, content_, boost::is_any_of("\n")); - - std::size_t size = 0; - - for (const std::string& line : lines) { - os << indent_str; - - if (line.size() + size > kMaxDumpSize) { - os.write(line.c_str(), kMaxDumpSize - size); - os << "..." << std::endl; - break; - } else { - os << line << std::endl; - size += line.size(); - } - } - } - } + os << prefix << std::endl; + + body_->Dump(os, prefix); } -std::string Message::Dump(std::size_t indent, - const std::string& prefix) const { +std::string Message::Dump() const { std::stringstream ss; - Dump(ss, indent, prefix); + Dump(ss); return ss.str(); } diff --git a/webcc/message.h b/webcc/message.h index 07387ad..8b027b8 100644 --- a/webcc/message.h +++ b/webcc/message.h @@ -1,48 +1,36 @@ #ifndef WEBCC_MESSAGE_H_ #define WEBCC_MESSAGE_H_ -#include +#include #include #include // for move() #include +#include "webcc/body.h" #include "webcc/common.h" #include "webcc/globals.h" namespace webcc { -class Message; -std::ostream& operator<<(std::ostream& os, const Message& message); - -// Base class for HTTP request and response messages. class Message { public: - Message() : content_length_(kInvalidLength) { - } + Message(); virtual ~Message() = default; - const std::string& start_line() const { - return start_line_; - } - - void set_start_line(const std::string& start_line) { - start_line_ = start_line; - } + // --------------------------------------------------------------------------- - std::size_t content_length() const { - return content_length_; - } + void SetBody(BodyPtr body, bool set_length); - void set_content_length(std::size_t content_length) { - content_length_ = content_length; + BodyPtr body() const { + return body_; } - const std::string& content() const { - return content_; - } + // Get the data from the string body. + // Exception Error(kDataError) will be thrown if the body is FormBody. + const std::string& data() const; - bool IsConnectionKeepAlive() const; + // --------------------------------------------------------------------------- void SetHeader(Header&& header) { headers_.Set(std::move(header.first), std::move(header.second)); @@ -61,70 +49,72 @@ public: return headers_.Has(key); } - ContentEncoding GetContentEncoding() const; + // --------------------------------------------------------------------------- - // Return true if header Accept-Encoding contains "gzip". - bool AcceptEncodingGzip() const; + const std::string& start_line() const { + return start_line_; + } + + void set_start_line(const std::string& start_line) { + start_line_ = start_line; + } - const ContentType& content_type() const { - return content_type_; + std::size_t content_length() const { + return content_length_; } - // TODO: Set header? - void SetContentType(const ContentType& content_type) { - content_type_ = content_type; + void set_content_length(std::size_t content_length) { + content_length_ = content_length; } + // --------------------------------------------------------------------------- + + // Check `Connection` header to see if it's "Keep-Alive". + bool IsConnectionKeepAlive() const; + + // Determine content encoding (gzip, deflate or unknown) from + // `Content-Encoding` header. + ContentEncoding GetContentEncoding() const; + + // Check `Accept-Encoding` header to see if it contains "gzip". + bool AcceptEncodingGzip() const; + + // Set `Content-Type` header. E.g., + // SetContentType("application/json; charset=utf-8") void SetContentType(const std::string& content_type) { SetHeader(headers::kContentType, content_type); } - // Example: SetContentType("application/json", "utf-8") + // Set `Content-Type` header. E.g., + // SetContentType("application/json", "utf-8") void SetContentType(const std::string& media_type, const std::string& charset); - void SetContent(std::string&& content, bool set_length); + // --------------------------------------------------------------------------- - // Prepare payload. - virtual void Prepare(); + // Make the message complete in order to be sent. + virtual void Prepare() = 0; - const Payload& payload() const { - return payload_; - } - - // Copy the exact payload to the given output stream. - void CopyPayload(std::ostream& os) const; - - // Copy the exact payload to the given string. - void CopyPayload(std::string* str) const; + // Get the payload for the socket to write. + // This doesn't include the payload(s) of the body! + Payload GetPayload() const; - // Dump to output stream. - void Dump(std::ostream& os, std::size_t indent = 0, - const std::string& prefix = "") const; + // --------------------------------------------------------------------------- - // Dump to string, only used by logger. - std::string Dump(std::size_t indent = 0, - const std::string& prefix = "") const; + // Dump to output stream for logging purpose. + void Dump(std::ostream& os) const; -protected: - void SetContentLength(std::size_t content_length) { - content_length_ = content_length; - SetHeader(headers::kContentLength, std::to_string(content_length)); - } + // Dump to string for logging purpose. + std::string Dump() const; protected: - std::string start_line_; + BodyPtr body_; - std::string content_; + Headers headers_; - ContentType content_type_; + std::string start_line_; std::size_t content_length_; - - Headers headers_; - - // NOTE: The payload itself doesn't hold the memory! - Payload payload_; }; } // namespace webcc diff --git a/webcc/parser.cc b/webcc/parser.cc index f4aca5c..2ce8f44 100644 --- a/webcc/parser.cc +++ b/webcc/parser.cc @@ -72,6 +72,7 @@ void Parser::Reset() { content_.clear(); content_length_ = kInvalidLength; + content_type_.Reset(); start_line_parsed_ = false; content_length_parsed_ = false; header_ended_ = false; @@ -161,12 +162,10 @@ bool Parser::ParseHeaderLine(const std::string& line) { return false; } } else if (boost::iequals(header.first, headers::kContentType)) { - ContentType content_type(header.second); - if (!content_type.Valid()) { + content_type_.Parse(header.second); + if (!content_type_.Valid()) { LOG_ERRO("Invalid content-type header: %s", header.second.c_str()); return false; - } else { - message_->SetContentType(content_type); } } else if (boost::iequals(header.first, headers::kTransferEncoding)) { if (header.second == "chunked") { @@ -209,7 +208,7 @@ bool Parser::ParseFixedContent(const char* data, std::size_t length) { // Don't have to firstly put the data to the pending data. AppendContent(data, length); - if (IsContentFull()) { + if (IsFixedContentFull()) { // All content has been read. Finish(); } @@ -306,7 +305,8 @@ bool Parser::Finish() { message_->set_content_length(content_length_); if (!IsContentCompressed()) { - message_->SetContent(std::move(content_), false); + auto body = std::make_shared(std::move(content_)); + message_->SetBody(body, false); return true; } @@ -320,7 +320,8 @@ bool Parser::Finish() { return false; } - message_->SetContent(std::move(decompressed), false); + auto body = std::make_shared(std::move(decompressed)); + message_->SetBody(body, false); return true; @@ -328,7 +329,8 @@ bool Parser::Finish() { LOG_WARN("Compressed HTTP content remains untouched."); - message_->SetContent(std::move(content_), false); + auto body = std::make_shared(std::move(content_)); + message_->SetBody(body, false); return true; @@ -343,7 +345,7 @@ void Parser::AppendContent(const std::string& data) { content_.append(data); } -bool Parser::IsContentFull() const { +bool Parser::IsFixedContentFull() const { return content_length_ != kInvalidLength && content_length_ <= content_.length(); } diff --git a/webcc/parser.h b/webcc/parser.h index cfadd8b..f724a97 100644 --- a/webcc/parser.h +++ b/webcc/parser.h @@ -22,9 +22,13 @@ public: void Init(Message* message); - bool finished() const { return finished_; } + bool finished() const { + return finished_; + } - std::size_t content_length() const { return content_length_; } + std::size_t content_length() const { + return content_length_; + } bool Parse(const char* data, std::size_t length); @@ -59,14 +63,13 @@ protected: void AppendContent(const char* data, std::size_t count); void AppendContent(const std::string& data); - // TODO: Rename to IsFixedContentFull. - bool IsContentFull() const; + bool IsFixedContentFull() const; // Check header Content-Encoding to see if the content is compressed. bool IsContentCompressed() const; protected: - // The result HTTP message. + // The message parsed. Message* message_; // Data waiting to be parsed. @@ -74,6 +77,7 @@ protected: // Temporary data and helper flags for parsing. std::size_t content_length_; + ContentType content_type_; std::string content_; bool start_line_parsed_; bool content_length_parsed_; diff --git a/webcc/request.cc b/webcc/request.cc index b689d12..c53960f 100644 --- a/webcc/request.cc +++ b/webcc/request.cc @@ -1,70 +1,22 @@ #include "webcc/request.h" -#include "webcc/logger.h" -#include "webcc/utility.h" - namespace webcc { -// ----------------------------------------------------------------------------- - -namespace misc_strings { - -// Literal strings can't be used because they have an extra '\0'. - -const char CRLF[] = { '\r', '\n' }; -const char DOUBLE_DASHES[] = { '-', '-' }; - -} // misc_strings - -// ----------------------------------------------------------------------------- - -void Request::Prepare() { - CreateStartLine(); - - if (url_.port().empty()) { - SetHeader(headers::kHost, url_.host()); - } else { - SetHeader(headers::kHost, url_.host() + ":" + url_.port()); - } - - if (form_parts_.empty()) { - Message::Prepare(); - return; - } - - // Multipart form data. - - // Another choice to generate the boundary is like what Apache does. - // See: https://stackoverflow.com/a/5686863 - if (boundary_.empty()) { - boundary_ = utility::RandomUuid(); - } - - SetContentType("multipart/form-data; boundary=" + boundary_); - - Payload data_payload; +bool Request::IsForm() const { + return !!std::dynamic_pointer_cast(body_); +} - for (auto& part : form_parts_) { - AddBoundary(data_payload); - part->Prepare(&data_payload); - } - AddBoundary(data_payload, true); +const std::vector& Request::form_parts() const { + auto form_body = std::dynamic_pointer_cast(body_); - // Update Content-Length header. - std::size_t content_length = 0; - for (auto& buffer : data_payload) { - content_length += buffer.size(); + if (!form_body) { + throw Error{ Error::kDataError, "Not a form body" }; } - SetContentLength(content_length); - // Prepare start line and headers. - Message::Prepare(); - - // Append payload of content data. - payload_.insert(payload_.end(), data_payload.begin(), data_payload.end()); + return form_body->parts(); } -void Request::CreateStartLine() { +void Request::Prepare() { if (!start_line_.empty()) { return; } @@ -83,17 +35,12 @@ void Request::CreateStartLine() { start_line_ += " "; start_line_ += target; start_line_ += " HTTP/1.1"; -} - -void Request::AddBoundary(Payload& payload, bool end) { - using boost::asio::buffer; - payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); - payload.push_back(buffer(boundary_)); - if (end) { - payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + if (url_.port().empty()) { + SetHeader(headers::kHost, url_.host()); + } else { + SetHeader(headers::kHost, url_.host() + ":" + url_.port()); } - payload.push_back(buffer(misc_strings::CRLF)); } } // namespace webcc diff --git a/webcc/request.h b/webcc/request.h index 727e9ad..d19e97a 100644 --- a/webcc/request.h +++ b/webcc/request.h @@ -10,62 +10,60 @@ namespace webcc { -class Request; -using RequestPtr = std::shared_ptr; - class Request : public Message { public: Request() = default; - Request(const std::string& method, const std::string& url) - : method_(method), url_(url) { + explicit Request(const std::string& method) : method_(method) { } ~Request() override = default; - const std::string& method() const { return method_; } - void set_method(const std::string& method) { method_ = method; } - - const Url& url() const { return url_; } - void set_url(const std::string& url) { url_.Init(url); } + const std::string& method() const { + return method_; + } - const std::string& host() const { return url_.host(); } - const std::string& port() const { return url_.port(); } + void set_method(const std::string& method) { + method_ = method; + } - UrlQuery query() const { return UrlQuery(url_.query()); } + const Url& url() const { + return url_; + } - // TODO: Remove - void AddQuery(const std::string& key, const std::string& value) { - url_.AddQuery(key, value); + void set_url(Url&& url) { + url_ = std::move(url); } - const UrlArgs& args() const { return args_; } - void set_args(const UrlArgs& args) { args_ = args; } + const std::string& host() const { + return url_.host(); + } - std::string port(const std::string& default_port) const { - return port().empty() ? default_port : port(); + const std::string& port() const { + return url_.port(); } - const std::vector& form_parts() const { - return form_parts_; + UrlQuery query() const { + return UrlQuery(url_.query()); } - void set_form_parts(std::vector&& form_parts) { - form_parts_ = std::move(form_parts); + const UrlArgs& args() const { + return args_; } - void AddFormPart(FormPartPtr form_part) { - form_parts_.push_back(form_part); + void set_args(const UrlArgs& args) { + args_ = args; } - // Prepare payload. - void Prepare() override; + // Check if the body is a multi-part form data. + bool IsForm() const; -private: - void CreateStartLine(); + // Get the form parts from the body. + // Only applicable to FormBody (i.e., multi-part form data). + // Otherwise, exception Error(kDataError) will be thrown. + const std::vector& form_parts() const; - // Add boundary to the payload for multipart form data. - void AddBoundary(Payload& payload, bool end = false); + void Prepare() override; private: std::string method_; @@ -75,12 +73,10 @@ private: // The URL regex matched arguments (usually resource ID's). // Used by server only. UrlArgs args_; - - std::vector form_parts_; - - std::string boundary_; }; +using RequestPtr = std::shared_ptr; + } // namespace webcc #endif // WEBCC_REQUEST_H_ diff --git a/webcc/request_builder.cc b/webcc/request_builder.cc index 4442cd4..493cb8e 100644 --- a/webcc/request_builder.cc +++ b/webcc/request_builder.cc @@ -11,33 +11,41 @@ namespace webcc { RequestPtr RequestBuilder::operator()() { - assert(parameters_.size() % 2 == 0); assert(headers_.size() % 2 == 0); - auto request = std::make_shared(method_, url_); + auto request = std::make_shared(method_); - for (std::size_t i = 1; i < parameters_.size(); i += 2) { - request->AddQuery(parameters_[i - 1], parameters_[i]); - } + request->set_url(std::move(url_)); for (std::size_t i = 1; i < headers_.size(); i += 2) { request->SetHeader(std::move(headers_[i - 1]), std::move(headers_[i])); } - // No keep-alive? + // If no Keep-Alive, explicitly set `Connection` to "Close". if (!keep_alive_) { request->SetHeader(headers::kConnection, "Close"); } - if (!data_.empty()) { - SetContent(request, std::move(data_)); + if (body_) { + request->SetContentType(media_type_, charset_); - // TODO: Request-level charset. - if (json_) { - request->SetContentType(media_types::kApplicationJson, ""); +#if WEBCC_ENABLE_GZIP + if (gzip_ && body_->Compress()) { + request->SetHeader(headers::kContentEncoding, "gzip"); } +#endif } else if (!form_parts_.empty()) { - request->set_form_parts(std::move(form_parts_)); + // Another choice to generate the boundary is like what Apache does. + // See: https://stackoverflow.com/a/5686863 + auto boundary = utility::RandomUuid(); + + request->SetContentType("multipart/form-data; boundary=" + boundary); + + body_ = std::make_shared(form_parts_, boundary); + } + + if (body_) { + request->SetBody(body_, true); } return request; @@ -84,21 +92,4 @@ RequestBuilder& RequestBuilder::Date() { return *this; } -void RequestBuilder::SetContent(RequestPtr request, std::string&& data) { -#if WEBCC_ENABLE_GZIP - if (gzip_ && data.size() > kGzipThreshold) { - std::string compressed; - if (gzip::Compress(data, &compressed)) { - request->SetContent(std::move(compressed), true); - request->SetHeader(headers::kContentEncoding, "gzip"); - return; - } - - LOG_WARN("Cannot compress the content data!"); - } -#endif // WEBCC_ENABLE_GZIP - - request->SetContent(std::move(data), true); -} - } // namespace webcc diff --git a/webcc/request_builder.h b/webcc/request_builder.h index 95a1f65..a8654eb 100644 --- a/webcc/request_builder.h +++ b/webcc/request_builder.h @@ -5,6 +5,7 @@ #include #include "webcc/request.h" +#include "webcc/url.h" namespace webcc { @@ -18,13 +19,6 @@ public: // Build the request. RequestPtr operator()(); - RequestBuilder& Get() { return Method(methods::kGet); } - RequestBuilder& Head() { return Method(methods::kHead); } - RequestBuilder& Post() { return Method(methods::kPost); } - RequestBuilder& Put() { return Method(methods::kPut); } - RequestBuilder& Delete() { return Method(methods::kDelete); } - RequestBuilder& Patch() { return Method(methods::kPatch); } - // NOTE: // The naming convention doesn't follow Google C++ Style for // consistency and simplicity. @@ -35,32 +29,73 @@ public: } RequestBuilder& Url(const std::string& url) { - url_ = url; + url_.Init(url); return *this; } + RequestBuilder& Get(const std::string& url) { + return Method(methods::kGet).Url(url); + } + + RequestBuilder& Head(const std::string& url) { + return Method(methods::kHead).Url(url); + } + + RequestBuilder& Post(const std::string& url) { + return Method(methods::kPost).Url(url); + } + + RequestBuilder& Put(const std::string& url) { + return Method(methods::kPut).Url(url); + } + + RequestBuilder& Delete(const std::string& url) { + return Method(methods::kDelete).Url(url); + } + + RequestBuilder& Patch(const std::string& url) { + return Method(methods::kPatch).Url(url); + } + + // Add a query parameter. RequestBuilder& Query(const std::string& key, const std::string& value) { - parameters_.push_back(key); - parameters_.push_back(value); + url_.AddQuery(key, value); + return *this; + } + + RequestBuilder& MediaType(const std::string& media_type) { + media_type_ = media_type; + return *this; + } + + RequestBuilder& Charset(const std::string& charset) { + charset_ = charset; + return *this; + } + + // Set Media Type to "application/json". + RequestBuilder& Json() { + media_type_ = media_types::kApplicationJson; return *this; } - RequestBuilder& Data(const std::string& data) { - data_ = data; + // Set Charset to "utf-8". + RequestBuilder& Utf8() { + charset_ = charsets::kUtf8; return *this; } - RequestBuilder& Data(std::string&& data) { - data_ = std::move(data); + RequestBuilder& Body(const std::string& data) { + body_.reset(new StringBody{ data }); return *this; } - RequestBuilder& Json(bool json = true) { - json_ = json; + RequestBuilder& Body(std::string&& data) { + body_.reset(new StringBody{ std::move(data) }); return *this; } - // Upload a file. + // Add a file to upload. RequestBuilder& File(const std::string& name, const Path& path, const std::string& media_type = ""); @@ -72,11 +107,6 @@ public: RequestBuilder& Form(const std::string& name, std::string&& data, const std::string& media_type = ""); - RequestBuilder& Gzip(bool gzip = true) { - gzip_ = gzip; - return *this; - } - RequestBuilder& Header(const std::string& key, const std::string& value) { headers_.push_back(key); headers_.push_back(value); @@ -91,44 +121,53 @@ public: RequestBuilder& Auth(const std::string& type, const std::string& credentials); RequestBuilder& AuthBasic(const std::string& login, - const std::string& password); + const std::string& password); RequestBuilder& AuthToken(const std::string& token); - // Add the Date header to the request. + // Add the `Date` header to the request. RequestBuilder& Date(); -private: - void SetContent(RequestPtr request, std::string&& data); - +#if WEBCC_ENABLE_GZIP + RequestBuilder& Gzip(bool gzip = true) { + gzip_ = gzip; + return *this; + } +#endif // WEBCC_ENABLE_GZIP + private: std::string method_; - std::string url_; + // Namespace is added to avoid the conflict with `Url()` method. + webcc::Url url_; - // URL query parameters. - std::vector parameters_; + // Request body. + BodyPtr body_; - // Data to send in the body of the request. - std::string data_; + // Media type of the body (e.g., "application/json"). + std::string media_type_; - // Is the data to send a JSON string? - bool json_ = false; + // Character set of the body (e.g., "utf-8"). + std::string charset_; // Files to upload for a POST request. std::vector form_parts_; - // Compress the content. - // NOTE: Most servers don't support compressed requests. - // Even the requests module from Python doesn't have a built-in support. - // See: https://github.com/kennethreitz/requests/issues/1753 - bool gzip_ = false; - - // Additional headers. - std::vector headers_; + // Additional headers with the following sequence: + // { key1, value1, key2, value2, ... } + Strings headers_; // Persistent connection. bool keep_alive_ = true; + +#if WEBCC_ENABLE_GZIP + // Compress the body data (only for string body). + // NOTE: + // Most servers don't support compressed requests. + // Even the requests module from Python doesn't have a built-in support. + // See: https://github.com/kennethreitz/requests/issues/1753 + bool gzip_ = false; +#endif // WEBCC_ENABLE_GZIP }; } // namespace webcc diff --git a/webcc/request_handler.cc b/webcc/request_handler.cc index aa10d18..60c84a3 100644 --- a/webcc/request_handler.cc +++ b/webcc/request_handler.cc @@ -183,18 +183,22 @@ bool RequestHandler::ServeStatic(ConnectionPtr connection) { Path p = doc_root_ / path; - std::string content; - if (!ReadFile(p, &content)) { + std::string data; + if (!ReadFile(p, &data)) { connection->SendResponse(Status::kNotFound); return false; } auto response = std::make_shared(Status::kOK); - if (!content.empty()) { + if (!data.empty()) { std::string extension = p.extension().string(); response->SetContentType(media_types::FromExtension(extension), ""); - response->SetContent(std::move(content), true); + + // TODO: Use FileBody instead for streaming. + // TODO: gzip + auto body = std::make_shared(std::move(data)); + response->SetBody(body, true); } // Send response back to client. @@ -203,21 +207,4 @@ bool RequestHandler::ServeStatic(ConnectionPtr connection) { return true; } -void RequestHandler::SetContent(RequestPtr request, ResponsePtr response, - std::string&& content) { -#if WEBCC_ENABLE_GZIP - // Only support gzip (no deflate) for response compression. - if (content.size() > kGzipThreshold && request->AcceptEncodingGzip()) { - std::string compressed; - if (gzip::Compress(content, &compressed)) { - response->SetHeader(headers::kContentEncoding, "gzip"); - response->SetContent(std::move(compressed), true); - return; - } - } -#endif // WEBCC_ENABLE_GZIP - - response->SetContent(std::move(content), true); -} - } // namespace webcc diff --git a/webcc/request_handler.h b/webcc/request_handler.h index de9dd42..b6d5a66 100644 --- a/webcc/request_handler.h +++ b/webcc/request_handler.h @@ -73,9 +73,6 @@ private: // TODO bool ServeStatic(ConnectionPtr connection); - void SetContent(RequestPtr request, ResponsePtr response, - std::string&& content); - private: struct RouteInfo { std::string url; diff --git a/webcc/request_parser.cc b/webcc/request_parser.cc index 00295f1..9fe294a 100644 --- a/webcc/request_parser.cc +++ b/webcc/request_parser.cc @@ -28,7 +28,7 @@ bool RequestParser::ParseStartLine(const std::string& line) { } request_->set_method(std::move(strs[0])); - request_->set_url(std::move(strs[1])); + request_->set_url(Url(strs[1])); // HTTP version is ignored. @@ -39,7 +39,7 @@ bool RequestParser::ParseContent(const char* data, std::size_t length) { if (chunked_) { return ParseChunkedContent(data, length); } else { - if (request_->content_type().multipart()) { + if (content_type_.multipart()) { return ParseMultipartContent(data, length); } else { return ParseFixedContent(data, length); @@ -122,8 +122,8 @@ bool RequestParser::ParseMultipartContent(const char* data, return false; } - // Add this part to request. - request_->AddFormPart(part_); + // Save this part + form_parts_.push_back(part_); // Reset for next part. part_.reset(); @@ -142,6 +142,14 @@ bool RequestParser::ParseMultipartContent(const char* data, if (step_ == Step::kEnded) { LOG_INFO("Multipart data has ended."); + + // Create a body and set to the request. + + auto body = std::make_shared(form_parts_, + content_type_.boundary()); + + request_->SetBody(body, false); // TODO: set_length? + Finish(); } @@ -229,7 +237,7 @@ bool RequestParser::GetNextBoundaryLine(std::size_t* b_off, bool RequestParser::IsBoundary(const std::string& str, std::size_t off, std::size_t count, bool* end) const { - const std::string& boundary = request_->content_type().boundary(); + const std::string& boundary = content_type_.boundary(); if (count != boundary.size() + 2 && count != boundary.size() + 4) { return false; diff --git a/webcc/request_parser.h b/webcc/request_parser.h index 9c1e685..d18f650 100644 --- a/webcc/request_parser.h +++ b/webcc/request_parser.h @@ -37,6 +37,7 @@ private: private: Request* request_; + // Form data parsing step. enum Step { kStart, kBoundaryParsed, @@ -45,7 +46,11 @@ private: }; Step step_ = kStart; + // The current form part being parsed. FormPartPtr part_; + + // All form parts parsed. + std::vector form_parts_; }; } // namespace webcc diff --git a/webcc/response.cc b/webcc/response.cc index d299c6b..3720e32 100644 --- a/webcc/response.cc +++ b/webcc/response.cc @@ -4,15 +4,6 @@ namespace webcc { -void Response::Prepare() { - PrepareStatusLine(); - - SetHeader(headers::kServer, utility::UserAgent()); - SetHeader(headers::kDate, utility::GetTimestamp()); - - Message::Prepare(); -} - static const std::pair kTable[] = { { Status::kOK, "OK" }, { Status::kCreated, "Created" }, @@ -35,7 +26,7 @@ static const char* GetReason(int status) { return ""; } -void Response::PrepareStatusLine() { +void Response::Prepare() { if (!start_line_.empty()) { return; } @@ -49,6 +40,8 @@ void Response::PrepareStatusLine() { } else { start_line_ += reason_; } + + SetHeader(headers::kServer, utility::UserAgent()); } } // namespace webcc diff --git a/webcc/response.h b/webcc/response.h index e79e825..f684047 100644 --- a/webcc/response.h +++ b/webcc/response.h @@ -8,9 +8,6 @@ namespace webcc { -class Response; -using ResponsePtr = std::shared_ptr; - class Response : public Message { public: explicit Response(Status status = Status::kOK) : status_(status) { @@ -36,14 +33,13 @@ public: void Prepare() override; -private: - void PrepareStatusLine(); - private: int status_; // Status code std::string reason_; // Reason phrase }; +using ResponsePtr = std::shared_ptr; + } // namespace webcc #endif // WEBCC_RESPONSE_H_ diff --git a/webcc/response_builder.cc b/webcc/response_builder.cc index 05dc635..ea5f426 100644 --- a/webcc/response_builder.cc +++ b/webcc/response_builder.cc @@ -13,22 +13,30 @@ namespace webcc { ResponsePtr ResponseBuilder::operator()() { assert(headers_.size() % 2 == 0); - auto request = std::make_shared(code_); + auto response = std::make_shared(code_); for (std::size_t i = 1; i < headers_.size(); i += 2) { - request->SetHeader(std::move(headers_[i - 1]), std::move(headers_[i])); + response->SetHeader(std::move(headers_[i - 1]), std::move(headers_[i])); } - if (!data_.empty()) { - SetContent(request, std::move(data_)); + if (body_) { + response->SetContentType(media_type_, charset_); - // TODO: charset. - if (json_) { - request->SetContentType(media_types::kApplicationJson, ""); +#if WEBCC_ENABLE_GZIP + if (gzip_) { + // Don't try to compress the response if the request doesn't accept gzip. + if (request_ && request_->AcceptEncodingGzip()) { + if (body_->Compress()) { + response->SetHeader(headers::kContentEncoding, "gzip"); + } + } } +#endif // WEBCC_ENABLE_GZIP + + response->SetBody(body_, true); } - return request; + return response; } ResponseBuilder& ResponseBuilder::Date() { @@ -37,21 +45,4 @@ ResponseBuilder& ResponseBuilder::Date() { return *this; } -void ResponseBuilder::SetContent(ResponsePtr response, std::string&& data) { -#if WEBCC_ENABLE_GZIP - if (gzip_ && data.size() > kGzipThreshold) { - std::string compressed; - if (gzip::Compress(data, &compressed)) { - response->SetContent(std::move(compressed), true); - response->SetHeader(headers::kContentEncoding, "gzip"); - return; - } - - LOG_WARN("Cannot compress the content data!"); - } -#endif // WEBCC_ENABLE_GZIP - - response->SetContent(std::move(data), true); -} - } // namespace webcc diff --git a/webcc/response_builder.h b/webcc/response_builder.h index 0934365..5ad24c3 100644 --- a/webcc/response_builder.h +++ b/webcc/response_builder.h @@ -4,6 +4,7 @@ #include #include +#include "webcc/request.h" #include "webcc/response.h" namespace webcc { @@ -12,6 +13,12 @@ class ResponseBuilder { public: ResponseBuilder() = default; + // NOTE: + // Currently, |request| is necessary only when Gzip is enabled and the client + // does want to accept Gzip compressed response. + explicit ResponseBuilder(RequestPtr request) : request_(request) { + } + ResponseBuilder(const ResponseBuilder&) = delete; ResponseBuilder& operator=(const ResponseBuilder&) = delete; @@ -34,23 +41,35 @@ public: return *this; } - ResponseBuilder& Data(const std::string& data) { - data_ = data; + ResponseBuilder& MediaType(const std::string& media_type) { + media_type_ = media_type; return *this; } - ResponseBuilder& Data(std::string&& data) { - data_ = std::move(data); + ResponseBuilder& Charset(const std::string& charset) { + charset_ = charset; return *this; } - ResponseBuilder& Json(bool json = true) { - json_ = json; + // Set Media Type to "application/json". + ResponseBuilder& Json() { + media_type_ = media_types::kApplicationJson; return *this; } - ResponseBuilder& Gzip(bool gzip = true) { - gzip_ = gzip; + // Set Charset to "utf-8". + ResponseBuilder& Utf8() { + charset_ = charsets::kUtf8; + return *this; + } + + ResponseBuilder& Body(const std::string& data) { + body_.reset(new StringBody{ data }); + return *this; + } + + ResponseBuilder& Body(std::string&& data) { + body_.reset(new StringBody{ std::move(data) }); return *this; } @@ -60,24 +79,35 @@ public: return *this; } - // Add the Date header to the response. + // Add the `Date` header to the response. ResponseBuilder& Date(); -private: - void SetContent(ResponsePtr response, std::string&& data); +#if WEBCC_ENABLE_GZIP + ResponseBuilder& Gzip(bool gzip = true) { + gzip_ = gzip; + return *this; + } +#endif // WEBCC_ENABLE_GZIP private: + RequestPtr request_; + // Status code. Status code_ = Status::kOK; - // Data to send in the body of the request. - std::string data_; + // Response body. + BodyPtr body_; + + // Media type of the body (e.g., "application/json"). + std::string media_type_; - // Is the data to send a JSON string? - bool json_ = false; + // Character set of the body (e.g., "utf-8"). + std::string charset_; - // Compress the response content. +#if WEBCC_ENABLE_GZIP + // Compress the body data (only for string body). bool gzip_ = false; +#endif // WEBCC_ENABLE_GZIP // Additional headers. std::vector headers_; diff --git a/webcc/response_parser.cc b/webcc/response_parser.cc index 3e1be1d..f149d5e 100644 --- a/webcc/response_parser.cc +++ b/webcc/response_parser.cc @@ -2,19 +2,12 @@ #include "boost/algorithm/string.hpp" -#include "webcc/response.h" #include "webcc/logger.h" +#include "webcc/response.h" namespace webcc { -ResponseParser::ResponseParser(Response* response) - : Parser(response), response_(response) { -} - -void ResponseParser::Init(Response* response) { - Parser::Init(response); - response_ = response; -} +// ----------------------------------------------------------------------------- namespace { @@ -43,6 +36,17 @@ void SplitStartLine(const std::string& line, std::vector* parts) { } // namespace +// ----------------------------------------------------------------------------- + +ResponseParser::ResponseParser(Response* response) + : Parser(response), response_(response) { +} + +void ResponseParser::Init(Response* response) { + Parser::Init(response); + response_ = response; +} + bool ResponseParser::ParseStartLine(const std::string& line) { std::vector parts; SplitStartLine(line, &parts); diff --git a/webcc/socket.cc b/webcc/socket.cc index ffca6a6..38a7c8c 100644 --- a/webcc/socket.cc +++ b/webcc/socket.cc @@ -23,19 +23,21 @@ namespace webcc { // ----------------------------------------------------------------------------- -Socket::Socket(boost::asio::io_context& io_context) - : socket_(io_context) { +Socket::Socket(boost::asio::io_context& io_context) : socket_(io_context) { } -void Socket::Connect(const std::string& host, const Endpoints& endpoints, +bool Socket::Connect(const std::string& host, const Endpoints& endpoints, boost::system::error_code* ec) { boost::ignore_unused(host); boost::asio::connect(socket_, endpoints, *ec); + + return !(*ec); } -void Socket::Write(const Request& request, boost::system::error_code* ec) { - boost::asio::write(socket_, request.payload(), *ec); +bool Socket::Write(const Payload& payload, boost::system::error_code* ec) { + boost::asio::write(socket_, payload, *ec); + return !(*ec); } void Socket::AsyncReadSome(ReadHandler&& handler, std::vector* buffer) { @@ -102,19 +104,20 @@ SslSocket::SslSocket(boost::asio::io_context& io_context, bool ssl_verify) #endif // defined(_WIN32) || defined(_WIN64) } -void SslSocket::Connect(const std::string& host, const Endpoints& endpoints, +bool SslSocket::Connect(const std::string& host, const Endpoints& endpoints, boost::system::error_code* ec) { boost::asio::connect(ssl_socket_.lowest_layer(), endpoints, *ec); if (*ec) { - return; + return false; } - Handshake(host, ec); + return Handshake(host, ec); } -void SslSocket::Write(const Request& request, boost::system::error_code* ec) { - boost::asio::write(ssl_socket_, request.payload(), *ec); +bool SslSocket::Write(const Payload& payload, boost::system::error_code* ec) { + boost::asio::write(ssl_socket_, payload, *ec); + return !(*ec); } void SslSocket::AsyncReadSome(ReadHandler&& handler, @@ -126,7 +129,7 @@ void SslSocket::Close(boost::system::error_code* ec) { ssl_socket_.lowest_layer().close(*ec); } -void SslSocket::Handshake(const std::string& host, +bool SslSocket::Handshake(const std::string& host, boost::system::error_code* ec) { if (ssl_verify_) { ssl_socket_.set_verify_mode(ssl::verify_peer); @@ -141,7 +144,10 @@ void SslSocket::Handshake(const std::string& host, if (*ec) { LOG_ERRO("Handshake error (%s).", ec->message().c_str()); + return false; } + + return true; } #endif // WEBCC_ENABLE_SSL diff --git a/webcc/socket.h b/webcc/socket.h index 8cbd1f6..9ddea48 100644 --- a/webcc/socket.h +++ b/webcc/socket.h @@ -26,10 +26,10 @@ public: std::function; // TODO: Remove |host| - virtual void Connect(const std::string& host, const Endpoints& endpoints, + virtual bool Connect(const std::string& host, const Endpoints& endpoints, boost::system::error_code* ec) = 0; - virtual void Write(const Request& request, boost::system::error_code* ec) = 0; + virtual bool Write(const Payload& payload, boost::system::error_code* ec) = 0; virtual void AsyncReadSome(ReadHandler&& handler, std::vector* buffer) = 0; @@ -43,10 +43,10 @@ class Socket : public SocketBase { public: explicit Socket(boost::asio::io_context& io_context); - void Connect(const std::string& host, const Endpoints& endpoints, + bool Connect(const std::string& host, const Endpoints& endpoints, boost::system::error_code* ec) override; - void Write(const Request& request, boost::system::error_code* ec) override; + bool Write(const Payload& payload, boost::system::error_code* ec) override; void AsyncReadSome(ReadHandler&& handler, std::vector* buffer) override; @@ -65,17 +65,17 @@ public: explicit SslSocket(boost::asio::io_context& io_context, bool ssl_verify = true); - void Connect(const std::string& host, const Endpoints& endpoints, + bool Connect(const std::string& host, const Endpoints& endpoints, boost::system::error_code* ec) override; - void Write(const Request& request, boost::system::error_code* ec) override; + bool Write(const Payload& payload, boost::system::error_code* ec) override; void AsyncReadSome(ReadHandler&& handler, std::vector* buffer) override; void Close(boost::system::error_code* ec) override; private: - void Handshake(const std::string& host, boost::system::error_code* ec); + bool Handshake(const std::string& host, boost::system::error_code* ec); boost::asio::ssl::context ssl_context_;