1
0
Protocol/old/world/Web/HTTPSClient.cpp
2025-09-02 20:25:42 -05:00

419 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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 its 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 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 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 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();
}
});
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());
});
});
});
}