1
0

HTTP Persistence for world client/server peering

This commit is contained in:
Emagi 2025-07-01 20:24:15 -04:00
parent 93b7620364
commit 9e1f77e28e
3 changed files with 491 additions and 363 deletions

View File

@ -86,17 +86,28 @@ std::string base64_encode(const std::string& input) {
return encoded_string;
}
HTTPSClient::HTTPSClient(const std::string& certFile, const std::string& keyFile)
: certFile(certFile), keyFile(keyFile) {
// SSL and TCP setup
sslCtx = createSSLContext();
}
HTTPSClient::HTTPSClient(const std::string& certFile,
const std::string& keyFile)
: certFile(certFile)
, keyFile(keyFile)
, ioc_()
, workGuard_(boost::asio::make_work_guard(ioc_)) // ◀︎ keep run() from returning
, sslCtx(createSSLContext())
, pool_(ioc_, *sslCtx) // pass sslCtx here
{
// fire up the background I/O thread
runner_ = std::thread([&]{ ioc_.run(); });
}
HTTPSClient::~HTTPSClient() {
workGuard_.reset();
ioc_.stop();
runner_.join();
}
std::shared_ptr<boost::asio::ssl::context> HTTPSClient::createSSLContext() {
auto sslCtx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tlsv13_client);
sslCtx->set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use);
sslCtx->use_certificate_file(certFile, boost::asio::ssl::context::pem);
sslCtx->use_private_key_file(keyFile, boost::asio::ssl::context::pem);
sslCtx->set_verify_mode(ssl::verify_peer);
sslCtx->set_default_verify_paths();
return sslCtx;
@ -127,324 +138,281 @@ std::string HTTPSClient::buildCookieHeader() const {
return cookieHeader;
}
std::string HTTPSClient::sendRequest(const std::string& server, const std::string& port, const std::string& target) {
try {
boost::asio::io_context ioContext;
std::string HTTPSClient::sendRequest(
const std::string& server,
const std::string& port,
const std::string& target) {
// promise/future to block until async completes
std::promise<std::pair<boost::system::error_code, std::string>> p;
auto f = p.get_future();
auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(ioContext, *sslCtx);
auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(ioContext);
auto results = resolver->resolve(server, port);
// call the async overload
sendRequest(server, port, target,
[&p](boost::system::error_code ec, std::string body) {
p.set_value({ec, std::move(body)});
});
// Persistent objects to manage response, request, and buffer
auto res = std::make_shared<http::response<http::string_body>>();
auto buffer = std::make_shared<boost::beast::flat_buffer>();
auto req = std::make_shared<http::request<http::string_body>>(http::verb::get, target, 11);
// SNI hostname (required for many hosts)
if (!SSL_set_tlsext_host_name(stream->native_handle(), server.c_str())) {
throw boost::beast::system_error(
boost::beast::error_code(static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()));
}
// Prepare request headers
req->set(http::field::host, server);
req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req->set(boost::beast::http::field::connection, "close");
req->set(http::field::content_type, "application/json");
if (!cookies.empty()) {
req->set(http::field::cookie, buildCookieHeader());
}
else {
std::string credentials = net.GetCmdUser() + ":" + net.GetCmdPassword();
std::string encodedCredentials = base64_encode(credentials);
req->set(http::field::authorization, "Basic " + encodedCredentials);
}
// Step 1: Asynchronous connect with timeout
auto connect_timer = std::make_shared<boost::asio::steady_timer>(ioContext);
connect_timer->expires_after(std::chrono::seconds(2));
connect_timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel(); // Cancel operation on timeout
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
auto timer = std::make_shared<boost::asio::steady_timer>(ioContext, std::chrono::seconds(2));
boost::asio::async_connect(stream->lowest_layer(), results,
[stream, connect_timer, req, buffer, res, timer, server, port, target](boost::system::error_code ec, const auto&) {
connect_timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 2: Asynchronous handshake with timeout
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
stream->async_handshake(boost::asio::ssl::stream_base::client,
[stream, timer, req, buffer, res, server, port, target](boost::system::error_code ec) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 3: Asynchronous write request
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
http::async_write(*stream, *req,
[stream, buffer, res, timer, server, port, target](boost::system::error_code ec, std::size_t) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 4: Asynchronous read response
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
http::async_read(*stream, *buffer, *res,
[stream, timer, res, server, port, target](boost::system::error_code ec, std::size_t) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 5: Shutdown the stream
stream->async_shutdown([stream, server, port](boost::system::error_code ec) {
if (ec && ec != boost::asio::error::eof) {
// ignore these
//std::cerr << "Shutdown error: " << ec.message() << std::endl;
}
});
});
});
});
});
ioContext.run();
// Store cookies from the response
if (res->base().count(http::field::set_cookie) > 0) {
auto set_cookie_value = res->base()[http::field::set_cookie].to_string();
std::istringstream streamdata(set_cookie_value);
std::string token;
// Parse "Set-Cookie" field for name-value pairs
while (std::getline(streamdata, token, ';')) {
auto pos = token.find('=');
if (pos != std::string::npos) {
std::string name = token.substr(0, pos);
std::string value = token.substr(pos + 1);
cookies[name] = value; // Store each cookie
}
}
}
if (res->body() == "Unauthorized") {
cookies.clear();
}
// Return the response body, if available
return res->body();
}
catch (const std::exception& e) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Request Error %s for %s:%s/%s", __FUNCTION__, e.what() ? e.what() : "??", server.c_str(), port.c_str(), target.c_str());
return {};
}
auto [ec, body] = f.get();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering",
"%s: Request Error %s", __FUNCTION__, ec.message().c_str());
return {};
}
return body;
}
std::string HTTPSClient::sendPostRequest(const std::string& server, const std::string& port, const std::string& target, const std::string& jsonPayload) {
try {
boost::asio::io_context ioContext;
// async GET
void HTTPSClient::sendRequest(
const std::string& server,
const std::string& port,
const std::string& target,
std::function<void(boost::system::error_code, std::string)> done)
{
pool_.acquire(server, port,
[this, server, port, target, done](auto ps, auto ec) {
if (ec) return done(ec, "");
// SSL and TCP setup
auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(ioContext, *sslCtx);
auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(ioContext);
auto results = resolver->resolve(server, port);
auto req = std::make_shared<
http::request<http::string_body>>(
http::verb::get, target, 11);
// Persistent objects to manage response, request, and buffer
auto res = std::make_shared<http::response<http::string_body>>();
auto buffer = std::make_shared<boost::beast::flat_buffer>();
auto req = std::make_shared<http::request<http::string_body>>(http::verb::post, target, 11);
req->set(http::field::host, server);
req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req->set(http::field::connection, "keep-alive");
if (!cookies.empty()) {
req->set(http::field::cookie, buildCookieHeader());
} else {
auto creds = net.GetCmdUser() + ":" + net.GetCmdPassword();
req->set(http::field::authorization,
"Basic " + base64_encode(creds));
}
// SNI hostname (required for many hosts)
if (!SSL_set_tlsext_host_name(stream->native_handle(), server.c_str())) {
throw boost::beast::system_error(
boost::beast::error_code(static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()));
auto buffer = std::make_shared<boost::beast::flat_buffer>();
auto res = std::make_shared<
http::response<http::string_body>>();
auto write_timer = std::make_shared<boost::asio::steady_timer>(ioc_);
auto read_timer = std::make_shared<boost::asio::steady_timer>(ioc_);
write_timer->expires_after(std::chrono::seconds(2));
write_timer->async_wait([ps](auto ec){
if (!ec) {
// cancel the write if its still pending
ps->stream.lowest_layer().cancel();
}
// Prepare HTTP POST request with JSON payload
req->set(http::field::host, server);
req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req->set(boost::beast::http::field::connection, "close");
req->set(http::field::content_type, "application/json");
if (!cookies.empty()) {
req->set(http::field::cookie, buildCookieHeader());
}
else {
std::string credentials = net.GetCmdUser() + ":" + net.GetCmdPassword();
std::string encodedCredentials = base64_encode(credentials);
req->set(http::field::authorization, "Basic " + encodedCredentials);
}
req->body() = jsonPayload;
req->prepare_payload();
// Step 1: Asynchronous connect with timeout
auto connect_timer = std::make_shared<boost::asio::steady_timer>(ioContext);
connect_timer->expires_after(std::chrono::seconds(2));
connect_timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
});
// capture 'req' so it sticks around till write completes
http::async_write(ps->stream, *req,
[this, ps, req, buffer, res, write_timer, read_timer, server, port, done]
(boost::system::error_code ec, std::size_t) {
write_timer->cancel();
if (ec) {
// write failed—drop this connection entirely
ps->stream.lowest_layer().close();
return done(ec, "");
}
read_timer->expires_after(std::chrono::seconds(5));
read_timer->async_wait([ps](auto ec){
if (!ec) {
stream->lowest_layer().cancel(); // Cancel operation on timeout
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
// cancel the read if its still pending
ps->stream.lowest_layer().cancel();
}
});
});
http::async_read(ps->stream, *buffer, *res,
[this, ps, buffer, res, read_timer, server, port, done]
(boost::system::error_code ec, std::size_t) {
read_timer->cancel();
if (ec) {
// read failed or timed out—drop it
ps->stream.lowest_layer().close();
return done(ec, "");
}
pool_.release(server, port, ps);
auto timer = std::make_shared<boost::asio::steady_timer>(ioContext, std::chrono::seconds(2));
boost::asio::async_connect(stream->lowest_layer(), results,
[stream, connect_timer, req, buffer, res, timer, server, port, target](boost::system::error_code ec, const auto&) {
connect_timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
auto status = res->result();
if (status == http::status::unauthorized) {
cookies.clear(); // clear out any bad cookies
return done({},
"Unauthorized");
}
// Step 2: Asynchronous handshake with timeout
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
stream->async_handshake(boost::asio::ssl::stream_base::client,
[stream, timer, req, buffer, res, server, port, target](boost::system::error_code ec) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 3: Asynchronous write request
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
http::async_write(*stream, *req,
[stream, buffer, res, timer, server, port, target](boost::system::error_code ec, std::size_t) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 4: Asynchronous read response
timer->expires_after(std::chrono::seconds(2));
timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
if (!ec) {
stream->lowest_layer().cancel();
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
}
});
http::async_read(*stream, *buffer, *res,
[stream, timer, res, server, port, target](boost::system::error_code ec, std::size_t) {
timer->cancel();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Error %s for %s:%s/%s", __FUNCTION__, ec.message().c_str(), server.c_str(), port.c_str(), target.c_str());
peer_manager.SetPeerErrorState(server, port);
return;
}
// Step 5: Shutdown the stream
stream->async_shutdown([stream, server, port](boost::system::error_code ec) {
if (ec && ec != boost::asio::error::eof) {
// ignore these
//std::cerr << "Shutdown error: " << ec.message() << std::endl;
}
});
});
});
});
});
ioContext.run();
// Store cookies from the response
if (res->base().count(http::field::set_cookie) > 0) {
auto set_cookie_value = res->base()[http::field::set_cookie].to_string();
std::istringstream stream(set_cookie_value);
std::string token;
// Parse "Set-Cookie" field for name-value pairs
while (std::getline(stream, token, ';')) {
auto pos = token.find('=');
if (pos != std::string::npos) {
std::string name = token.substr(0, pos);
std::string value = token.substr(pos + 1);
cookies[name] = value; // Store each cookie
if (status != http::status::ok) {
LogWrite(PEERING__ERROR, 0, "Peering",
"%s: HTTP error %u", __FUNCTION__, status);
return done(
boost::system::error_code(
static_cast<int>(status),
boost::asio::error::get_ssl_category()
),
"");
}
// cookie logic
if (res->base().count(http::field::set_cookie)) {
auto hdr = res->base()[http::field::set_cookie]
.to_string();
std::istringstream ss(hdr);
std::string token;
while (std::getline(ss, token, ';')) {
auto pos = token.find('=');
if (pos!=std::string::npos) {
cookies[token.substr(0,pos)] =
token.substr(pos+1);
}
}
}
if (res->body() == "Unauthorized")
cookies.clear();
done({}, res->body());
});
});
});
}
std::string HTTPSClient::sendPostRequest(
const std::string& server,
const std::string& port,
const std::string& target,
const std::string& jsonPayload) {
std::promise<std::pair<boost::system::error_code, std::string>> p;
auto f = p.get_future();
// call the async version internally
sendPostRequest(server, port, target, jsonPayload,
[&p](boost::system::error_code ec, std::string body) {
p.set_value({ec, std::move(body)});
});
auto [ec, body] = f.get();
if (ec) {
LogWrite(PEERING__ERROR, 0, "Peering",
"%s: error %s", __FUNCTION__, ec.message().c_str());
return {};
}
return body;
}
// async POST
void HTTPSClient::sendPostRequest(
const std::string& server,
const std::string& port,
const std::string& target,
const std::string& jsonPayload,
std::function<void(boost::system::error_code, std::string)> done)
{
pool_.acquire(server, port,
[this, server, port, target, jsonPayload, done](auto ps, auto ec) {
if (ec) return done(ec, "");
// — heap-allocated POST req —
auto req = std::make_shared<
http::request<http::string_body>>(
http::verb::post, target, 11);
req->set(http::field::host, server);
req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
req->set(http::field::connection, "keep-alive");
req->set(http::field::content_type,
"application/json");
if (!cookies.empty()) {
req->set(http::field::cookie,
buildCookieHeader());
} else {
auto creds = net.GetCmdUser() + ":" +
net.GetCmdPassword();
req->set(http::field::authorization,
"Basic " + base64_encode(creds));
}
req->body() = jsonPayload;
req->prepare_payload();
auto buffer = std::make_shared<
boost::beast::flat_buffer>();
auto res = std::make_shared<
http::response<http::string_body>>();
auto write_timer = std::make_shared<boost::asio::steady_timer>(ioc_);
auto read_timer = std::make_shared<boost::asio::steady_timer>(ioc_);
write_timer->expires_after(std::chrono::seconds(2));
write_timer->async_wait([ps](auto ec){
if (!ec) {
// cancel the write if its still pending
ps->stream.lowest_layer().cancel();
}
});
// keep 'req' alive until write finishes
http::async_write(ps->stream, *req,
[this, ps, req, buffer, res, write_timer, read_timer, server, port, done]
(boost::system::error_code ec, std::size_t) {
write_timer->cancel();
if (ec) {
// write failed—drop this connection entirely
ps->stream.lowest_layer().close();
return done(ec, "");
}
read_timer->expires_after(std::chrono::seconds(5));
read_timer->async_wait([ps](auto ec){
if (!ec) {
// cancel the read if its still pending
ps->stream.lowest_layer().cancel();
}
}
});
if (res->body() == "Unauthorized") {
cookies.clear();
}
http::async_read(ps->stream, *buffer, *res,
[this, ps, buffer, res, read_timer, server, port, done]
(boost::system::error_code ec, std::size_t) {
read_timer->cancel();
if (ec) {
// read failed or timed out—drop it
ps->stream.lowest_layer().close();
return done(ec, "");
}
pool_.release(server, port, ps);
// Return the response body, if available
return res->body();
}
catch (const std::exception& e) {
LogWrite(PEERING__ERROR, 0, "Peering", "%s: Request Error %s for %s:%s/%s", __FUNCTION__, e.what() ? e.what() : "??", server.c_str(), port.c_str(), target.c_str());
return {};
}
}
auto status = res->result();
if (status == http::status::unauthorized) {
cookies.clear(); // clear out any bad cookies
return done({},
"Unauthorized");
}
if (status != http::status::ok) {
LogWrite(PEERING__ERROR, 0, "Peering",
"%s: HTTP error %u", __FUNCTION__, status);
return done(
boost::system::error_code(
static_cast<int>(status),
boost::asio::error::get_ssl_category()
),
"");
}
// cookie logic
if (res->base().count(http::field::set_cookie)) {
auto hdr = res->base()[http::field::set_cookie]
.to_string();
std::istringstream ss(hdr);
std::string token;
while (std::getline(ss, token, ';')) {
auto pos = token.find('=');
if (pos!=std::string::npos) {
cookies[token.substr(0,pos)] =
token.substr(pos+1);
}
}
}
if (res->body() == "Unauthorized")
cookies.clear();
done({}, res->body());
});
});
});
}

