diff --git a/CMakeLists.txt b/CMakeLists.txt index f9802f1d..6033f0f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -322,6 +322,7 @@ endif() add_subdirectory(dWorldServer) add_subdirectory(dAuthServer) add_subdirectory(dChatServer) +add_subdirectory(dDashboardServer) add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries target_precompile_headers( diff --git a/dChatServer/ChatWeb.cpp b/dChatServer/ChatWeb.cpp index 72af5e84..020817c4 100644 --- a/dChatServer/ChatWeb.cpp +++ b/dChatServer/ChatWeb.cpp @@ -26,12 +26,14 @@ void HandleHTTPPlayersRequest(HTTPReply& reply, std::string body) { const json data = Game::playerContainer; reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump(); + reply.contentType = ContentType::JSON; } void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) { const json data = TeamContainer::GetTeamContainer(); reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); + reply.contentType = ContentType::JSON; } void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { @@ -39,6 +41,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { if (!data) { reply.status = eHTTPStatusCode::BAD_REQUEST; reply.message = "{\"error\":\"Invalid JSON\"}"; + reply.contentType = ContentType::JSON; return; } @@ -47,6 +50,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { if (!check.empty()) { reply.status = eHTTPStatusCode::BAD_REQUEST; reply.message = check; + reply.contentType = ContentType::JSON; } else { ChatPackets::Announcement announcement; @@ -56,6 +60,7 @@ void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { reply.status = eHTTPStatusCode::OK; reply.message = "{\"status\":\"Announcement Sent\"}"; + reply.contentType = ContentType::JSON; } } diff --git a/dDashboardServer/CMakeLists.txt b/dDashboardServer/CMakeLists.txt new file mode 100644 index 00000000..84b0f71d --- /dev/null +++ b/dDashboardServer/CMakeLists.txt @@ -0,0 +1,8 @@ +set(DDASHBOARDSERVER_SOURCES + "DashboardWeb.cpp" +) + +add_executable(DashboardServer "DashboardServer.cpp" "DashboardWeb.cpp") +target_link_libraries(DashboardServer ${COMMON_LIBRARIES} dServer dWeb inja) +target_include_directories(DashboardServer PRIVATE ${PROJECT_SOURCE_DIR}/dServer ${PROJECT_SOURCE_DIR}/dWeb) +add_compile_definitions(DashboardServer PRIVATE PROJECT_VERSION="\"${PROJECT_VERSION}\"") diff --git a/dDashboardServer/DashboardServer.cpp b/dDashboardServer/DashboardServer.cpp new file mode 100644 index 00000000..b81c5dcf --- /dev/null +++ b/dDashboardServer/DashboardServer.cpp @@ -0,0 +1,182 @@ +#include +#include +#include +#include + +//DLU Includes: +#include "dCommonVars.h" +#include "dServer.h" +#include "Logger.h" +#include "Database.h" +#include "dConfig.h" +#include "Diagnostics.h" +#include "AssetManager.h" +#include "BinaryPathFinder.h" +#include "ServiceType.h" +#include "StringifiedEnum.h" + +#include "Game.h" +#include "Server.h" + +//RakNet includes: +#include "RakNetDefines.h" +#include "MessageIdentifiers.h" + +#include "DashboardWeb.h" + +namespace Game { + Logger* logger = nullptr; + dServer* server = nullptr; + dConfig* config = nullptr; + AssetManager* assetManager = nullptr; + Game::signal_t lastSignal = 0; + std::mt19937 randomEngine; +} + +int main(int argc, char** argv) { + constexpr uint32_t dashboardFramerate = mediumFramerate; + constexpr uint32_t dashboardFrameDelta = mediumFrameDelta; + Diagnostics::SetProcessName("Dashboard"); + Diagnostics::SetProcessFileName(argv[0]); + Diagnostics::Initialize(); + + std::signal(SIGINT, Game::OnSignal); + std::signal(SIGTERM, Game::OnSignal); + + Game::config = new dConfig("dashboardconfig.ini"); + + //Create all the objects we need to run our service: + Server::SetupLogger("DashboardServer"); + if (!Game::logger) return EXIT_FAILURE; + Game::config->LogSettings(); + + //Read our config: + + LOG("Starting Dashboard server..."); + LOG("Version: %s", PROJECT_VERSION); + LOG("Compiled on: %s", __TIMESTAMP__); + + try { + std::string clientPathStr = Game::config->GetValue("client_location"); + if (clientPathStr.empty()) clientPathStr = "./res"; + std::filesystem::path clientPath = std::filesystem::path(clientPathStr); + if (clientPath.is_relative()) { + clientPath = BinaryPathFinder::GetBinaryDir() / clientPath; + } + + Game::assetManager = new AssetManager(clientPath); + } catch (std::runtime_error& ex) { + LOG("Got an error while setting up assets: %s", ex.what()); + delete Game::logger; + delete Game::config; + return EXIT_FAILURE; + } + + //Connect to the Database + try { + Database::Connect(); + } catch (std::exception& ex) { + LOG("Got an error while connecting to the database: %s", ex.what()); + Database::Destroy("DashboardServer"); + delete Game::logger; + delete Game::config; + return EXIT_FAILURE; + } + + // setup the chat api web server + const uint32_t web_server_port = GeneralUtils::TryParse(Game::config->GetValue("web_server_port")).value_or(80); + if (!Game::web.Startup("localhost", web_server_port)) { + // if we want the web server and it fails to start, exit + LOG("Failed to start web server, shutting down."); + Database::Destroy("DashboardServer"); + delete Game::logger; + delete Game::config; + return EXIT_FAILURE; + } + + DashboardWeb::RegisterRoutes(); + + //Find out the master's IP: + std::string masterIP; + uint32_t masterPort = 1000; + std::string masterPassword; + auto masterInfo = Database::Get()->GetMasterInfo(); + if (masterInfo) { + masterIP = masterInfo->ip; + masterPort = masterInfo->port; + masterPassword = masterInfo->password; + } + + //It's safe to pass 'localhost' here, as the IP is only used as the external IP. + std::string ourIP = "localhost"; + const uint32_t maxClients = GeneralUtils::TryParse(Game::config->GetValue("max_clients")).value_or(999); + const uint32_t ourPort = GeneralUtils::TryParse(Game::config->GetValue("dashboard_server_port")).value_or(2006); + const auto externalIPString = Game::config->GetValue("external_ip"); + if (!externalIPString.empty()) ourIP = externalIPString; + + Game::server = new dServer(ourIP, ourPort, 0, maxClients, false, true, Game::logger, masterIP, masterPort, ServiceType::COMMON, Game::config, &Game::lastSignal, masterPassword); + + Game::randomEngine = std::mt19937(time(0)); + + //Run it until server gets a kill message from Master: + auto t = std::chrono::high_resolution_clock::now(); + Packet* packet = nullptr; + constexpr uint32_t logFlushTime = 30 * dashboardFramerate; // 30 seconds in frames + constexpr uint32_t sqlPingTime = 10 * 60 * dashboardFramerate; // 10 minutes in frames + uint32_t framesSinceLastFlush = 0; + uint32_t framesSinceMasterDisconnect = 0; + uint32_t framesSinceLastSQLPing = 0; + + auto lastTime = std::chrono::high_resolution_clock::now(); + + Game::logger->Flush(); // once immediately before main loop + while (!Game::ShouldShutdown()) { + //Check if we're still connected to master: + if (!Game::server->GetIsConnectedToMaster()) { + framesSinceMasterDisconnect++; + + if (framesSinceMasterDisconnect >= dashboardFramerate) + break; //Exit our loop, shut down. + } else framesSinceMasterDisconnect = 0; + + const auto currentTime = std::chrono::high_resolution_clock::now(); + const float deltaTime = std::chrono::duration(currentTime - lastTime).count(); + lastTime = currentTime; + + // Check and handle web requests: + Game::web.ReceiveRequests(); + + //Push our log every 30s: + if (framesSinceLastFlush >= logFlushTime) { + Game::logger->Flush(); + framesSinceLastFlush = 0; + } else framesSinceLastFlush++; + + //Every 10 min we ping our sql server to keep it alive hopefully: + if (framesSinceLastSQLPing >= sqlPingTime) { + //Find out the master's IP for absolutely no reason: + std::string masterIP; + uint32_t masterPort; + + auto masterInfo = Database::Get()->GetMasterInfo(); + if (masterInfo) { + masterIP = masterInfo->ip; + masterPort = masterInfo->port; + } + + framesSinceLastSQLPing = 0; + } else framesSinceLastSQLPing++; + + //Sleep our thread since auth can afford to. + t += std::chrono::milliseconds(dashboardFrameDelta); //Chat can run at a lower "fps" + std::this_thread::sleep_until(t); + } + + //Delete our objects here: + Database::Destroy("DashboardServer"); + delete Game::server; + delete Game::logger; + delete Game::config; + + return EXIT_SUCCESS; +} diff --git a/dDashboardServer/DashboardWeb.cpp b/dDashboardServer/DashboardWeb.cpp new file mode 100644 index 00000000..e0bbd07e --- /dev/null +++ b/dDashboardServer/DashboardWeb.cpp @@ -0,0 +1,11 @@ +#include "DashboardWeb.h" + +#include + +// default home page + +namespace DashboardWeb { + void RegisterRoutes() { + + } +} diff --git a/dDashboardServer/DashboardWeb.h b/dDashboardServer/DashboardWeb.h new file mode 100644 index 00000000..3a239bd1 --- /dev/null +++ b/dDashboardServer/DashboardWeb.h @@ -0,0 +1,14 @@ +#ifndef __DASHBOARDWEB_H__ +#define __DASHBOARDWEB_H__ + +#include +#include + +#include "Web.h" + +namespace DashboardWeb { + void RegisterRoutes(); +}; + + +#endif // __DASHBOARDWEB_H__ diff --git a/dWeb/Web.cpp b/dWeb/Web.cpp index abf9bd36..6e11be01 100644 --- a/dWeb/Web.cpp +++ b/dWeb/Web.cpp @@ -13,7 +13,6 @@ namespace Game { } namespace { - const char* jsonContentType = "Content-Type: application/json\r\n"; const std::string wsSubscribed = "{\"status\":\"subscribed\"}"; const std::string wsUnsubscribed = "{\"status\":\"unsubscribed\"}"; std::map, HTTPRoute> g_HTTPRoutes; @@ -73,7 +72,7 @@ void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_ms reply.status = eHTTPStatusCode::UNAUTHORIZED; reply.message = "{\"error\":\"Unauthorized\"}"; } - mg_http_reply(connection, static_cast(reply.status), jsonContentType, reply.message.c_str()); + mg_http_reply(connection, static_cast(reply.status), reply.contentType.c_str(), reply.message.c_str()); } diff --git a/dWeb/Web.h b/dWeb/Web.h index 1752f755..43b7c9ab 100644 --- a/dWeb/Web.h +++ b/dWeb/Web.h @@ -20,10 +20,36 @@ enum class eHTTPMethod; // Forward declaration for mongoose manager typedef struct mg_mgr mg_mgr; +namespace ContentType { + const std::string JSON = "Content-Type: application/json\r\n"; + const std::string HTML = "Content-Type: text/html\r\n"; + const std::string PLAIN = "Content-Type: text/plain\r\n"; + const std::string CSS = "Content-Type: text/css\r\n"; + const std::string JAVASCRIPT = "Content-Type: application/javascript\r\n"; + const std::string ICO = "Content-Type: image/x-icon\r\n"; + const std::string PNG = "Content-Type: image/png\r\n"; + const std::string SVG = "Content-Type: image/svg+xml\r\n"; + const std::string JPG = "Content-Type: image/jpeg\r\n"; + const std::string GIF = "Content-Type: image/gif\r\n"; + const std::string WEBP = "Content-Type: image/webp\r\n"; + const std::string MP4 = "Content-Type: video/mp4\r\n"; + const std::string OGG = "Content-Type: audio/ogg\r\n"; + const std::string MP3 = "Content-Type: audio/mpeg\r\n"; + const std::string BINARY = "Content-Type: application/octet-stream\r\n"; + const std::string FORM = "Content-Type: application/x-www-form-urlencoded\r\n"; + const std::string MULTIPART = "Content-Type: multipart/form-data\r\n"; + const std::string ZIP = "Content-Type: application/zip\r\n"; + const std::string PDF = "Content-Type: application/pdf\r\n"; + const std::string XML = "Content-Type: application/xml\r\n"; + const std::string CSV = "Content-Type: text/csv\r\n"; + const std::string YAML = "Content-Type: application/x-yaml\r\n"; +} + // For passing HTTP messages between functions struct HTTPReply { eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND; std::string message = "{\"error\":\"Not Found\"}"; + std::string contentType = ContentType::JSON; }; // HTTP route structure diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index fe72f2da..8d1a839e 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -77,6 +77,16 @@ FetchContent_Declare( FetchContent_MakeAvailable(glm) +FetchContent_Declare( + inja + GIT_REPOSITORY https://github.com/pantor/inja.git + GIT_TAG b2276440be8334aeba9cd5d628c2731d0f6a5809 #refs/tags/v3.4.0 + GIT_PROGRESS TRUE + GIT_SHALLOW 1 +) + +FetchContent_MakeAvailable(inja) + add_subdirectory(MD5) add_subdirectory(mongoose)