diff --git a/example/http_client/main.cc b/example/http_client/main.cc index 79422a7..d4ccb1d 100644 --- a/example/http_client/main.cc +++ b/example/http_client/main.cc @@ -3,23 +3,37 @@ #include "webcc/http_client_session.h" #include "webcc/logger.h" -void GetBoostOrgLicense(webcc::HttpClientSession& session) { - auto r = session.Get("https://www.boost.org/LICENSE_1_0.txt"); +using namespace webcc; + +// ----------------------------------------------------------------------------- + +#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 + +// ----------------------------------------------------------------------------- + +void GetBoostOrgLicense(HttpClientSession& session) { + try { + auto r = session.Request(HttpRequestArgs{ "GET" }. + url("https://www.boost.org/LICENSE_1_0.txt"). + ssl_verify(kSslVerify)); - if (r) { std::cout << r->content() << std::endl; + + } catch (const webcc::Exception& e) { + std::cout << "Exception: " << e.what() << std::endl; } } -int main() { - WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); - - using namespace webcc; - +// TODO +void Test(HttpClientSession& session) { HttpResponsePtr r; - HttpClientSession session; - // --------------------------------------------------------------------------- r = session.Request(HttpRequestArgs{ "GET" }. @@ -66,6 +80,73 @@ int main() { } catch (const webcc::Exception& e) { std::cout << "Exception: " << e.what() << std::endl; } +} + +void TestKeepAlive1(HttpClientSession& session) { + try { + auto r = session.Request(HttpRequestArgs{ "GET" }. + url("http://httpbin.org/get"). + parameters({ "key1", "value1", "key2", "value2" }). + headers({ "Accept", "application/json" }). + buffer_size(1000)); + + std::cout << r->content() << std::endl; + + } catch (const Exception& e) { + std::cout << "Exception: " << e.what() << std::endl; + } +} + +void TestKeepAlive2(HttpClientSession& session) { + try { + auto r = session.Request(webcc::HttpRequestArgs("GET"). + url("https://api.github.com/events"). + ssl_verify(false).buffer_size(1500)); + + //std::cout << r->content() << std::endl; + + } catch (const Exception& e) { + std::cout << "Exception: " << e.what() << std::endl; + } +} + +void TestKeepAlive3(HttpClientSession& session) { + try { + auto r = session.Request(webcc::HttpRequestArgs("GET"). + url("https://www.boost.org/LICENSE_1_0.txt"). + ssl_verify(false)); + + //std::cout << r->content() << std::endl; + + } catch (const Exception& e) { + std::cout << "Exception: " << e.what() << std::endl; + } +} + +void TestKeepAlive4(HttpClientSession& session) { + try { + auto r = session.Request(webcc::HttpRequestArgs("GET"). + url("https://www.google.com"). + ssl_verify(false)); + + //std::cout << r->content() << std::endl; + + } catch (const Exception& e) { + std::cout << "Exception: " << e.what() << std::endl; + } +} + +// ----------------------------------------------------------------------------- + +int main() { + WEBCC_LOG_INIT("", LOG_CONSOLE); + + HttpClientSession session; + + GetBoostOrgLicense(session); + + //TestKeepAlive1(session); + //TestKeepAlive1(session); return 0; } diff --git a/webcc/CMakeLists.txt b/webcc/CMakeLists.txt index e479ce9..6edde78 100644 --- a/webcc/CMakeLists.txt +++ b/webcc/CMakeLists.txt @@ -15,8 +15,8 @@ include(GNUInstallDirs) set(HEADERS globals.h - http_client_base.h http_client.h + http_client_pool.h http_client_session.h http_connection.h http_message.h @@ -28,7 +28,7 @@ set(HEADERS http_response.h http_response_parser.h http_server.h - http_ssl_client.h + http_socket.h queue.h url.h utility.h @@ -37,8 +37,8 @@ set(HEADERS set(SOURCES globals.cc - http_client_base.cc http_client.cc + http_client_pool.cc http_client_session.cc http_connection.cc http_message.cc @@ -49,7 +49,7 @@ set(SOURCES http_response.cc http_response_parser.cc http_server.cc - http_ssl_client.cc + http_socket.cc logger.cc url.cc utility.cc diff --git a/webcc/http_client.cc b/webcc/http_client.cc index 72a3fa6..b3f3f5b 100644 --- a/webcc/http_client.cc +++ b/webcc/http_client.cc @@ -1,31 +1,243 @@ #include "webcc/http_client.h" -#include "boost/asio/connect.hpp" -#include "boost/asio/read.hpp" -#include "boost/asio/write.hpp" +#include "boost/date_time/posix_time/posix_time.hpp" + +#include "webcc/logger.h" +#include "webcc/utility.h" + +using boost::asio::ip::tcp; namespace webcc { -HttpClient::HttpClient(std::size_t buffer_size) - : HttpClientBase(buffer_size), socket_(io_context_) { +HttpClient::HttpClient(std::size_t buffer_size, bool ssl_verify) + : buffer_(buffer_size == 0 ? kBufferSize : buffer_size), + deadline_(io_context_), + ssl_verify_(ssl_verify), + timeout_seconds_(kMaxReadSeconds), + stopped_(false), + timed_out_(false), + error_(kNoError) { } -void HttpClient::SocketConnect(const Endpoints& endpoints, - boost::system::error_code* ec) { - boost::asio::connect(socket_, endpoints, *ec); +void HttpClient::SetTimeout(int seconds) { + if (seconds > 0) { + timeout_seconds_ = seconds; + } } -void HttpClient::SocketWrite(const HttpRequest& request, - boost::system::error_code* ec) { - boost::asio::write(socket_, request.ToBuffers(), *ec); +bool HttpClient::Request(const HttpRequest& request, + std::size_t buffer_size, + bool connect) { + io_context_.restart(); + + response_.reset(new HttpResponse()); + response_parser_.reset(new HttpResponseParser(response_.get())); + + stopped_ = false; + timed_out_ = false; + error_ = kNoError; + + BufferResizer buffer_resizer(&buffer_, buffer_size); + + if (connect) { + if ((error_ = Connect(request)) != kNoError) { + return false; + } + } + + if ((error_ = WriteReqeust(request)) != kNoError) { + return false; + } + + if ((error_ = ReadResponse()) != kNoError) { + return false; + } + + return true; +} + +Error HttpClient::Connect(const HttpRequest& request) { + if (request.url().scheme() == "https") { + socket_.reset(new HttpSslSocket{ io_context_, ssl_verify_ }); + return DoConnect(request, kPort443); + } else { + socket_.reset(new HttpSocket{ io_context_ }); + return DoConnect(request, kPort80); + } +} + +Error HttpClient::DoConnect(const HttpRequest& request, + const std::string& default_port) { + tcp::resolver resolver(io_context_); + + std::string port = request.port(default_port); + + boost::system::error_code ec; + auto endpoints = resolver.resolve(tcp::v4(), request.host(), port, ec); + + if (ec) { + LOG_ERRO("Host resolve error (%s): %s, %s.", ec.message().c_str(), + request.host().c_str(), port.c_str()); + return kHostResolveError; + } + + 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) { + LOG_ERRO("Socket connect error (%s).", ec.message().c_str()); + Stop(); + return kEndpointConnectError; + } + + LOG_VERB("Socket connected."); + + return kNoError; } -void HttpClient::SocketAsyncReadSome(ReadHandler&& handler) { - socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler)); +Error HttpClient::WriteReqeust(const HttpRequest& request) { + LOG_VERB("HTTP request:\n%s", request.Dump(4, "> ").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. + + boost::system::error_code ec; + + // Use sync API directly since we don't need timeout control. + socket_->Write(request, &ec); + + if (ec) { + LOG_ERRO("Socket write error (%s).", ec.message().c_str()); + Stop(); + return kSocketWriteError; + } + + LOG_INFO("Request sent."); + + return kNoError; } -void HttpClient::SocketClose(boost::system::error_code* ec) { - socket_.close(*ec); +Error HttpClient::ReadResponse() { + LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_); + + deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_)); + DoWaitDeadline(); + + Error error = kNoError; + DoReadResponse(&error); + + if (error == kNoError) { + LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); + } + + return error; +} + +void HttpClient::DoReadResponse(Error* error) { + boost::system::error_code ec = boost::asio::error::would_block; + + auto handler = [this, &ec, error](boost::system::error_code inner_ec, + std::size_t length) { + ec = inner_ec; + + LOG_VERB("Socket async read handler."); + + if (ec || length == 0) { + Stop(); + *error = kSocketReadError; + LOG_ERRO("Socket read error (%s).", ec.message().c_str()); + return; + } + + LOG_INFO("Read data, length: %u.", length); + + // Parse the response piece just read. + if (!response_parser_->Parse(buffer_.data(), length)) { + Stop(); + *error = kHttpError; + LOG_ERRO("Failed to parse HTTP response."); + return; + } + + if (response_parser_->finished()) { + // Stop trying to read once all content has been received, because + // some servers will block extra call to read_some(). + Stop(); // TODO: Keep-Alive + + LOG_INFO("Finished to read and parse HTTP response."); + + return; + } + + if (!stopped_) { + DoReadResponse(error); + } + }; + + socket_->AsyncReadSome(std::move(handler), &buffer_); + + // Block until the asynchronous operation has completed. + do { + io_context_.run_one(); + } while (ec == boost::asio::error::would_block); +} + +void HttpClient::DoWaitDeadline() { + deadline_.async_wait( + std::bind(&HttpClient::OnDeadline, this, std::placeholders::_1)); +} + +void HttpClient::OnDeadline(boost::system::error_code ec) { + if (stopped_) { + return; + } + + LOG_VERB("OnDeadline."); + + // NOTE: Can't check this: + // if (ec == boost::asio::error::operation_aborted) { + // LOG_VERB("Deadline timer canceled."); + // return; + // } + + if (deadline_.expires_at() <= + boost::asio::deadline_timer::traits_type::now()) { + // The deadline has passed. + // The socket is closed so that any outstanding asynchronous operations + // are canceled. + LOG_WARN("HTTP client timed out."); + timed_out_ = true; + Stop(); + return; + } + + // Put the actor back to sleep. + DoWaitDeadline(); +} + +void HttpClient::Stop() { + if (stopped_) { + return; + } + + stopped_ = true; + + LOG_INFO("Close socket..."); + + boost::system::error_code ec; + socket_->Close(&ec); + + if (ec) { + LOG_ERRO("Socket close error (%s).", ec.message().c_str()); + } + + LOG_INFO("Cancel deadline timer..."); + deadline_.cancel(); } } // namespace webcc diff --git a/webcc/http_client.h b/webcc/http_client.h index b2fa253..5d34f03 100644 --- a/webcc/http_client.h +++ b/webcc/http_client.h @@ -1,33 +1,110 @@ #ifndef WEBCC_HTTP_CLIENT_H_ #define WEBCC_HTTP_CLIENT_H_ -#include "webcc/http_client_base.h" +#include +#include +#include +#include + +#include "boost/asio/deadline_timer.hpp" +#include "boost/asio/io_context.hpp" +#include "boost/asio/ip/tcp.hpp" + +#include "webcc/globals.h" +#include "webcc/http_request.h" +#include "webcc/http_response.h" +#include "webcc/http_response_parser.h" +#include "webcc/http_socket.h" namespace webcc { -// HTTP synchronous client. -class HttpClient : public HttpClientBase { +class HttpSocketBase; + +// The base class of synchronous HTTP clients. +// In synchronous mode, a request won't return until the response is received +// or timeout occurs. +// Please don't use the same client object in multiple threads. +class HttpClient { public: - explicit HttpClient(std::size_t buffer_size = 0); + // The |buffer_size| is the bytes of the buffer for reading response. + // 0 means default value (e.g., 1024) will be used. + explicit HttpClient(std::size_t buffer_size = 0, bool ssl_verify = true); + + virtual ~HttpClient() = default; + + HttpClient(const HttpClient&) = delete; + HttpClient& operator=(const HttpClient&) = delete; + + // Set the timeout seconds for reading response. + // The |seconds| is only effective when greater than 0. + void SetTimeout(int seconds); - ~HttpClient() = default; + // Connect to server, send request, wait until response is received. + // Set |buffer_size| to non-zero to use a different buffer size for this + // specific request. + bool Request(const HttpRequest& request, + std::size_t buffer_size = 0, + bool connect = true); -private: - Error Connect(const HttpRequest& request) final { - return DoConnect(request, kPort80); + HttpResponsePtr response() const { return response_; } + + int response_status() const { + assert(response_); + return response_->status(); + } + + const std::string& response_content() const { + assert(response_); + return response_->content(); } - void SocketConnect(const Endpoints& endpoints, - boost::system::error_code* ec) final; + bool timed_out() const { return timed_out_; } + + Error error() const { return error_; } + +public: + Error Connect(const HttpRequest& request); + + Error DoConnect(const HttpRequest& request, const std::string& default_port); + + Error WriteReqeust(const HttpRequest& request); + + Error ReadResponse(); + + void DoReadResponse(Error* error); + + void DoWaitDeadline(); + void OnDeadline(boost::system::error_code ec); + + void Stop(); + + boost::asio::io_context io_context_; + + std::unique_ptr socket_; + + std::vector buffer_; + + HttpResponsePtr response_; + std::unique_ptr response_parser_; + + // Timer for the timeout control. + boost::asio::deadline_timer deadline_; + + // Verify the certificate of the peer (remote server) or not. + // HTTPS only. + bool ssl_verify_; - void SocketWrite(const HttpRequest& request, - boost::system::error_code* ec) final; + // Maximum seconds to wait before the client cancels the operation. + // Only for reading response from server. + int timeout_seconds_; - void SocketAsyncReadSome(ReadHandler&& handler) final; + // Request stopped due to timeout or socket error. + bool stopped_; - void SocketClose(boost::system::error_code* ec) final; + // If the error was caused by timeout or not. + bool timed_out_; - boost::asio::ip::tcp::socket socket_; + Error error_; }; } // namespace webcc diff --git a/webcc/http_client_base.cc b/webcc/http_client_base.cc deleted file mode 100644 index 8060bbf..0000000 --- a/webcc/http_client_base.cc +++ /dev/null @@ -1,229 +0,0 @@ -#include "webcc/http_client_base.h" - -#include "boost/date_time/posix_time/posix_time.hpp" - -#include "webcc/logger.h" -#include "webcc/utility.h" - -using boost::asio::ip::tcp; - -namespace webcc { - -HttpClientBase::HttpClientBase(std::size_t buffer_size) - : buffer_(buffer_size == 0 ? kBufferSize : buffer_size), - deadline_(io_context_), - timeout_seconds_(kMaxReadSeconds), - stopped_(false), - timed_out_(false), - error_(kNoError) { -} - -void HttpClientBase::SetTimeout(int seconds) { - if (seconds > 0) { - timeout_seconds_ = seconds; - } -} - -bool HttpClientBase::Request(const HttpRequest& request, - std::size_t buffer_size) { - io_context_.restart(); - - response_.reset(new HttpResponse()); - response_parser_.reset(new HttpResponseParser(response_.get())); - - stopped_ = false; - timed_out_ = false; - error_ = kNoError; - - BufferResizer buffer_resizer(&buffer_, buffer_size); - - if ((error_ = Connect(request)) != kNoError) { - return false; - } - - if ((error_ = WriteReqeust(request)) != kNoError) { - return false; - } - - if ((error_ = ReadResponse()) != kNoError) { - return false; - } - - return true; -} - -Error HttpClientBase::DoConnect(const HttpRequest& request, - const std::string& default_port) { - tcp::resolver resolver(io_context_); - - std::string port = request.port(default_port); - - boost::system::error_code ec; - auto endpoints = resolver.resolve(tcp::v4(), request.host(), port, ec); - - if (ec) { - LOG_ERRO("Host resolve error (%s): %s, %s.", ec.message().c_str(), - request.host().c_str(), port.c_str()); - return kHostResolveError; - } - - LOG_VERB("Connect to server..."); - - // Use sync API directly since we don't need timeout control. - SocketConnect(endpoints, &ec); - - // Determine whether a connection was successfully established. - if (ec) { - LOG_ERRO("Socket connect error (%s).", ec.message().c_str()); - Stop(); - return kEndpointConnectError; - } - - LOG_VERB("Socket connected."); - - return kNoError; -} - -Error HttpClientBase::WriteReqeust(const HttpRequest& request) { - LOG_VERB("HTTP request:\n%s", request.Dump(4, "> ").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. - - boost::system::error_code ec; - - // Use sync API directly since we don't need timeout control. - SocketWrite(request, &ec); - - if (ec) { - LOG_ERRO("Socket write error (%s).", ec.message().c_str()); - Stop(); - return kSocketWriteError; - } - - LOG_INFO("Request sent."); - - return kNoError; -} - -Error HttpClientBase::ReadResponse() { - LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_); - - deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_)); - DoWaitDeadline(); - - Error error = kNoError; - DoReadResponse(&error); - - if (error == kNoError) { - LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); - } - - return error; -} - -void HttpClientBase::DoReadResponse(Error* error) { - boost::system::error_code ec = boost::asio::error::would_block; - - auto handler = [this, &ec, error](boost::system::error_code inner_ec, - std::size_t length) { - ec = inner_ec; - - LOG_VERB("Socket async read handler."); - - if (ec || length == 0) { - Stop(); - *error = kSocketReadError; - LOG_ERRO("Socket read error (%s).", ec.message().c_str()); - return; - } - - LOG_INFO("Read data, length: %u.", length); - - // Parse the response piece just read. - if (!response_parser_->Parse(buffer_.data(), length)) { - Stop(); - *error = kHttpError; - LOG_ERRO("Failed to parse HTTP response."); - return; - } - - if (response_parser_->finished()) { - // Stop trying to read once all content has been received, because - // some servers will block extra call to read_some(). - Stop(); - - LOG_INFO("Finished to read and parse HTTP response."); - - return; - } - - if (!stopped_) { - DoReadResponse(error); - } - }; - - SocketAsyncReadSome(std::move(handler)); - - // Block until the asynchronous operation has completed. - do { - io_context_.run_one(); - } while (ec == boost::asio::error::would_block); -} - -void HttpClientBase::DoWaitDeadline() { - deadline_.async_wait( - std::bind(&HttpClientBase::OnDeadline, this, std::placeholders::_1)); -} - -void HttpClientBase::OnDeadline(boost::system::error_code ec) { - if (stopped_) { - return; - } - - LOG_VERB("OnDeadline."); - - // NOTE: Can't check this: - // if (ec == boost::asio::error::operation_aborted) { - // LOG_VERB("Deadline timer canceled."); - // return; - // } - - if (deadline_.expires_at() <= - boost::asio::deadline_timer::traits_type::now()) { - // The deadline has passed. - // The socket is closed so that any outstanding asynchronous operations - // are canceled. - LOG_WARN("HTTP client timed out."); - timed_out_ = true; - Stop(); - return; - } - - // Put the actor back to sleep. - DoWaitDeadline(); -} - -void HttpClientBase::Stop() { - if (stopped_) { - return; - } - - stopped_ = true; - - LOG_INFO("Close socket..."); - - boost::system::error_code ec; - SocketClose(&ec); - - if (ec) { - LOG_ERRO("Socket close error (%s).", ec.message().c_str()); - } - - LOG_INFO("Cancel deadline timer..."); - deadline_.cancel(); -} - -} // namespace webcc diff --git a/webcc/http_client_base.h b/webcc/http_client_base.h deleted file mode 100644 index a6152ae..0000000 --- a/webcc/http_client_base.h +++ /dev/null @@ -1,116 +0,0 @@ -#ifndef WEBCC_HTTP_CLIENT_BASE_H_ -#define WEBCC_HTTP_CLIENT_BASE_H_ - -#include -#include -#include -#include - -#include "boost/asio/deadline_timer.hpp" -#include "boost/asio/io_context.hpp" -#include "boost/asio/ip/tcp.hpp" - -#include "webcc/globals.h" -#include "webcc/http_request.h" -#include "webcc/http_response.h" -#include "webcc/http_response_parser.h" - -namespace webcc { - -// The base class of synchronous HTTP clients. -// In synchronous mode, a request won't return until the response is received -// or timeout occurs. -// Please don't use the same client object in multiple threads. -class HttpClientBase { -public: - // The |buffer_size| is the bytes of the buffer for reading response. - // 0 means default value (e.g., 1024) will be used. - explicit HttpClientBase(std::size_t buffer_size = 0); - - virtual ~HttpClientBase() = default; - - HttpClientBase(const HttpClientBase&) = delete; - HttpClientBase& operator=(const HttpClientBase&) = delete; - - // Set the timeout seconds for reading response. - // The |seconds| is only effective when greater than 0. - void SetTimeout(int seconds); - - // Connect to server, send request, wait until response is received. - // Set |buffer_size| to non-zero to use a different buffer size for this - // specific request. - bool Request(const HttpRequest& request, std::size_t buffer_size = 0); - - HttpResponsePtr response() const { return response_; } - - int response_status() const { - assert(response_); - return response_->status(); - } - - const std::string& response_content() const { - assert(response_); - return response_->content(); - } - - bool timed_out() const { return timed_out_; } - - Error error() const { return error_; } - -public: - typedef boost::asio::ip::tcp::resolver::results_type Endpoints; - - typedef std::function - ReadHandler; - - virtual Error Connect(const HttpRequest& request) = 0; - - Error DoConnect(const HttpRequest& request, const std::string& default_port); - - Error WriteReqeust(const HttpRequest& request); - - Error ReadResponse(); - - void DoReadResponse(Error* error); - - void DoWaitDeadline(); - void OnDeadline(boost::system::error_code ec); - - void Stop(); - - virtual void SocketConnect(const Endpoints& endpoints, - boost::system::error_code* ec) = 0; - - virtual void SocketWrite(const HttpRequest& request, - boost::system::error_code* ec) = 0; - - virtual void SocketAsyncReadSome(ReadHandler&& handler) = 0; - - virtual void SocketClose(boost::system::error_code* ec) = 0; - - boost::asio::io_context io_context_; - - std::vector buffer_; - - HttpResponsePtr response_; - std::unique_ptr response_parser_; - - // Timer for the timeout control. - boost::asio::deadline_timer deadline_; - - // Maximum seconds to wait before the client cancels the operation. - // Only for reading response from server. - int timeout_seconds_; - - // Request stopped due to timeout or socket error. - bool stopped_; - - // If the error was caused by timeout or not. - bool timed_out_; - - Error error_; -}; - -} // namespace webcc - -#endif // WEBCC_HTTP_CLIENT_BASE_H_ diff --git a/webcc/http_client_pool.cc b/webcc/http_client_pool.cc new file mode 100644 index 0000000..8843dcb --- /dev/null +++ b/webcc/http_client_pool.cc @@ -0,0 +1,16 @@ +#include "webcc/http_client_pool.h" + +namespace webcc { + +HttpClientPtr HttpClientPool::Get(const Url& url) const { + for (const auto& client : clients_) { + return client; + } + return HttpClientPtr{}; +} + +void HttpClientPool::Add(HttpClientPtr client) { + clients_.push_back(client); +} + +} // namespace webcc diff --git a/webcc/http_client_pool.h b/webcc/http_client_pool.h new file mode 100644 index 0000000..ed30c44 --- /dev/null +++ b/webcc/http_client_pool.h @@ -0,0 +1,31 @@ +#ifndef WEBCC_HTTP_CLIENT_POOL_H_ +#define WEBCC_HTTP_CLIENT_POOL_H_ + +// HTTP client connection pool for keep-alive connections. + +#include +#include +#include + +#include "webcc/http_client.h" +#include "webcc/url.h" + +namespace webcc { + +typedef std::shared_ptr HttpClientPtr; + +class HttpClientPool { +public: + HttpClientPool() = default; + + HttpClientPtr Get(const Url& url) const; + + void Add(HttpClientPtr client); + +private: + std::list clients_; +}; + +} // namespace webcc + +#endif // WEBCC_HTTP_CLIENT_POOL_H_ diff --git a/webcc/http_client_session.cc b/webcc/http_client_session.cc index d323f44..d350cb5 100644 --- a/webcc/http_client_session.cc +++ b/webcc/http_client_session.cc @@ -1,12 +1,12 @@ #include "webcc/http_client_session.h" #include "webcc/http_client.h" -#include "webcc/http_ssl_client.h" #include "webcc/url.h" namespace webcc { -HttpClientSession::HttpClientSession() { +HttpClientSession::HttpClientSession() + : pool_(new HttpClientPool{}) { InitHeaders(); } @@ -45,18 +45,18 @@ HttpResponsePtr HttpClientSession::Request(HttpRequestArgs&& args) { request.Prepare(); - std::shared_ptr impl; + bool connect = false; + HttpClientPtr impl = pool_->Get(request.url()); - if (request.url().scheme() == "http") { - impl.reset(new HttpClient); - } else if (request.url().scheme() == "https") { - impl.reset(new HttpSslClient{args.ssl_verify_}); - } else { - throw Exception(kSchemaError, false, - "unknown schema: " + request.url().scheme()); + if (!impl) { + impl.reset(new HttpClient{ 0, args.ssl_verify_ }); + + connect = true; + + pool_->Add(impl); } - if (!impl->Request(request, args.buffer_size_)) { + if (!impl->Request(request, args.buffer_size_, connect)) { throw Exception(impl->error(), impl->timed_out()); } @@ -67,17 +67,21 @@ HttpResponsePtr HttpClientSession::Get(const std::string& url, std::vector&& parameters, std::vector&& headers, HttpRequestArgs&& args) { - return Request(args.method(http::kGet).url(url) - .parameters(std::move(parameters)) - .headers(std::move(headers))); + return Request(args.method(http::kGet) + .url(url) + .parameters(std::move(parameters)) + .headers(std::move(headers))); } HttpResponsePtr HttpClientSession::Post(const std::string& url, std::string&& data, bool json, std::vector&& headers, HttpRequestArgs&& args) { - return Request(args.method(http::kPost).url(url).data(std::move(data)) - .json(json).headers(std::move(headers))); + return Request(args.method(http::kPost) + .url(url) + .data(std::move(data)) + .json(json) + .headers(std::move(headers))); } void HttpClientSession::InitHeaders() { @@ -89,8 +93,8 @@ void HttpClientSession::InitHeaders() { headers_.Add(http::headers::kAccept, "*/*"); - // TODO: Support Keep-Alive connection. - //headers_.Add(http::headers::kConnection, "close"); + // TODO + headers_.Add(http::headers::kConnection, "Keep-Alive"); } } // namespace webcc diff --git a/webcc/http_client_session.h b/webcc/http_client_session.h index 999239f..81095f0 100644 --- a/webcc/http_client_session.h +++ b/webcc/http_client_session.h @@ -1,9 +1,11 @@ #ifndef WEBCC_HTTP_CLIENT_SESSION_H_ #define WEBCC_HTTP_CLIENT_SESSION_H_ +#include #include #include +#include "webcc/http_client_pool.h" #include "webcc/http_request_args.h" #include "webcc/http_response.h" @@ -49,6 +51,8 @@ private: // Headers for each request sent from this session. HttpHeaderDict headers_; + + std::unique_ptr pool_; }; } // namespace webcc diff --git a/webcc/http_socket.cc b/webcc/http_socket.cc new file mode 100644 index 0000000..aee1a56 --- /dev/null +++ b/webcc/http_socket.cc @@ -0,0 +1,92 @@ +#include "webcc/http_socket.h" + +#include "boost/asio/connect.hpp" +#include "boost/asio/read.hpp" +#include "boost/asio/write.hpp" + +#include "webcc/logger.h" + +namespace ssl = boost::asio::ssl; + +namespace webcc { + +// ----------------------------------------------------------------------------- + +HttpSocket::HttpSocket(boost::asio::io_context& io_context) + : socket_(io_context) { +} + +void HttpSocket::Connect(const std::string& /*host*/, + const Endpoints& endpoints, + boost::system::error_code* ec) { + boost::asio::connect(socket_, endpoints, *ec); +} + +void HttpSocket::Write(const HttpRequest& request, + boost::system::error_code* ec) { + boost::asio::write(socket_, request.ToBuffers(), *ec); +} + +void HttpSocket::AsyncReadSome(ReadHandler&& handler, std::vector* buffer) { + socket_.async_read_some(boost::asio::buffer(*buffer), std::move(handler)); +} + +void HttpSocket::Close(boost::system::error_code* ec) { + socket_.close(*ec); +} + +// ----------------------------------------------------------------------------- + +HttpSslSocket::HttpSslSocket(boost::asio::io_context& io_context, + bool ssl_verify) + : ssl_context_(ssl::context::sslv23), + ssl_socket_(io_context, ssl_context_), + ssl_verify_(ssl_verify) { + // Use the default paths for finding CA certificates. + ssl_context_.set_default_verify_paths(); +} + +void HttpSslSocket::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; + } + + Handshake(host, ec); +} + +void HttpSslSocket::Write(const HttpRequest& request, + boost::system::error_code* ec) { + boost::asio::write(ssl_socket_, request.ToBuffers(), *ec); +} + +void HttpSslSocket::AsyncReadSome(ReadHandler&& handler, std::vector* buffer) { + ssl_socket_.async_read_some(boost::asio::buffer(*buffer), std::move(handler)); +} + +void HttpSslSocket::Close(boost::system::error_code* ec) { + ssl_socket_.lowest_layer().close(*ec); +} + +void HttpSslSocket::Handshake(const std::string& host, + boost::system::error_code* ec) { + if (ssl_verify_) { + ssl_socket_.set_verify_mode(ssl::verify_peer); + } else { + ssl_socket_.set_verify_mode(ssl::verify_none); + } + + ssl_socket_.set_verify_callback(ssl::rfc2818_verification(host)); + + // Use sync API directly since we don't need timeout control. + ssl_socket_.handshake(ssl::stream_base::client, *ec); + + if (*ec) { + LOG_ERRO("Handshake error (%s).", ec->message().c_str()); + } +} + +} // namespace webcc diff --git a/webcc/http_socket.h b/webcc/http_socket.h new file mode 100644 index 0000000..8a73d1b --- /dev/null +++ b/webcc/http_socket.h @@ -0,0 +1,90 @@ +#ifndef WEBCC_HTTP_SOCKET_H_ +#define WEBCC_HTTP_SOCKET_H_ + +#include + +#include "boost/asio/ip/tcp.hpp" +#include "boost/asio/ssl.hpp" + +#include "webcc/http_request.h" + +namespace webcc { + +// ----------------------------------------------------------------------------- + +class HttpSocketBase { +public: + virtual ~HttpSocketBase() = default; + + typedef boost::asio::ip::tcp::resolver::results_type Endpoints; + + typedef std::function + ReadHandler; + + // TODO: Remove |host| + virtual void Connect(const std::string& host, + const Endpoints& endpoints, + boost::system::error_code* ec) = 0; + + virtual void Write(const HttpRequest& request, + boost::system::error_code* ec) = 0; + + virtual void AsyncReadSome(ReadHandler&& handler, + std::vector* buffer) = 0; + + virtual void Close(boost::system::error_code* ec) = 0; +}; + +// ----------------------------------------------------------------------------- + +class HttpSocket : public HttpSocketBase { +public: + explicit HttpSocket(boost::asio::io_context& io_context); + + void Connect(const std::string& host, + const Endpoints& endpoints, + boost::system::error_code* ec) final; + + void Write(const HttpRequest& request, + boost::system::error_code* ec) final; + + void AsyncReadSome(ReadHandler&& handler, std::vector* buffer) final; + + void Close(boost::system::error_code* ec) final; + +private: + boost::asio::ip::tcp::socket socket_; +}; + +// ----------------------------------------------------------------------------- + +class HttpSslSocket : public HttpSocketBase { +public: + explicit HttpSslSocket(boost::asio::io_context& io_context, + bool ssl_verify = true); + + void Connect(const std::string& host, + const Endpoints& endpoints, + boost::system::error_code* ec) final; + + void Write(const HttpRequest& request, + boost::system::error_code* ec) final; + + void AsyncReadSome(ReadHandler&& handler, std::vector* buffer) final; + + void Close(boost::system::error_code* ec) final; + +private: + void Handshake(const std::string& host, boost::system::error_code* ec); + + boost::asio::ssl::context ssl_context_; + + boost::asio::ssl::stream ssl_socket_; + + // Verify the certificate of the peer (remote server) or not. + bool ssl_verify_; +}; + +} // namespace webcc + +#endif // WEBCC_HTTP_SOCKET_H_ diff --git a/webcc/http_ssl_client.cc b/webcc/http_ssl_client.cc deleted file mode 100644 index f567138..0000000 --- a/webcc/http_ssl_client.cc +++ /dev/null @@ -1,72 +0,0 @@ -#include "webcc/http_ssl_client.h" - -#include "boost/asio/connect.hpp" -#include "boost/asio/read.hpp" -#include "boost/asio/write.hpp" - -#include "webcc/logger.h" - -namespace ssl = boost::asio::ssl; - -namespace webcc { - -HttpSslClient::HttpSslClient(bool ssl_verify, std::size_t buffer_size) - : HttpClientBase(buffer_size), - ssl_context_(ssl::context::sslv23), - ssl_socket_(io_context_, ssl_context_), - ssl_verify_(ssl_verify) { - // Use the default paths for finding CA certificates. - ssl_context_.set_default_verify_paths(); -} - -Error HttpSslClient::Connect(const HttpRequest& request) { - Error error = DoConnect(request, kPort443); - - if (error != kNoError) { - return error; - } - - return Handshake(request.host()); -} - -// NOTE: Don't check timeout. It doesn't make much sense. -Error HttpSslClient::Handshake(const std::string& host) { - if (ssl_verify_) { - ssl_socket_.set_verify_mode(ssl::verify_peer); - } else { - ssl_socket_.set_verify_mode(ssl::verify_none); - } - - ssl_socket_.set_verify_callback(ssl::rfc2818_verification(host)); - - // Use sync API directly since we don't need timeout control. - boost::system::error_code ec; - ssl_socket_.handshake(ssl::stream_base::client, ec); - - if (ec) { - LOG_ERRO("Handshake error (%s).", ec.message().c_str()); - return kHandshakeError; - } - - return kNoError; -} - -void HttpSslClient::SocketConnect(const Endpoints& endpoints, - boost::system::error_code* ec) { - boost::asio::connect(ssl_socket_.lowest_layer(), endpoints, *ec); -} - -void HttpSslClient::SocketWrite(const HttpRequest& request, - boost::system::error_code* ec) { - boost::asio::write(ssl_socket_, request.ToBuffers(), *ec); -} - -void HttpSslClient::SocketAsyncReadSome(ReadHandler&& handler) { - ssl_socket_.async_read_some(boost::asio::buffer(buffer_), std::move(handler)); -} - -void HttpSslClient::SocketClose(boost::system::error_code* ec) { - ssl_socket_.lowest_layer().close(*ec); -} - -} // namespace webcc diff --git a/webcc/http_ssl_client.h b/webcc/http_ssl_client.h deleted file mode 100644 index 890368e..0000000 --- a/webcc/http_ssl_client.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef WEBCC_HTTP_SSL_CLIENT_H_ -#define WEBCC_HTTP_SSL_CLIENT_H_ - -#include "webcc/http_client_base.h" - -#include "boost/asio/ssl.hpp" - -namespace webcc { - -// HTTP SSL (a.k.a., HTTPS) synchronous client. -class HttpSslClient : public HttpClientBase { -public: - // 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(bool ssl_verify = true, std::size_t buffer_size = 0); - - ~HttpSslClient() = default; - -private: - Error Handshake(const std::string& host); - - // Override to do handshake after connected. - Error Connect(const HttpRequest& request) final; - - void SocketConnect(const Endpoints& endpoints, - boost::system::error_code* ec) final; - - void SocketWrite(const HttpRequest& request, - boost::system::error_code* ec) final; - - void SocketAsyncReadSome(ReadHandler&& handler) final; - - void SocketClose(boost::system::error_code* ec) final; - - boost::asio::ssl::context ssl_context_; - boost::asio::ssl::stream ssl_socket_; - - // Verify the certificate of the peer (remote server) or not. - bool ssl_verify_; -}; - -} // namespace webcc - -#endif // WEBCC_HTTP_SSL_CLIENT_H_