From 848c9302926165a6337842778ba86826010ca431 Mon Sep 17 00:00:00 2001 From: Aaron Kimbre Date: Sun, 26 Jan 2025 00:44:17 -0600 Subject: [PATCH] linker errors --- CMakeLists.txt | 1 + dChatServer/CMakeLists.txt | 10 +- .../{JSONUtils.cpp => ChatJSONUtils.cpp} | 15 +- dChatServer/{JSONUtils.h => ChatJSONUtils.h} | 11 +- dChatServer/ChatPacketHandler.cpp | 26 +- dChatServer/ChatServer.cpp | 15 +- dChatServer/ChatWeb.cpp | 138 +++++++++ dChatServer/ChatWeb.h | 24 ++ dChatServer/ChatWebAPI.cpp | 292 ------------------ dChatServer/PlayerContainer.cpp | 6 +- dCommon/CMakeLists.txt | 1 + dCommon/Game.h | 3 - dCommon/JSONUtils.cpp | 18 ++ dCommon/JSONUtils.h | 11 + dWeb/CMakeLists.txt | 7 + dWeb/Web.cpp | 162 ++++++++++ dChatServer/ChatWebAPI.h => dWeb/Web.h | 35 ++- 17 files changed, 424 insertions(+), 351 deletions(-) rename dChatServer/{JSONUtils.cpp => ChatJSONUtils.cpp} (74%) rename dChatServer/{JSONUtils.h => ChatJSONUtils.h} (57%) create mode 100644 dChatServer/ChatWeb.cpp create mode 100644 dChatServer/ChatWeb.h delete mode 100644 dChatServer/ChatWebAPI.cpp create mode 100644 dCommon/JSONUtils.cpp create mode 100644 dCommon/JSONUtils.h create mode 100644 dWeb/CMakeLists.txt create mode 100644 dWeb/Web.cpp rename dChatServer/ChatWebAPI.h => dWeb/Web.h (59%) diff --git a/CMakeLists.txt b/CMakeLists.txt index feeb7d71..5e0757b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -301,6 +301,7 @@ add_subdirectory(dZoneManager) add_subdirectory(dNavigation) add_subdirectory(dPhysics) add_subdirectory(dServer) +add_subdirectory(dWeb) # Create a list of common libraries shared between all binaries set(COMMON_LIBRARIES "dCommon" "dDatabase" "dNet" "raknet" "magic_enum") diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt index 313df6d5..6124b146 100644 --- a/dChatServer/CMakeLists.txt +++ b/dChatServer/CMakeLists.txt @@ -1,9 +1,13 @@ set(DCHATSERVER_SOURCES "ChatIgnoreList.cpp" + "ChatJSONUtils.cpp" "ChatPacketHandler.cpp" "PlayerContainer.cpp" - "ChatWebAPI.cpp" - "JSONUtils.cpp" + "ChatWeb.cpp" +) + +include_directories( + ${PROJECT_SOURCE_DIR}/dWeb ) add_executable(ChatServer "ChatServer.cpp") @@ -14,5 +18,5 @@ add_library(dChatServer ${DCHATSERVER_SOURCES}) target_include_directories(dChatServer PRIVATE "${PROJECT_SOURCE_DIR}/dServer") target_link_libraries(dChatServer ${COMMON_LIBRARIES} dChatFilter) -target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose) +target_link_libraries(ChatServer ${COMMON_LIBRARIES} dChatFilter dChatServer dServer mongoose dWeb) diff --git a/dChatServer/JSONUtils.cpp b/dChatServer/ChatJSONUtils.cpp similarity index 74% rename from dChatServer/JSONUtils.cpp rename to dChatServer/ChatJSONUtils.cpp index 1c32409c..7bc76015 100644 --- a/dChatServer/JSONUtils.cpp +++ b/dChatServer/ChatJSONUtils.cpp @@ -1,4 +1,4 @@ -#include "JSONUtils.h" +#include "ChatJSONUtils.h" #include "json.hpp" @@ -47,16 +47,3 @@ void to_json(json& data, const TeamData& teamData) { members.push_back(playerData); } } - -std::string JSONUtils::CheckRequiredData(const json& data, const std::vector& requiredData) { - json check; - check["error"] = json::array(); - for (const auto& required : requiredData) { - if (!data.contains(required)) { - check["error"].push_back("Missing Parameter: " + required); - } else if (data[required] == "") { - check["error"].push_back("Empty Parameter: " + required); - } - } - return check["error"].empty() ? "" : check.dump(); -} diff --git a/dChatServer/JSONUtils.h b/dChatServer/ChatJSONUtils.h similarity index 57% rename from dChatServer/JSONUtils.h rename to dChatServer/ChatJSONUtils.h index a46a1667..06cc15c9 100644 --- a/dChatServer/JSONUtils.h +++ b/dChatServer/ChatJSONUtils.h @@ -1,5 +1,5 @@ -#ifndef __JSONUTILS_H__ -#define __JSONUTILS_H__ +#ifndef __CHATJSONUTILS_H__ +#define __CHATJSONUTILS_H__ #include "json_fwd.hpp" #include "PlayerContainer.h" @@ -9,9 +9,4 @@ void to_json(nlohmann::json& data, const PlayerContainer& playerContainer); void to_json(nlohmann::json& data, const TeamContainer& teamData); void to_json(nlohmann::json& data, const TeamData& teamData); -namespace JSONUtils { - // check required data for reqeust - std::string CheckRequiredData(const nlohmann::json& data, const std::vector& requiredData); -} - -#endif // __JSONUTILS_H__ +#endif // __CHATJSONUTILS_H__ diff --git a/dChatServer/ChatPacketHandler.cpp b/dChatServer/ChatPacketHandler.cpp index 96259f4f..5f05c594 100644 --- a/dChatServer/ChatPacketHandler.cpp +++ b/dChatServer/ChatPacketHandler.cpp @@ -19,8 +19,8 @@ #include "StringifiedEnum.h" #include "eGameMasterLevel.h" #include "ChatPackets.h" -#include "ChatWebAPI.h" #include "json.hpp" +#include "Web.h" void ChatPacketHandler::HandleFriendlistRequest(Packet* packet) { //Get from the packet which player we want to do something with: @@ -447,18 +447,20 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { inStream.Read(message); LOG("Got message %s from (%s) via [%s]: %s", message.GetAsString().c_str(), sender.playerName.c_str(), StringifiedEnum::ToString(channel).data(), message.GetAsString().c_str()); + + // build chat json data + nlohmann::json data; + data["action"] = "chat"; + data["playerName"] = sender.playerName; + data["message"] = message.GetAsString(); + auto& zoneID = data["zone_id"]; + zoneID["map_id"] = sender.zoneID.GetMapID(); + zoneID["instance_id"] = sender.zoneID.GetInstanceID(); + zoneID["clone_id"] = sender.zoneID.GetCloneID(); + switch (channel) { case eChatChannel::LOCAL: { - // Send to connected websockets - nlohmann::json data; - data["action"] = "chat"; - data["playerName"] = sender.playerName; - data["message"] = message.GetAsString(); - auto& zoneID = data["zone_id"]; - zoneID["map_id"] = sender.zoneID.GetMapID(); - zoneID["instance_id"] = sender.zoneID.GetInstanceID(); - zoneID["clone_id"] = sender.zoneID.GetCloneID(); - Game::chatwebapi.SendWSMessage(data.dump()); + Game::web.SendWSMessage("WorldChat", data.dump()); break; } case eChatChannel::TEAM: { @@ -469,6 +471,8 @@ void ChatPacketHandler::HandleChatMessage(Packet* packet) { const auto& otherMember = Game::playerContainer.GetPlayerData(memberId); if (!otherMember) return; SendPrivateChatMessage(sender, otherMember, otherMember, message, eChatChannel::TEAM, eChatMessageResponseCode::SENT); + data["teamID"] = team->teamID; + Game::web.SendWSMessage("teamchat", data.dump()); } break; } diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 0af7320f..ee164347 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -28,7 +28,7 @@ #include "RakNetDefines.h" #include "MessageIdentifiers.h" -#include "ChatWebAPI.h" +#include "ChatWeb.h" namespace Game { Logger* logger = nullptr; @@ -39,7 +39,6 @@ namespace Game { Game::signal_t lastSignal = 0; std::mt19937 randomEngine; PlayerContainer playerContainer; - ChatWebAPI chatwebapi; } void HandlePacket(Packet* packet); @@ -93,16 +92,20 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } - // seyup the chat api web server + // setup the chat api web server bool web_server_enabled = Game::config->GetValue("web_server_enabled") == "1"; - if (web_server_enabled && !Game::chatwebapi.Startup()){ + const uint32_t web_server_port = GeneralUtils::TryParse(Game::config->GetValue("web_server_port")).value_or(2005); + if (web_server_enabled && !Game::web.Startup("localhost", web_server_port)) { // if we want the web api and it fails to start, exit LOG("Failed to start web server, shutting down."); Database::Destroy("ChatServer"); delete Game::logger; delete Game::config; return EXIT_FAILURE; - }; + } + if (web_server_enabled) { + ChatWeb::RegisterRoutes(); + } //Find out the master's IP: std::string masterIP; @@ -168,7 +171,7 @@ int main(int argc, char** argv) { //Check and handle web requests: if (web_server_enabled) { - Game::chatwebapi.ReceiveRequests(); + Game::web.ReceiveRequests(); } //Push our log every 30s: diff --git a/dChatServer/ChatWeb.cpp b/dChatServer/ChatWeb.cpp new file mode 100644 index 00000000..26e73fea --- /dev/null +++ b/dChatServer/ChatWeb.cpp @@ -0,0 +1,138 @@ +#include "ChatWeb.h" + +#include "Logger.h" +#include "Game.h" +#include "json.hpp" +#include "dCommonVars.h" +#include "MessageType/Chat.h" +#include "dServer.h" +#include "dConfig.h" +#include "PlayerContainer.h" +#include "GeneralUtils.h" +#include "eHTTPMethod.h" +#include "magic_enum.hpp" +#include "ChatPackets.h" +#include "StringifiedEnum.h" +#include "Database.h" +#include "ChatJSONUtils.h" +#include "JSONUtils.h" + +using json = nlohmann::json; + +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(); +} + +void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) { + const json data = Game::playerContainer.GetTeamContainer(); + reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; + reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); +} + +void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { + auto data = GeneralUtils::TryParse(body); + if (!data) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + return; + } + + const auto& good_data = data.value(); + auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" }); + if (!check.empty()) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = check; + } else { + + ChatPackets::Announcement announcement; + announcement.title = good_data["title"]; + announcement.message = good_data["message"]; + announcement.Send(); + + reply.status = eHTTPStatusCode::OK; + reply.message = "{\"status\":\"Announcement Sent\"}"; + } +} + + +void HandleWSChat(mg_connection* connection, json data) { + auto check = JSONUtils::CheckRequiredData(data, { "user", "message" }); + if (!check.empty()) { + LOG_DEBUG("Received invalid websocket message: %s", check.c_str()); + } else { + const auto user = data["user"].get(); + const auto message = data["message"].get(); + LOG_DEBUG("EXTERNAL Chat message from %s: %s", user.c_str(), message.c_str()); + //TODO: Send chat message to corret world server to broadcast to players + } +} + +void HandleWSSubscribe(mg_connection* connection, json data) { + auto check = JSONUtils::CheckRequiredData(data, { "type" }); + if (!check.empty()) { + LOG_DEBUG("Received invalid websocket message: %s", check.c_str()); + } else { + const auto type = data["type"].get(); + LOG_DEBUG("type %s subscribed", type.c_str()); + const auto sub = magic_enum::enum_cast(type).value_or(eWSSubscription::INVALID); + if (sub != eWSSubscription::INVALID) { + connection->data[GeneralUtils::ToUnderlying(sub)] = 1; + mg_ws_send(connection, "{\"status\":\"subscribed\"}", 18, WEBSOCKET_OP_TEXT); + } + } +} + +void HandleWSUnsubscribe(mg_connection* connection, json data) { + auto check = JSONUtils::CheckRequiredData(data, { "type" }); + if (!check.empty()) { + LOG_DEBUG("Received invalid websocket message: %s", check.c_str()); + } else { + const auto type = data["type"].get(); + LOG_DEBUG("type %s unsubscribed", type.c_str()); + const auto sub = magic_enum::enum_cast(type).value_or(eWSSubscription::INVALID); + if (sub != eWSSubscription::INVALID) { + connection->data[GeneralUtils::ToUnderlying(sub)] = 0; + mg_ws_send(connection, "{\"status\":\"unsubscribed\"}", 18, WEBSOCKET_OP_TEXT); + } + } +} + +void RegisterRoutes() { + // REST API v1 routes + std::string v1_route = "/api/v1/"; + Game::web.RegisterHTTPRoute({ + .path = v1_route + "players", + .method = eHTTPMethod::GET, + .handle = HandleHTTPPlayersRequest + }); + + Game::web.RegisterHTTPRoute({ + .path = v1_route + "teams", + .method = eHTTPMethod::GET, + .handle = HandleHTTPTeamsRequest + }); + + Game::web.RegisterHTTPRoute({ + .path = v1_route + "announce", + .method = eHTTPMethod::POST, + .handle = HandleHTTPAnnounceRequest + }); + + // WebSocket Actions + Game::web.RegisterWSAction({ + .action = "subscribe", + .handle = HandleWSSubscribe + }); + + Game::web.RegisterWSAction({ + .action = "unsubscribe", + .handle = HandleWSUnsubscribe + }); + + Game::web.RegisterWSAction({ + .action = "chat", + .handle = HandleWSChat + }); +} diff --git a/dChatServer/ChatWeb.h b/dChatServer/ChatWeb.h new file mode 100644 index 00000000..70a8e81c --- /dev/null +++ b/dChatServer/ChatWeb.h @@ -0,0 +1,24 @@ +#ifndef __CHATWEB_H__ +#define __CHATWEB_H__ + +#include +#include + +#include "Web.h" + +enum class eWSSubscription { + WORLD_CHAT, + PRIVATE_CHAT, + TEAM_CHAT, + TEAM, + PLAYER, + INVALID +}; + +namespace ChatWeb { + void RegisterRoutes(); +}; + + +#endif // __CHATWEB_H__ + diff --git a/dChatServer/ChatWebAPI.cpp b/dChatServer/ChatWebAPI.cpp deleted file mode 100644 index c59a98b9..00000000 --- a/dChatServer/ChatWebAPI.cpp +++ /dev/null @@ -1,292 +0,0 @@ -#include "ChatWebAPI.h" - -#include "Logger.h" -#include "Game.h" -#include "json.hpp" -#include "dCommonVars.h" -#include "MessageType/Chat.h" -#include "dServer.h" -#include "dConfig.h" -#include "PlayerContainer.h" -#include "JSONUtils.h" -#include "GeneralUtils.h" -#include "eHTTPMethod.h" -#include "magic_enum.hpp" -#include "ChatPackets.h" -#include "StringifiedEnum.h" -#include "Database.h" - -#ifdef DARKFLAME_PLATFORM_WIN32 -#pragma push_macro("DELETE") -#undef DELETE -#endif - -using json = nlohmann::json; - -typedef struct mg_connection mg_connection; -typedef struct mg_http_message mg_http_message; - -namespace { - const char* json_content_type = "Content-Type: application/json\r\n"; - std::map, HTTPRoute> HTTPRoutes {}; - std::map WSactions {}; -} - -bool ValidateAuthentication(const mg_http_message* http_msg) { - // TO DO: This is just a placeholder for now - // use tokens or something at a later point if we want to implement authentication - // bit using the listen bind address to limit external access is good enough to start with - return true; -} - -bool ValidateJSON(std::optional data, HTTPReply& reply) { - if (!data) { - reply.status = eHTTPStatusCode::BAD_REQUEST; - reply.message = "{\"error\":\"Invalid JSON\"}"; - return false; - } - return true; -} - -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(); -} - -void HandleHTTPTeamsRequest(HTTPReply& reply, std::string body) { - const json data = Game::playerContainer.GetTeamContainer(); - reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; - reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); -} - -void HandleHTTPAnnounceRequest(HTTPReply& reply, std::string body) { - auto data = GeneralUtils::TryParse(body); - if (!ValidateJSON(data, reply)) return; - - const auto& good_data = data.value(); - auto check = JSONUtils::CheckRequiredData(good_data, { "title", "message" }); - if (!check.empty()) { - reply.status = eHTTPStatusCode::BAD_REQUEST; - reply.message = check; - } else { - - ChatPackets::Announcement announcement; - announcement.title = good_data["title"]; - announcement.message = good_data["message"]; - announcement.Send(); - - reply.status = eHTTPStatusCode::OK; - reply.message = "{\"status\":\"Announcement Sent\"}"; - } -} - -void HandleHTTPInvalidRoute(HTTPReply& reply) { - reply.status = eHTTPStatusCode::NOT_FOUND; - reply.message = "{\"error\":\"Invalid Route\"}"; -} - -void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) { - HTTPReply reply; - - if (!http_msg) { - reply.status = eHTTPStatusCode::BAD_REQUEST; - reply.message = "{\"error\":\"Invalid Request\"}"; - } else if (ValidateAuthentication(http_msg)) { - - // convert method from cstring to std string - std::string method_string(http_msg->method.buf, http_msg->method.len); - // get mehtod from mg to enum - const eHTTPMethod method = magic_enum::enum_cast(method_string).value_or(eHTTPMethod::INVALID); - - // convert uri from cstring to std string - std::string uri(http_msg->uri.buf, http_msg->uri.len); - std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower); - - // convert body from cstring to std string - std::string body(http_msg->body.buf, http_msg->body.len); - - // Special case for websocket - if (uri == "/ws" && method == eHTTPMethod::GET) { - mg_ws_upgrade(connection, const_cast(http_msg), NULL); - return; - } - - const auto routeItr = HTTPRoutes.find({method, uri}); - if (routeItr != HTTPRoutes.end()) { - const auto& [_, route] = *routeItr; - route.handle(reply, body); - } else HandleHTTPInvalidRoute(reply); - } else { - reply.status = eHTTPStatusCode::UNAUTHORIZED; - reply.message = "{\"error\":\"Unauthorized\"}"; - } - mg_http_reply(connection, static_cast(reply.status), json_content_type, reply.message.c_str()); -} - -void HandleWSAnnounce(json data){ - auto check = JSONUtils::CheckRequiredData(data, { "title", "message" }); - if (!check.empty()) { - LOG("Received invalid websocket message: %s", check.c_str()); - } else { - ChatPackets::Announcement announcement; - announcement.title = data["title"]; - announcement.message = data["message"]; - announcement.Send(); - } -} - -void HandleWSChat(json data) { - auto check = JSONUtils::CheckRequiredData(data, { "user", "message" }); - if (!check.empty()) { - LOG("Received invalid websocket message: %s", check.c_str()); - } else { - const auto user = data["user"].get(); - const auto message = data["message"].get(); - LOG("EXTERNAL Chat message from %s: %s", user.c_str(), message.c_str()); - //TODO: Send chat message to corret world server to broadcast to players - } -} - -void HandleWSMessage(mg_connection* connection, const mg_ws_message* ws_msg) { - std::string reply = "{\"status\":\"Error\"}"; - if (!ws_msg) { - LOG("Received invalid websocket message"); - return; - } else { - LOG("Received websocket message: %.*s", static_cast(ws_msg->data.len), ws_msg->data.buf); - auto data = GeneralUtils::TryParse(std::string(ws_msg->data.buf, ws_msg->data.len)); - if (data) { - const auto& good_data = data.value(); - auto check = JSONUtils::CheckRequiredData(good_data, { "action" }); - if (!check.empty()) { - LOG("Received invalid websocket message: %s", check.c_str()); - reply = "{\"status\":\"no action\"}"; - } else { - const auto action = good_data["action"].get(); - const auto actionItr = WSactions.find(action); - if (actionItr != WSactions.end()) { - const auto& [_, action] = *actionItr; - action.handle(good_data); - reply = "{\"status\":\"OK\"}"; - } else { - LOG("Received invalid websocket action: %s", action.c_str()); - reply = "{\"status\":\"invalid action\"}"; - } - } - } else { - LOG("Received invalid websocket message: %.*s", static_cast(ws_msg->data.len), ws_msg->data.buf); - } - } - mg_ws_send(connection, reply.c_str(), reply.size(), WEBSOCKET_OP_TEXT); -} - - - -void HandleMessages(mg_connection* connection, int message, void* message_data) { - switch (message) { - case MG_EV_HTTP_MSG: - HandleHTTPMessage(connection, static_cast(message_data)); - break; - case MG_EV_WS_MSG: - HandleWSMessage(connection, static_cast(message_data)); - break; - default: - break; - } -} - -void ChatWebAPI::RegisterHTTPRoute(HTTPRoute route) { - auto [_, success] = HTTPRoutes.try_emplace({ route.method, route.path }, route); - if (!success) { - LOG_DEBUG("Failed to register HTTP route %s", route.path.c_str()); - } else { - LOG_DEBUG("Registered HTTP route %s", route.path.c_str()); - } -} - -void ChatWebAPI::RegisterWSAction(WSAction action) { - auto [_, success] = WSactions.try_emplace(action.action, action); - if (!success) { - LOG_DEBUG("Failed to register WS action %s", action.action.c_str()); - } else { - LOG_DEBUG("Registered WS action %s", action.action.c_str()); - } -} - -ChatWebAPI::ChatWebAPI() { - mg_log_set(MG_LL_NONE); - mg_mgr_init(&mgr); // Initialize event manager -} - -ChatWebAPI::~ChatWebAPI() { - mg_mgr_free(&mgr); -} - -bool ChatWebAPI::Startup() { - // Make listen address - std::string listen_ip = Game::config->GetValue("web_server_listen_ip"); - if (listen_ip == "localhost") listen_ip = "127.0.0.1"; - - const std::string& listen_port = Game::config->GetValue("web_server_listen_port"); - const std::string& listen_address = "http://" + listen_ip + ":" + listen_port; - LOG("Starting web server on %s", listen_address.c_str()); - - // Create HTTP listener - if (!mg_http_listen(&mgr, listen_address.c_str(), HandleMessages, NULL)) { - LOG("Failed to create web server listener on %s", listen_port.c_str()); - return false; - } - - // Register routes - - // API v1 routes - std::string v1_route = "/api/v1/"; - RegisterHTTPRoute({ - .path = v1_route + "players", - .method = eHTTPMethod::GET, - .handle = HandleHTTPPlayersRequest - }); - - RegisterHTTPRoute({ - .path = v1_route + "teams", - .method = eHTTPMethod::GET, - .handle = HandleHTTPTeamsRequest - }); - - RegisterHTTPRoute({ - .path = v1_route + "announce", - .method = eHTTPMethod::POST, - .handle = HandleHTTPAnnounceRequest - }); - - // WebSocket Actions - RegisterWSAction({ - .action = "announce", - .handle = HandleWSAnnounce - }); - - RegisterWSAction({ - .action = "chat", - .handle = HandleWSChat - }); - - - return true; -} - -void ChatWebAPI::ReceiveRequests() { - mg_mgr_poll(&mgr, 15); -} - -void ChatWebAPI::SendWSMessage(const std::string& message) { - for (struct mg_connection *wc = mgr.conns; wc != NULL; wc = wc->next) { - if (wc->is_websocket) { - mg_ws_send(wc, message.c_str(), message.size(), WEBSOCKET_OP_TEXT); - } - } -} - -#ifdef DARKFLAME_PLATFORM_WIN32 -#pragma pop_macro("DELETE") -#endif diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index b286b9f5..bd33871b 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -13,7 +13,7 @@ #include "dConfig.h" #include "MessageType/Chat.h" #include "json.hpp" -#include "ChatWebAPI.h" +#include "Web.h" void PlayerContainer::Initialize() { m_MaxNumberOfBestFriends = @@ -70,7 +70,7 @@ void PlayerContainer::InsertPlayer(Packet* packet) { zoneID["map_id"] = data.zoneID.GetMapID(); zoneID["instance_id"] = data.zoneID.GetInstanceID(); zoneID["clone_id"] = data.zoneID.GetCloneID(); - Game::chatwebapi.SendWSMessage(wsdata.dump()); + Game::web.SendWSMessage("player", wsdata.dump()); Database::Get()->UpdateActivityLog(data.playerID, eActivityType::PlayerLoggedIn, data.zoneID.GetMapID()); m_PlayersToRemove.erase(playerId); } @@ -130,7 +130,7 @@ void PlayerContainer::RemovePlayer(const LWOOBJID playerID) { wsdata["type"] = "remove"; wsdata["playerName"] = player.playerName; wsdata["playerID"] = player.playerID; - Game::chatwebapi.SendWSMessage(wsdata.dump()); + Game::web.SendWSMessage("player", wsdata.dump()); m_PlayerCount--; LOG("Removed user: %llu", playerID); diff --git a/dCommon/CMakeLists.txt b/dCommon/CMakeLists.txt index 18fda0ed..c075f361 100644 --- a/dCommon/CMakeLists.txt +++ b/dCommon/CMakeLists.txt @@ -7,6 +7,7 @@ set(DCOMMON_SOURCES "Logger.cpp" "Game.cpp" "GeneralUtils.cpp" + "JSONUtils.cpp" "LDFFormat.cpp" "Metrics.cpp" "NiPoint3.cpp" diff --git a/dCommon/Game.h b/dCommon/Game.h index de83d537..d8113497 100644 --- a/dCommon/Game.h +++ b/dCommon/Game.h @@ -15,7 +15,6 @@ struct SystemAddress; class EntityManager; class dZoneManager; class PlayerContainer; -class ChatWebAPI; namespace Game { using signal_t = volatile std::sig_atomic_t; @@ -33,8 +32,6 @@ namespace Game { extern dZoneManager* zoneManager; extern PlayerContainer playerContainer; extern std::string projectVersion; - extern ChatWebAPI chatwebapi; - inline bool ShouldShutdown() { return lastSignal != 0; diff --git a/dCommon/JSONUtils.cpp b/dCommon/JSONUtils.cpp new file mode 100644 index 00000000..3ccea130 --- /dev/null +++ b/dCommon/JSONUtils.cpp @@ -0,0 +1,18 @@ +#include "JSONUtils.h" +#include "json.hpp" + +using json = nlohmann::json; + +std::string JSONUtils::CheckRequiredData(const json& data, const std::vector& requiredData) { + json check; + check["error"] = json::array(); + for (const auto& required : requiredData) { + if (!data.contains(required)) { + check["error"].push_back("Missing Parameter: " + required); + } else if (data[required] == "") { + check["error"].push_back("Empty Parameter: " + required); + } + } + return check["error"].empty() ? "" : check.dump(); +} + diff --git a/dCommon/JSONUtils.h b/dCommon/JSONUtils.h new file mode 100644 index 00000000..8f50647d --- /dev/null +++ b/dCommon/JSONUtils.h @@ -0,0 +1,11 @@ +#ifndef _JSONUTILS_H_ +#define _JSONUTILS_H_ + +#include "json_fwd.hpp" + +namespace JSONUtils { + // check required data for reqeust + std::string CheckRequiredData(const nlohmann::json& data, const std::vector& requiredData); +} + +#endif // _JSONUTILS_H_ diff --git a/dWeb/CMakeLists.txt b/dWeb/CMakeLists.txt new file mode 100644 index 00000000..14ef1b04 --- /dev/null +++ b/dWeb/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DWEB_SOURCES + "Web.cpp") + +add_library(dWeb STATIC ${DWEB_SOURCES}) + +target_include_directories(dWeb PUBLIC ".") +target_link_libraries(dWeb dCommon) diff --git a/dWeb/Web.cpp b/dWeb/Web.cpp new file mode 100644 index 00000000..0c9e48fe --- /dev/null +++ b/dWeb/Web.cpp @@ -0,0 +1,162 @@ +#include "Web.h" +#include "Game.h" +#include "magic_enum.hpp" +#include "json.hpp" +#include "Logger.h" +#include "eHTTPMethod.h" +#include "GeneralUtils.h" +#include "JSONUtils.h" + + +namespace { + const char * json_content_type = "application/json"; + std::map, HTTPRoute> g_HTTPRoutes; + std::map g_WSactions; +} + +using json = nlohmann::json; + +bool ValidateAuthentication(const mg_http_message* http_msg) { + // TO DO: This is just a placeholder for now + // use tokens or something at a later point if we want to implement authentication + // bit using the listen bind address to limit external access is good enough to start with + return true; +} + +void HandleHTTPMessage(mg_connection* connection, const mg_http_message* http_msg) { + if (g_HTTPRoutes.empty()) return; + + HTTPReply reply; + + if (!http_msg) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid Request\"}"; + } else if (ValidateAuthentication(http_msg)) { + + // convert method from cstring to std string + std::string method_string(http_msg->method.buf, http_msg->method.len); + // get mehtod from mg to enum + const eHTTPMethod method = magic_enum::enum_cast(method_string).value_or(eHTTPMethod::INVALID); + + // convert uri from cstring to std string + std::string uri(http_msg->uri.buf, http_msg->uri.len); + std::transform(uri.begin(), uri.end(), uri.begin(), ::tolower); + + // convert body from cstring to std string + std::string body(http_msg->body.buf, http_msg->body.len); + + // Special case for websocket + if (uri == "/ws" && method == eHTTPMethod::GET) { + mg_ws_upgrade(connection, const_cast(http_msg), NULL); + LOG("Upgraded connection to websocket: %d.%d.%d.%d:%i", MG_IPADDR_PARTS(&connection->rem.ip), connection->rem.port); + return; + } + + const auto routeItr = g_HTTPRoutes.find({method, uri}); + if (routeItr != g_HTTPRoutes.end()) { + const auto& [_, route] = *routeItr; + route.handle(reply, body); + } else { + reply.status = eHTTPStatusCode::NOT_FOUND; + reply.message = "{\"error\":\"Not Found\"}"; + } + } else { + reply.status = eHTTPStatusCode::UNAUTHORIZED; + reply.message = "{\"error\":\"Unauthorized\"}"; + } + mg_http_reply(connection, static_cast(reply.status), json_content_type, reply.message.c_str()); +} + + +void HandleWSMessage(mg_connection* connection, const mg_ws_message* ws_msg) { + if (!ws_msg) { + LOG_DEBUG("Received invalid websocket message"); + return; + } else { + LOG_DEBUG("Received websocket message: %.*s", static_cast(ws_msg->data.len), ws_msg->data.buf); + auto data = GeneralUtils::TryParse(std::string(ws_msg->data.buf, ws_msg->data.len)); + if (data) { + const auto& good_data = data.value(); + auto check = JSONUtils::CheckRequiredData(good_data, { "action" }); + if (!check.empty()) { + LOG_DEBUG("Received invalid websocket message: %s", check.c_str()); + } else { + const auto action = good_data["action"].get(); + const auto actionItr = g_WSactions.find(action); + if (actionItr != g_WSactions.end()) { + const auto& [_, action] = *actionItr; + action.handle(connection, good_data); + } else { + LOG_DEBUG("Received invalid websocket action: %s", action.c_str()); + } + } + } else { + LOG_DEBUG("Received invalid websocket message: %.*s", static_cast(ws_msg->data.len), ws_msg->data.buf); + } + } +} + +void HandleMessages(mg_connection* connection, int message, void* message_data) { + switch (message) { + case MG_EV_HTTP_MSG: + HandleHTTPMessage(connection, static_cast(message_data)); + break; + case MG_EV_WS_MSG: + HandleWSMessage(connection, static_cast(message_data)); + break; + default: + break; + } +} + +void Web::RegisterHTTPRoute(HTTPRoute route) { + auto [_, success] = g_HTTPRoutes.try_emplace({ route.method, route.path }, route); + if (!success) { + LOG_DEBUG("Failed to register HTTP route %s", route.path.c_str()); + } else { + LOG_DEBUG("Registered HTTP route %s", route.path.c_str()); + } +} + +void Web::RegisterWSAction(WSAction action) { + auto [_, success] = g_WSactions.try_emplace(action.action, action); + if (!success) { + LOG_DEBUG("Failed to register WS action %s", action.action.c_str()); + } else { + LOG_DEBUG("Registered WS action %s", action.action.c_str()); + } +} + +Web::Web() { + mg_log_set(MG_LL_NONE); + mg_mgr_init(&mgr); // Initialize event manager +} + +Web::~Web() { + mg_mgr_free(&mgr); +} + +bool Web::Startup(const std::string& listen_ip, const uint32_t listen_port) { + // Make listen address + const std::string& listen_address = "http://" + listen_ip + ":" + std::to_string(listen_port); + LOG("Starting web server on %s", listen_address.c_str()); + + // Create HTTP listener + if (!mg_http_listen(&mgr, listen_address.c_str(), HandleMessages, NULL)) { + LOG("Failed to create web server listener on %s", listen_address.c_str()); + return false; + } + return true; +} + +void Web::ReceiveRequests() { + mg_mgr_poll(&mgr, 15); +} + +void Web::SendWSMessage(const std::string subscription, const std::string& message) { + for (struct mg_connection *wc = Game::web.mgr.conns; wc != NULL; wc = wc->next) { + if (wc->is_websocket /* && wc->data[GeneralUtils::ToUnderlying(sub)] == 1*/) { + mg_ws_send(wc, message.c_str(), message.size(), WEBSOCKET_OP_TEXT); + } + } +} \ No newline at end of file diff --git a/dChatServer/ChatWebAPI.h b/dWeb/Web.h similarity index 59% rename from dChatServer/ChatWebAPI.h rename to dWeb/Web.h index e4e47a14..45b1c599 100644 --- a/dChatServer/ChatWebAPI.h +++ b/dWeb/Web.h @@ -1,12 +1,15 @@ -#ifndef __CHATWEBAPI_H__ -#define __CHATWEBAPI_H__ -#include -#include +#ifndef __WEB_H__ +#define __WEB_H__ +#include +#include +#include #include "mongoose.h" #include "json_fwd.hpp" #include "eHTTPStatusCode.h" + + enum class eHTTPMethod; typedef struct mg_mgr mg_mgr; @@ -24,20 +27,30 @@ struct HTTPRoute { struct WSAction { std::string action; - std::function handle; + std::function handle; }; -class ChatWebAPI { +struct WSMessage { + uint32_t id; + std::string sub; + std::string message; +}; + +class Web { public: - ChatWebAPI(); - ~ChatWebAPI(); + Web(); + ~Web(); void ReceiveRequests(); + void static SendWSMessage(std::string sub, const std::string& message); + bool Startup(const std::string& listen_ip, const uint32_t listen_port); void RegisterHTTPRoute(HTTPRoute route); void RegisterWSAction(WSAction action); - void SendWSMessage(const std::string& message); - bool Startup(); private: mg_mgr mgr; }; -#endif // __CHATWEBAPI_H__ +namespace Game { + Web web; +} + +#endif // !__WEB_H__