diff --git a/source/WorldServer/Web/HTTPSClient.cpp b/source/WorldServer/Web/HTTPSClient.cpp index b68d6d4..ae4cca1 100644 --- a/source/WorldServer/Web/HTTPSClient.cpp +++ b/source/WorldServer/Web/HTTPSClient.cpp @@ -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 HTTPSClient::createSSLContext() { auto sslCtx = std::make_shared(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> p; + auto f = p.get_future(); - auto stream = std::make_shared>(ioContext, *sslCtx); - auto resolver = std::make_shared(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>(); - auto buffer = std::make_shared(); - auto req = std::make_shared>(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(::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(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(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 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>(ioContext, *sslCtx); - auto resolver = std::make_shared(ioContext); - auto results = resolver->resolve(server, port); + auto req = std::make_shared< + http::request>( + http::verb::get, target, 11); - // Persistent objects to manage response, request, and buffer - auto res = std::make_shared>(); - auto buffer = std::make_shared(); - auto req = std::make_shared>(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(::ERR_get_error()), boost::asio::error::get_ssl_category())); + auto buffer = std::make_shared(); + auto res = std::make_shared< + http::response>(); + auto write_timer = std::make_shared(ioc_); + auto read_timer = std::make_shared(ioc_); + + write_timer->expires_after(std::chrono::seconds(2)); + write_timer->async_wait([ps](auto ec){ + if (!ec) { + // cancel the write if it’s 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(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 it’s 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(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(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> 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 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::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>(); + auto write_timer = std::make_shared(ioc_); + auto read_timer = std::make_shared(ioc_); + + write_timer->expires_after(std::chrono::seconds(2)); + write_timer->async_wait([ps](auto ec){ + if (!ec) { + // cancel the write if it’s 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 it’s 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 {}; - } -} \ No newline at end of file + 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(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()); + }); + }); + }); +} diff --git a/source/WorldServer/Web/HTTPSClient.h b/source/WorldServer/Web/HTTPSClient.h index a5b05ff..00712d5 100644 --- a/source/WorldServer/Web/HTTPSClient.h +++ b/source/WorldServer/Web/HTTPSClient.h @@ -26,21 +26,169 @@ along with EQ2Emu. If not, see . #include #include #include +#include +#include namespace ssl = boost::asio::ssl; namespace asio = boost::asio; namespace beast = boost::beast; namespace http = beast::http; +// at the top of your .cpp: +#include +#include // for SSL_set_tlsext_host_name + +// … + +struct PooledStream { + boost::asio::ssl::stream 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 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(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, 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(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 ps) + { + std::string key = server + ":" + port; + // clear any leftover data in the buffer + // (you might want to reset ps->buffer here if it’s 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>> 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 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 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 cookies; std::shared_ptr sslCtx; - + boost::asio::io_context ioc_; + boost::asio::executor_work_guard workGuard_; + ConnectionPool pool_; + std::thread runner_; + std::string certFile; std::string keyFile; std::string server; diff --git a/source/common/Web/WebServer.cpp b/source/common/Web/WebServer.cpp index 861ec86..b563db4 100644 --- a/source/common/Web/WebServer.cpp +++ b/source/common/Web/WebServer.cpp @@ -76,11 +76,11 @@ std::string WebServer::my_password_callback( //void handle_root(const http::request& req, http::response& 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 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 client’s 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 stream(std::move(socket), ssl_ctx_); - stream.handshake(ssl::stream_base::server); - + ssl::stream stream(std::move(socket), ssl_ctx_); + stream.handshake(ssl::stream_base::server); + bool close = false; beast::flat_buffer buffer; while (!close) { http::request 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 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 void WebServer::handle_request(http::request>&& req, std::function&&)> send) { auto it = noauth_routes_.find(req.target().to_string());