/** * Copyright (C) 2016-2017 Jessica James. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * * Written by Jessica James */ #include "Jupiter/IRC_Client.h" #include "Jupiter/HTTP.h" #include "Jupiter/HTTP_QueryString.h" #include "HTTPServer.h" #include "RenX_Core.h" #include "RenX_Server.h" #include "RenX_Functions.h" #include "RenX_PlayerInfo.h" #include "RenX_ServerList.h" using namespace Jupiter::literals; static STRING_LITERAL_AS_NAMED_REFERENCE(CONTENT_TYPE_APPLICATION_JSON, "application/json"); const Jupiter::ReferenceString server_list_game_header = ""_jrs; const Jupiter::ReferenceString server_list_game_footer = "\n"_jrs; Jupiter::String jsonify(const Jupiter::ReadableString &in_str) { const unsigned char *ptr = reinterpret_cast(in_str.ptr()); const unsigned char *end_ptr = ptr + in_str.size(); Jupiter::String result(in_str.size()); while (ptr < end_ptr) { if (*ptr == '\\') // backslash { result += '\\'; result += '\\'; } else if (*ptr == '\"') // quotation { result += '\\'; result += '\"'; } else if (*ptr < 0x20) // control characters result.aformat("\\u%04x", *ptr); else if ((*ptr & 0x80) != 0) // UTF-8 sequence; copy to bypass above processing { result += *ptr; if ((*ptr & 0x40) != 0) { // this is a 2+ byte sequence if ((*ptr & 0x20) != 0) { // this is a 3+ byte sequence if ((*ptr & 0x10) != 0) { // this is a 4 byte sequnce result += *++ptr; } result += *++ptr; } result += *++ptr; } } else // Character in standard ASCII table result += *ptr; ++ptr; } return result; } Jupiter::String sanitize_game(const Jupiter::ReadableString &in_str) { const unsigned char *ptr = reinterpret_cast(in_str.ptr()); const unsigned char *end_ptr = ptr + in_str.size(); Jupiter::String result(in_str.size()); while (ptr < end_ptr != 0) { if (*ptr == '\\') // backslash { result += '\\'; result += '\\'; } else if (*ptr == '\"') // quotation { result += '\\'; result += '\"'; } else if (*ptr < 0x20) // control characters result.aformat("\\u%04x", *ptr); else if (*ptr == '~') // Game server list control character result += "\\u007E"_jrs; else if (*ptr == ';') // Game server list control character result += "\\u003B"_jrs; else if ((*ptr & 0x80) != 0) // UTF-8 sequence; copy to bypass above processing { result += *ptr; if ((*ptr & 0x40) != 0) { // this is a 2+ byte sequence if ((*ptr & 0x20) != 0) { // this is a 3+ byte sequence if ((*ptr & 0x10) != 0) { // this is a 4 byte sequnce result += *++ptr; } result += *++ptr; } result += *++ptr; } } else // Character in standard ASCII table result += *ptr; ++ptr; } return result; } bool RenX_ServerListPlugin::initialize() { RenX_ServerListPlugin::web_hostname = this->config.get("Hostname"_jrs, ""_jrs); RenX_ServerListPlugin::web_path = this->config.get("Path"_jrs, "/"_jrs); RenX_ServerListPlugin::server_list_page_name = this->config.get("ServersPageName"_jrs, "servers.jsp"_jrs); RenX_ServerListPlugin::server_list_long_page_name = this->config.get("HumanServersPageName"_jrs, "servers_long.jsp"_jrs); RenX_ServerListPlugin::server_page_name = this->config.get("ServerPageName"_jrs, "server.jsp"_jrs); RenX_ServerListPlugin::game_server_list_page_name = this->config.get("ServersGamePageName"_jrs, "browser.jsp"_jrs); /** Initialize content */ Jupiter::HTTP::Server &server = getHTTPServer(); // Server list page Jupiter::HTTP::Server::Content *content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::server_list_page_name, handle_server_list_page); content->language = &Jupiter::HTTP::Content::Language::ENGLISH; content->type = &CONTENT_TYPE_APPLICATION_JSON; content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::UTF8; content->free_result = false; server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); // Server list (long) page content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::server_list_long_page_name, handle_server_list_long_page); content->language = &Jupiter::HTTP::Content::Language::ENGLISH; content->type = &CONTENT_TYPE_APPLICATION_JSON; content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::UTF8; content->free_result = true; server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); // Server page (GUIDs) content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::server_page_name, handle_server_page); content->language = &Jupiter::HTTP::Content::Language::ENGLISH; content->type = &CONTENT_TYPE_APPLICATION_JSON; content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::UTF8; content->free_result = true; server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); // Game server list page content = new Jupiter::HTTP::Server::Content(RenX_ServerListPlugin::game_server_list_page_name, handle_game_server_list_page); content->language = &Jupiter::HTTP::Content::Language::ENGLISH; content->type = &Jupiter::HTTP::Content::Type::Text::HTML; content->charset = &Jupiter::HTTP::Content::Type::Text::Charset::ASCII; content->free_result = false; server.hook(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, content); this->updateServerList(); return true; } RenX_ServerListPlugin::~RenX_ServerListPlugin() { Jupiter::HTTP::Server &server = getHTTPServer(); server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::server_list_page_name); server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::server_list_long_page_name); server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::server_page_name); server.remove(RenX_ServerListPlugin::web_hostname, RenX_ServerListPlugin::web_path, RenX_ServerListPlugin::game_server_list_page_name); } Jupiter::ReadableString *RenX_ServerListPlugin::getServerListJSON() { return std::addressof(RenX_ServerListPlugin::server_list_json); } Jupiter::ReadableString *RenX_ServerListPlugin::getServerListGame() { return std::addressof(RenX_ServerListPlugin::server_list_game); } constexpr const char *json_bool_as_cstring(bool in) { return in ? "true" : "false"; } Jupiter::StringS server_as_json(const RenX::Server &server) { Jupiter::String server_json_block(128); Jupiter::String server_name = jsonify(server.getName()); Jupiter::String server_map = jsonify(server.getMap().name); Jupiter::String server_version = jsonify(server.getGameVersion()); server_json_block.format(R"json({"Name":"%.*s","Current Map":"%.*s","Bots":%u,"Players":%u,"Game Version":"%.*s","Variables":{"Mine Limit":%d,"bSteamRequired":%s,"bPrivateMessageTeamOnly":%s,"bPassworded":%s,"bAllowPrivateMessaging":%s,"bRanked":%s,"Game Type":%d,"Player Limit":%d,"Vehicle Limit":%d,"bAutoBalanceTeams":%s,"Team Mode":%d,"bSpawnCrates":%s,"CrateRespawnAfterPickup":%f,"Time Limit":%d},"Port":%u,"IP":"%.*s")json", server_name.size(), server_name.ptr(), server_map.size(), server_map.ptr(), server.getBotCount(), server.players.size() - server.getBotCount(), server_version.size(), server_version.ptr(), server.getMineLimit(), json_bool_as_cstring(server.isSteamRequired()), json_bool_as_cstring(server.isPrivateMessageTeamOnly()), json_bool_as_cstring(server.isPassworded()), json_bool_as_cstring(server.isPrivateMessagingEnabled()), json_bool_as_cstring(server.isRanked()), server.getGameType(), server.getPlayerLimit(), server.getVehicleLimit(), json_bool_as_cstring(server.getTeamMode() == 3), server.getTeamMode(), json_bool_as_cstring(server.isCratesEnabled()), server.getCrateRespawnDelay(), server.getTimeLimit(), server.getPort(), server.getSocketHostname().size(), server.getSocketHostname().ptr()); server_json_block += '}'; return server_json_block; } Jupiter::StringS server_as_game(const RenX::Server &server) { Jupiter::String server_game_block(256); Jupiter::String server_name = sanitize_game(server.getName()); Jupiter::String server_map = sanitize_game(server.getMap().name); Jupiter::String server_version = sanitize_game(server.getGameVersion()); Jupiter::String server_levels; RenX::Map *map; if (server.maps.size() != 0) { map = server.maps.get(0); server_levels = sanitize_game(RenX::formatGUID(*map)); server_levels += '='; server_levels += sanitize_game(map->name); for (size_t index = 1; index != server.maps.size(); ++index) { map = server.maps.get(index); server_levels += ';'; server_levels += sanitize_game(RenX::formatGUID(*map)); server_levels += '='; server_levels += sanitize_game(map->name); } } server_game_block.format("\n<@>%.*s~%.*s~%u~%s~%.*s~" "%d;%d;%d;%s;%d;%d;%d;%s;%s;%s;%.*s;%s" "~%u~%d~%s~%s~%.*s", server_name.size(), server_name.ptr(), server.getSocketHostname().size(), server.getSocketHostname().ptr(), server.getPort(), json_bool_as_cstring(server.isPassworded()), server_map.size(), server_map.ptr(), //START OPTIONS server.getPlayerLimit(), server.getVehicleLimit(), server.getMineLimit(), json_bool_as_cstring(server.isCratesEnabled()), server.getGameType(), server.getTeamMode(), server.getTimeLimit(), json_bool_as_cstring(server.isPrivateMessagingEnabled()), json_bool_as_cstring(server.isPrivateMessageTeamOnly()), json_bool_as_cstring(server.isSteamRequired()), server_version.size(), server_version.ptr(), json_bool_as_cstring(server.isBotsEnabled()), //END OPTIONS server.players.size() - server.getBotCount(), server.getPlayerLimit(), json_bool_as_cstring(server.isRanked()), json_bool_as_cstring(server.isMatchInProgress()), server_levels.size(), server_levels.ptr()); return server_game_block; } Jupiter::StringS server_as_long_json(const RenX::Server &server) { Jupiter::String server_json_block(128); Jupiter::String server_name = jsonify(server.getName()); Jupiter::String server_map = jsonify(server.getMap().name); Jupiter::String server_version = jsonify(server.getGameVersion()); server_json_block.format(R"json({ "Name": "%.*s", "Current Map": "%.*s", "Bots": %u, "Players": %u, "Game Version": "%.*s", "Variables": { "Mine Limit": %d, "bSteamRequired": %s, "bPrivateMessageTeamOnly": %s, "bPassworded": %s, "bAllowPrivateMessaging": %s, "Player Limit": %d, "Vehicle Limit": %d, "bAutoBalanceTeams": %s, "Team Mode": %d, "bSpawnCrates": %s, "CrateRespawnAfterPickup": %f, "Time Limit": %d }, "Port": %u, "IP": "%.*s")json", server_name.size(), server_name.ptr(), server_map.size(), server_map.ptr(), server.getBotCount(), server.players.size() - server.getBotCount(), server_version.size(), server_version.ptr(), server.getMineLimit(), json_bool_as_cstring(server.isSteamRequired()), json_bool_as_cstring(server.isPrivateMessageTeamOnly()), json_bool_as_cstring(server.isPassworded()), json_bool_as_cstring(server.isPrivateMessagingEnabled()), server.getPlayerLimit(), server.getVehicleLimit(), json_bool_as_cstring(server.getTeamMode() == 3), server.getTeamMode(), json_bool_as_cstring(server.isCratesEnabled()), server.getCrateRespawnDelay(), server.getTimeLimit(), server.getPort(), server.getSocketHostname().size(), server.getSocketHostname().ptr()); // Level Rotation if (server.maps.size() != 0) { server_json_block += ",\n\t\t\"Levels\": ["_jrs; server_json_block += "\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(server.maps.get(0)->name); server_json_block += "\",\n\t\t\t\t\"GUID\": \""_jrs; server_json_block += RenX::formatGUID(*server.maps.get(0)); server_json_block += "\"\n\t\t\t}"_jrs; for (size_t index = 1; index != server.maps.size(); ++index) { server_json_block += ",\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(server.maps.get(index)->name); server_json_block += "\",\n\t\t\t\t\"GUID\": \""_jrs; server_json_block += RenX::formatGUID(*server.maps.get(index)); server_json_block += "\"\n\t\t\t}"_jrs; } server_json_block += "\n\t\t]"_jrs; } // Mutators if (server.mutators.size() != 0) { server_json_block += ",\n\t\t\"Mutators\": ["_jrs; server_json_block += "\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(*server.mutators.get(0)); server_json_block += "\"\n\t\t\t}"_jrs; for (size_t index = 1; index != server.mutators.size(); ++index) { server_json_block += ",\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(*server.mutators.get(index)); server_json_block += "\"\n\t\t\t}"_jrs; } server_json_block += "\n\t\t]"_jrs; } // Player List if (server.players.size() != 0 && server.players.size() != server.getBotCount()) { server_json_block += ",\n\t\t\"PlayerList\": ["_jrs; auto node = server.players.begin(); while (node != server.players.end()) { if (node->isBot == false) { server_json_block += "\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(node->name); server_json_block += "\"\n\t\t\t}"_jrs; ++node; break; } ++node; } while (node != server.players.end()) { server_json_block += ",\n\t\t\t{\n\t\t\t\t\"Name\": \""_jrs; server_json_block += jsonify(node->name); server_json_block += "\"\n\t\t\t}"_jrs; ++node; } server_json_block += "\n\t\t]"_jrs; } server_json_block += "\n\t}"_jrs; return server_json_block; } void RenX_ServerListPlugin::addServerToServerList(RenX::Server &server) { Jupiter::String server_json_block(256); // append to server_list_json if (RenX_ServerListPlugin::server_list_json.isEmpty()) { RenX_ServerListPlugin::server_list_json = '['; RenX_ServerListPlugin::server_list_json += server_as_json(server); RenX_ServerListPlugin::server_list_json += ']'; } else { RenX_ServerListPlugin::server_list_json.truncate(1); // remove trailing ']'. RenX_ServerListPlugin::server_list_json += ','; RenX_ServerListPlugin::server_list_json += server_as_json(server); RenX_ServerListPlugin::server_list_json += ']'; } // append to server_list_game if (RenX_ServerListPlugin::server_list_game.isEmpty()) { RenX_ServerListPlugin::server_list_game = server_list_game_header; RenX_ServerListPlugin::server_list_game += server_as_game(server); RenX_ServerListPlugin::server_list_game += server_list_game_footer; } else { RenX_ServerListPlugin::server_list_game.truncate(server_list_game_footer.size()); // remove trailing "" RenX_ServerListPlugin::server_list_game += server_as_game(server); RenX_ServerListPlugin::server_list_game += server_list_game_footer; } // add to individual listing server_json_block = '{'; if (server.maps.size() != 0) { server_json_block += "\"Levels\":["_jrs; server_json_block += "{\"Name\":\""_jrs; server_json_block += jsonify(server.maps.get(0)->name); server_json_block += "\",\"GUID\":\""_jrs; server_json_block += RenX::formatGUID(*server.maps.get(0)); server_json_block += "\"}"_jrs; for (size_t index = 1; index != server.maps.size(); ++index) { server_json_block += ",{\"Name\":\""_jrs; server_json_block += jsonify(server.maps.get(index)->name); server_json_block += "\",\"GUID\":\""_jrs; server_json_block += RenX::formatGUID(*server.maps.get(index)); server_json_block += "\"}"_jrs; } server_json_block += ']'; } // Mutators if (server.mutators.size() != 0) { if (server.maps.size() != 0) server_json_block += ","_jrs; server_json_block += "\"Mutators\":["_jrs; server_json_block += "{\"Name\":\""_jrs; server_json_block += jsonify(*server.mutators.get(0)); server_json_block += "\"}"_jrs; for (size_t index = 1; index != server.mutators.size(); ++index) { server_json_block += ",{\"Name\":\""_jrs; server_json_block += jsonify(*server.mutators.get(index)); server_json_block += "\"}"_jrs; } server_json_block += ']'; } // Player List if (server.players.size() != 0 && server.players.size() != server.getBotCount()) { server_json_block += ",\"PlayerList\":["_jrs; auto node = server.players.begin(); while (node != server.players.end()) { if (node->isBot == false) { server_json_block += "{\"Name\":\""_jrs; server_json_block += jsonify(node->name); server_json_block += "\"}"_jrs; ++node; break; } ++node; } while (node != server.players.end()) { server_json_block += ",{\"Name\":\""_jrs; server_json_block += jsonify(node->name); server_json_block += "\"}"_jrs; ++node; } server_json_block += "]"_jrs; } server_json_block += '}'; server.varData[this->name].set("j"_jrs, server_json_block); } void RenX_ServerListPlugin::updateServerList() { Jupiter::ArrayList servers = RenX::getCore()->getServers(); size_t index = 0; RenX::Server *server; // regenerate server_list_json and server_list_Game RenX_ServerListPlugin::server_list_json = '['; RenX_ServerListPlugin::server_list_game = server_list_game_header; while (index != servers.size()) { server = servers.get(index); if (server->isConnected() && server->isFullyConnected()) { RenX_ServerListPlugin::server_list_json += server_as_json(*server); RenX_ServerListPlugin::server_list_game += server_as_game(*server); ++index; break; } ++index; } while (index != servers.size()) { server = servers.get(index); if (server->isConnected() && server->isFullyConnected()) { RenX_ServerListPlugin::server_list_json += ','; RenX_ServerListPlugin::server_list_json += server_as_json(*server); RenX_ServerListPlugin::server_list_game += server_as_game(*server); } ++index; } RenX_ServerListPlugin::server_list_json += ']'; RenX_ServerListPlugin::server_list_game += server_list_game_footer; } void RenX_ServerListPlugin::RenX_OnServerFullyConnected(RenX::Server &server) { this->addServerToServerList(server); } void RenX_ServerListPlugin::RenX_OnServerDisconnect(RenX::Server &server, RenX::DisconnectReason) { this->updateServerList(); // remove from individual listing server.varData[this->name].remove("j"_jrs); } void RenX_ServerListPlugin::RenX_OnJoin(RenX::Server &, const RenX::PlayerInfo &) { this->updateServerList(); } void RenX_ServerListPlugin::RenX_OnPart(RenX::Server &server, const RenX::PlayerInfo &) { if (server.isTravelling() == false || server.isSeamless()) this->updateServerList(); } void RenX_ServerListPlugin::RenX_OnMapLoad(RenX::Server &server, const Jupiter::ReadableString &map) { this->updateServerList(); } // Plugin instantiation and entry point. RenX_ServerListPlugin pluginInstance; Jupiter::ReadableString *handle_server_list_page(const Jupiter::ReadableString &) { return pluginInstance.getServerListJSON(); } Jupiter::ReadableString *handle_server_list_long_page(const Jupiter::ReadableString &) { Jupiter::ArrayList servers = RenX::getCore()->getServers(); size_t index = 0; RenX::Server *server; Jupiter::String *server_list_long_json = new Jupiter::String(256 * servers.size()); // regenerate server_list_json *server_list_long_json = "["_jrs; while (index != servers.size()) { server = servers.get(index); if (server->isConnected() && server->isFullyConnected()) { *server_list_long_json += "\n\t"_jrs; *server_list_long_json += server_as_long_json(*server); ++index; break; } ++index; } while (index != servers.size()) { server = servers.get(index); if (server->isConnected() && server->isFullyConnected()) { *server_list_long_json += ",\n\t"_jrs; *server_list_long_json += server_as_long_json(*server); } ++index; } *server_list_long_json += "\n]"_jrs; return server_list_long_json; } Jupiter::ReadableString *handle_server_page(const Jupiter::ReadableString &query_string) { Jupiter::HTTP::HTMLFormResponse html_form_response(query_string); Jupiter::ReferenceString address; int port = 0; RenX::Server *server; // parse form data if (html_form_response.table.size() < 2) return new Jupiter::ReferenceString(); if (html_form_response.table.size() != 0) { address = html_form_response.table.get("ip"_jrs, address); port = html_form_response.table.getCast("port"_jrs, port); } // search for server Jupiter::ArrayList servers = RenX::getCore()->getServers(); size_t index = 0; while (true) { if (index == servers.size()) return new Jupiter::ReferenceString(); server = servers.get(index); if (server->getSocketHostname().equals(address) && server->getPort() == port) break; ++index; } // return server data return new Jupiter::ReferenceString(server->varData[pluginInstance.getName()].get("j"_jrs)); } Jupiter::ReadableString *handle_game_server_list_page(const Jupiter::ReadableString &query_string) { return pluginInstance.getServerListGame(); } extern "C" __declspec(dllexport) Jupiter::Plugin *getPlugin() { return &pluginInstance; }