HTTPSClient.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. /*
  2. EQ2Emu: Everquest II Server Emulator
  3. Copyright (C) 2007-2025 EQ2Emu Development Team (https://www.eq2emu.com)
  4. This file is part of EQ2Emu.
  5. EQ2Emu is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. EQ2Emu is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with EQ2Emu. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #include "HTTPSClient.h"
  17. #include "PeerManager.h"
  18. #include "../net.h"
  19. #include "../../common/Log.h"
  20. #include <boost/property_tree/ptree.hpp>
  21. #include <boost/property_tree/json_parser.hpp>
  22. #include <boost/beast/ssl.hpp>
  23. #include <boost/beast/core.hpp>
  24. #include <boost/beast/http.hpp>
  25. #include <boost/asio.hpp>
  26. #include <boost/asio/connect.hpp>
  27. #include <boost/asio/steady_timer.hpp>
  28. #include <iostream>
  29. #include <sstream>
  30. #include <string>
  31. #include <vector>
  32. namespace boost_net = boost::asio; // From <boost/asio.hpp>
  33. extern NetConnection net;
  34. extern PeerManager peer_manager;
  35. static const std::string base64_chars =
  36. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  37. "abcdefghijklmnopqrstuvwxyz"
  38. "0123456789+/";
  39. std::string base64_encode(const std::string& input) {
  40. std::string encoded_string;
  41. unsigned char const* bytes_to_encode = reinterpret_cast<const unsigned char*>(input.c_str());
  42. size_t in_len = input.size();
  43. int i = 0;
  44. int j = 0;
  45. unsigned char char_array_3[3];
  46. unsigned char char_array_4[4];
  47. while (in_len--) {
  48. char_array_3[i++] = *(bytes_to_encode++);
  49. if (i == 3) {
  50. char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
  51. char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
  52. char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
  53. char_array_4[3] = char_array_3[2] & 0x3f;
  54. for (i = 0; (i < 4); i++)
  55. encoded_string += base64_chars[char_array_4[i]];
  56. i = 0;
  57. }
  58. }
  59. if (i) {
  60. for (j = i; j < 3; j++)
  61. char_array_3[j] = '\0';
  62. char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
  63. char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
  64. char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
  65. char_array_4[3] = char_array_3[2] & 0x3f;
  66. for (j = 0; (j < i + 1); j++)
  67. encoded_string += base64_chars[char_array_4[j]];
  68. while ((i++ < 3))
  69. encoded_string += '=';
  70. }
  71. return encoded_string;
  72. }
  73. HTTPSClient::HTTPSClient(const std::string& certFile, const std::string& keyFile)
  74. : certFile(certFile), keyFile(keyFile) {}
  75. std::shared_ptr<boost::asio::ssl::context> HTTPSClient::createSSLContext() {
  76. auto sslCtx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tlsv13_client);
  77. sslCtx->set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use);
  78. sslCtx->use_certificate_file(certFile, boost::asio::ssl::context::pem);
  79. sslCtx->use_private_key_file(keyFile, boost::asio::ssl::context::pem);
  80. sslCtx->set_verify_mode(ssl::verify_peer);
  81. sslCtx->set_default_verify_paths();
  82. return sslCtx;
  83. }
  84. void HTTPSClient::parseAndStoreCookies(const http::response<http::string_body>& res) {
  85. if (res.count(http::field::set_cookie)) {
  86. std::istringstream stream(res[http::field::set_cookie].to_string());
  87. std::string token;
  88. // Parse "Set-Cookie" field for name-value pairs
  89. while (std::getline(stream, token, ';')) {
  90. auto pos = token.find('=');
  91. if (pos != std::string::npos) {
  92. std::string name = token.substr(0, pos);
  93. std::string value = token.substr(pos + 1);
  94. cookies[name] = value; // Store each cookie
  95. }
  96. }
  97. }
  98. }
  99. std::string HTTPSClient::buildCookieHeader() const {
  100. std::string cookieHeader;
  101. for (const auto& [name, value] : cookies) {
  102. cookieHeader += name + "=" + value;
  103. }
  104. return cookieHeader;
  105. }
  106. std::string HTTPSClient::sendRequest(const std::string& server, const std::string& port, const std::string& target) {
  107. try {
  108. boost::asio::io_context ioContext;
  109. // SSL and TCP setup
  110. auto sslCtx = createSSLContext();
  111. auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(ioContext, *sslCtx);
  112. auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(ioContext);
  113. auto results = resolver->resolve(server, port);
  114. // Persistent objects to manage response, request, and buffer
  115. auto res = std::make_shared<http::response<http::string_body>>();
  116. auto buffer = std::make_shared<boost::beast::flat_buffer>();
  117. auto req = std::make_shared<http::request<http::string_body>>(http::verb::get, target, 11);
  118. // SNI hostname (required for many hosts)
  119. if (!SSL_set_tlsext_host_name(stream->native_handle(), server.c_str())) {
  120. throw boost::beast::system_error(
  121. boost::beast::error_code(static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()));
  122. }
  123. // Prepare request headers
  124. req->set(http::field::host, server);
  125. req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
  126. req->set(boost::beast::http::field::connection, "close");
  127. req->set(http::field::content_type, "application/json");
  128. if (!cookies.empty()) {
  129. req->set(http::field::cookie, buildCookieHeader());
  130. }
  131. else {
  132. std::string credentials = net.GetCmdUser() + ":" + net.GetCmdPassword();
  133. std::string encodedCredentials = base64_encode(credentials);
  134. req->set(http::field::authorization, "Basic " + encodedCredentials);
  135. }
  136. // Step 1: Asynchronous connect with timeout
  137. auto connect_timer = std::make_shared<boost::asio::steady_timer>(ioContext);
  138. connect_timer->expires_after(std::chrono::seconds(2));
  139. connect_timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  140. if (!ec) {
  141. stream->lowest_layer().cancel(); // Cancel operation on timeout
  142. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  143. peer_manager.SetPeerErrorState(server, port);
  144. }
  145. });
  146. auto timer = std::make_shared<boost::asio::steady_timer>(ioContext, std::chrono::seconds(2));
  147. boost::asio::async_connect(stream->lowest_layer(), results,
  148. [stream, connect_timer, req, buffer, res, timer, server, port, target](boost::system::error_code ec, const auto&) {
  149. connect_timer->cancel();
  150. if (ec) {
  151. 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());
  152. peer_manager.SetPeerErrorState(server, port);
  153. return;
  154. }
  155. // Step 2: Asynchronous handshake with timeout
  156. timer->expires_after(std::chrono::seconds(2));
  157. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  158. if (!ec) {
  159. stream->lowest_layer().cancel();
  160. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  161. peer_manager.SetPeerErrorState(server, port);
  162. }
  163. });
  164. stream->async_handshake(boost::asio::ssl::stream_base::client,
  165. [stream, timer, req, buffer, res, server, port, target](boost::system::error_code ec) {
  166. timer->cancel();
  167. if (ec) {
  168. 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());
  169. peer_manager.SetPeerErrorState(server, port);
  170. return;
  171. }
  172. // Step 3: Asynchronous write request
  173. timer->expires_after(std::chrono::seconds(2));
  174. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  175. if (!ec) {
  176. stream->lowest_layer().cancel();
  177. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  178. peer_manager.SetPeerErrorState(server, port);
  179. }
  180. });
  181. http::async_write(*stream, *req,
  182. [stream, buffer, res, timer, server, port, target](boost::system::error_code ec, std::size_t) {
  183. timer->cancel();
  184. if (ec) {
  185. 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());
  186. peer_manager.SetPeerErrorState(server, port);
  187. return;
  188. }
  189. // Step 4: Asynchronous read response
  190. timer->expires_after(std::chrono::seconds(2));
  191. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  192. if (!ec) {
  193. stream->lowest_layer().cancel();
  194. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  195. peer_manager.SetPeerErrorState(server, port);
  196. }
  197. });
  198. http::async_read(*stream, *buffer, *res,
  199. [stream, timer, res, server, port, target](boost::system::error_code ec, std::size_t) {
  200. timer->cancel();
  201. if (ec) {
  202. 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());
  203. peer_manager.SetPeerErrorState(server, port);
  204. return;
  205. }
  206. // Step 5: Shutdown the stream
  207. stream->async_shutdown([stream, server, port](boost::system::error_code ec) {
  208. if (ec && ec != boost::asio::error::eof) {
  209. // ignore these
  210. //std::cerr << "Shutdown error: " << ec.message() << std::endl;
  211. }
  212. });
  213. });
  214. });
  215. });
  216. });
  217. ioContext.run();
  218. // Store cookies from the response
  219. if (res->base().count(http::field::set_cookie) > 0) {
  220. auto set_cookie_value = res->base()[http::field::set_cookie].to_string();
  221. std::istringstream stream(set_cookie_value);
  222. std::string token;
  223. // Parse "Set-Cookie" field for name-value pairs
  224. while (std::getline(stream, token, ';')) {
  225. auto pos = token.find('=');
  226. if (pos != std::string::npos) {
  227. std::string name = token.substr(0, pos);
  228. std::string value = token.substr(pos + 1);
  229. cookies[name] = value; // Store each cookie
  230. }
  231. }
  232. }
  233. if (res->body() == "Unauthorized") {
  234. cookies.clear();
  235. }
  236. // Return the response body, if available
  237. return res->body();
  238. }
  239. catch (const std::exception& e) {
  240. 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());
  241. return {};
  242. }
  243. }
  244. std::string HTTPSClient::sendPostRequest(const std::string& server, const std::string& port, const std::string& target, const std::string& jsonPayload) {
  245. try {
  246. boost::asio::io_context ioContext;
  247. // SSL and TCP setup
  248. auto sslCtx = createSSLContext();
  249. auto stream = std::make_shared<boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>(ioContext, *sslCtx);
  250. auto resolver = std::make_shared<boost::asio::ip::tcp::resolver>(ioContext);
  251. auto results = resolver->resolve(server, port);
  252. // Persistent objects to manage response, request, and buffer
  253. auto res = std::make_shared<http::response<http::string_body>>();
  254. auto buffer = std::make_shared<boost::beast::flat_buffer>();
  255. auto req = std::make_shared<http::request<http::string_body>>(http::verb::post, target, 11);
  256. // SNI hostname (required for many hosts)
  257. if (!SSL_set_tlsext_host_name(stream->native_handle(), server.c_str())) {
  258. throw boost::beast::system_error(
  259. boost::beast::error_code(static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category()));
  260. }
  261. // Prepare HTTP POST request with JSON payload
  262. req->set(http::field::host, server);
  263. req->set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
  264. req->set(boost::beast::http::field::connection, "close");
  265. req->set(http::field::content_type, "application/json");
  266. if (!cookies.empty()) {
  267. req->set(http::field::cookie, buildCookieHeader());
  268. }
  269. else {
  270. std::string credentials = net.GetCmdUser() + ":" + net.GetCmdPassword();
  271. std::string encodedCredentials = base64_encode(credentials);
  272. req->set(http::field::authorization, "Basic " + encodedCredentials);
  273. }
  274. req->body() = jsonPayload;
  275. req->prepare_payload();
  276. // Step 1: Asynchronous connect with timeout
  277. auto connect_timer = std::make_shared<boost::asio::steady_timer>(ioContext);
  278. connect_timer->expires_after(std::chrono::seconds(2));
  279. connect_timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  280. if (!ec) {
  281. stream->lowest_layer().cancel(); // Cancel operation on timeout
  282. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Connect Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  283. peer_manager.SetPeerErrorState(server, port);
  284. }
  285. });
  286. auto timer = std::make_shared<boost::asio::steady_timer>(ioContext, std::chrono::seconds(2));
  287. boost::asio::async_connect(stream->lowest_layer(), results,
  288. [stream, connect_timer, req, buffer, res, timer, server, port, target](boost::system::error_code ec, const auto&) {
  289. connect_timer->cancel();
  290. if (ec) {
  291. 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());
  292. peer_manager.SetPeerErrorState(server, port);
  293. return;
  294. }
  295. // Step 2: Asynchronous handshake with timeout
  296. timer->expires_after(std::chrono::seconds(2));
  297. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  298. if (!ec) {
  299. stream->lowest_layer().cancel();
  300. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Handshake Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  301. peer_manager.SetPeerErrorState(server, port);
  302. }
  303. });
  304. stream->async_handshake(boost::asio::ssl::stream_base::client,
  305. [stream, timer, req, buffer, res, server, port, target](boost::system::error_code ec) {
  306. timer->cancel();
  307. if (ec) {
  308. 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());
  309. peer_manager.SetPeerErrorState(server, port);
  310. return;
  311. }
  312. // Step 3: Asynchronous write request
  313. timer->expires_after(std::chrono::seconds(2));
  314. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  315. if (!ec) {
  316. stream->lowest_layer().cancel();
  317. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Write Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  318. peer_manager.SetPeerErrorState(server, port);
  319. }
  320. });
  321. http::async_write(*stream, *req,
  322. [stream, buffer, res, timer, server, port, target](boost::system::error_code ec, std::size_t) {
  323. timer->cancel();
  324. if (ec) {
  325. 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());
  326. peer_manager.SetPeerErrorState(server, port);
  327. return;
  328. }
  329. // Step 4: Asynchronous read response
  330. timer->expires_after(std::chrono::seconds(2));
  331. timer->async_wait([stream, server, port, target](boost::system::error_code ec) {
  332. if (!ec) {
  333. stream->lowest_layer().cancel();
  334. LogWrite(PEERING__ERROR, 0, "Peering", "%s: Read Timeout for %s:%s/%s", __FUNCTION__, server.c_str(), port.c_str(), target.c_str());
  335. peer_manager.SetPeerErrorState(server, port);
  336. }
  337. });
  338. http::async_read(*stream, *buffer, *res,
  339. [stream, timer, res, server, port, target](boost::system::error_code ec, std::size_t) {
  340. timer->cancel();
  341. if (ec) {
  342. 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());
  343. peer_manager.SetPeerErrorState(server, port);
  344. return;
  345. }
  346. // Step 5: Shutdown the stream
  347. stream->async_shutdown([stream, server, port](boost::system::error_code ec) {
  348. if (ec && ec != boost::asio::error::eof) {
  349. // ignore these
  350. //std::cerr << "Shutdown error: " << ec.message() << std::endl;
  351. }
  352. });
  353. });
  354. });
  355. });
  356. });
  357. ioContext.run();
  358. // Store cookies from the response
  359. if (res->base().count(http::field::set_cookie) > 0) {
  360. auto set_cookie_value = res->base()[http::field::set_cookie].to_string();
  361. std::istringstream stream(set_cookie_value);
  362. std::string token;
  363. // Parse "Set-Cookie" field for name-value pairs
  364. while (std::getline(stream, token, ';')) {
  365. auto pos = token.find('=');
  366. if (pos != std::string::npos) {
  367. std::string name = token.substr(0, pos);
  368. std::string value = token.substr(pos + 1);
  369. cookies[name] = value; // Store each cookie
  370. }
  371. }
  372. }
  373. if (res->body() == "Unauthorized") {
  374. cookies.clear();
  375. }
  376. // Return the response body, if available
  377. return res->body();
  378. }
  379. catch (const std::exception& e) {
  380. 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());
  381. return {};
  382. }
  383. }