HTTP Persistence for world client/server peering
This commit is contained in:
parent
93b7620364
commit
9e1f77e28e
@ -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 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<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 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<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 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 {};
|
||||
}
|
||||
}
|
||||
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());
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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 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<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;
|
||||
|
@ -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 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<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());
|
||||
|
Loading…
x
Reference in New Issue
Block a user