From 88376c233c7773176d50ecee5c0b183ebbfa78aa Mon Sep 17 00:00:00 2001 From: Aaron Kimbre Date: Mon, 6 Jan 2025 01:23:29 -0600 Subject: [PATCH] cleanup and make the web routes cleaner --- dChatServer/CMakeLists.txt | 1 + dChatServer/ChatServer.cpp | 5 +- dChatServer/ChatWebAPI.cpp | 198 +++++++++++++++++++++---------- dChatServer/ChatWebAPI.h | 11 ++ dChatServer/JSONUtils.cpp | 58 +++++++++ dChatServer/JSONUtils.h | 17 +++ dChatServer/PlayerContainer.cpp | 44 ------- dChatServer/PlayerContainer.h | 5 - dCommon/dEnums/eHTTPMethod.h | 21 ++++ dCommon/dEnums/eHTTPStatusCode.h | 72 +++++++++++ dNet/ChatPackets.cpp | 10 ++ dNet/ChatPackets.h | 7 ++ 12 files changed, 337 insertions(+), 112 deletions(-) create mode 100644 dChatServer/JSONUtils.cpp create mode 100644 dChatServer/JSONUtils.h create mode 100644 dCommon/dEnums/eHTTPMethod.h create mode 100644 dCommon/dEnums/eHTTPStatusCode.h diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt index 4d9531a9..313df6d5 100644 --- a/dChatServer/CMakeLists.txt +++ b/dChatServer/CMakeLists.txt @@ -3,6 +3,7 @@ set(DCHATSERVER_SOURCES "ChatPacketHandler.cpp" "PlayerContainer.cpp" "ChatWebAPI.cpp" + "JSONUtils.cpp" ) add_executable(ChatServer "ChatServer.cpp") diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index 8bb075c0..b501157b 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -298,12 +298,11 @@ void HandlePacket(Packet* packet) { case MessageType::Chat::LOGIN_SESSION_NOTIFY: Game::playerContainer.InsertPlayer(packet); break; - case MessageType::Chat::GM_ANNOUNCE: { + case MessageType::Chat::GM_ANNOUNCE: // we just forward this packet to every connected server inStream.ResetReadPointer(); Game::server->Send(inStream, packet->systemAddress, true); // send to everyone except origin - } - break; + break; case MessageType::Chat::UNEXPECTED_DISCONNECT: Game::playerContainer.ScheduleRemovePlayer(packet); break; diff --git a/dChatServer/ChatWebAPI.cpp b/dChatServer/ChatWebAPI.cpp index 37b122fb..d12d1fe5 100644 --- a/dChatServer/ChatWebAPI.cpp +++ b/dChatServer/ChatWebAPI.cpp @@ -9,8 +9,13 @@ #include "dConfig.h" #include "PlayerContainer.h" #include "GeneralUtils.h" +#include "JSONUtils.h" +#include "eHTTPMethod.h" +#include "eHTTPStatusCode.h" +#include "magic_enum.hpp" +#include "ChatPackets.h" -#include "json.hpp" +#include "StringifiedEnum.h" using json = nlohmann::json; @@ -22,75 +27,148 @@ namespace { const char* json_content_type = "Content-Type: application/json\r\n"; } -struct HttpReply { - uint32_t status = 404; +struct HTTPReply { + eHTTPStatusCode status = eHTTPStatusCode::NOT_FOUND; std::string message = "{\"error\":\"Not Found\"}"; }; -void HandleRequests(mg_connection* connection, int request, void* request_data) { - if (request == MG_EV_HTTP_MSG) { - HttpReply reply; - const mg_http_message* const http_msg = static_cast(request_data); - if (!http_msg) { - reply.status = 400; - reply.message = "{\"error\":\"Invalid Request\"}"; - } else { - // Handle Post requests - if (mg_strcmp(http_msg->method, mg_str("POST")) == 0) { - auto data = GeneralUtils::TryParse(http_msg->body.buf); - if (!data) { - reply.status = 400; - reply.message = "{\"error\":\"Invalid JSON\"}"; - } else if (mg_match(http_msg->uri, mg_str((root_path + "announce").c_str()), NULL)) { - auto& jsonBuffer = data.value(); - // handle announcements +bool CheckValidJSON(std::optional data, HTTPReply& reply) { + if (!data) { + reply.status = eHTTPStatusCode::BAD_REQUEST; + reply.message = "{\"error\":\"Invalid JSON\"}"; + return false; + } + return true; +} - if (!jsonBuffer.contains("title")) { - reply.status = 400; - reply.message = "{\"error\":\"Missing paramater: title\"}"; - } else if (!jsonBuffer.contains("message")) { - reply.status = 400; - reply.message = "{\"error\":\"Missing paramater: message\"}"; - } else { - std::string title = jsonBuffer["title"]; - std::string message = jsonBuffer["message"]; +void HandlePlayersRequest(HTTPReply& reply) { + const json data = Game::playerContainer; + reply.status = data.empty() ? eHTTPStatusCode::NO_CONTENT : eHTTPStatusCode::OK; + reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump(); +} - // build and send the packet to all world servers - CBITSTREAM; - BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_ANNOUNCE); - bitStream.Write(title.size()); - bitStream.Write(title); - bitStream.Write(message.size()); - bitStream.Write(message); - SEND_PACKET_BROADCAST; +void HandleTeamsRequest(HTTPReply& reply) { + 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(); +} - reply.status = 200; - reply.message = "{\"status\":\"Announcement Sent\"}"; - } - } - // Handle GET Requests - } else if (mg_strcmp(http_msg->method, mg_str("GET")) == 0) { - // Get All Online players - if (mg_match(http_msg->uri, mg_str((root_path + "players").c_str()), NULL)) { - const json data = Game::playerContainer; +void HandleInvalidRoute(HTTPReply& reply) { + reply.status = eHTTPStatusCode::NOT_FOUND; + reply.message = "{\"error\":\"Invalid Route\"}"; +} - reply.status = data.empty() ? 204 : 200; - reply.message = data.empty() ? "{\"error\":\"No Players Online\"}" : data.dump(); - } else if (mg_match(http_msg->uri, mg_str((root_path + "teams").c_str()), NULL)) { - // Get Teams - const json data = Game::playerContainer.GetTeamContainer(); - - reply.status = data.empty() ? 204 : 200; - reply.message = data.empty() ? "{\"error\":\"No Teams Online\"}" : data.dump(); - } - } - } - - LOG_DEBUG("Replying with status %d: %s", reply.status, reply.message.c_str()); - mg_http_reply(connection, reply.status, json_content_type, reply.message.c_str()); +void HandleGET(HTTPReply& reply, const ChatWebAPI::eRoute& route , const std::string& body) { + switch (route) { + case ChatWebAPI::eRoute::PLAYERS: + HandlePlayersRequest(reply); + break; + case ChatWebAPI::eRoute::TEAMS: + HandleTeamsRequest(reply); + break; + case ChatWebAPI::eRoute::INVALID: + default: + HandleInvalidRoute(reply); + break; } } +void HandleAnnounceRequest(HTTPReply& reply, const std::optional& data) { + if (!CheckValidJSON(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 HandlePOST(HTTPReply& reply, const ChatWebAPI::eRoute& route , const std::string& body) { + auto data = GeneralUtils::TryParse(body); + switch (route) { + case ChatWebAPI::eRoute::ANNOUNCE:{ + HandleAnnounceRequest(reply, data.value()); + break; + } + case ChatWebAPI::eRoute::INVALID: + default: + HandleInvalidRoute(reply); + break; + } +} + +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 { + + // 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); + // check for root path + if (uri.find(root_path) == 0) { + // remove root path from uri + uri.erase(0, root_path.length()); + // convert uri to uppercase + std::transform(uri.begin(), uri.end(), uri.begin(), ::toupper); + // convert uri string to route enum + ChatWebAPI::eRoute route = magic_enum::enum_cast(uri).value_or(ChatWebAPI::eRoute::INVALID); + + // convert body from cstring to std string + std::string body(http_msg->body.buf, http_msg->body.len); + + switch (method) { + case eHTTPMethod::GET: + HandleGET(reply, route, body); + break; + case eHTTPMethod::POST: + HandlePOST(reply, route, body); + break; + case eHTTPMethod::PUT: + case eHTTPMethod::DELETE: + case eHTTPMethod::HEAD: + case eHTTPMethod::CONNECT: + case eHTTPMethod::OPTIONS: + case eHTTPMethod::TRACE: + case eHTTPMethod::PATCH: + case eHTTPMethod::INVALID: + default: + reply.status = eHTTPStatusCode::METHOD_NOT_ALLOWED; + reply.message = "{\"error\":\"Invalid Method\"}"; + break; + } + } + } + mg_http_reply(connection, static_cast(reply.status), json_content_type, reply.message.c_str()); +} + + +void HandleRequests(mg_connection* connection, int request, void* request_data) { + switch (request) { + case MG_EV_HTTP_MSG: + HandleHTTPMessage(connection, static_cast(request_data)); + break; + default: + break; + } +} ChatWebAPI::ChatWebAPI() { mg_log_set(MG_LL_NONE); diff --git a/dChatServer/ChatWebAPI.h b/dChatServer/ChatWebAPI.h index 0a296e14..c8e4c04c 100644 --- a/dChatServer/ChatWebAPI.h +++ b/dChatServer/ChatWebAPI.h @@ -11,8 +11,19 @@ public: ~ChatWebAPI(); void ReceiveRequests(); void Listen(); + enum class eRoute { + // GET + PLAYERS, + TEAMS, + // POST + ANNOUNCE, + // INVALID + INVALID + }; private: mg_mgr mgr; + + }; #endif diff --git a/dChatServer/JSONUtils.cpp b/dChatServer/JSONUtils.cpp new file mode 100644 index 00000000..b3dfc4e7 --- /dev/null +++ b/dChatServer/JSONUtils.cpp @@ -0,0 +1,58 @@ +#include "JSONUtils.h" + +#include "json.hpp" + +using json = nlohmann::json; + +void to_json(json& data, const PlayerData& playerData) { + data["id"] = playerData.playerID; + data["name"] = playerData.playerName; + data["gm_level"] = playerData.gmLevel; + data["muted"] = playerData.GetIsMuted(); + + auto& zoneID = data["zone_id"]; + zoneID["map_id"] = playerData.zoneID.GetMapID(); + zoneID["instance_id"] = playerData.zoneID.GetInstanceID(); + zoneID["clone_id"] = playerData.zoneID.GetCloneID(); +} + +void to_json(json& data, const PlayerContainer& playerContainer) { + data = playerContainer.GetAllPlayers(); +} + +void to_json(json& data, const TeamContainer& teamContainer) { + for (auto& teamData : Game::playerContainer.GetTeams()) { + if (!teamData) continue; + data.push_back(*teamData); + } +} + +void to_json(json& data, const TeamData& teamData) { + data["id"] = teamData.teamID; + data["loot_flag"] = teamData.lootFlag; + data["local"] = teamData.local; + + auto& leader = Game::playerContainer.GetPlayerData(teamData.leaderID); + data["leader"] = leader.playerName; + + auto& members = data["members"]; + for (auto& member : teamData.memberIDs) { + auto& playerData = Game::playerContainer.GetPlayerData(member); + + if (!playerData) continue; + 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/JSONUtils.h new file mode 100644 index 00000000..a46a1667 --- /dev/null +++ b/dChatServer/JSONUtils.h @@ -0,0 +1,17 @@ +#ifndef __JSONUTILS_H__ +#define __JSONUTILS_H__ + +#include "json_fwd.hpp" +#include "PlayerContainer.h" + +void to_json(nlohmann::json& data, const PlayerData& playerData); +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__ diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 5c11db1b..510905dc 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -13,50 +13,6 @@ #include "dConfig.h" #include "MessageType/Chat.h" -#include "json.hpp" - -using json = nlohmann::json; - -void to_json(json& data, const PlayerData& playerData) { - data["id"] = playerData.playerID; - data["name"] = playerData.playerName; - data["gm_level"] = playerData.gmLevel; - data["muted"] = playerData.GetIsMuted(); - - auto& zoneID = data["zone_id"]; - zoneID["map_id"] = playerData.zoneID.GetMapID(); - zoneID["instance_id"] = playerData.zoneID.GetInstanceID(); - zoneID["clone_id"] = playerData.zoneID.GetCloneID(); -} - -void to_json(json& data, const PlayerContainer& playerContainer) { - data = playerContainer.GetAllPlayers(); -} - -void to_json(json& data, const TeamContainer& teamContainer) { - for (auto& teamData : Game::playerContainer.GetTeams()) { - if (!teamData) continue; - data.push_back(*teamData); - } -} - -void to_json(json& data, const TeamData& teamData) { - data["id"] = teamData.teamID; - data["loot_flag"] = teamData.lootFlag; - data["local"] = teamData.local; - - auto& leader = Game::playerContainer.GetPlayerData(teamData.leaderID); - data["leader"] = leader.playerName; - - auto& members = data["members"]; - for (auto& member : teamData.memberIDs) { - auto& playerData = Game::playerContainer.GetPlayerData(member); - - if (!playerData) continue; - members.push_back(playerData); - } -} - void PlayerContainer::Initialize() { m_MaxNumberOfBestFriends = GeneralUtils::TryParse(Game::config->GetValue("max_number_of_best_friends")).value_or(m_MaxNumberOfBestFriends); diff --git a/dChatServer/PlayerContainer.h b/dChatServer/PlayerContainer.h index 00ef9e75..18149a24 100644 --- a/dChatServer/PlayerContainer.h +++ b/dChatServer/PlayerContainer.h @@ -6,7 +6,6 @@ #include "Game.h" #include "dServer.h" #include -#include "json_fwd.hpp" enum class eGameMasterLevel : uint8_t; @@ -55,10 +54,6 @@ struct PlayerData { bool isFTP = false; }; -void to_json(nlohmann::json& data, const PlayerData& playerData); -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); struct TeamData { TeamData(); diff --git a/dCommon/dEnums/eHTTPMethod.h b/dCommon/dEnums/eHTTPMethod.h new file mode 100644 index 00000000..8e0343ee --- /dev/null +++ b/dCommon/dEnums/eHTTPMethod.h @@ -0,0 +1,21 @@ +#ifndef __EHTTPMETHODS__H__ +#define __EHTTPMETHODS__H__ + +#include + +#include "magic_enum.hpp" + +enum class eHTTPMethod : uint16_t { + GET, + POST, + PUT, + DELETE, + HEAD, + CONNECT, + OPTIONS, + TRACE, + PATCH, + INVALID +}; + +#endif // __EHTTPMETHODS__H__ \ No newline at end of file diff --git a/dCommon/dEnums/eHTTPStatusCode.h b/dCommon/dEnums/eHTTPStatusCode.h new file mode 100644 index 00000000..27238602 --- /dev/null +++ b/dCommon/dEnums/eHTTPStatusCode.h @@ -0,0 +1,72 @@ +#ifndef __EHTTPSTATUSCODE__H__ +#define __EHTTPSTATUSCODE__H__ + +#include +// verbose list of http codes +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status +enum class eHTTPStatusCode : uint32_t { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + SWITCH_PROXY = 306, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + I_AM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511 +}; + +#endif // !__EHTTPSTATUSCODE__H__ \ No newline at end of file diff --git a/dNet/ChatPackets.cpp b/dNet/ChatPackets.cpp index bf3c9d62..8f4015e9 100644 --- a/dNet/ChatPackets.cpp +++ b/dNet/ChatPackets.cpp @@ -97,3 +97,13 @@ void ChatPackets::SendMessageFail(const SystemAddress& sysAddr) { //docs say there's a wstring here-- no idea what it's for, or if it's even needed so leaving it as is for now. SEND_PACKET; } + +void ChatPackets::Announcement::Send() { + CBITSTREAM; + BitStreamUtils::WriteHeader(bitStream, eConnectionType::CHAT, MessageType::Chat::GM_ANNOUNCE); + bitStream.Write(title.size()); + bitStream.Write(title); + bitStream.Write(message.size()); + bitStream.Write(message); + SEND_PACKET_BROADCAST; +} diff --git a/dNet/ChatPackets.h b/dNet/ChatPackets.h index c33d00dd..0f70c8e2 100644 --- a/dNet/ChatPackets.h +++ b/dNet/ChatPackets.h @@ -27,6 +27,13 @@ struct FindPlayerRequest{ }; namespace ChatPackets { + + struct Announcement { + std::string title; + std::string message; + void Send(); + }; + void SendChatMessage(const SystemAddress& sysAddr, char chatChannel, const std::string& senderName, LWOOBJID playerObjectID, bool senderMythran, const std::u16string& message); void SendSystemMessage(const SystemAddress& sysAddr, const std::u16string& message, bool broadcast = false); void SendMessageFail(const SystemAddress& sysAddr);