diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a47bb6..f4269eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,7 @@ project(webcc) option(WEBCC_ENABLE_LOG "Enable logging?" ON) option(WEBCC_ENABLE_SOAP "Enable SOAP support (need pugixml)?" ON) +option(WEBCC_ENABLE_SSL "Enable SSL/HTTPS support (need OpenSSL)?" OFF) option(WEBCC_BUILD_UNITTEST "Build unit test?" ON) option(WEBCC_BUILD_EXAMPLE "Build examples?" ON) @@ -38,6 +39,10 @@ if(WEBCC_ENABLE_SOAP) add_definitions(-DWEBCC_ENABLE_SOAP) endif() +if(WEBCC_ENABLE_SSL) + add_definitions(-DWEBCC_ENABLE_SSL) +endif() + if(WEBCC_BUILD_UNITTEST) enable_testing() endif() @@ -100,6 +105,15 @@ if(Boost_FOUND) message(STATUS ${Boost_LIBRARIES}) endif() +if(WEBCC_ENABLE_SSL) + set(OPENSSL_USE_STATIC_LIBS ON) + set(OPENSSL_MSVC_STATIC_RT ON) + find_package(OpenSSL) + if(OPENSSL_FOUND) + include_directories(${OPENSSL_INCLUDE_DIR}) + endif() +endif() + # For including its own headers as "webcc/http_client.h". include_directories(${PROJECT_SOURCE_DIR}) @@ -137,6 +151,10 @@ if(WEBCC_BUILD_EXAMPLE) add_subdirectory(${PROJECT_SOURCE_DIR}/example/soap_book_server) add_subdirectory(${PROJECT_SOURCE_DIR}/example/soap_book_client) endif() + + if(WEBCC_ENABLE_SSL) + add_subdirectory(${PROJECT_SOURCE_DIR}/example/http_ssl_client) + endif() endif() if(WEBCC_BUILD_UNITTEST) diff --git a/example/http_hello_async_client/main.cc b/example/http_hello_async_client/main.cc index ce60274..06fd495 100644 --- a/example/http_hello_async_client/main.cc +++ b/example/http_hello_async_client/main.cc @@ -2,7 +2,7 @@ #include "boost/asio/io_context.hpp" -#include "webcc/async_http_client.h" +#include "webcc/http_async_client.h" #include "webcc/logger.h" // In order to test this client, create a file index.html whose content is @@ -10,7 +10,7 @@ // $ python -m http.server // The default port number should be 8000. -void Test(boost::asio::io_context& ioc) { +void Test(boost::asio::io_context& io_context) { webcc::HttpRequestPtr request(new webcc::HttpRequest()); request->set_method(webcc::kHttpGet); @@ -18,7 +18,7 @@ void Test(boost::asio::io_context& ioc) { request->SetHost("localhost", "8000"); request->UpdateStartLine(); - webcc::HttpAsyncClientPtr client(new webcc::AsyncHttpClient(ioc)); + webcc::HttpAsyncClientPtr client(new webcc::HttpAsyncClient(io_context)); // Response handler. auto handler = [](std::shared_ptr response, @@ -41,13 +41,13 @@ void Test(boost::asio::io_context& ioc) { int main() { WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); - boost::asio::io_context ioc; + boost::asio::io_context io_context; - Test(ioc); - Test(ioc); - Test(ioc); + Test(io_context); + Test(io_context); + Test(io_context); - ioc.run(); + io_context.run(); return 0; } diff --git a/example/rest_book_async_client/main.cc b/example/rest_book_async_client/main.cc index fe8c470..266f41f 100644 --- a/example/rest_book_async_client/main.cc +++ b/example/rest_book_async_client/main.cc @@ -2,7 +2,7 @@ #include "json/json.h" -#include "webcc/async_rest_client.h" +#include "webcc/rest_async_client.h" #include "webcc/logger.h" // ----------------------------------------------------------------------------- @@ -44,7 +44,7 @@ class BookListClient { } private: - webcc::AsyncRestClient rest_client_; + webcc::RestAsyncClient rest_client_; }; // ----------------------------------------------------------------------------- @@ -84,7 +84,7 @@ public: } private: - webcc::AsyncRestClient rest_client_; + webcc::RestAsyncClient rest_client_; }; // ----------------------------------------------------------------------------- diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index edc641c..9808306 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -208,5 +208,7 @@ int main(int argc, char* argv[]) { list_client.ListBooks(); + getchar(); + return 0; } diff --git a/webcc/CMakeLists.txt b/webcc/CMakeLists.txt index 1d39292..73dc7f1 100644 --- a/webcc/CMakeLists.txt +++ b/webcc/CMakeLists.txt @@ -6,12 +6,10 @@ if(MSVC) endif() set(SRCS - async_http_client.cc - async_http_client.h - async_rest_client.cc - async_rest_client.h globals.cc globals.h + http_async_client.cc + http_async_client.h http_client.cc http_client.h http_connection.cc @@ -35,6 +33,8 @@ set(SRCS logger.cc logger.h queue.h + rest_async_client.cc + rest_async_client.h rest_client.cc rest_client.h rest_request_handler.cc @@ -50,6 +50,13 @@ set(SRCS utility.h ) +if(WEBCC_ENABLE_SSL) + set(SRCS ${SRCS} + http_ssl_client.cc + http_ssl_client.h + ) +endif() + if(WEBCC_ENABLE_SOAP) # SOAP specific sources. set(SOAP_SRCS diff --git a/webcc/globals.cc b/webcc/globals.cc index be9cb25..adbceb4 100644 --- a/webcc/globals.cc +++ b/webcc/globals.cc @@ -41,6 +41,8 @@ const char* DescribeError(Error error) { return "Host resolve error"; case kEndpointConnectError: return "Endpoint connect error"; + case kHandshakeError: + return "Handshake error"; case kSocketReadError: return "Socket read error"; case kSocketWriteError: diff --git a/webcc/globals.h b/webcc/globals.h index 3145501..1b14106 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -76,6 +76,7 @@ enum Error { kNoError = 0, kHostResolveError, kEndpointConnectError, + kHandshakeError, kSocketReadError, kSocketWriteError, kHttpError, diff --git a/webcc/async_http_client.cc b/webcc/http_async_client.cc similarity index 58% rename from webcc/async_http_client.cc rename to webcc/http_async_client.cc index f00b31f..c0b6c40 100644 --- a/webcc/async_http_client.cc +++ b/webcc/http_async_client.cc @@ -1,4 +1,4 @@ -#include "webcc/async_http_client.h" +#include "webcc/http_async_client.h" #include "boost/asio/connect.hpp" #include "boost/asio/read.hpp" @@ -7,16 +7,12 @@ #include "webcc/logger.h" #include "webcc/utility.h" -// NOTE: -// The timeout control is inspired by the following Asio example: -// example\cpp03\timeouts\async_tcp_client.cpp - namespace webcc { extern void AdjustBufferSize(std::size_t content_length, std::vector* buffer); -AsyncHttpClient::AsyncHttpClient(boost::asio::io_context& io_context) +HttpAsyncClient::HttpAsyncClient(boost::asio::io_context& io_context) : socket_(io_context), resolver_(new tcp::resolver(io_context)), buffer_(kBufferSize), @@ -26,8 +22,8 @@ AsyncHttpClient::AsyncHttpClient(boost::asio::io_context& io_context) timed_out_(false) { } -Error AsyncHttpClient::Request(std::shared_ptr request, - HttpResponseHandler response_handler) { +void HttpAsyncClient::Request(std::shared_ptr request, + HttpResponseHandler response_handler) { assert(request); assert(response_handler); @@ -44,94 +40,82 @@ Error AsyncHttpClient::Request(std::shared_ptr request, port = "80"; } - auto handler = std::bind(&AsyncHttpClient::ResolveHandler, - shared_from_this(), - std::placeholders::_1, - std::placeholders::_2); - - resolver_->async_resolve(tcp::v4(), request->host(), port, handler); - - return kNoError; + resolver_->async_resolve(tcp::v4(), request->host(), port, + std::bind(&HttpAsyncClient::ResolveHandler, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); } -void AsyncHttpClient::Stop() { - stopped_ = true; +void HttpAsyncClient::Stop() { + if (!stopped_) { + stopped_ = true; - boost::system::error_code ignored_ec; - socket_.close(ignored_ec); + LOG_INFO("Close socket..."); - deadline_.cancel(); + boost::system::error_code ec; + socket_.close(ec); + if (ec) { + LOG_ERRO("Failed to close socket."); + } + + deadline_.cancel(); + } } -void AsyncHttpClient::ResolveHandler(boost::system::error_code ec, - tcp::resolver::results_type results) { +void HttpAsyncClient::ResolveHandler(boost::system::error_code ec, + tcp::resolver::results_type endpoints) { if (ec) { LOG_ERRO("Can't resolve host (%s): %s, %s", ec.message().c_str(), request_->host().c_str(), request_->port().c_str()); response_handler_(response_, kHostResolveError, timed_out_); } else { // Start the connect actor. - endpoints_ = results; - AsyncConnect(endpoints_.begin()); + endpoints_ = endpoints; + + // Set a deadline for the connect operation. + deadline_.expires_from_now(boost::posix_time::seconds(kMaxConnectSeconds)); + + // ConnectHandler: void(boost::system::error_code, tcp::endpoint) + boost::asio::async_connect(socket_, endpoints_, + std::bind(&HttpAsyncClient::ConnectHandler, + shared_from_this(), + std::placeholders::_1, + std::placeholders::_2)); // Start the deadline actor. You will note that we're not setting any // particular deadline here. Instead, the connect and input actors will // update the deadline prior to each asynchronous operation. - deadline_.async_wait(std::bind(&AsyncHttpClient::CheckDeadline, + deadline_.async_wait(std::bind(&HttpAsyncClient::CheckDeadline, shared_from_this())); } } -void AsyncHttpClient::AsyncConnect(EndpointIterator endpoint_iter) { - if (endpoint_iter != endpoints_.end()) { - LOG_VERB("Connecting to [%s]...", - EndpointToString(endpoint_iter->endpoint()).c_str()); - - // Set a deadline for the connect operation. - deadline_.expires_from_now(boost::posix_time::seconds(kMaxConnectSeconds)); - - timed_out_ = false; - - // Start the asynchronous connect operation. - socket_.async_connect(endpoint_iter->endpoint(), - std::bind(&AsyncHttpClient::ConnectHandler, - shared_from_this(), - std::placeholders::_1, - endpoint_iter)); - } else { - // There are no more endpoints to try. Shut down the client. +void HttpAsyncClient::ConnectHandler(boost::system::error_code ec, + tcp::endpoint endpoint) { + if (ec) { + LOG_ERRO("Socket connect error: %s", ec.message().c_str()); Stop(); response_handler_(response_, kEndpointConnectError, timed_out_); + return; } -} -void AsyncHttpClient::ConnectHandler(boost::system::error_code ec, - EndpointIterator endpoint_iter) { + LOG_VERB("Socket connected."); + + // The deadline actor may have had a chance to run and close our socket, even + // though the connect operation notionally succeeded. if (stopped_) { + // |timed_out_| should be true in this case. + LOG_ERRO("Socket connect timed out."); + response_handler_(response_, kEndpointConnectError, timed_out_); return; } - if (!socket_.is_open()) { - // The async_connect() function automatically opens the socket at the start - // of the asynchronous operation. If the socket is closed at this time then - // the timeout handler must have run first. - LOG_WARN("Connect timed out."); - // Try the next available endpoint. - AsyncConnect(++endpoint_iter); - } else if (ec) { - // The connect operation failed before the deadline expired. - // We need to close the socket used in the previous connection attempt - // before starting a new one. - socket_.close(); - // Try the next available endpoint. - AsyncConnect(++endpoint_iter); - } else { - // Connection established. - AsyncWrite(); - } + // Connection established. + AsyncWrite(); } -void AsyncHttpClient::AsyncWrite() { +void HttpAsyncClient::AsyncWrite() { if (stopped_) { return; } @@ -140,12 +124,12 @@ void AsyncHttpClient::AsyncWrite() { boost::asio::async_write(socket_, request_->ToBuffers(), - std::bind(&AsyncHttpClient::WriteHandler, + std::bind(&HttpAsyncClient::WriteHandler, shared_from_this(), std::placeholders::_1)); } -void AsyncHttpClient::WriteHandler(boost::system::error_code ec) { +void HttpAsyncClient::WriteHandler(boost::system::error_code ec) { if (stopped_) { return; } @@ -159,15 +143,15 @@ void AsyncHttpClient::WriteHandler(boost::system::error_code ec) { } } -void AsyncHttpClient::AsyncRead() { +void HttpAsyncClient::AsyncRead() { socket_.async_read_some(boost::asio::buffer(buffer_), - std::bind(&AsyncHttpClient::ReadHandler, + std::bind(&HttpAsyncClient::ReadHandler, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } -void AsyncHttpClient::ReadHandler(boost::system::error_code ec, +void HttpAsyncClient::ReadHandler(boost::system::error_code ec, std::size_t length) { if (stopped_) { return; @@ -212,7 +196,7 @@ void AsyncHttpClient::ReadHandler(boost::system::error_code ec, AsyncRead(); } -void AsyncHttpClient::CheckDeadline() { +void HttpAsyncClient::CheckDeadline() { if (stopped_) { return; } @@ -228,7 +212,7 @@ void AsyncHttpClient::CheckDeadline() { } // Put the actor back to sleep. - deadline_.async_wait(std::bind(&AsyncHttpClient::CheckDeadline, + deadline_.async_wait(std::bind(&HttpAsyncClient::CheckDeadline, shared_from_this())); } diff --git a/webcc/async_http_client.h b/webcc/http_async_client.h similarity index 73% rename from webcc/async_http_client.h rename to webcc/http_async_client.h index 311bc01..6526af9 100644 --- a/webcc/async_http_client.h +++ b/webcc/http_async_client.h @@ -1,5 +1,5 @@ -#ifndef WEBCC_ASYNC_HTTP_CLIENT_H_ -#define WEBCC_ASYNC_HTTP_CLIENT_H_ +#ifndef WEBCC_HTTP_ASYNC_CLIENT_H_ +#define WEBCC_HTTP_ASYNC_CLIENT_H_ #include #include @@ -19,11 +19,11 @@ namespace webcc { // Request handler/callback. typedef std::function HttpResponseHandler; -class AsyncHttpClient : public std::enable_shared_from_this { +class HttpAsyncClient : public std::enable_shared_from_this { public: - explicit AsyncHttpClient(boost::asio::io_context& io_context); + explicit HttpAsyncClient(boost::asio::io_context& io_context); - DELETE_COPY_AND_ASSIGN(AsyncHttpClient); + DELETE_COPY_AND_ASSIGN(HttpAsyncClient); void set_timeout_seconds(int timeout_seconds) { timeout_seconds_ = timeout_seconds; @@ -31,7 +31,7 @@ class AsyncHttpClient : public std::enable_shared_from_this { // Asynchronously connect to the server, send the request, read the response, // and call the |response_handler| when all these finish. - Error Request(HttpRequestPtr request, HttpResponseHandler response_handler); + void Request(HttpRequestPtr request, HttpResponseHandler response_handler); // Terminate all the actors to shut down the connection. It may be called by // the user of the client class, or by the class itself in response to @@ -40,15 +40,11 @@ class AsyncHttpClient : public std::enable_shared_from_this { private: using tcp = boost::asio::ip::tcp; - typedef tcp::resolver::results_type::iterator EndpointIterator; void ResolveHandler(boost::system::error_code ec, tcp::resolver::results_type results); - void AsyncConnect(EndpointIterator endpoint_iter); - - void ConnectHandler(boost::system::error_code ec, - EndpointIterator endpoint_iter); + void ConnectHandler(boost::system::error_code ec, tcp::endpoint endpoint); void AsyncWrite(); void WriteHandler(boost::system::error_code ec); @@ -80,8 +76,8 @@ class AsyncHttpClient : public std::enable_shared_from_this { bool timed_out_; }; -typedef std::shared_ptr HttpAsyncClientPtr; +typedef std::shared_ptr HttpAsyncClientPtr; } // namespace webcc -#endif // WEBCC_ASYNC_HTTP_CLIENT_H_ +#endif // WEBCC_HTTP_ASYNC_CLIENT_H_ diff --git a/webcc/http_client.cc b/webcc/http_client.cc index 9020afc..5a9519a 100644 --- a/webcc/http_client.cc +++ b/webcc/http_client.cc @@ -11,10 +11,6 @@ #include "webcc/logger.h" -// NOTE: -// The timeout control is inspired by the following Asio example: -// example\cpp03\timeouts\blocking_tcp_client.cpp - namespace webcc { // Adjust buffer size according to content length. @@ -74,15 +70,6 @@ bool HttpClient::Request(const HttpRequest& request) { return true; } -void HttpClient::Stop() { - stopped_ = true; - - boost::system::error_code ignored_ec; - socket_.close(ignored_ec); - - deadline_.cancel(); -} - Error HttpClient::Connect(const HttpRequest& request) { using boost::asio::ip::tcp; @@ -102,12 +89,20 @@ Error HttpClient::Connect(const HttpRequest& request) { return kHostResolveError; } + LOG_VERB("Connect to server..."); + deadline_.expires_from_now(boost::posix_time::seconds(kMaxConnectSeconds)); ec = boost::asio::error::would_block; - boost::asio::async_connect(socket_, - endpoints, + // ConnectHandler: void (boost::system::error_code, tcp::endpoint) + // Using |boost::lambda::var()| is identical to: + // boost::asio::async_connect( + // socket_, endpoints, + // [this, &ec](boost::system::error_code inner_ec, tcp::endpoint) { + // ec = inner_ec; + // }); + boost::asio::async_connect(socket_, endpoints, boost::lambda::var(ec) = boost::lambda::_1); // Block until the asynchronous operation has completed. @@ -115,16 +110,20 @@ Error HttpClient::Connect(const HttpRequest& request) { io_context_.run_one(); } while (ec == boost::asio::error::would_block); - // Determine whether a connection was successfully established. The - // deadline actor may have had a chance to run and close our socket, even - // though the connect operation notionally succeeded. Therefore we must - // check whether the socket is still open before deciding if we succeeded - // or failed. - if (ec || !socket_.is_open()) { + // Determine whether a connection was successfully established. + if (ec) { + LOG_ERRO("Socket connect error: %s", ec.message().c_str()); Stop(); - if (!ec) { - timed_out_ = true; - } + return kEndpointConnectError; + } + + LOG_VERB("Socket connected."); + + // The deadline actor may have had a chance to run and close our socket, even + // though the connect operation notionally succeeded. + if (stopped_) { + // |timed_out_| should be true in this case. + LOG_ERRO("Socket connect timed out."); return kEndpointConnectError; } @@ -132,14 +131,19 @@ Error HttpClient::Connect(const HttpRequest& request) { } Error HttpClient::SendReqeust(const HttpRequest& request) { + LOG_VERB("Send request (timeout: %ds)...", kMaxSendSeconds); 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. deadline_.expires_from_now(boost::posix_time::seconds(kMaxSendSeconds)); boost::system::error_code ec = boost::asio::error::would_block; - boost::asio::async_write(socket_, - request.ToBuffers(), + // WriteHandler: void (boost::system::error_code, std::size_t) + boost::asio::async_write(socket_, request.ToBuffers(), boost::lambda::var(ec) = boost::lambda::_1); // Block until the asynchronous operation has completed. @@ -148,14 +152,23 @@ Error HttpClient::SendReqeust(const HttpRequest& request) { } while (ec == boost::asio::error::would_block); if (ec) { + LOG_ERRO("Socket write error: %s", ec.message().c_str()); Stop(); return kSocketWriteError; } + if (stopped_) { + // |timed_out_| should be true in this case. + LOG_ERRO("Socket write timed out."); + return kSocketWriteError; + } + return kNoError; } Error HttpClient::ReadResponse() { + LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_); + deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_)); Error error = kNoError; @@ -171,6 +184,7 @@ Error HttpClient::ReadResponse() { void HttpClient::DoReadResponse(Error* error) { boost::system::error_code ec = boost::asio::error::would_block; + // ReadHandler: void(boost::system::error_code, std::size_t) socket_.async_read_some( boost::asio::buffer(buffer_), [this, &ec, error](boost::system::error_code inner_ec, @@ -230,6 +244,8 @@ void HttpClient::CheckDeadline() { return; } + LOG_VERB("Check deadline."); + if (deadline_.expires_at() <= boost::asio::deadline_timer::traits_type::now()) { // The deadline has passed. @@ -244,4 +260,20 @@ void HttpClient::CheckDeadline() { deadline_.async_wait(std::bind(&HttpClient::CheckDeadline, this)); } +void HttpClient::Stop() { + if (!stopped_) { + stopped_ = true; + + LOG_INFO("Close socket..."); + + boost::system::error_code ec; + socket_.close(ec); + if (ec) { + LOG_ERRO("Failed to close socket."); + } + + deadline_.cancel(); + } +} + } // namespace webcc diff --git a/webcc/http_client.h b/webcc/http_client.h index 838d24e..a9f93b2 100644 --- a/webcc/http_client.h +++ b/webcc/http_client.h @@ -28,19 +28,16 @@ class HttpClient { timeout_seconds_ = timeout_seconds; } + // Connect to server, send request, wait until response is received. + bool Request(const HttpRequest& request); + HttpResponsePtr response() const { return response_; } bool timed_out() const { return timed_out_; } Error error() const { return error_; } - // Connect to server, send request, wait until response is received. - bool Request(const HttpRequest& request); - private: - // Terminate all the actors to shut down the connection. - void Stop(); - Error Connect(const HttpRequest& request); Error SendReqeust(const HttpRequest& request); @@ -51,7 +48,10 @@ class HttpClient { void CheckDeadline(); + void Stop(); + boost::asio::io_context io_context_; + boost::asio::ip::tcp::socket socket_; std::vector buffer_; diff --git a/webcc/http_connection.cc b/webcc/http_connection.cc index 8dea87c..c00b570 100644 --- a/webcc/http_connection.cc +++ b/webcc/http_connection.cc @@ -24,8 +24,12 @@ void HttpConnection::Start() { } void HttpConnection::Close() { - boost::system::error_code ignored_ec; - socket_.close(ignored_ec); + LOG_INFO("Close socket..."); + boost::system::error_code ec; + socket_.close(ec); + if (ec) { + LOG_ERRO("Failed to close socket."); + } } void HttpConnection::SetResponseContent(std::string&& content, @@ -51,6 +55,7 @@ void HttpConnection::AsyncRead() { void HttpConnection::ReadHandler(boost::system::error_code ec, std::size_t length) { if (ec) { + LOG_ERRO("Socket read error: %s", ec.message().c_str()); if (ec != boost::asio::error::operation_aborted) { Close(); } @@ -99,9 +104,13 @@ void HttpConnection::WriteHandler(boost::system::error_code ec, LOG_INFO("Response has been sent back, length: %u.", length); // Initiate graceful connection closure. + LOG_INFO("Close socket gracefully..."); + // TODO: shutdown(both) should be identical to close(). boost::system::error_code ec; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); - + if (ec) { + LOG_ERRO("Failed to close socket."); + } } else { LOG_ERRO("Sending response error: %s", ec.message().c_str()); diff --git a/webcc/http_server.cc b/webcc/http_server.cc index 146ea05..78a8da1 100644 --- a/webcc/http_server.cc +++ b/webcc/http_server.cc @@ -23,8 +23,6 @@ HttpServer::HttpServer(std::uint16_t port, std::size_t workers) signals_.add(SIGQUIT); #endif - AsyncAwaitStop(); - // NOTE: // "reuse_addr=true" means option SO_REUSEADDR will be set. // For more details about SO_REUSEADDR, see: @@ -35,8 +33,6 @@ HttpServer::HttpServer(std::uint16_t port, std::size_t workers) acceptor_.reset(new tcp::acceptor(io_context_, tcp::endpoint(tcp::v4(), port), true)); // reuse_addr - - AsyncAccept(); } void HttpServer::Run() { @@ -44,6 +40,10 @@ void HttpServer::Run() { LOG_INFO("Server is going to run..."); + AsyncAwaitStop(); + + AsyncAccept(); + // Start worker threads. GetRequestHandler()->Start(workers_); diff --git a/webcc/http_ssl_client.cc b/webcc/http_ssl_client.cc new file mode 100644 index 0000000..a3b7f32 --- /dev/null +++ b/webcc/http_ssl_client.cc @@ -0,0 +1,289 @@ +#include "webcc/http_ssl_client.h" + +#include + +#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 "boost/lambda/bind.hpp" +#include "boost/lambda/lambda.hpp" + +#include "webcc/logger.h" + +using boost::asio::ip::tcp; +namespace ssl = boost::asio::ssl; + +namespace webcc { + +extern void AdjustBufferSize(std::size_t content_length, + std::vector* buffer); + +HttpSslClient::HttpSslClient() + : ssl_context_(ssl::context::sslv23), + ssl_socket_(io_context_, ssl_context_), + buffer_(kBufferSize), + deadline_(io_context_), + timeout_seconds_(kMaxReceiveSeconds), + stopped_(false), + timed_out_(false), + error_(kNoError) { + // Use the default paths for finding CA certificates. + ssl_context_.set_default_verify_paths(); +} + +bool HttpSslClient::Request(const HttpRequest& request) { + response_.reset(new HttpResponse()); + response_parser_.reset(new HttpResponseParser(response_.get())); + + stopped_ = false; + timed_out_ = false; + + // Start the persistent actor that checks for deadline expiry. + deadline_.expires_at(boost::posix_time::pos_infin); + CheckDeadline(); + + if ((error_ = Connect(request)) != kNoError) { + return false; + } + + if ((error_ = Handshake(request.host())) != kNoError) { + return false; + } + + if ((error_ = SendReqeust(request)) != kNoError) { + return false; + } + + if ((error_ = ReadResponse()) != kNoError) { + return false; + } + + return true; +} + +Error HttpSslClient::Connect(const HttpRequest& request) { + using boost::asio::ip::tcp; + + tcp::resolver resolver(io_context_); + + std::string port = request.port(); + if (port.empty()) { + port = "443"; // 443 is the default port of HTTPs. + } + + boost::system::error_code ec; + auto endpoints = resolver.resolve(tcp::v4(), request.host(), port, ec); + + if (ec) { + LOG_ERRO("Can't resolve host (%s): %s, %s", ec.message().c_str(), + request.host().c_str(), port.c_str()); + return kHostResolveError; + } + + LOG_VERB("Connect to server..."); + + deadline_.expires_from_now(boost::posix_time::seconds(kMaxConnectSeconds)); + + ec = boost::asio::error::would_block; + + // ConnectHandler: void (boost::system::error_code, tcp::endpoint) + boost::asio::async_connect(ssl_socket_.lowest_layer(), endpoints, + boost::lambda::var(ec) = boost::lambda::_1); + + // Block until the asynchronous operation has completed. + do { + io_context_.run_one(); + } while (ec == boost::asio::error::would_block); + + // 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."); + + // The deadline actor may have had a chance to run and close our socket, even + // though the connect operation notionally succeeded. + if (stopped_) { + // |timed_out_| should be true in this case. + LOG_ERRO("Socket connect timed out."); + return kEndpointConnectError; + } + + return kNoError; +} + +// NOTE: Don't check timeout. It doesn't make much sense. +Error HttpSslClient::Handshake(const std::string& host) { + boost::system::error_code ec = boost::asio::error::would_block; + + ssl_socket_.set_verify_mode(ssl::verify_peer); + ssl_socket_.set_verify_callback(ssl::rfc2818_verification(host)); + + // HandshakeHandler: void (boost::system::error_code) + ssl_socket_.async_handshake(ssl::stream_base::client, + boost::lambda::var(ec) = boost::lambda::_1); + + // Block until the asynchronous operation has completed. + do { + io_context_.run_one(); + } while (ec == boost::asio::error::would_block); + + if (ec) { + LOG_ERRO("Handshake error: %s", ec.message().c_str()); + return kHandshakeError; + } + + return kNoError; +} + +Error HttpSslClient::SendReqeust(const HttpRequest& request) { + LOG_VERB("Send request (timeout: %ds)...", kMaxSendSeconds); + 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. + deadline_.expires_from_now(boost::posix_time::seconds(kMaxSendSeconds)); + + boost::system::error_code ec = boost::asio::error::would_block; + + // WriteHandler: void (boost::system::error_code, std::size_t) + boost::asio::async_write(ssl_socket_, request.ToBuffers(), + boost::lambda::var(ec) = boost::lambda::_1); + + // Block until the asynchronous operation has completed. + do { + io_context_.run_one(); + } while (ec == boost::asio::error::would_block); + + if (ec) { + LOG_ERRO("Socket write error: %s", ec.message().c_str()); + Stop(); + return kSocketWriteError; + } + + if (stopped_) { + // |timed_out_| should be true in this case. + LOG_ERRO("Socket write timed out."); + return kSocketWriteError; + } + + return kNoError; +} + +Error HttpSslClient::ReadResponse() { + LOG_VERB("Read response (timeout: %ds)...", timeout_seconds_); + + deadline_.expires_from_now(boost::posix_time::seconds(timeout_seconds_)); + + Error error = kNoError; + DoReadResponse(&error); + + if (error == kNoError) { + LOG_VERB("HTTP response:\n%s", response_->Dump(4, "> ").c_str()); + } + + return error; +} + +void HttpSslClient::DoReadResponse(Error* error) { + boost::system::error_code ec = boost::asio::error::would_block; + + // ReadHandler: void(boost::system::error_code, std::size_t) + ssl_socket_.async_read_some( + boost::asio::buffer(buffer_), + [this, &ec, error](boost::system::error_code inner_ec, + std::size_t length) { + ec = inner_ec; + + LOG_VERB("Socket async read handler."); + + if (stopped_) { + return; + } + + if (inner_ec || length == 0) { + Stop(); + *error = kSocketReadError; + LOG_ERRO("Socket read error."); + return; + } + + LOG_INFO("Read data, length: %u.", length); + + bool content_length_parsed = response_parser_->content_length_parsed(); + + // 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 (!content_length_parsed && + response_parser_->content_length_parsed()) { + // Content length just has been parsed. + AdjustBufferSize(response_parser_->content_length(), &buffer_); + } + + 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; + } + + DoReadResponse(error); + }); + + // Block until the asynchronous operation has completed. + do { + io_context_.run_one(); + } while (ec == boost::asio::error::would_block); +} + +void HttpSslClient::CheckDeadline() { + if (stopped_) { + return; + } + + LOG_VERB("Check deadline."); + + 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."); + Stop(); + timed_out_ = true; + } + + // Put the actor back to sleep. + deadline_.async_wait(std::bind(&HttpSslClient::CheckDeadline, this)); +} + +void HttpSslClient::Stop() { + if (!stopped_) { + stopped_ = true; + + LOG_INFO("Close socket..."); + + boost::system::error_code ec; + ssl_socket_.lowest_layer().close(ec); + if (ec) { + LOG_ERRO("Failed to close socket."); + } + + deadline_.cancel(); + } +} + +} // namespace webcc diff --git a/webcc/http_ssl_client.h b/webcc/http_ssl_client.h new file mode 100644 index 0000000..fec7d35 --- /dev/null +++ b/webcc/http_ssl_client.h @@ -0,0 +1,83 @@ +#ifndef WEBCC_HTTP_SSL_CLIENT_H_ +#define WEBCC_HTTP_SSL_CLIENT_H_ + +#include +#include +#include + +#include "boost/asio/deadline_timer.hpp" +#include "boost/asio/io_context.hpp" +#include "boost/asio/ip/tcp.hpp" +#include "boost/asio/ssl.hpp" + +#include "webcc/globals.h" +#include "webcc/http_request.h" +#include "webcc/http_response.h" +#include "webcc/http_response_parser.h" + +namespace webcc { + +class HttpSslClient { + public: + HttpSslClient(); + + ~HttpSslClient() = default; + + DELETE_COPY_AND_ASSIGN(HttpSslClient); + + void set_timeout_seconds(int timeout_seconds) { + assert(timeout_seconds > 0); + timeout_seconds_ = timeout_seconds; + } + + // Connect to server, send request, wait until response is received. + bool Request(const HttpRequest& request); + + HttpResponsePtr response() const { return response_; } + + bool timed_out() const { return timed_out_; } + + Error error() const { return error_; } + +private: + Error Connect(const HttpRequest& request); + + Error Handshake(const std::string& host); + + Error SendReqeust(const HttpRequest& request); + + Error ReadResponse(); + + void DoReadResponse(Error* error); + + void CheckDeadline(); + + void Stop(); + + boost::asio::io_context io_context_; + + boost::asio::ssl::context ssl_context_; + boost::asio::ssl::stream ssl_socket_; + + std::vector buffer_; + + HttpResponsePtr response_; + std::unique_ptr response_parser_; + + boost::asio::deadline_timer deadline_; + + // Maximum seconds to wait before the client cancels the operation. + // Only for receiving response from server. + int timeout_seconds_; + + bool stopped_; + + // If the error was caused by timeout or not. + bool timed_out_; + + Error error_; +}; + +} // namespace webcc + +#endif // WEBCC_HTTP_SSL_CLIENT_H_ diff --git a/webcc/logger.cc b/webcc/logger.cc index 6d31cac..071c489 100644 --- a/webcc/logger.cc +++ b/webcc/logger.cc @@ -22,7 +22,8 @@ struct Logger { void Init(const std::string& path, int _modes) { modes = _modes; - if (!path.empty()) { + // Create log file only if necessary. + if ((modes & LOG_FILE) != 0 && !path.empty()) { if ((modes & LOG_OVERWRITE) != 0) { file = fopen(path.c_str(), "w+"); } else { @@ -72,8 +73,12 @@ static bfs::path InitLogPath(const std::string& dir) { } void LogInit(const std::string& dir, int modes) { - bfs::path path = InitLogPath(dir); - g_logger.Init(path.string(), modes); + if ((modes & LOG_FILE) != 0) { + bfs::path path = InitLogPath(dir); + g_logger.Init(path.string(), modes); + } else { + g_logger.Init("", modes); + } // Suppose LogInit() is called from the main thread. g_main_thread_id = std::this_thread::get_id(); @@ -116,6 +121,23 @@ static std::string GetThreadID() { return ss.str(); } +static void WriteToFile(FILE* fd, int level, const char* file, int line, + const char* format, va_list args) { + std::lock_guard lock(g_logger.mutex); + + fprintf(fd, "%s, %s, %7s, %24s, %4d, ", + GetTimestamp().c_str(), kLevelNames[level], GetThreadID().c_str(), + file, line); + + vfprintf(fd, format, args); + + fprintf(fd, "\n"); + + if ((g_logger.modes & LOG_FLUSH) != 0) { + fflush(fd); + } +} + void LogWrite(int level, const char* file, int line, const char* format, ...) { assert(format != nullptr); @@ -123,34 +145,11 @@ void LogWrite(int level, const char* file, int line, const char* format, ...) { va_start(args, format); if ((g_logger.modes & LOG_FILE) != 0 && g_logger.file != nullptr) { - std::lock_guard lock(g_logger.mutex); - - fprintf(g_logger.file, "%s, %s, %5s, %24s, %4d, ", - GetTimestamp().c_str(), kLevelNames[level], GetThreadID().c_str(), - file, line); - - vfprintf(g_logger.file, format, args); - - fprintf(g_logger.file, "\n"); - - if ((g_logger.modes & LOG_FLUSH) != 0) { - fflush(g_logger.file); - } + WriteToFile(g_logger.file, level, file, line, format, args); } if ((g_logger.modes & LOG_CONSOLE) != 0) { - std::lock_guard lock(g_logger.mutex); - - fprintf(stderr, "%s, %s, %5s, %24s, %4d, ", - GetTimestamp().c_str(), kLevelNames[level], GetThreadID().c_str(), - file, line); - - vfprintf(stderr, format, args); - fprintf(stderr, "\n"); - - if ((g_logger.modes & LOG_FLUSH) != 0) { - fflush(stderr); - } + WriteToFile(stderr, level, file, line, format, args); } va_end(args); diff --git a/webcc/async_rest_client.cc b/webcc/rest_async_client.cc similarity index 80% rename from webcc/async_rest_client.cc rename to webcc/rest_async_client.cc index 31b4525..5dfb680 100644 --- a/webcc/async_rest_client.cc +++ b/webcc/rest_async_client.cc @@ -1,14 +1,14 @@ -#include "webcc/async_rest_client.h" +#include "webcc/rest_async_client.h" namespace webcc { -AsyncRestClient::AsyncRestClient(boost::asio::io_context& io_context, +RestAsyncClient::RestAsyncClient(boost::asio::io_context& io_context, const std::string& host, const std::string& port) : io_context_(io_context), host_(host), port_(port), timeout_seconds_(0) { } -void AsyncRestClient::Request(const std::string& method, +void RestAsyncClient::Request(const std::string& method, const std::string& url, std::string&& content, HttpResponseHandler response_handler) { @@ -27,7 +27,7 @@ void AsyncRestClient::Request(const std::string& method, request->UpdateStartLine(); - HttpAsyncClientPtr http_client(new AsyncHttpClient(io_context_)); + HttpAsyncClientPtr http_client(new HttpAsyncClient(io_context_)); if (timeout_seconds_ > 0) { http_client->set_timeout_seconds(timeout_seconds_); diff --git a/webcc/async_rest_client.h b/webcc/rest_async_client.h similarity index 86% rename from webcc/async_rest_client.h rename to webcc/rest_async_client.h index 64e09d3..7d05994 100644 --- a/webcc/async_rest_client.h +++ b/webcc/rest_async_client.h @@ -1,16 +1,16 @@ -#ifndef WEBCC_ASYNC_REST_CLIENT_H_ -#define WEBCC_ASYNC_REST_CLIENT_H_ +#ifndef WEBCC_REST_ASYNC_CLIENT_H_ +#define WEBCC_REST_ASYNC_CLIENT_H_ #include #include // for move() -#include "webcc/async_http_client.h" +#include "webcc/http_async_client.h" namespace webcc { -class AsyncRestClient { +class RestAsyncClient { public: - AsyncRestClient(boost::asio::io_context& io_context, // NOLINT + RestAsyncClient(boost::asio::io_context& io_context, // NOLINT const std::string& host, const std::string& port); void set_timeout_seconds(int timeout_seconds) { @@ -59,4 +59,4 @@ class AsyncRestClient { } // namespace webcc -#endif // WEBCC_ASYNC_REST_CLIENT_H_ +#endif // WEBCC_REST_ASYNC_CLIENT_H_