/* 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 . */ #include "HTTPSClient.h" #include "PeerManager.h" #include "../net.h" #include "../../common/Log.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace boost_net = boost::asio; // From 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(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 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->set_verify_mode(ssl::verify_peer); sslCtx->set_default_verify_paths(); return sslCtx; } void HTTPSClient::parseAndStoreCookies(const http::response& 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> 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 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::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(); 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(); } }); // 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(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(); } }); 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(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()); }); }); }); }