fix ssl handshake and parsing issues
This commit is contained in:
@@ -30,6 +30,10 @@ if(WEBCC_ENABLE_SSL)
|
|||||||
add_executable(github_client github_client.cc)
|
add_executable(github_client github_client.cc)
|
||||||
target_link_libraries(github_client ${EXAMPLE_LIBS} jsoncpp)
|
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()
|
endif()
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|||||||
@@ -15,27 +15,24 @@ int main() {
|
|||||||
webcc::ResponsePtr r;
|
webcc::ResponsePtr r;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
r = session.Send(webcc::RequestBuilder{}.
|
r = session.Send(WEBCC_GET("http://httpbin.org/get")
|
||||||
Get("http://httpbin.org/get").
|
.Query("name", "Adam Gu", true)
|
||||||
Query("name", "Adam Gu", /*encode*/true).Date()
|
.Date()());
|
||||||
());
|
|
||||||
|
|
||||||
assert(r->status() == webcc::Status::kOK);
|
assert(r->status() == webcc::Status::kOK);
|
||||||
assert(!r->data().empty());
|
assert(!r->data().empty());
|
||||||
|
|
||||||
r = session.Send(webcc::RequestBuilder{}.
|
r = session.Send(WEBCC_POST("http://httpbin.org/post")
|
||||||
Post("http://httpbin.org/post").
|
.Body("{'name'='Adam', 'age'=20}")
|
||||||
Body("{'name'='Adam', 'age'=20}").Json().Utf8()
|
.Json()
|
||||||
());
|
.Utf8()());
|
||||||
|
|
||||||
assert(r->status() == webcc::Status::kOK);
|
assert(r->status() == webcc::Status::kOK);
|
||||||
assert(!r->data().empty());
|
assert(!r->data().empty());
|
||||||
|
|
||||||
#if WEBCC_ENABLE_SSL
|
#if WEBCC_ENABLE_SSL
|
||||||
|
|
||||||
r = session.Send(webcc::RequestBuilder{}.
|
r = session.Send(WEBCC_GET("https://httpbin.org/get")());
|
||||||
Get("https://httpbin.org/get")
|
|
||||||
());
|
|
||||||
|
|
||||||
assert(r->status() == webcc::Status::kOK);
|
assert(r->status() == webcc::Status::kOK);
|
||||||
assert(!r->data().empty());
|
assert(!r->data().empty());
|
||||||
|
|||||||
@@ -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 <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
@@ -74,7 +74,7 @@ static bool UseSystemCertificateStore(SSL_CTX* ssl_ctx) {
|
|||||||
ClientSession::ClientSession(std::size_t buffer_size)
|
ClientSession::ClientSession(std::size_t buffer_size)
|
||||||
: work_guard_(boost::asio::make_work_guard(io_context_)),
|
: work_guard_(boost::asio::make_work_guard(io_context_)),
|
||||||
#if WEBCC_ENABLE_SSL
|
#if WEBCC_ENABLE_SSL
|
||||||
ssl_context_(boost::asio::ssl::context::sslv23),
|
ssl_context_(boost::asio::ssl::context::sslv23_client),
|
||||||
#endif
|
#endif
|
||||||
buffer_size_(buffer_size) {
|
buffer_size_(buffer_size) {
|
||||||
#if WEBCC_ENABLE_SSL
|
#if WEBCC_ENABLE_SSL
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ private:
|
|||||||
using ExecutorType = boost::asio::io_context::executor_type;
|
using ExecutorType = boost::asio::io_context::executor_type;
|
||||||
boost::asio::executor_work_guard<ExecutorType> work_guard_;
|
boost::asio::executor_work_guard<ExecutorType> work_guard_;
|
||||||
|
|
||||||
// TODO
|
|
||||||
#if WEBCC_ENABLE_SSL
|
#if WEBCC_ENABLE_SSL
|
||||||
boost::asio::ssl::context ssl_context_;
|
boost::asio::ssl::context ssl_context_;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+18
-10
@@ -333,9 +333,23 @@ bool Parser::ParseChunkedContent(const char* data, std::size_t length) {
|
|||||||
pending_data_.append(data, length);
|
pending_data_.append(data, length);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (pending_data_.empty()) {
|
||||||
|
// Wait for more data from next read.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Read chunk-size if necessary.
|
// Read chunk-size if necessary.
|
||||||
if (chunk_size_ == kInvalidLength) {
|
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;
|
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
|
if (chunk_size_ + 2 <= pending_data_.size()) { // +2 for CRLF
|
||||||
body_handler_->AddContent(pending_data_.c_str(), chunk_size_);
|
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);
|
pending_data_.erase(0, chunk_size_ + 2);
|
||||||
|
|
||||||
// Reset chunk-size (NOT to 0).
|
// Reset chunk-size (NOT to 0).
|
||||||
@@ -380,18 +396,10 @@ bool Parser::ParseChunkedContent(const char* data, std::size_t length) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::ParseChunkSize() {
|
bool Parser::ParseChunkSize(const std::string& line) {
|
||||||
LOG_VERB("Parse chunk size");
|
|
||||||
|
|
||||||
std::string line;
|
|
||||||
if (!GetNextLine(0, &line, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_VERB("Chunk size line: [%s]", line.c_str());
|
LOG_VERB("Chunk size line: [%s]", line.c_str());
|
||||||
|
|
||||||
std::string hex_str; // e.g., "cf0" (3312)
|
std::string hex_str; // e.g., "cf0" (3312)
|
||||||
|
|
||||||
std::size_t pos = line.find(' ');
|
std::size_t pos = line.find(' ');
|
||||||
if (pos != std::string::npos) {
|
if (pos != std::string::npos) {
|
||||||
hex_str = line.substr(0, pos);
|
hex_str = line.substr(0, pos);
|
||||||
|
|||||||
+4
-1
@@ -121,6 +121,8 @@ public:
|
|||||||
return content_length_;
|
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);
|
bool Parse(const char* data, std::size_t length);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -152,7 +154,8 @@ protected:
|
|||||||
bool ParseFixedContent(const char* data, std::size_t length);
|
bool ParseFixedContent(const char* data, std::size_t length);
|
||||||
|
|
||||||
bool ParseChunkedContent(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;
|
bool IsFixedContentFull() const;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ using tcp = boost::asio::ip::tcp;
|
|||||||
namespace webcc {
|
namespace webcc {
|
||||||
|
|
||||||
// NOTE:
|
// NOTE:
|
||||||
// Using `asio::strand` is possible but not neccessary:
|
// Using `asio::strand` is possible but not necessary:
|
||||||
// Define a memeber variable:
|
// Define a memeber variable:
|
||||||
// asio::strand<asio::io_context::executor_type> strand_;
|
// asio::strand<asio::io_context::executor_type> strand_;
|
||||||
// Initialize the strand with io_context:
|
// Initialize the strand with io_context:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "boost/asio/connect.hpp"
|
#include "boost/asio/connect.hpp"
|
||||||
#include "boost/asio/read.hpp"
|
#include "boost/asio/read.hpp"
|
||||||
|
#include "boost/asio/ssl.hpp"
|
||||||
#include "boost/asio/write.hpp"
|
#include "boost/asio/write.hpp"
|
||||||
|
|
||||||
#include "webcc/logger.h"
|
#include "webcc/logger.h"
|
||||||
@@ -69,6 +70,14 @@ void SslSocket::AsyncConnect(const std::string& host,
|
|||||||
ConnectHandler&& handler) {
|
ConnectHandler&& handler) {
|
||||||
connect_handler_ = std::move(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
|
// Modes `ssl::verify_fail_if_no_peer_cert` and `ssl::verify_client_once` are
|
||||||
// for server only. `ssl::verify_none` is not secure.
|
// for server only. `ssl::verify_none` is not secure.
|
||||||
// See: https://stackoverflow.com/a/12621528
|
// See: https://stackoverflow.com/a/12621528
|
||||||
@@ -97,6 +106,25 @@ void SslSocket::AsyncReadSome(ReadHandler&& handler,
|
|||||||
|
|
||||||
bool SslSocket::Shutdown() {
|
bool SslSocket::Shutdown() {
|
||||||
boost::system::error_code ec;
|
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);
|
ssl_stream_.lowest_layer().shutdown(tcp::socket::shutdown_both, ec);
|
||||||
|
|
||||||
if (ec) {
|
if (ec) {
|
||||||
|
|||||||
Reference in New Issue
Block a user