419 lines
12 KiB
C++
419 lines
12 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/>.
|
||
*/
|
||
|
||
#include "HTTPSClient.h"
|
||
#include "PeerManager.h"
|
||
|
||
#include "../net.h"
|
||
#include "../../common/Log.h"
|
||
#include <boost/property_tree/ptree.hpp>
|
||
#include <boost/property_tree/json_parser.hpp>
|
||
#include <boost/beast/ssl.hpp>
|
||
#include <boost/beast/core.hpp>
|
||
#include <boost/beast/http.hpp>
|
||
#include <boost/asio.hpp>
|
||
#include <boost/asio/connect.hpp>
|
||
#include <boost/asio/steady_timer.hpp>
|
||
#include <iostream>
|
||
#include <sstream>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace boost_net = boost::asio; // From <boost/asio.hpp>
|
||
extern NetConnection net;
|
||
extern PeerManager peer_manager;
|
||
static const std::string base64_chars =
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
"abcdefghijklmnopqrstuvwxyz"
|
||
"0123456789+/";
|
||
|
||
std::string base64_encode(const std::string& input) {
|
||
std::string encoded_string;
|
||
unsigned char const* bytes_to_encode = reinterpret_cast<const unsigned char*>(input.c_str());
|
||
size_t in_len = input.size();
|
||
int i = 0;
|
||
int j = 0;
|
||
unsigned char char_array_3[3];
|
||
unsigned char char_array_4[4];
|
||
|
||
while (in_len--) {
|
||
char_array_3[i++] = *(bytes_to_encode++);
|
||
if (i == 3) {
|
||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||
|
||
for (i = 0; (i < 4); i++)
|
||
encoded_string += base64_chars[char_array_4[i]];
|
||
i = 0;
|
||
}
|
||
}
|
||
|
||
if (i) {
|
||
for (j = i; j < 3; j++)
|
||
char_array_3[j] = '\0';
|
||
|
||
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
|
||
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
|
||
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
|
||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||
|
||
for (j = 0; (j < i + 1); j++)
|
||
encoded_string += base64_chars[char_array_4[j]];
|
||
|
||
while ((i++ < 3))
|
||
encoded_string += '=';
|
||
}
|
||
|
||
return encoded_string;
|
||
}
|
||
|
||
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->set_verify_mode(ssl::verify_peer);
|
||
sslCtx->set_default_verify_paths();
|
||
return sslCtx;
|
||
}
|
||
|
||
void HTTPSClient::parseAndStoreCookies(const http::response<http::string_body>& res) {
|
||
if (res.count(http::field::set_cookie)) {
|
||
std::istringstream stream(res[http::field::set_cookie].to_string());
|
||
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
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
std::string HTTPSClient::buildCookieHeader() const {
|
||
std::string cookieHeader;
|
||
for (const auto& [name, value] : cookies) {
|
||
cookieHeader += name + "=" + value;
|
||
}
|
||
return cookieHeader;
|
||
}
|
||
|
||
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();
|
||
|
||
// call the async overload
|
||
sendRequest(server, port, target,
|
||
[&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: Request Error %s", __FUNCTION__, ec.message().c_str());
|
||
return {};
|
||
}
|
||
return body;
|
||
}
|
||
|
||
// 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, "");
|
||
|
||
auto req = std::make_shared<
|
||
http::request<http::string_body>>(
|
||
http::verb::get, 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));
|
||
}
|
||
|
||
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();
|
||
}
|
||
});
|
||
// 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) {
|
||
// 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 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());
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
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();
|
||
}
|
||
});
|
||
|
||
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 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());
|
||
});
|
||
});
|
||
});
|
||
}
|