Rework the body of request and response
This commit is contained in:
@@ -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 (<NotFound>) {
|
||||
// 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(<JsonStringOfTheBook>).Json()();
|
||||
}
|
||||
|
||||
// Convert the book to JSON string and set as response data.
|
||||
return webcc::ResponseBuilder{}.OK().Data(<JsonStringOfTheBook>).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[]) {
|
||||
// ...
|
||||
|
||||
server.Route("/books", std::make_shared<BookListView>(), { "GET", "POST" });
|
||||
try {
|
||||
webcc::Server server(8080, 2);
|
||||
|
||||
server.Route(webcc::R("/books/(\\d+)"), std::make_shared<BookDetailView>(),
|
||||
{ "GET", "PUT", "DELETE" });
|
||||
server.Route("/books", std::make_shared<BookListView>(), { "GET", "POST" });
|
||||
|
||||
server.Run();
|
||||
server.Route(webcc::R("/books/(\\d+)"), std::make_shared<BookDetailView>(),
|
||||
{ "GET", "PUT", "DELETE" });
|
||||
|
||||
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.
|
||||
|
||||
+140
-13
@@ -1,3 +1,5 @@
|
||||
#include <iostream>
|
||||
|
||||
#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)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
());
|
||||
|
||||
@@ -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;
|
||||
|
||||
for (auto& part : request->form_parts()) {
|
||||
std::cout << "name: " << part->name() << std::endl;
|
||||
std::cout << "data: " << std::endl << part->data() << std::endl;
|
||||
}
|
||||
|
||||
// TODO: media_type: webcc::media_types::kTextPlain; charset = "utf-8";
|
||||
return webcc::ResponseBuilder{}.Created().Data("OK")();
|
||||
return Post(request);
|
||||
}
|
||||
|
||||
return webcc::ResponseBuilder{}.NotImplemented()();
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
webcc::ResponsePtr Post(webcc::RequestPtr request) {
|
||||
std::cout << "form parts: " << request->form_parts().size() << std::endl;
|
||||
|
||||
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{}.Created().Body("OK")();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+10
-10
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+144
@@ -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<std::string> 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<FormPartPtr>& 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
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
#ifndef WEBCC_BODY_H_
|
||||
#define WEBCC_BODY_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility> // 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<Body>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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<FormPartPtr>& parts,
|
||||
const std::string& boundary);
|
||||
|
||||
std::size_t GetSize() const override;
|
||||
|
||||
const std::vector<FormPartPtr>& 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<FormPartPtr> parts_;
|
||||
std::string boundary_;
|
||||
|
||||
// Index for iterating the payload.
|
||||
std::size_t index_ = 0;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_BODY_H_
|
||||
+21
-8
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -18,9 +18,6 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
class Client;
|
||||
using ClientPtr = std::shared_ptr<Client>;
|
||||
|
||||
// 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<Client>;
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_CLIENT_H_
|
||||
|
||||
+47
-30
@@ -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<std::string>& 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<std::string>& headers,
|
||||
}
|
||||
|
||||
ResponsePtr ClientSession::Get(const std::string& url,
|
||||
const std::vector<std::string>& parameters,
|
||||
const std::vector<std::string>& headers) {
|
||||
const Strings& parameters,
|
||||
const Strings& headers) {
|
||||
RequestBuilder builder;
|
||||
builder.Get().Url(url);
|
||||
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.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<std::string>& 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<std::string>& 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<std::string>& 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<std::string>& 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());
|
||||
}
|
||||
|
||||
+19
-13
@@ -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<std::string>& parameters = {},
|
||||
const std::vector<std::string>& 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<std::string>& headers = {});
|
||||
const Strings& headers = {});
|
||||
|
||||
// Shortcut for PUT request.
|
||||
ResponsePtr Put(const std::string& url, std::string&& data, bool json,
|
||||
const std::vector<std::string>& headers = {});
|
||||
const Strings& headers = {});
|
||||
|
||||
// Shortcut for DELETE request.
|
||||
ResponsePtr Delete(const std::string& url,
|
||||
const std::vector<std::string>& 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<std::string>& 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_;
|
||||
};
|
||||
|
||||
|
||||
+32
-5
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
+55
-24
@@ -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());
|
||||
|
||||
if (ec != boost::asio::error::operation_aborted) {
|
||||
pool_->Close(shared_from_this());
|
||||
}
|
||||
OnWriteError(ec);
|
||||
} else {
|
||||
LOG_INFO("Response has been sent back, length: %u.", length);
|
||||
// Write the body payload by payload.
|
||||
response_->body()->InitPayload();
|
||||
DoWriteBody();
|
||||
}
|
||||
}
|
||||
|
||||
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::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 {
|
||||
// No more body payload left, we're done.
|
||||
OnWriteOK();
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -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();
|
||||
|
||||
@@ -171,6 +171,7 @@ public:
|
||||
kSocketWriteError,
|
||||
kParseError,
|
||||
kFileError,
|
||||
kDataError,
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
+51
-85
@@ -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<StringBody>(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);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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(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));
|
||||
payload.push_back(buffer(misc_strings::CRLF));
|
||||
|
||||
if (!content_.empty()) {
|
||||
payload_.push_back(buffer(content_));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
void Message::CopyPayload(std::ostream& os) const {
|
||||
for (const boost::asio::const_buffer& b : payload_) {
|
||||
os.write(static_cast<const char*>(b.data()), b.size());
|
||||
}
|
||||
}
|
||||
void Message::Dump(std::ostream& os) const {
|
||||
const std::string prefix = " > ";
|
||||
|
||||
void Message::CopyPayload(std::string* str) const {
|
||||
std::stringstream ss;
|
||||
CopyPayload(ss);
|
||||
*str = ss.str();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
os << prefix << 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<std::string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
+58
-68
@@ -1,48 +1,36 @@
|
||||
#ifndef WEBCC_MESSAGE_H_
|
||||
#define WEBCC_MESSAGE_H_
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility> // for move()
|
||||
#include <vector>
|
||||
|
||||
#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 SetBody(BodyPtr body, bool set_length);
|
||||
|
||||
BodyPtr body() const {
|
||||
return body_;
|
||||
}
|
||||
|
||||
void set_start_line(const std::string& start_line) {
|
||||
start_line_ = start_line;
|
||||
}
|
||||
// Get the data from the string body.
|
||||
// Exception Error(kDataError) will be thrown if the body is FormBody.
|
||||
const std::string& data() const;
|
||||
|
||||
std::size_t content_length() const {
|
||||
return content_length_;
|
||||
}
|
||||
|
||||
void set_content_length(std::size_t content_length) {
|
||||
content_length_ = content_length;
|
||||
}
|
||||
|
||||
const std::string& content() const {
|
||||
return content_;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 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;
|
||||
|
||||
// Return true if header Accept-Encoding contains "gzip".
|
||||
// Check `Accept-Encoding` header to see if it contains "gzip".
|
||||
bool AcceptEncodingGzip() const;
|
||||
|
||||
const ContentType& content_type() const {
|
||||
return content_type_;
|
||||
}
|
||||
|
||||
// TODO: Set header?
|
||||
void SetContentType(const ContentType& content_type) {
|
||||
content_type_ = content_type;
|
||||
}
|
||||
|
||||
// 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_;
|
||||
}
|
||||
// Get the payload for the socket to write.
|
||||
// This doesn't include the payload(s) of the body!
|
||||
Payload GetPayload() const;
|
||||
|
||||
// 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;
|
||||
// Dump to output stream for logging purpose.
|
||||
void Dump(std::ostream& os) 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 string for logging purpose.
|
||||
std::string Dump() const;
|
||||
|
||||
protected:
|
||||
void SetContentLength(std::size_t content_length) {
|
||||
content_length_ = content_length;
|
||||
SetHeader(headers::kContentLength, std::to_string(content_length));
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string start_line_;
|
||||
|
||||
std::string content_;
|
||||
|
||||
ContentType content_type_;
|
||||
|
||||
std::size_t content_length_;
|
||||
BodyPtr body_;
|
||||
|
||||
Headers headers_;
|
||||
|
||||
// NOTE: The payload itself doesn't hold the memory!
|
||||
Payload payload_;
|
||||
std::string start_line_;
|
||||
|
||||
std::size_t content_length_;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+11
-9
@@ -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<StringBody>(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<StringBody>(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<StringBody>(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();
|
||||
}
|
||||
|
||||
+9
-5
@@ -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_;
|
||||
|
||||
+17
-70
@@ -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;
|
||||
|
||||
for (auto& part : form_parts_) {
|
||||
AddBoundary(data_payload);
|
||||
part->Prepare(&data_payload);
|
||||
}
|
||||
AddBoundary(data_payload, true);
|
||||
|
||||
// Update Content-Length header.
|
||||
std::size_t content_length = 0;
|
||||
for (auto& buffer : data_payload) {
|
||||
content_length += buffer.size();
|
||||
}
|
||||
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());
|
||||
bool Request::IsForm() const {
|
||||
return !!std::dynamic_pointer_cast<FormBody>(body_);
|
||||
}
|
||||
|
||||
void Request::CreateStartLine() {
|
||||
const std::vector<FormPartPtr>& Request::form_parts() const {
|
||||
auto form_body = std::dynamic_pointer_cast<FormBody>(body_);
|
||||
|
||||
if (!form_body) {
|
||||
throw Error{ Error::kDataError, "Not a form body" };
|
||||
}
|
||||
|
||||
return form_body->parts();
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+37
-41
@@ -10,63 +10,61 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
class Request;
|
||||
using RequestPtr = std::shared_ptr<Request>;
|
||||
|
||||
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& host() const { return url_.host(); }
|
||||
const std::string& port() const { return url_.port(); }
|
||||
|
||||
UrlQuery query() const { return UrlQuery(url_.query()); }
|
||||
|
||||
// TODO: Remove
|
||||
void AddQuery(const std::string& key, const std::string& value) {
|
||||
url_.AddQuery(key, value);
|
||||
const std::string& method() const {
|
||||
return method_;
|
||||
}
|
||||
|
||||
const UrlArgs& args() const { return args_; }
|
||||
void set_args(const UrlArgs& args) { args_ = args; }
|
||||
|
||||
std::string port(const std::string& default_port) const {
|
||||
return port().empty() ? default_port : port();
|
||||
void set_method(const std::string& method) {
|
||||
method_ = method;
|
||||
}
|
||||
|
||||
const std::vector<FormPartPtr>& form_parts() const {
|
||||
return form_parts_;
|
||||
const Url& url() const {
|
||||
return url_;
|
||||
}
|
||||
|
||||
void set_form_parts(std::vector<FormPartPtr>&& form_parts) {
|
||||
form_parts_ = std::move(form_parts);
|
||||
void set_url(Url&& url) {
|
||||
url_ = std::move(url);
|
||||
}
|
||||
|
||||
void AddFormPart(FormPartPtr form_part) {
|
||||
form_parts_.push_back(form_part);
|
||||
const std::string& host() const {
|
||||
return url_.host();
|
||||
}
|
||||
|
||||
// Prepare payload.
|
||||
const std::string& port() const {
|
||||
return url_.port();
|
||||
}
|
||||
|
||||
UrlQuery query() const {
|
||||
return UrlQuery(url_.query());
|
||||
}
|
||||
|
||||
const UrlArgs& args() const {
|
||||
return args_;
|
||||
}
|
||||
|
||||
void set_args(const UrlArgs& args) {
|
||||
args_ = args;
|
||||
}
|
||||
|
||||
// Check if the body is a multi-part form data.
|
||||
bool IsForm() const;
|
||||
|
||||
// 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<FormPartPtr>& form_parts() const;
|
||||
|
||||
void Prepare() override;
|
||||
|
||||
private:
|
||||
void CreateStartLine();
|
||||
|
||||
// Add boundary to the payload for multipart form data.
|
||||
void AddBoundary(Payload& payload, bool end = false);
|
||||
|
||||
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<FormPartPtr> form_parts_;
|
||||
|
||||
std::string boundary_;
|
||||
};
|
||||
|
||||
using RequestPtr = std::shared_ptr<Request>;
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_REQUEST_H_
|
||||
|
||||
+20
-29
@@ -11,33 +11,41 @@
|
||||
namespace webcc {
|
||||
|
||||
RequestPtr RequestBuilder::operator()() {
|
||||
assert(parameters_.size() % 2 == 0);
|
||||
assert(headers_.size() % 2 == 0);
|
||||
|
||||
auto request = std::make_shared<Request>(method_, url_);
|
||||
auto request = std::make_shared<Request>(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<FormBody>(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
|
||||
|
||||
+80
-41
@@ -5,6 +5,7 @@
|
||||
#include <vector>
|
||||
|
||||
#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& Data(const std::string& data) {
|
||||
data_ = data;
|
||||
RequestBuilder& MediaType(const std::string& media_type) {
|
||||
media_type_ = media_type;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder& Data(std::string&& data) {
|
||||
data_ = std::move(data);
|
||||
RequestBuilder& Charset(const std::string& charset) {
|
||||
charset_ = charset;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder& Json(bool json = true) {
|
||||
json_ = json;
|
||||
// Set Media Type to "application/json".
|
||||
RequestBuilder& Json() {
|
||||
media_type_ = media_types::kApplicationJson;
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Upload a file.
|
||||
// Set Charset to "utf-8".
|
||||
RequestBuilder& Utf8() {
|
||||
charset_ = charsets::kUtf8;
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder& Body(const std::string& data) {
|
||||
body_.reset(new StringBody{ data });
|
||||
return *this;
|
||||
}
|
||||
|
||||
RequestBuilder& Body(std::string&& data) {
|
||||
body_.reset(new StringBody{ std::move(data) });
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 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<std::string> 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<FormPartPtr> 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<std::string> 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
|
||||
|
||||
@@ -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<Response>(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<StringBody>(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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+13
-5
@@ -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<FormBody>(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;
|
||||
|
||||
@@ -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<FormPartPtr> form_parts_;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+3
-10
@@ -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<int, const char*> 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
|
||||
|
||||
+2
-6
@@ -8,9 +8,6 @@
|
||||
|
||||
namespace webcc {
|
||||
|
||||
class Response;
|
||||
using ResponsePtr = std::shared_ptr<Response>;
|
||||
|
||||
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<Response>;
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_RESPONSE_H_
|
||||
|
||||
+16
-25
@@ -13,22 +13,30 @@ namespace webcc {
|
||||
ResponsePtr ResponseBuilder::operator()() {
|
||||
assert(headers_.size() % 2 == 0);
|
||||
|
||||
auto request = std::make_shared<Response>(code_);
|
||||
auto response = std::make_shared<Response>(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
|
||||
|
||||
+46
-16
@@ -4,6 +4,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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_;
|
||||
|
||||
// Is the data to send a JSON string?
|
||||
bool json_ = false;
|
||||
// Media type of the body (e.g., "application/json").
|
||||
std::string media_type_;
|
||||
|
||||
// Compress the response content.
|
||||
// Character set of the body (e.g., "utf-8").
|
||||
std::string charset_;
|
||||
|
||||
#if WEBCC_ENABLE_GZIP
|
||||
// Compress the body data (only for string body).
|
||||
bool gzip_ = false;
|
||||
#endif // WEBCC_ENABLE_GZIP
|
||||
|
||||
// Additional headers.
|
||||
std::vector<std::string> headers_;
|
||||
|
||||
@@ -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<std::string>* 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<std::string> parts;
|
||||
SplitStartLine(line, &parts);
|
||||
|
||||
+17
-11
@@ -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<char>* 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
|
||||
|
||||
+7
-7
@@ -26,10 +26,10 @@ public:
|
||||
std::function<void(boost::system::error_code, std::size_t)>;
|
||||
|
||||
// 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<char>* 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<char>* 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<char>* 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_;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user