diff --git a/example/github_rest_client/main.cc b/example/github_rest_client/main.cc index 3a20d56..975bf9e 100644 --- a/example/github_rest_client/main.cc +++ b/example/github_rest_client/main.cc @@ -1,13 +1,27 @@ #include +#include #include "json/json.h" #include "webcc/logger.h" #include "webcc/rest_ssl_client.h" -const bool kSslVerify = false; +// ----------------------------------------------------------------------------- -#define PRINT_CONTENT 0 +// Change to 1 to print response JSON. +#define PRINT_RESPONSE 0 + +#if (defined(WIN32) || defined(_WIN64)) +// You need to set environment variable SSL_CERT_FILE properly to enable +// SSL verification. +bool kSslVerify = false; +#else +bool kSslVerify = true; +#endif + +const std::string kGithubHost = "api.github.com"; + +// ----------------------------------------------------------------------------- static Json::Value StringToJson(const std::string& str) { Json::Value json; @@ -22,64 +36,76 @@ static Json::Value StringToJson(const std::string& str) { return json; } -void ListPublicEvents() { - webcc::RestSslClient client("api.github.com", "", kSslVerify); +// Print the JSON string in pretty format. +static void PrettyPrintJsonString(const std::string& str) { + Json::Value json = StringToJson(str); - if (client.Get("/events")) { -#if PRINT_CONTENT - Json::Value json = StringToJson(client.response()->content()); + Json::StreamWriterBuilder builder; + builder["indentation"] = " "; - // Pretty print the JSON. + std::unique_ptr writer(builder.newStreamWriter()); + writer->write(json, &std::cout); - Json::StreamWriterBuilder builder; - builder["indentation"] = " "; + std::cout << std::endl; +} - std::unique_ptr writer(builder.newStreamWriter()); - writer->write(json, &std::cout); +// ----------------------------------------------------------------------------- - std::cout << std::endl; -#endif // PRINT_CONTENT - } else { - std::cout << webcc::DescribeError(client.error()); - if (client.timed_out()) { - std::cout << " (timed out)"; - } - std::cout << std::endl; +#if PRINT_RESPONSE +#define PRINT_JSON_STRING(str) PrettyPrintJsonString(str) +#else +#define PRINT_JSON_STRING(str) +#endif // PRINT_RESPONSE + +static void PrintError(const webcc::RestSslClient& client) { + std::cout << webcc::DescribeError(client.error()); + if (client.timed_out()) { + std::cout << " (timed out)"; } + std::cout << std::endl; } -void ListUserFollowers() { - webcc::RestSslClient client("api.github.com", "", kSslVerify); - - if (client.Get("/users/sprinfall/followers")) { -#if PRINT_CONTENT - Json::Value json = StringToJson(client.response()->content()); - - // Pretty print the JSON. +// ----------------------------------------------------------------------------- - Json::StreamWriterBuilder builder; - builder["indentation"] = " "; +// List public events. +static void ListEvents(webcc::RestSslClient& client) { + if (client.Get("/events")) { + PRINT_JSON_STRING(client.response_content()); + } else { + PrintError(client); + } +} - std::unique_ptr writer(builder.newStreamWriter()); - writer->write(json, &std::cout); +// List the followers of the given user. +static void ListUserFollowers(webcc::RestSslClient& client, + const std::string& user) { + if (client.Get("/users/" + user + "/followers")) { + PRINT_JSON_STRING(client.response_content()); + } else { + PrintError(client); + } +} - std::cout << std::endl; -#endif // PRINT_CONTENT +// List the followers of the current authorized user. +// Header syntax: Authorization: +static void ListAuthorizedUserFollowers(webcc::RestSslClient& client, + const std::string& auth) { + if (client.Get("/user/followers", { { "Authorization", auth } })) { + PRINT_JSON_STRING(client.response_content()); } else { - std::cout << webcc::DescribeError(client.error()); - if (client.timed_out()) { - std::cout << " (timed out)"; - } - std::cout << std::endl; + PrintError(client); } } +// ----------------------------------------------------------------------------- + int main() { WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); - //ListPublicEvents(); + webcc::RestSslClient client(kGithubHost, "", kSslVerify, {}, 1500); - ListUserFollowers(); + //ListAuthorizedUserFollowers(client, "Basic c3ByaW5mYWxsQGdtYWlsLmNvbTpYaWFvTHVhbjFA"); + ListAuthorizedUserFollowers(client, "Token 1d42e2cce49929f2d24b1b6e96260003e5b3e1b0"); return 0; } diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index b2e37c8..5bf0c5c 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -23,25 +23,38 @@ class BookClientBase { public: BookClientBase(const std::string& host, const std::string& port, int timeout_seconds) - : rest_client_(host, port) { - rest_client_.SetTimeout(timeout_seconds); + : host_(host), port_(port) { + http_client_.SetTimeout(timeout_seconds); } virtual ~BookClientBase() = default; protected: + // Helper function to make a request. + webcc::HttpRequestPtr MakeRequest(const std::string& method, + const std::string& url, + std::string&& content = "") { + auto request = webcc::HttpRequest::New(method, url, host_, port_); + request->AcceptAppJson(); + if (!content.empty()) { + request->SetContentInAppJsonUtf8(JsonToString(content), true); + } + request->Prepare(); + return request; + } + // Log the socket communication error. void LogError() { - if (rest_client_.timed_out()) { - LOG_ERRO("%s (timed out)", webcc::DescribeError(rest_client_.error())); + if (http_client_.timed_out()) { + LOG_ERRO("%s (timed out)", webcc::DescribeError(http_client_.error())); } else { - LOG_ERRO(webcc::DescribeError(rest_client_.error())); + LOG_ERRO(webcc::DescribeError(http_client_.error())); } } // Check HTTP response status. bool CheckStatus(webcc::http::Status expected_status) { - int status = rest_client_.response_status(); + int status = http_client_.response_status(); if (status != expected_status) { LOG_ERRO("HTTP status error (actual: %d, expected: %d).", status, expected_status); @@ -50,7 +63,9 @@ class BookClientBase { return true; } - webcc::RestClient rest_client_; + std::string host_; + std::string port_; + webcc::HttpClient http_client_; }; // ----------------------------------------------------------------------------- @@ -63,7 +78,9 @@ class BookListClient : public BookClientBase { } bool ListBooks(std::list* books) { - if (!rest_client_.Get("/books")) { + auto request = MakeRequest(webcc::kHttpGet, "/books"); + + if (!http_client_.Request(*request)) { // Socket communication error. LogError(); return false; @@ -74,7 +91,7 @@ class BookListClient : public BookClientBase { return false; } - Json::Value rsp_json = StringToJson(rest_client_.response_content()); + Json::Value rsp_json = StringToJson(http_client_.response_content()); if (!rsp_json.isArray()) { return false; // Should be a JSON array of books. @@ -92,7 +109,10 @@ class BookListClient : public BookClientBase { req_json["title"] = title; req_json["price"] = price; - if (!rest_client_.Post("/books", JsonToString(req_json))) { + auto request = MakeRequest(webcc::kHttpPost, "/books", + JsonToString(req_json)); + + if (!http_client_.Request(*request)) { LogError(); return false; } @@ -101,7 +121,7 @@ class BookListClient : public BookClientBase { return false; } - Json::Value rsp_json = StringToJson(rest_client_.response_content()); + Json::Value rsp_json = StringToJson(http_client_.response_content()); *id = rsp_json["id"].asString(); return !id->empty(); @@ -118,7 +138,9 @@ class BookDetailClient : public BookClientBase { } bool GetBook(const std::string& id, Book* book) { - if (!rest_client_.Get("/books/" + id)) { + auto request = MakeRequest(webcc::kHttpGet, "/books/" + id); + + if (!http_client_.Request(*request)) { LogError(); return false; } @@ -127,7 +149,7 @@ class BookDetailClient : public BookClientBase { return false; } - return JsonStringToBook(rest_client_.response_content(), book); + return JsonStringToBook(http_client_.response_content(), book); } bool UpdateBook(const std::string& id, const std::string& title, @@ -136,7 +158,10 @@ class BookDetailClient : public BookClientBase { json["title"] = title; json["price"] = price; - if (!rest_client_.Put("/books/" + id, JsonToString(json))) { + auto request = MakeRequest(webcc::kHttpPut, "/books/" + id, + JsonToString(json)); + + if (!http_client_.Request(*request)) { LogError(); return false; } @@ -149,7 +174,9 @@ class BookDetailClient : public BookClientBase { } bool DeleteBook(const std::string& id) { - if (!rest_client_.Delete("/books/" + id)) { + auto request = MakeRequest(webcc::kHttpDelete, "/books/" + id); + + if (!http_client_.Request(*request)) { LogError(); return false; } @@ -238,26 +265,26 @@ int main(int argc, char* argv[]) { PrintBook(book); } - PrintSeparator(); + //PrintSeparator(); - detail_client.UpdateBook(id, "1Q84", 32.1); + //detail_client.UpdateBook(id, "1Q84", 32.1); - PrintSeparator(); + //PrintSeparator(); - if (detail_client.GetBook(id, &book)) { - PrintBook(book); - } + //if (detail_client.GetBook(id, &book)) { + // PrintBook(book); + //} - PrintSeparator(); + //PrintSeparator(); - detail_client.DeleteBook(id); + //detail_client.DeleteBook(id); - PrintSeparator(); + //PrintSeparator(); - books.clear(); - if (list_client.ListBooks(&books)) { - PrintBookList(books); - } + //books.clear(); + //if (list_client.ListBooks(&books)) { + // PrintBookList(books); + //} return 0; } diff --git a/webcc/http_async_client_base.h b/webcc/http_async_client_base.h index a0224b3..c69b3cf 100644 --- a/webcc/http_async_client_base.h +++ b/webcc/http_async_client_base.h @@ -31,6 +31,8 @@ class HttpAsyncClientBase explicit HttpAsyncClientBase(boost::asio::io_context& io_context, std::size_t buffer_size = 0); + virtual ~HttpAsyncClientBase() = default; + WEBCC_DELETE_COPY_ASSIGN(HttpAsyncClientBase); // Set the timeout seconds for reading response. diff --git a/webcc/http_client_base.h b/webcc/http_client_base.h index a7f0ff7..3ce4cbc 100644 --- a/webcc/http_client_base.h +++ b/webcc/http_client_base.h @@ -27,7 +27,7 @@ class HttpClientBase { // 0 means default value (e.g., 1024) will be used. explicit HttpClientBase(std::size_t buffer_size = 0); - ~HttpClientBase() = default; + virtual ~HttpClientBase() = default; WEBCC_DELETE_COPY_ASSIGN(HttpClientBase); diff --git a/webcc/http_message.cc b/webcc/http_message.cc index 1f1d947..26d25a5 100644 --- a/webcc/http_message.cc +++ b/webcc/http_message.cc @@ -58,6 +58,12 @@ void HttpMessage::SetContent(std::string&& content, bool set_length) { } } +void HttpMessage::SetContentInAppJsonUtf8(std::string&& content, + bool set_length) { + SetContent(std::move(content), set_length); + SetContentType(http::media_types::kApplicationJson, http::charsets::kUtf8); +} + // ATTENTION: The buffers don't hold the memory! std::vector HttpMessage::ToBuffers() const { assert(!start_line_.empty()); diff --git a/webcc/http_message.h b/webcc/http_message.h index f1d4fbe..5a2d918 100644 --- a/webcc/http_message.h +++ b/webcc/http_message.h @@ -34,16 +34,21 @@ class HttpMessage { const std::string& content() const { return content_; } + // TODO: Rename to AddHeader. void SetHeader(const std::string& name, const std::string& value); + // TODO: Remove void SetHeader(std::string&& name, std::string&& value); // E.g., "application/json; charset=utf-8" void SetContentType(const std::string& media_type, const std::string& charset); + // TODO: Remove parameter |set_length|. void SetContent(std::string&& content, bool set_length); + void SetContentInAppJsonUtf8(std::string&& content, bool set_length); + // Make the message (e.g., update start line). // Must be called before ToBuffers()! virtual void Prepare() = 0; diff --git a/webcc/http_request.h b/webcc/http_request.h index e29f115..d0224fd 100644 --- a/webcc/http_request.h +++ b/webcc/http_request.h @@ -39,6 +39,16 @@ class HttpRequest : public HttpMessage { return port_.empty() ? default_port : port_; } + // Shortcut to set `Accept` header. + void Accept(const std::string& media_type) { + SetHeader(http::headers::kAccept, media_type); + } + + // Shortcut to set `Accept` header. + void AcceptAppJson() { + SetHeader(http::headers::kAccept, http::media_types::kApplicationJson); + } + // Prepare payload. // Compose start line, set Host header, etc. void Prepare() override; diff --git a/webcc/http_ssl_client.cc b/webcc/http_ssl_client.cc index a5384a7..295d49a 100644 --- a/webcc/http_ssl_client.cc +++ b/webcc/http_ssl_client.cc @@ -10,7 +10,7 @@ namespace ssl = boost::asio::ssl; namespace webcc { -HttpSslClient::HttpSslClient(std::size_t buffer_size, bool ssl_verify) +HttpSslClient::HttpSslClient(bool ssl_verify, std::size_t buffer_size) : HttpClientBase(buffer_size), ssl_context_(ssl::context::sslv23), ssl_socket_(io_context_, ssl_context_), diff --git a/webcc/http_ssl_client.h b/webcc/http_ssl_client.h index 5482661..8b70f91 100644 --- a/webcc/http_ssl_client.h +++ b/webcc/http_ssl_client.h @@ -13,7 +13,7 @@ class HttpSslClient : public HttpClientBase { // SSL verification (|ssl_verify|) needs CA certificates to be found // in the default verify paths of OpenSSL. On Windows, it means you need to // set environment variable SSL_CERT_FILE properly. - explicit HttpSslClient(std::size_t buffer_size = 0, bool ssl_verify = true); + explicit HttpSslClient(bool ssl_verify = true, std::size_t buffer_size = 0); ~HttpSslClient() = default; diff --git a/webcc/rest_request_handler.cc b/webcc/rest_request_handler.cc index 8171030..d556109 100644 --- a/webcc/rest_request_handler.cc +++ b/webcc/rest_request_handler.cc @@ -37,6 +37,7 @@ void RestRequestHandler::HandleSession(HttpSessionPtr session) { return; } + // TODO: Let the service to provide the media-type and charset. RestResponse rest_response; service->Handle(rest_request, &rest_response); diff --git a/webcc/rest_ssl_client.cc b/webcc/rest_ssl_client.cc index 9e1eed0..9a47ca8 100644 --- a/webcc/rest_ssl_client.cc +++ b/webcc/rest_ssl_client.cc @@ -1,28 +1,50 @@ #include "webcc/rest_ssl_client.h" + +#include "boost/algorithm/string.hpp" + #include "webcc/utility.h" namespace webcc { +RestClientBase::RestClientBase(HttpClientBase* http_client_base, + const SSMap& headers) + : http_client_base_(http_client_base), + content_media_type_(http::media_types::kApplicationJson), + content_charset_(http::charsets::kUtf8), + headers_(headers) { +} + + RestSslClient::RestSslClient(const std::string& host, const std::string& port, - std::size_t buffer_size, bool ssl_verify) - : host_(host), port_(port), - http_client_(buffer_size, ssl_verify) { + bool ssl_verify, const SSMap& headers, + std::size_t buffer_size) + : RestClientBase(&http_ssl_client_, headers), + host_(host), port_(port), + http_ssl_client_(ssl_verify, buffer_size) { AdjustHostPort(host_, port_); } bool RestSslClient::Request(const std::string& method, const std::string& url, - std::string&& content, std::size_t buffer_size) { + std::string&& content, const SSMap& headers, + std::size_t buffer_size) { HttpRequest http_request(method, url, host_, port_); if (!content.empty()) { http_request.SetContent(std::move(content), true); - http_request.SetContentType(http::media_types::kApplicationJson, - http::charsets::kUtf8); + http_request.SetContentType(content_media_type_, content_charset_); + } + + for (auto& h : headers_) { + http_request.SetHeader(h.first, h.second); + } + + for (auto& h : headers) { + http_request.SetHeader(h.first, h.second); } http_request.Prepare(); - if (!http_client_.Request(http_request, buffer_size)) { + if (!http_ssl_client_.Request(http_request, buffer_size)) { return false; } diff --git a/webcc/rest_ssl_client.h b/webcc/rest_ssl_client.h index 7201555..9c61666 100644 --- a/webcc/rest_ssl_client.h +++ b/webcc/rest_ssl_client.h @@ -2,6 +2,7 @@ #define WEBCC_REST_SSL_CLIENT_H_ #include +#include #include #include // for move() @@ -12,54 +13,30 @@ namespace webcc { -class RestSslClient { - public: - // If |port| is empty, |host| will be checked to see if it contains port or - // not (separated by ':'). - explicit RestSslClient(const std::string& host, - const std::string& port = "", - std::size_t buffer_size = 0, - bool ssl_verify = true); - - ~RestSslClient() = default; - - WEBCC_DELETE_COPY_ASSIGN(RestSslClient); - - void SetTimeout(int seconds) { - http_client_.SetTimeout(seconds); - } - - // NOTE: - // The return value of the following methods (Get, Post, etc.) only indicates - // if the socket communication is successful or not. Check error() and - // timed_out() for more information if it's failed. Check response_status() - // instead for the HTTP status code. +typedef std::map SSMap; - inline bool Get(const std::string& url, std::size_t buffer_size = 0) { - return Request(kHttpGet, url, "", buffer_size); - } +class RestClientBase { +public: + RestClientBase(HttpClientBase* http_client_base, + const SSMap& headers = {}); - inline bool Post(const std::string& url, std::string&& content, - std::size_t buffer_size = 0) { - return Request(kHttpPost, url, std::move(content), buffer_size); - } + virtual ~RestClientBase() = default; - inline bool Put(const std::string& url, std::string&& content, - std::size_t buffer_size = 0) { - return Request(kHttpPut, url, std::move(content), buffer_size); - } + WEBCC_DELETE_COPY_ASSIGN(RestClientBase); - inline bool Patch(const std::string& url, std::string&& content, - std::size_t buffer_size = 0) { - return Request(kHttpPatch, url, std::move(content), buffer_size); + void SetTimeout(int seconds) { + http_client_base_->SetTimeout(seconds); } - inline bool Delete(const std::string& url, std::size_t buffer_size = 0) { - return Request(kHttpDelete, url, "", buffer_size); + // Overwrite the default content type (json & utf-8). + void SetContentType(const std::string& media_type, + const std::string& charset) { + content_media_type_ = media_type; + content_charset_ = charset; } HttpResponsePtr response() const { - return http_client_.response(); + return http_client_base_->response(); } int response_status() const { @@ -73,21 +50,79 @@ class RestSslClient { } bool timed_out() const { - return http_client_.timed_out(); + return http_client_base_->timed_out(); } Error error() const { - return http_client_.error(); + return http_client_base_->error(); } +protected: + // Default: "application/json". + std::string content_media_type_; + // Default: "utf-8". + std::string content_charset_; + + // Default headers for all session requests. + std::map headers_; + private: + HttpClientBase* http_client_base_; +}; + + +class RestSslClient : public RestClientBase { +public: + // If |port| is empty, |host| will be checked to see if it contains port or + // not (separated by ':'). + explicit RestSslClient(const std::string& host, + const std::string& port = "", + bool ssl_verify = true, + const SSMap& headers = {}, + std::size_t buffer_size = 0); + + ~RestSslClient() = default; + + // NOTE: + // The return value of the following methods (Get, Post, etc.) only indicates + // if the socket communication is successful or not. Check error() and + // timed_out() for more information if it's failed. Check response_status() + // instead for the HTTP status code. + + inline bool Get(const std::string& url, const SSMap& headers = {}, + std::size_t buffer_size = 0) { + return Request(kHttpGet, url, "", headers, buffer_size); + } + + inline bool Post(const std::string& url, std::string&& content, + const SSMap& headers = {}, std::size_t buffer_size = 0) { + return Request(kHttpPost, url, std::move(content), headers, buffer_size); + } + + inline bool Put(const std::string& url, std::string&& content, + const SSMap& headers = {}, std::size_t buffer_size = 0) { + return Request(kHttpPut, url, std::move(content), headers, buffer_size); + } + + inline bool Patch(const std::string& url, std::string&& content, + const SSMap& headers = {}, std::size_t buffer_size = 0) { + return Request(kHttpPatch, url, std::move(content), headers, buffer_size); + } + + inline bool Delete(const std::string& url, const SSMap& headers = {}, + std::size_t buffer_size = 0) { + return Request(kHttpDelete, url, "", headers, buffer_size); + } + bool Request(const std::string& method, const std::string& url, - std::string&& content, std::size_t buffer_size); + std::string&& content, const SSMap& headers = {}, + std::size_t buffer_size = 0); + private: std::string host_; std::string port_; - HttpSslClient http_client_; + HttpSslClient http_ssl_client_; }; } // namespace webcc