Rework the body of request and response

master
Chunting Gu 6 years ago
parent b9f2ba6a41
commit 98aeeae012

@ -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[]) {
// ...
try {
webcc::Server server(8080, 2);
server.Route("/books", std::make_shared<BookListView>(), { "GET", "POST" });
server.Route("/books", std::make_shared<BookListView>(), { "GET", "POST" });
server.Route(webcc::R("/books/(\\d+)"), std::make_shared<BookDetailView>(),
{ "GET", "PUT", "DELETE" });
server.Route(webcc::R("/books/(\\d+)"), std::make_shared<BookDetailView>(),
{ "GET", "PUT", "DELETE" });
server.Run();
server.Run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return 1;
}
return 0;
```
Please see [examples/rest_book_server.cc](https://github.com/sprinfall/webcc/tree/master/examples/rest_book_server.cc) for more details.

@ -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;
return Post(request);
}
return {};
}
for (auto& part : request->form_parts()) {
std::cout << "name: " << part->name() << std::endl;
std::cout << "data: " << std::endl << part->data() << std::endl;
}
private:
webcc::ResponsePtr Post(webcc::RequestPtr request) {
std::cout << "form parts: " << request->form_parts().size() << std::endl;
// TODO: media_type: webcc::media_types::kTextPlain; charset = "utf-8";
return webcc::ResponseBuilder{}.Created().Data("OK")();
for (auto& part : request->form_parts()) {
std::cout << "name: " << part->name() << std::endl;
std::cout << "data: " << std::endl << part->data() << std::endl;
}
return webcc::ResponseBuilder{}.NotImplemented()();
return webcc::ResponseBuilder{}.Created().Body("OK")();
}
};

@ -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());
}

@ -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

@ -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_

@ -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());
}
}

@ -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_

@ -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);
assert(parameters.size() % 2 == 0);
for (std::size_t i = 1; i < parameters.size(); i += 2) {
builder.Query(parameters[i - 1], parameters[i]);
}
SetHeaders(headers, &builder);
return Request(builder());
}
ResponsePtr ClientSession::Head(const std::string& url,
const Strings& parameters,
const Strings& headers) {
RequestBuilder builder;
builder.Get().Url(url);
builder.Head(url);
assert(parameters.size() % 2 == 0);
for (std::size_t i = 1; i < parameters.size(); i += 2) {
@ -71,37 +82,41 @@ ResponsePtr ClientSession::Get(const std::string& url,
}
ResponsePtr ClientSession::Post(const std::string& url, std::string&& data,
bool json,
const std::vector<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());
}

@ -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_;
};

@ -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();

@ -91,7 +91,7 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) {
return;
}
LOG_VERB("HTTP request:\n%s", request_->Dump(4, "> ").c_str());
LOG_VERB("HTTP request:\n%s", request_->Dump().c_str());
// Enqueue this connection.
// Some worker thread will handle it later.
@ -99,36 +99,67 @@ void Connection::OnRead(boost::system::error_code ec, std::size_t length) {
}
void Connection::DoWrite() {
LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str());
LOG_VERB("HTTP response:\n%s", response_->Dump().c_str());
boost::asio::async_write(socket_, response_->payload(),
std::bind(&Connection::OnWrite, shared_from_this(),
std::placeholders::_1,
// Firstly, write the headers.
boost::asio::async_write(socket_, response_->GetPayload(),
std::bind(&Connection::OnWriteHeaders,
shared_from_this(), std::placeholders::_1,
std::placeholders::_2));
}
// NOTE:
// This write handler will be called from main thread (the thread calling
// io_context.run), even though AsyncWrite() is invoked by worker threads.
// This is ensured by Asio.
void Connection::OnWrite(boost::system::error_code ec, std::size_t length) {
void Connection::OnWriteHeaders(boost::system::error_code ec,
std::size_t length) {
if (ec) {
LOG_ERRO("Socket write error (%s).", ec.message().c_str());
OnWriteError(ec);
} else {
// Write the body payload by payload.
response_->body()->InitPayload();
DoWriteBody();
}
}
if (ec != boost::asio::error::operation_aborted) {
pool_->Close(shared_from_this());
}
void Connection::DoWriteBody() {
auto payload = response_->body()->NextPayload();
if (!payload.empty()) {
boost::asio::async_write(socket_, payload,
std::bind(&Connection::OnWriteBody,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2));
} else {
LOG_INFO("Response has been sent back, length: %u.", length);
// No more body payload left, we're done.
OnWriteOK();
}
}
if (request_->IsConnectionKeepAlive()) {
LOG_INFO("The client asked for a keep-alive connection.");
LOG_INFO("Continue to read the next request...");
Start();
} else {
Shutdown();
pool_->Close(shared_from_this());
}
void Connection::OnWriteBody(boost::system::error_code ec, std::size_t length) {
if (ec) {
OnWriteError(ec);
} else {
DoWriteBody();
}
}
void Connection::OnWriteOK() {
LOG_INFO("Response has been sent back.");
if (request_->IsConnectionKeepAlive()) {
LOG_INFO("The client asked for a keep-alive connection.");
LOG_INFO("Continue to read the next request...");
Start();
} else {
Shutdown();
pool_->Close(shared_from_this());
}
}
void Connection::OnWriteError(boost::system::error_code ec) {
LOG_ERRO("Socket write error (%s).", ec.message().c_str());
if (ec != boost::asio::error::operation_aborted) {
pool_->Close(shared_from_this());
}
}

@ -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:

@ -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);
}
}
void Message::SetContent(std::string&& content, bool set_length) {
content_ = std::move(content);
if (set_length) {
SetContentLength(content_.size());
SetHeader(kContentType, media_type + "; charset=" + charset);
}
}
void Message::Prepare() {
assert(!start_line_.empty());
Payload Message::GetPayload() const {
using boost::asio::buffer;
payload_.clear();
Payload payload;
payload_.push_back(buffer(start_line_));
payload_.push_back(buffer(misc_strings::CRLF));
payload.push_back(buffer(start_line_));
payload.push_back(buffer(misc_strings::CRLF));
for (const Header& h : headers_.data()) {
payload_.push_back(buffer(h.first));
payload_.push_back(buffer(misc_strings::HEADER_SEPARATOR));
payload_.push_back(buffer(h.second));
payload_.push_back(buffer(misc_strings::CRLF));
}
payload_.push_back(buffer(misc_strings::CRLF));
if (!content_.empty()) {
payload_.push_back(buffer(content_));
payload.push_back(buffer(h.first));
payload.push_back(buffer(misc_strings::HEADER_SEPARATOR));
payload.push_back(buffer(h.second));
payload.push_back(buffer(misc_strings::CRLF));
}
}
void Message::CopyPayload(std::ostream& os) const {
for (const boost::asio::const_buffer& b : payload_) {
os.write(static_cast<const char*>(b.data()), b.size());
}
}
payload.push_back(buffer(misc_strings::CRLF));
void Message::CopyPayload(std::string* str) const {
std::stringstream ss;
CopyPayload(ss);
*str = ss.str();
return payload;
}
void Message::Dump(std::ostream& os, std::size_t indent,
const std::string& prefix) const {
std::string indent_str;
if (indent > 0) {
indent_str.append(indent, ' ');
}
indent_str.append(prefix);
void Message::Dump(std::ostream& os) const {
const std::string prefix = " > ";
os << indent_str << start_line_ << std::endl;
os << prefix << start_line_ << std::endl;
for (const Header& h : headers_.data()) {
os << indent_str << h.first << ": " << h.second << std::endl;
os << prefix << h.first << ": " << h.second << std::endl;
}
os << indent_str << std::endl;
// NOTE:
// - The content will be truncated if it's too large to display.
// - Binary content will not be dumped (TODO).
if (!content_.empty()) {
if (indent == 0) {
if (content_.size() > kMaxDumpSize) {
os.write(content_.c_str(), kMaxDumpSize);
os << "..." << std::endl;
} else {
os << content_ << std::endl;
}
} else {
// Split by EOL to achieve more readability.
std::vector<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();
}
}
}
}
os << prefix << std::endl;
body_->Dump(os, prefix);
}
std::string Message::Dump(std::size_t indent,
const std::string& prefix) const {
std::string Message::Dump() const {
std::stringstream ss;
Dump(ss, indent, prefix);
Dump(ss);
return ss.str();
}

@ -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 set_start_line(const std::string& start_line) {
start_line_ = start_line;
}
// ---------------------------------------------------------------------------
std::size_t content_length() const {
return content_length_;
}
void SetBody(BodyPtr body, bool set_length);
void set_content_length(std::size_t content_length) {
content_length_ = content_length;
BodyPtr body() const {
return body_;
}
const std::string& content() const {
return content_;
}
// Get the data from the string body.
// Exception Error(kDataError) will be thrown if the body is FormBody.
const std::string& data() const;
bool IsConnectionKeepAlive() const;
// ---------------------------------------------------------------------------
void SetHeader(Header&& header) {
headers_.Set(std::move(header.first), std::move(header.second));
@ -61,70 +49,72 @@ public:
return headers_.Has(key);
}
ContentEncoding GetContentEncoding() const;
// ---------------------------------------------------------------------------
// Return true if header Accept-Encoding contains "gzip".
bool AcceptEncodingGzip() const;
const std::string& start_line() const {
return start_line_;
}
void set_start_line(const std::string& start_line) {
start_line_ = start_line;
}
const ContentType& content_type() const {
return content_type_;
std::size_t content_length() const {
return content_length_;
}
// TODO: Set header?
void SetContentType(const ContentType& content_type) {
content_type_ = content_type;
void set_content_length(std::size_t content_length) {
content_length_ = content_length;
}
// ---------------------------------------------------------------------------
// Check `Connection` header to see if it's "Keep-Alive".
bool IsConnectionKeepAlive() const;
// Determine content encoding (gzip, deflate or unknown) from
// `Content-Encoding` header.
ContentEncoding GetContentEncoding() const;
// Check `Accept-Encoding` header to see if it contains "gzip".
bool AcceptEncodingGzip() const;
// Set `Content-Type` header. E.g.,
// SetContentType("application/json; charset=utf-8")
void SetContentType(const std::string& content_type) {
SetHeader(headers::kContentType, content_type);
}
// Example: SetContentType("application/json", "utf-8")
// Set `Content-Type` header. E.g.,
// SetContentType("application/json", "utf-8")
void SetContentType(const std::string& media_type,
const std::string& charset);
void SetContent(std::string&& content, bool set_length);
// ---------------------------------------------------------------------------
// Prepare payload.
virtual void Prepare();
// Make the message complete in order to be sent.
virtual void Prepare() = 0;
const Payload& payload() const {
return payload_;
}
// Copy the exact payload to the given output stream.
void CopyPayload(std::ostream& os) const;
// Copy the exact payload to the given string.
void CopyPayload(std::string* str) const;
// Get the payload for the socket to write.
// This doesn't include the payload(s) of the body!
Payload GetPayload() const;
// Dump to output stream.
void Dump(std::ostream& os, std::size_t indent = 0,
const std::string& prefix = "") const;
// ---------------------------------------------------------------------------
// Dump to string, only used by logger.
std::string Dump(std::size_t indent = 0,
const std::string& prefix = "") const;
// Dump to output stream for logging purpose.
void Dump(std::ostream& os) const;
protected:
void SetContentLength(std::size_t content_length) {
content_length_ = content_length;
SetHeader(headers::kContentLength, std::to_string(content_length));
}
// Dump to string for logging purpose.
std::string Dump() const;
protected:
std::string start_line_;
BodyPtr body_;
std::string content_;
Headers headers_;
ContentType content_type_;
std::string start_line_;
std::size_t content_length_;
Headers headers_;
// NOTE: The payload itself doesn't hold the memory!
Payload payload_;
};
} // namespace webcc

@ -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();
}

@ -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_;

@ -1,70 +1,22 @@
#include "webcc/request.h"
#include "webcc/logger.h"
#include "webcc/utility.h"
namespace webcc {
// -----------------------------------------------------------------------------
namespace misc_strings {
// Literal strings can't be used because they have an extra '\0'.
const char CRLF[] = { '\r', '\n' };
const char DOUBLE_DASHES[] = { '-', '-' };
} // misc_strings
// -----------------------------------------------------------------------------
void Request::Prepare() {
CreateStartLine();
if (url_.port().empty()) {
SetHeader(headers::kHost, url_.host());
} else {
SetHeader(headers::kHost, url_.host() + ":" + url_.port());
}
if (form_parts_.empty()) {
Message::Prepare();
return;
}
// Multipart form data.
// Another choice to generate the boundary is like what Apache does.
// See: https://stackoverflow.com/a/5686863
if (boundary_.empty()) {
boundary_ = utility::RandomUuid();
}
SetContentType("multipart/form-data; boundary=" + boundary_);
Payload data_payload;
bool Request::IsForm() const {
return !!std::dynamic_pointer_cast<FormBody>(body_);
}
for (auto& part : form_parts_) {
AddBoundary(data_payload);
part->Prepare(&data_payload);
}
AddBoundary(data_payload, true);
const std::vector<FormPartPtr>& Request::form_parts() const {
auto form_body = std::dynamic_pointer_cast<FormBody>(body_);
// Update Content-Length header.
std::size_t content_length = 0;
for (auto& buffer : data_payload) {
content_length += buffer.size();
if (!form_body) {
throw Error{ Error::kDataError, "Not a form body" };
}
SetContentLength(content_length);
// Prepare start line and headers.
Message::Prepare();
// Append payload of content data.
payload_.insert(payload_.end(), data_payload.begin(), data_payload.end());
return form_body->parts();
}
void Request::CreateStartLine() {
void Request::Prepare() {
if (!start_line_.empty()) {
return;
}
@ -83,17 +35,12 @@ void Request::CreateStartLine() {
start_line_ += " ";
start_line_ += target;
start_line_ += " HTTP/1.1";
}
void Request::AddBoundary(Payload& payload, bool end) {
using boost::asio::buffer;
payload.push_back(buffer(misc_strings::DOUBLE_DASHES));
payload.push_back(buffer(boundary_));
if (end) {
payload.push_back(buffer(misc_strings::DOUBLE_DASHES));
if (url_.port().empty()) {
SetHeader(headers::kHost, url_.host());
} else {
SetHeader(headers::kHost, url_.host() + ":" + url_.port());
}
payload.push_back(buffer(misc_strings::CRLF));
}
} // namespace webcc

@ -10,62 +10,60 @@
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& method() const {
return method_;
}
const std::string& host() const { return url_.host(); }
const std::string& port() const { return url_.port(); }
void set_method(const std::string& method) {
method_ = method;
}
UrlQuery query() const { return UrlQuery(url_.query()); }
const Url& url() const {
return url_;
}
// TODO: Remove
void AddQuery(const std::string& key, const std::string& value) {
url_.AddQuery(key, value);
void set_url(Url&& url) {
url_ = std::move(url);
}
const UrlArgs& args() const { return args_; }
void set_args(const UrlArgs& args) { args_ = args; }
const std::string& host() const {
return url_.host();
}
std::string port(const std::string& default_port) const {
return port().empty() ? default_port : port();
const std::string& port() const {
return url_.port();
}
const std::vector<FormPartPtr>& form_parts() const {
return form_parts_;
UrlQuery query() const {
return UrlQuery(url_.query());
}
void set_form_parts(std::vector<FormPartPtr>&& form_parts) {
form_parts_ = std::move(form_parts);
const UrlArgs& args() const {
return args_;
}
void AddFormPart(FormPartPtr form_part) {
form_parts_.push_back(form_part);
void set_args(const UrlArgs& args) {
args_ = args;
}
// Prepare payload.
void Prepare() override;
// Check if the body is a multi-part form data.
bool IsForm() const;
private:
void CreateStartLine();
// Get the form parts from the body.
// Only applicable to FormBody (i.e., multi-part form data).
// Otherwise, exception Error(kDataError) will be thrown.
const std::vector<FormPartPtr>& form_parts() const;
// Add boundary to the payload for multipart form data.
void AddBoundary(Payload& payload, bool end = false);
void Prepare() override;
private:
std::string method_;
@ -75,12 +73,10 @@ private:
// The URL regex matched arguments (usually resource ID's).
// Used by server only.
UrlArgs args_;
std::vector<FormPartPtr> form_parts_;
std::string boundary_;
};
using RequestPtr = std::shared_ptr<Request>;
} // namespace webcc
#endif // WEBCC_REQUEST_H_

@ -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

@ -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& MediaType(const std::string& media_type) {
media_type_ = media_type;
return *this;
}
RequestBuilder& Charset(const std::string& charset) {
charset_ = charset;
return *this;
}
// Set Media Type to "application/json".
RequestBuilder& Json() {
media_type_ = media_types::kApplicationJson;
return *this;
}
RequestBuilder& Data(const std::string& data) {
data_ = data;
// Set Charset to "utf-8".
RequestBuilder& Utf8() {
charset_ = charsets::kUtf8;
return *this;
}
RequestBuilder& Data(std::string&& data) {
data_ = std::move(data);
RequestBuilder& Body(const std::string& data) {
body_.reset(new StringBody{ data });
return *this;
}
RequestBuilder& Json(bool json = true) {
json_ = json;
RequestBuilder& Body(std::string&& data) {
body_.reset(new StringBody{ std::move(data) });
return *this;
}
// Upload a file.
// Add a file to upload.
RequestBuilder& File(const std::string& name, const Path& path,
const std::string& media_type = "");
@ -72,11 +107,6 @@ public:
RequestBuilder& Form(const std::string& name, std::string&& data,
const std::string& media_type = "");
RequestBuilder& Gzip(bool gzip = true) {
gzip_ = gzip;
return *this;
}
RequestBuilder& Header(const std::string& key, const std::string& value) {
headers_.push_back(key);
headers_.push_back(value);
@ -91,44 +121,53 @@ public:
RequestBuilder& Auth(const std::string& type, const std::string& credentials);
RequestBuilder& AuthBasic(const std::string& login,
const std::string& password);
const std::string& password);
RequestBuilder& AuthToken(const std::string& token);
// Add the Date header to the request.
// Add the `Date` header to the request.
RequestBuilder& Date();
private:
void SetContent(RequestPtr request, std::string&& data);
#if WEBCC_ENABLE_GZIP
RequestBuilder& Gzip(bool gzip = true) {
gzip_ = gzip;
return *this;
}
#endif // WEBCC_ENABLE_GZIP
private:
std::string method_;
std::string url_;
// Namespace is added to avoid the conflict with `Url()` method.
webcc::Url url_;
// URL query parameters.
std::vector<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;

@ -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

@ -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

@ -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_

@ -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

@ -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_;
// Media type of the body (e.g., "application/json").
std::string media_type_;
// Is the data to send a JSON string?
bool json_ = false;
// Character set of the body (e.g., "utf-8").
std::string charset_;
// Compress the response content.
#if WEBCC_ENABLE_GZIP
// Compress the body data (only for string body).
bool gzip_ = false;
#endif // WEBCC_ENABLE_GZIP
// Additional headers.
std::vector<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);

@ -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

@ -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_;

Loading…
Cancel
Save