From 44e0f3aa21411781cd487c58ec0da728428062d4 Mon Sep 17 00:00:00 2001 From: Lion Kortlepel Date: Thu, 1 Jul 2021 00:33:28 +0200 Subject: [PATCH] add MP.HttpsGET, MP.HttpsPOST --- include/Http.h | 5 +- src/Http.cpp | 105 +++++++++++++++++++++++++----------- src/THeartbeatThread.cpp | 4 +- src/TLuaFile.cpp | 112 +++++++++++++++++++++++++++++++++++++-- src/TNetwork.cpp | 4 +- 5 files changed, 188 insertions(+), 42 deletions(-) diff --git a/include/Http.h b/include/Http.h index 375aba3..7efba8f 100644 --- a/include/Http.h +++ b/include/Http.h @@ -4,6 +4,7 @@ #include namespace Http { -std::string GET(const std::string& host, int port, const std::string& target); -std::string POST(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType); +std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr); +std::string POST(const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr); +static inline const char* ErrorString = "!BeamMPHttpsError!"; } diff --git a/src/Http.cpp b/src/Http.cpp index 32f8745..00c557b 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -14,47 +14,85 @@ namespace net = boost::asio; // from namespace ssl = net::ssl; // from using tcp = net::ip::tcp; // from -std::string Http::GET(const std::string& host, int port, const std::string& target) { - // FIXME: doesn't support https - // if it causes issues, yell at me and I'll fix it asap. - Lion +std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) { try { - net::io_context io; - tcp::resolver resolver(io); - beast::tcp_stream stream(io); - auto const results = resolver.resolve(host, std::to_string(port)); - stream.connect(results); + // Check command line arguments. + int version = 11; - http::request req { http::verb::get, target, 11 /* http 1.1 */ }; + // The io_context is required for all I/O + net::io_context ioc; - req.set(http::field::host, host); - // tell the server what we are (boost beast) - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + // The SSL context is required, and holds certificates + ssl::context ctx(ssl::context::tlsv12_client); - http::write(stream, req); + // This holds the root certificate used for verification + // we don't do / have this + // load_root_certificates(ctx); - // used for reading - beast::flat_buffer buffer; - http::response response; + // Verify the remote server's certificate + ctx.set_verify_mode(ssl::verify_none); - http::read(stream, buffer, response); + // These objects perform our I/O + tcp::resolver resolver(ioc); + beast::ssl_stream stream(ioc, ctx); - std::string result(response.body()); - - beast::error_code ec; - stream.socket().shutdown(tcp::socket::shutdown_both, ec); - if (ec && ec != beast::errc::not_connected) { - throw beast::system_error { ec }; // goes down to `return "-1"` anyways + // Set SNI Hostname (many hosts need this to handshake successfully) + if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { + beast::error_code ec { static_cast(::ERR_get_error()), net::error::get_ssl_category() }; + throw beast::system_error { ec }; } - return result; + // Look up the domain name + auto const results = resolver.resolve(host.c_str(), std::to_string(port)); - } catch (const std::exception& e) { - Application::Console().Write(e.what()); - return "-1"; + // Make the connection on the IP address we get from a lookup + beast::get_lowest_layer(stream).connect(results); + + // Perform the SSL handshake + stream.handshake(ssl::stream_base::client); + + // Set up an HTTP GET request message + http::request req { http::verb::get, target, version }; + req.set(http::field::host, host); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + + // Send the HTTP request to the remote host + http::write(stream, req); + + // This buffer is used for reading and must be persisted + beast::flat_buffer buffer; + + // Declare a container to hold the response + http::response res; + + // Receive the HTTP response + http::read(stream, buffer, res); + + // Gracefully close the stream + beast::error_code ec; + stream.shutdown(ec); + if (ec == net::error::eof) { + // Rationale: + // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error + ec = {}; + } + + if (status) { + *status = res.base().result_int(); + } + + if (ec) + throw beast::system_error { ec }; + + // If we get here then the connection is closed gracefully + return std::string(res.body()); + } catch (std::exception const& e) { + Application::Console().Write(__func__ + std::string(": ") + e.what()); + return ErrorString; } } -std::string Http::POST(const std::string& host, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType) { +std::string Http::POST(const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status) { try { net::io_context io; @@ -68,7 +106,7 @@ std::string Http::POST(const std::string& host, const std::string& target, const decltype(resolver)::results_type results; auto try_connect_with_protocol = [&](tcp protocol) { try { - results = resolver.resolve(protocol, host, std::to_string(443)); + results = resolver.resolve(protocol, host, std::to_string(port)); if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { boost::system::error_code ec { static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category() }; // FIXME: we could throw and crash, if we like @@ -121,6 +159,10 @@ std::string Http::POST(const std::string& host, const std::string& target, const http::read(stream, buffer, response); + if (status) { + *status = response.base().result_int(); + } + std::stringstream result; result << response; @@ -131,11 +173,12 @@ std::string Http::POST(const std::string& host, const std::string& target, const // info(result.str()); std::string debug_response_str; std::getline(result, debug_response_str); + //debug("POST " + host + target + ": " + debug_response_str); return std::string(response.body()); } catch (const std::exception& e) { - Application::Console().Write(e.what()); - return "-1"; + Application::Console().Write(__func__ + std::string(": ") + e.what()); + return ErrorString; } } diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index 0d4f4b3..bacb235 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -35,12 +35,12 @@ void THeartbeatThread::operator()() { Body += "&pps=" + Application::PPS(); - T = Http::POST(Application::GetBackendHostname(), "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); + T = Http::POST(Application::GetBackendHostname(), 443, "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); if (T.substr(0, 2) != "20") { //Backend system refused server startup! std::this_thread::sleep_for(std::chrono::milliseconds(500)); - T = Http::POST(Application::GetBackendHostname(), "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); + T = Http::POST(Application::GetBackendHostname(), 443, "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); // TODO backup2 + HTTP flag (no TSL) if (T.substr(0, 2) != "20") { warn("Backend system refused server! Server might not show in the public list"); diff --git a/src/TLuaFile.cpp b/src/TLuaFile.cpp index 6c058b1..26a7bf9 100644 --- a/src/TLuaFile.cpp +++ b/src/TLuaFile.cpp @@ -650,7 +650,7 @@ int lua_Registered(lua_State* L) { lua_getstack(L, 0, &info); lua_getinfo(L, "n", &info); - if(auto it = TLuaEngine::mGlobals.find(info.name); it != TLuaEngine::mGlobals.end()){ + if (auto it = TLuaEngine::mGlobals.find(info.name); it != TLuaEngine::mGlobals.end()) { lua_getglobal(it->second, info.name); if (lua_isfunction(it->second, -1)) { lua_pcall(it->second, 0, 0, 0); //TODO revisit to allow arguments and return also we need to mutex this @@ -663,13 +663,13 @@ int lua_Registered(lua_State* L) { } int lua_Register(lua_State* L) { - if(lua_isstring(L, 1)){ + if (lua_isstring(L, 1)) { std::string Name(lua_tolstring(L, 1, nullptr)); lua_getglobal(L, Name.c_str()); if (lua_isfunction(L, -1)) { TLuaEngine::mGlobals.emplace(Name, L); for (auto& Script : Engine().LuaFiles()) { - if(Script->GetState() != L){ + if (Script->GetState() != L) { lua_register(Script->GetState(), Name.c_str(), lua_Registered); } } @@ -679,7 +679,7 @@ int lua_Register(lua_State* L) { ClearStack(L); } } else { - SendError(Engine(), L, "Register wrong arguments expected string"); + SendError(Engine(), L, "Wrong arguments to `Register`, expected string"); } return 0; } @@ -772,6 +772,106 @@ int lua_GetOSName(lua_State* L) { return 1; } +// status, body = HttpGET(host, port, target) +// example usage: +// send a GET https://example.com:443/index.html: +// status, body = MP.HttpGET("example.com", 443, "/index.html") +int lua_HttpsGET(lua_State* L) { + if (!lua_isstring(L, 1)) { + SendError(Engine(), L, "`HttpsGET` expects host (type string) as first argument."); + ClearStack(L); + return 0; + } + if (!lua_isnumber(L, 2)) { + SendError(Engine(), L, "`HttpsGET` expects port (type number) as second argument."); + ClearStack(L); + return 0; + } + if (!lua_isstring(L, 3)) { + SendError(Engine(), L, "`HttpsGET` expects target (type string) as third argument."); + ClearStack(L); + return 0; + } + + auto Host = lua_tostring(L, 1); + auto Port = int(lua_tointeger(L, 2)); + auto Target = lua_tostring(L, 3); + + ClearStack(L); + + unsigned int Status; + auto Body = Http::GET(Host, Port, Target, &Status); + lua_pushinteger(L, Status); + + auto PrettyRemote = "https://" + std::string(Host) + ":" + std::to_string(Port) + std::string(Target); + if (Body == Http::ErrorString) { + SendError(Engine(), L, "HTTPS GET " + PrettyRemote + " failed status " + std::to_string(Status) + ". Check the console or log for more info."); + return 1; + } else { + debug("GET " + PrettyRemote + " completed status " + std::to_string(Status)); + } + + lua_pushstring(L, Body.c_str()); + + return 2; +} + +// status, body = HttpsPOST(host, port, target, body, content_type) +int lua_HttpsPOST(lua_State* L) { + if (!lua_isstring(L, 1)) { + SendError(Engine(), L, "`HttpsPOST` expects host (type string) as 1. argument."); + ClearStack(L); + return 0; + } + if (!lua_isnumber(L, 2)) { + SendError(Engine(), L, "`HttpsPOST` expects port (type number) as 2. argument."); + ClearStack(L); + return 0; + } + if (!lua_isstring(L, 3)) { + SendError(Engine(), L, "`HttpsPOST` expects target (type string) as 3. argument."); + ClearStack(L); + return 0; + } + if (!lua_isstring(L, 4)) { + SendError(Engine(), L, "`HttpsPOST` expects body (type string) as 4. argument."); + ClearStack(L); + return 0; + } + if (!lua_isstring(L, 5)) { + SendError(Engine(), L, "`HttpsPOST` expects content_type (type string) as 5. argument."); + ClearStack(L); + return 0; + } + + auto Host = lua_tostring(L, 1); + auto Port = int(lua_tointeger(L, 2)); + auto Target = lua_tostring(L, 3); + auto RequestBody = lua_tostring(L, 4); + auto ContentType = lua_tostring(L, 5); + + ClearStack(L); + + // build fields + std::unordered_map Fields; + + unsigned int Status; + auto ResponseBody = Http::POST(Host, Port, Target, {}, RequestBody, ContentType, &Status); + + lua_pushinteger(L, Status); + + auto PrettyRemote = "https://" + std::string(Host) + ":" + std::to_string(Port) + std::string(Target); + if (ResponseBody == Http::ErrorString) { + SendError(Engine(), L, "HTTPS POST " + PrettyRemote + " failed status " + std::to_string(Status) + ". Check the console or log for more info."); + return 1; + } else { + debug("POST " + PrettyRemote + " completed status " + std::to_string(Status)); + } + + lua_pushstring(L, ResponseBody.c_str()); + return 2; +} + void TLuaFile::Load() { Assert(mLuaState); luaL_openlibs(mLuaState); @@ -799,6 +899,8 @@ void TLuaFile::Load() { LuaTable::InsertFunction(mLuaState, "Sleep", lua_Sleep); LuaTable::InsertFunction(mLuaState, "Set", lua_Set); LuaTable::InsertFunction(mLuaState, "GetOSName", lua_GetOSName); + LuaTable::InsertFunction(mLuaState, "HttpsGET", lua_HttpsGET); + LuaTable::InsertFunction(mLuaState, "HttpsPOST", lua_HttpsPOST); LuaTable::End(mLuaState, "MP"); lua_register(mLuaState, "print", lua_Print); @@ -874,7 +976,7 @@ void SendError(TLuaEngine& Engine, lua_State* L, const std::string& msg) { TLuaFile& S = MaybeS.value(); a = fs::path(S.GetFileName()).filename().string(); } - warn(a + (" | Incorrect Call of ") + msg); + warn(a + (" | Error in MP Lua call: ") + msg); } void TLuaArg::PushArgs(lua_State* State) { diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index eeb451c..279a236 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -281,12 +281,12 @@ void TNetwork::Authentication(SOCKET TCPSock) { } if (!Rc.empty()) { - Rc = Http::POST(Application::GetBackendUrlForAuth(), "/pkToUser", {}, R"({"key":")" + Rc + "\"}", "application/json"); + Rc = Http::POST(Application::GetBackendUrlForAuth(), 443, "/pkToUser", {}, R"({"key":")" + Rc + "\"}", "application/json"); } json::Document AuthResponse; AuthResponse.Parse(Rc.c_str()); - if (Rc == "-1" || AuthResponse.HasParseError()) { + if (Rc == Http::ErrorString || AuthResponse.HasParseError()) { ClientKick(*Client, "Invalid key! Please restart your game."); return; }