123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- /*
- EQ2Emu: Everquest II Server Emulator
- Copyright (C) 2007-2025 EQ2Emu Development Team (https://www.eq2emu.com)
- This file is part of EQ2Emu.
- EQ2Emu 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.
- EQ2Emu 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 EQ2Emu. 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) {}
- 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;
- }
- 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) {
- try {
- boost::asio::io_context ioContext;
- // SSL and TCP setup
- auto sslCtx = createSSLContext();
- 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);
- // 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 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 (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 {};
- }
- }
- 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;
- // SSL and TCP setup
- auto sslCtx = createSSLContext();
- 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);
- // 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);
- // 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 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) {
- 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 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 (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 {};
- }
- }
|