211 lines
6.1 KiB
C++
211 lines
6.1 KiB
C++
/*
|
||
EQ2Emulator: Everquest II Server Emulator
|
||
Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net)
|
||
|
||
This file is part of EQ2Emulator.
|
||
|
||
EQ2Emulator is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
EQ2Emulator is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#ifndef HTTPSCLIENT_H
|
||
#define HTTPSCLIENT_H
|
||
|
||
#include <boost/asio.hpp>
|
||
#include <boost/beast.hpp>
|
||
#include <boost/asio/ssl.hpp>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <deque>
|
||
#include <boost/asio/executor_work_guard.hpp>
|
||
#include <boost/asio/ssl.hpp>
|
||
#include <openssl/ssl.h> // for SSL_set_tlsext_host_name
|
||
|
||
namespace ssl = boost::asio::ssl;
|
||
namespace asio = boost::asio;
|
||
namespace beast = boost::beast;
|
||
namespace http = beast::http;
|
||
|
||
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);
|
||
|
||
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);
|
||
|
||
// — 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; }
|
||
|
||
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;
|
||
std::string port;
|
||
|
||
void parseAndStoreCookies(const http::response<http::string_body>& res);
|
||
std::shared_ptr<boost::asio::ssl::context> createSSLContext(); // New helper function
|
||
std::string buildCookieHeader() const;
|
||
};
|
||
|
||
#endif // HTTPSCLIENT_H
|