From e27338854cad31554c7b770df8892ed28f896ba0 Mon Sep 17 00:00:00 2001 From: Chunting Gu Date: Tue, 15 Jun 2021 17:09:04 +0800 Subject: [PATCH] fix ssl handshake and parsing issues --- examples/CMakeLists.txt | 34 +++++++++++++++++++--------------- examples/client_basics.cc | 19 ++++++++----------- examples/google_client.cc | 35 +++++++++++++++++++++++++++++++++++ webcc/CMakeLists.txt | 2 +- webcc/client_session.cc | 2 +- webcc/client_session.h | 1 - webcc/parser.cc | 28 ++++++++++++++++++---------- webcc/parser.h | 5 ++++- webcc/server.cc | 2 +- webcc/socket.cc | 28 ++++++++++++++++++++++++++++ 10 files changed, 115 insertions(+), 41 deletions(-) create mode 100644 examples/google_client.cc diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 611a2e4..15d0245 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -9,33 +9,37 @@ if(UNIX) endif() set(SIMPLE_EXAMPLES - concurrency_test - client_basics - hello_world_server - static_file_server - file_downloader - server_states - form_client - form_server - form_urlencoded_client - ) + concurrency_test + client_basics + hello_world_server + static_file_server + file_downloader + server_states + form_client + form_server + form_urlencoded_client + ) foreach(example ${SIMPLE_EXAMPLES}) - add_executable(${example} ${example}.cc) - target_link_libraries(${example} ${EXAMPLE_LIBS}) - set_target_properties(${example} PROPERTIES FOLDER "Examples") + add_executable(${example} ${example}.cc) + target_link_libraries(${example} ${EXAMPLE_LIBS}) + set_target_properties(${example} PROPERTIES FOLDER "Examples") endforeach() if(WEBCC_ENABLE_SSL) add_executable(github_client github_client.cc) target_link_libraries(github_client ${EXAMPLE_LIBS} jsoncpp) - set_target_properties(github_client PROPERTIES FOLDER "Examples") + set_target_properties(github_client PROPERTIES FOLDER "Examples") + + add_executable(google_client google_client.cc) + target_link_libraries(google_client ${EXAMPLE_LIBS}) + set_target_properties(google_client PROPERTIES FOLDER "Examples") endif() if(WIN32) add_executable(url_unicode url_unicode.cc encoding.cc encoding.h) target_link_libraries(url_unicode ${EXAMPLE_LIBS}) - set_target_properties(url_unicode PROPERTIES FOLDER "Examples") + set_target_properties(url_unicode PROPERTIES FOLDER "Examples") endif() add_subdirectory(book_server) diff --git a/examples/client_basics.cc b/examples/client_basics.cc index 93cea12..f73f140 100644 --- a/examples/client_basics.cc +++ b/examples/client_basics.cc @@ -15,27 +15,24 @@ int main() { webcc::ResponsePtr r; try { - r = session.Send(webcc::RequestBuilder{}. - Get("http://httpbin.org/get"). - Query("name", "Adam Gu", /*encode*/true).Date() - ()); + r = session.Send(WEBCC_GET("http://httpbin.org/get") + .Query("name", "Adam Gu", true) + .Date()()); assert(r->status() == webcc::Status::kOK); assert(!r->data().empty()); - r = session.Send(webcc::RequestBuilder{}. - Post("http://httpbin.org/post"). - Body("{'name'='Adam', 'age'=20}").Json().Utf8() - ()); + r = session.Send(WEBCC_POST("http://httpbin.org/post") + .Body("{'name'='Adam', 'age'=20}") + .Json() + .Utf8()()); assert(r->status() == webcc::Status::kOK); assert(!r->data().empty()); #if WEBCC_ENABLE_SSL - r = session.Send(webcc::RequestBuilder{}. - Get("https://httpbin.org/get") - ()); + r = session.Send(WEBCC_GET("https://httpbin.org/get")()); assert(r->status() == webcc::Status::kOK); assert(!r->data().empty()); diff --git a/examples/google_client.cc b/examples/google_client.cc new file mode 100644 index 0000000..0b579a8 --- /dev/null +++ b/examples/google_client.cc @@ -0,0 +1,35 @@ +// google_client.cc +// This example sends a GET request to https://www.google.com/. + +// NOTE: +// - The response body is chunked +// - The Google server requires the client to call `SSL_set_tlsext_host_name()` +// or the SSL handshake would fail. This is different from other HTTPS servers +// like api.github.com or httpbin.org. + +#include +#include + +#include "webcc/client_session.h" +#include "webcc/logger.h" + +int main() { + WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); + + webcc::ClientSession session; + + try { + auto r = session.Send(WEBCC_GET("https://www.google.com/")()); + + std::cout << std::endl; + std::cout << r->status() << std::endl; + std::cout << std::endl; + std::cout << r->data() << std::endl; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return 1; + } + + return 0; +} diff --git a/webcc/CMakeLists.txt b/webcc/CMakeLists.txt index f6619c9..653c6ea 100644 --- a/webcc/CMakeLists.txt +++ b/webcc/CMakeLists.txt @@ -46,7 +46,7 @@ target_link_libraries(${TARGET} ${Boost_LIBRARIES}) # ZLIB if(WEBCC_ENABLE_GZIP) - # The imported target ZLIB::ZLIB could be used instead. + # The imported target ZLIB::ZLIB could be used instead. target_link_libraries(${TARGET} ${ZLIB_LIBRARIES}) endif() diff --git a/webcc/client_session.cc b/webcc/client_session.cc index 355b130..d04ebe9 100644 --- a/webcc/client_session.cc +++ b/webcc/client_session.cc @@ -74,7 +74,7 @@ static bool UseSystemCertificateStore(SSL_CTX* ssl_ctx) { ClientSession::ClientSession(std::size_t buffer_size) : work_guard_(boost::asio::make_work_guard(io_context_)), #if WEBCC_ENABLE_SSL - ssl_context_(boost::asio::ssl::context::sslv23), + ssl_context_(boost::asio::ssl::context::sslv23_client), #endif buffer_size_(buffer_size) { #if WEBCC_ENABLE_SSL diff --git a/webcc/client_session.h b/webcc/client_session.h index 8a1d67e..cf6eac6 100644 --- a/webcc/client_session.h +++ b/webcc/client_session.h @@ -111,7 +111,6 @@ private: using ExecutorType = boost::asio::io_context::executor_type; boost::asio::executor_work_guard work_guard_; - // TODO #if WEBCC_ENABLE_SSL boost::asio::ssl::context ssl_context_; #endif diff --git a/webcc/parser.cc b/webcc/parser.cc index 524d136..0287ebf 100644 --- a/webcc/parser.cc +++ b/webcc/parser.cc @@ -333,9 +333,23 @@ bool Parser::ParseChunkedContent(const char* data, std::size_t length) { pending_data_.append(data, length); while (true) { + if (pending_data_.empty()) { + // Wait for more data from next read. + break; + } + // Read chunk-size if necessary. if (chunk_size_ == kInvalidLength) { - if (!ParseChunkSize()) { + std::string line; + if (!GetNextLine(0, &line, true)) { + // Need more data from next read. + // Normally, it shouldn't be here since the chunk size line is very + // short. + break; + } + + if (!ParseChunkSize(line)) { + // Invalid chunk size, stop the parsing. return false; } @@ -350,6 +364,8 @@ bool Parser::ParseChunkedContent(const char* data, std::size_t length) { if (chunk_size_ + 2 <= pending_data_.size()) { // +2 for CRLF body_handler_->AddContent(pending_data_.c_str(), chunk_size_); + // Pending data might become empty after erase. See the empty check at the + // beginning of the loop. pending_data_.erase(0, chunk_size_ + 2); // Reset chunk-size (NOT to 0). @@ -380,18 +396,10 @@ bool Parser::ParseChunkedContent(const char* data, std::size_t length) { return true; } -bool Parser::ParseChunkSize() { - LOG_VERB("Parse chunk size"); - - std::string line; - if (!GetNextLine(0, &line, true)) { - return true; - } - +bool Parser::ParseChunkSize(const std::string& line) { LOG_VERB("Chunk size line: [%s]", line.c_str()); std::string hex_str; // e.g., "cf0" (3312) - std::size_t pos = line.find(' '); if (pos != std::string::npos) { hex_str = line.substr(0, pos); diff --git a/webcc/parser.h b/webcc/parser.h index 926ac46..498da0b 100644 --- a/webcc/parser.h +++ b/webcc/parser.h @@ -121,6 +121,8 @@ public: return content_length_; } + // Parse the given length of data. + // Return false if the parsing is failed. bool Parse(const char* data, std::size_t length); protected: @@ -152,7 +154,8 @@ protected: bool ParseFixedContent(const char* data, std::size_t length); bool ParseChunkedContent(const char* data, std::size_t length); - bool ParseChunkSize(); + + bool ParseChunkSize(const std::string& line); bool IsFixedContentFull() const; diff --git a/webcc/server.cc b/webcc/server.cc index 5859c1f..927ade4 100644 --- a/webcc/server.cc +++ b/webcc/server.cc @@ -16,7 +16,7 @@ using tcp = boost::asio::ip::tcp; namespace webcc { // NOTE: -// Using `asio::strand` is possible but not neccessary: +// Using `asio::strand` is possible but not necessary: // Define a memeber variable: // asio::strand strand_; // Initialize the strand with io_context: diff --git a/webcc/socket.cc b/webcc/socket.cc index 98872bf..a58a525 100644 --- a/webcc/socket.cc +++ b/webcc/socket.cc @@ -2,6 +2,7 @@ #include "boost/asio/connect.hpp" #include "boost/asio/read.hpp" +#include "boost/asio/ssl.hpp" #include "boost/asio/write.hpp" #include "webcc/logger.h" @@ -69,6 +70,14 @@ void SslSocket::AsyncConnect(const std::string& host, ConnectHandler&& handler) { connect_handler_ = std::move(handler); + // Set SNI (server name indication) host name. + // Many hosts need this to handshake successfully (e.g., google.com). + // Inspired by Boost.Beast. + if (!SSL_set_tlsext_host_name(ssl_stream_.native_handle(), host.c_str())) { + // TODO: Call ERR_get_error() to get error. + LOG_ERRO("Failed to set SNI host name for SSL"); + } + // Modes `ssl::verify_fail_if_no_peer_cert` and `ssl::verify_client_once` are // for server only. `ssl::verify_none` is not secure. // See: https://stackoverflow.com/a/12621528 @@ -97,6 +106,25 @@ void SslSocket::AsyncReadSome(ReadHandler&& handler, bool SslSocket::Shutdown() { boost::system::error_code ec; + + ssl_stream_.lowest_layer().cancel(ec); + + // Shutdown SSL + // TODO: Use async_shutdown()? + ssl_stream_.shutdown(ec); + + if (ec == boost::asio::error::eof) { + // See: https://stackoverflow.com/a/25703699 + ec = {}; + } + + if (ec) { + LOG_WARN("SSL shutdown error (%s)", ec.message().c_str()); + return false; + } + + // Shutdown TCP + // TODO: Not sure if this is necessary? ssl_stream_.lowest_layer().shutdown(tcp::socket::shutdown_both, ec); if (ec) {