View File

@ -26,21 +26,169 @@ along with EQ2Emu. If not, see <http://www.gnu.org/licenses/>.
#include <boost/asio/ssl.hpp>
#include <string>
#include <unordered_map>
#include <deque>
#include <boost/asio/executor_work_guard.hpp>
namespace ssl = boost::asio::ssl;
namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
// at the top of your .cpp:
#include <boost/asio/ssl.hpp>
#include <openssl/ssl.h> // for SSL_set_tlsext_host_name
// …
struct PooledStream {
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> stream;
boost::asio::ip::tcp::resolver resolver;
boost::asio::steady_timer connect_timer;
boost::asio::steady_timer handshake_timer;
PooledStream(boost::asio::io_context& ioc,
boost::asio::ssl::context& ssl_ctx)
: stream(ioc, ssl_ctx)
, resolver(ioc)
, connect_timer(ioc)
, handshake_timer(ioc)
{}
void prepare(const std::string& server,
const std::string& port,
std::function<void(boost::system::error_code)> on_ready)
{
auto endpoints = resolver.resolve(server, port);
// — Step 1: async connect + 2s timeout
connect_timer.expires_after(std::chrono::seconds(2));
connect_timer.async_wait([this, on_ready](auto ec){
if (!ec) {
stream.lowest_layer().cancel();
on_ready(boost::asio::error::timed_out);
}
});
boost::asio::async_connect(
stream.lowest_layer(), endpoints,
[this, server, on_ready](auto ec, auto){
connect_timer.cancel();
if (ec) return on_ready(ec);
// ** SNI: must set the host name for TLS before handshake **
if (!SSL_set_tlsext_host_name(stream.native_handle(), server.c_str())) {
// pull the OpenSSL error and report it
auto err = ::ERR_get_error();
return on_ready(
boost::system::error_code(
static_cast<int>(err),
boost::asio::error::get_ssl_category()
)
);
}
// — Step 2: async handshake + 2s timeout
handshake_timer.expires_after(std::chrono::seconds(2));
handshake_timer.async_wait([this](auto ec){
if (!ec) stream.lowest_layer().cancel();
});
stream.async_handshake(
boost::asio::ssl::stream_base::client,
[this, on_ready](auto ec){
handshake_timer.cancel();
on_ready(ec);
}
);
}
);
}
};
// --- ConnectionPool.h ---------------------------------------
class ConnectionPool {
public:
ConnectionPool(boost::asio::io_context& ioc,
boost::asio::ssl::context& ssl_ctx)
: ioc_(ioc), ssl_ctx_(ssl_ctx) {}
// Acquire a ready PooledStream (connected + handshaken),
// or create one and run prepare().
void acquire(const std::string& server,
const std::string& port,
std::function<void(std::shared_ptr<PooledStream>, boost::system::error_code)> cb)
{
std::string key = server + ":" + port;
{
std::lock_guard lk(mutex_);
auto &dq = free_[key];
if (!dq.empty()) {
auto ps = dq.front();
dq.pop_front();
return cb(ps, {});
}
}
// no free stream → make a new one and prepare it
auto ps = std::make_shared<PooledStream>(ioc_, ssl_ctx_);
ps->prepare(server, port, [this, server, port, ps, cb](auto ec) {
if (ec) {
cb(nullptr, ec);
} else {
cb(ps, {});
}
});
}
// Return a stream to the free list
void release(const std::string& server,
const std::string& port,
std::shared_ptr<PooledStream> ps)
{
std::string key = server + ":" + port;
// clear any leftover data in the buffer
// (you might want to reset ps->buffer here if its stored inside)
std::lock_guard lk(mutex_);
free_[key].push_back(ps);
}
private:
boost::asio::io_context& ioc_;
boost::asio::ssl::context& ssl_ctx_;
std::mutex mutex_;
std::unordered_map<std::string, std::deque<std::shared_ptr<PooledStream>>> free_;
};
class HTTPSClient {
public:
HTTPSClient(const std::string& certFile, const std::string& keyFile);
~HTTPSClient();
// — Async overloads —
void sendRequest(
const std::string& server,
const std::string& port,
const std::string& target,
std::function<void(boost::system::error_code, std::string)> done);
// Send a request with stored cookies and return response as string
std::string sendRequest(const std::string& server, const std::string& port, const std::string& target);
void sendPostRequest(
const std::string& server,
const std::string& port,
const std::string& target,
const std::string& jsonPayload,
std::function<void(boost::system::error_code, std::string)> done);
// Send a POST request with a JSON payload and return response as string
std::string sendPostRequest(const std::string& server, const std::string& port, const std::string& target, const std::string& jsonPayload);
// — Legacy synchronous wrappers —
std::string sendRequest(
const std::string& server,
const std::string& port,
const std::string& target);
std::string sendPostRequest(
const std::string& server,
const std::string& port,
const std::string& target,
const std::string& jsonPayload);
std::string getServer() const { return server; }
std::string getPort() const { return port; }
@ -48,7 +196,11 @@ public:
private:
std::unordered_map<std::string, std::string> cookies;
std::shared_ptr<boost::asio::ssl::context> sslCtx;
boost::asio::io_context ioc_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> workGuard_;
ConnectionPool pool_;
std::thread runner_;
std::string certFile;
std::string keyFile;
std::string server;

View File

@ -76,11 +76,11 @@ std::string WebServer::my_password_callback(
//void handle_root(const http::request<http::string_body>& req, http::response<http::string_body>& res);
WebServer::WebServer(const std::string& address, unsigned short port, const std::string& cert_file, const std::string& key_file, const std::string& key_password, const std::string& hardcode_user, const std::string& hardcode_password)
: ioc_(1),
ssl_ctx_(ssl::context::tlsv13_server),
acceptor_(ioc_, {boost_net::ip::make_address(address), port}) {
: ioc_(1),
ssl_ctx_(ssl::context::tlsv13_server),
acceptor_(ioc_, {boost_net::ip::make_address(address), port}) {
keypasswd = key_password;
// Initialize SSL context
// Initialize SSL context
if(cert_file.size() < 1 || key_file.size() < 1) {
is_ssl = false;
}
@ -91,7 +91,7 @@ WebServer::WebServer(const std::string& address, unsigned short port, const std:
is_ssl = true;
}
keypasswd = ""; // reset no longer needed
// Initialize some test credentials
if(hardcode_user.size() > 0 && hardcode_password.size() > 0)
credentials_[hardcode_user] = hardcode_password;
@ -157,67 +157,75 @@ void WebServer::on_accept(beast::error_code ec, tcp::socket socket) {
do_accept();
}
void WebServer::do_session(tcp::socket socket) {
try {
bool close = false;
beast::flat_buffer buffer;
while (!close) {
// 1) Read a complete request
http::request<http::string_body> req;
http::read(socket, buffer, req);
// 2) Invoke your handler, giving it a lambda that
// sets up version/keep_alive on the response
handle_request(std::move(req), [&](auto&& response) {
// mirror HTTP version
response.version(req.version());
// propagate the clients keep-alive choice
response.keep_alive(req.keep_alive());
// if the client asked us to close, mark for shutdown
if (! req.keep_alive())
close = true;
http::write(socket, response);
});
// 3) Discard anything left in the buffer so the next
// http::read starts fresh
buffer.consume(buffer.size());
}
beast::error_code ec;
socket.shutdown(tcp::socket::shutdown_send, ec);
}
catch (const std::exception& e) {
// irrelevant spam for now really
}
}
void WebServer::do_session_ssl(tcp::socket socket) {
try {
ssl::stream<tcp::socket> stream(std::move(socket), ssl_ctx_);
stream.handshake(ssl::stream_base::server);
ssl::stream<tcp::socket> stream(std::move(socket), ssl_ctx_);
stream.handshake(ssl::stream_base::server);
bool close = false;
beast::flat_buffer buffer;
while (!close) {
http::request<http::string_body> req;
http::read(stream, buffer, req);
// Send the response
http::read(stream, buffer, req);
handle_request(std::move(req), [&](auto&& response) {
if (response.need_eof()) {
response.version(req.version());
response.keep_alive(req.keep_alive());
if (! req.keep_alive())
close = true;
}
http::write(stream, response);
http::write(stream, response);
});
if (close) break;
buffer.consume(buffer.size());
}
beast::error_code ec;
socket.shutdown(tcp::socket::shutdown_send, ec);
stream.next_layer().shutdown(tcp::socket::shutdown_send, ec);
}
catch (const std::exception& e) {
// irrelevant spam for now really
}
}
void WebServer::do_session(tcp::socket socket) {
try {
bool close = false;
beast::flat_buffer buffer;
while (!close) {
http::request<http::string_body> req;
http::read(socket, buffer, req);
// Send the response
handle_request(std::move(req), [&](auto&& response) {
if (response.need_eof()) {
close = true;
}
http::write(socket, response);
});
if (close) break;
}
beast::error_code ec;
socket.shutdown(tcp::socket::shutdown_send, ec);
}
catch (const std::exception& e) {
// irrelevant spam for now really
}
}
template <class Body, class Allocator>
void WebServer::handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, std::function<void(http::response<http::string_body>&&)> send) {
auto it = noauth_routes_.find(req.target().to_string());