From ebca11276937d7ae31b5840b712ec1fdf52d71ff Mon Sep 17 00:00:00 2001 From: KimLS Date: Thu, 16 May 2019 00:12:21 -0700 Subject: [PATCH] Implement a basic websockets server --- .gitmodules | 3 + CMakeLists.txt | 1 + common/CMakeLists.txt | 8 + common/eqemu_logsys.cpp | 5 +- common/eqemu_logsys.h | 4 +- common/net/websocket_server.cpp | 216 ++++++++++++ common/net/websocket_server.h | 63 ++++ common/net/websocket_server_connection.cpp | 189 +++++++++++ common/net/websocket_server_connection.h | 49 +++ submodules/websocketpp | 1 + zone/CMakeLists.txt | 8 +- ..._zone_data_service.cpp => api_service.cpp} | 319 ++++++++++-------- zone/{console.h => api_service.h} | 7 +- zone/console.cpp | 122 ------- zone/eqemu_api_zone_data_service.h | 32 -- zone/net.cpp | 14 +- 16 files changed, 734 insertions(+), 307 deletions(-) create mode 100644 common/net/websocket_server.cpp create mode 100644 common/net/websocket_server.h create mode 100644 common/net/websocket_server_connection.cpp create mode 100644 common/net/websocket_server_connection.h create mode 160000 submodules/websocketpp rename zone/{eqemu_api_zone_data_service.cpp => api_service.cpp} (89%) rename zone/{console.h => api_service.h} (81%) delete mode 100644 zone/console.cpp delete mode 100644 zone/eqemu_api_zone_data_service.h diff --git a/.gitmodules b/.gitmodules index 0dca85127..b93c5efd9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "submodules/recastnavigation"] path = submodules/recastnavigation url = https://github.com/recastnavigation/recastnavigation.git +[submodule "submodules/websocketpp"] + path = submodules/websocketpp + url = https://github.com/zaphoyd/websocketpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 752749232..83b324888 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -349,6 +349,7 @@ INCLUDE_DIRECTORIES(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/submodules/recastnavigat INCLUDE_DIRECTORIES(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/submodules/recastnavigation/DetourCrowd/Include") INCLUDE_DIRECTORIES(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/submodules/recastnavigation/DetourTileCache/Include") INCLUDE_DIRECTORIES(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/submodules/recastnavigation/Recast/Include") +INCLUDE_DIRECTORIES(SYSTEM "${CMAKE_CURRENT_SOURCE_DIR}/submodules/websocketpp") IF(EQEMU_BUILD_SERVER OR EQEMU_BUILD_LOGIN OR EQEMU_BUILD_TESTS OR EQEMU_BUILD_HC) ADD_SUBDIRECTORY(common) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index b26eb850c..2c94d618a 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -86,6 +86,8 @@ SET(common_sources net/servertalk_server_connection.cpp net/tcp_connection.cpp net/tcp_server.cpp + net/websocket_server.cpp + net/websocket_server_connection.cpp patches/patches.cpp patches/sod.cpp patches/sod_limits.cpp @@ -230,6 +232,8 @@ SET(common_headers net/servertalk_server_connection.h net/tcp_connection.h net/tcp_server.h + net/websocket_server.h + net/websocket_server_connection.h patches/patches.h patches/sod.h patches/sod_limits.h @@ -309,6 +313,10 @@ SOURCE_GROUP(Net FILES net/tcp_connection.h net/tcp_server.cpp net/tcp_server.h + net/websocket_server.cpp + net/websocket_server.h + net/websocket_server_connection.cpp + net/websocket_server_connection.h ) SOURCE_GROUP(Patches FILES diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 89728c809..d614415ea 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -95,6 +95,7 @@ enum GameChatColor { EQEmuLogSys::EQEmuLogSys() { on_log_gmsay_hook = [](uint16 log_type, const std::string &) {}; + on_log_console_hook = [](uint16 debug_level, uint16 log_type, const std::string &) {}; bool file_logs_enabled = false; int log_platform = 0; } @@ -346,6 +347,8 @@ void EQEmuLogSys::ProcessConsoleMessage(uint16 debug_level, uint16 log_category, #else std::cout << EQEmuLogSys::GetLinuxConsoleColorFromCategory(log_category) << message << LC_RESET << std::endl; #endif + + on_log_console_hook(debug_level, log_category, message); } /** @@ -505,4 +508,4 @@ void EQEmuLogSys::StartFileLogs(const std::string &log_name) std::ios_base::app | std::ios_base::out ); } -} \ No newline at end of file +} diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index c864e807c..05f4a5fee 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -240,7 +240,8 @@ public: */ uint16 GetGMSayColorFromCategory(uint16 log_category); - void OnLogHookCallBackZone(std::function f) { on_log_gmsay_hook = f; } + void SetGMSayHandler(std::function f) { on_log_gmsay_hook = f; } + void SetConsoleHandler(std::function f) { on_log_console_hook = f; } private: @@ -248,6 +249,7 @@ private: * Callback pointer to zone process for hooking logs to zone using GMSay */ std::function on_log_gmsay_hook; + std::function on_log_console_hook; /** * Formats log messages like '[Category] This is a log message' diff --git a/common/net/websocket_server.cpp b/common/net/websocket_server.cpp new file mode 100644 index 000000000..31242d23c --- /dev/null +++ b/common/net/websocket_server.cpp @@ -0,0 +1,216 @@ +#include "websocket_server.h" +#include "../event/event_loop.h" +#include "../event/timer.h" +#include +#include +#include + +struct MethodHandlerEntry +{ + MethodHandlerEntry(EQ::Net::WebsocketServer::MethodHandler h, int s) { + handler = h; + status = s; + } + + EQ::Net::WebsocketServer::MethodHandler handler; + int status; +}; + +struct EQ::Net::WebsocketServer::Impl +{ + std::unique_ptr server; + std::unique_ptr ping_timer; + std::map, std::unique_ptr> connections; + std::map methods; + websocket_server websocket_server; + LoginHandler login_handler; +}; + +EQ::Net::WebsocketServer::WebsocketServer(const std::string &addr, int port) +{ + _impl.reset(new Impl()); + _impl->server.reset(new EQ::Net::TCPServer()); + _impl->server->Listen(addr, port, false, [this](std::shared_ptr connection) { + auto wsc = _impl->websocket_server.get_connection(); + WebsocketServerConnection *c = new WebsocketServerConnection(this, connection, wsc); + _impl->connections.insert(std::make_pair(wsc, std::unique_ptr(c))); + }); + + _impl->websocket_server.set_write_handler( + [this](websocketpp::connection_hdl hdl, char const *data, size_t size) -> websocketpp::lib::error_code { + auto c = _impl->websocket_server.get_con_from_hdl(hdl); + auto iter = _impl->connections.find(c); + if (iter != _impl->connections.end()) { + iter->second->GetTCPConnection()->Write(data, size); + } + + return websocketpp::lib::error_code(); + }); + + _impl->ping_timer.reset(new EQ::Timer(5000, true, [this](EQ::Timer *t) { + auto iter = _impl->connections.begin(); + + while (iter != _impl->connections.end()) { + try { + auto &connection = iter->second; + connection->GetWebsocketConnection()->ping("keepalive"); + } + catch (std::exception) { + iter->second->GetTCPConnection()->Disconnect(); + } + + iter++; + } + })); + + _impl->methods.insert(std::make_pair("login", MethodHandlerEntry(std::bind(&WebsocketServer::Login, this, std::placeholders::_1, std::placeholders::_2), 0))); + _impl->methods.insert(std::make_pair("subscribe", MethodHandlerEntry(std::bind(&WebsocketServer::Subscribe, this, std::placeholders::_1, std::placeholders::_2), 0))); + _impl->methods.insert(std::make_pair("unsubscribe", MethodHandlerEntry(std::bind(&WebsocketServer::Unsubscribe, this, std::placeholders::_1, std::placeholders::_2), 0))); + _impl->login_handler = [](const WebsocketServerConnection* connection, const std::string& user, const std::string& pass) { + WebsocketLoginStatus ret; + ret.account_name = "admin"; + + if (connection->RemoteIP() == "127.0.0.1" || connection->RemoteIP() == "::") { + ret.logged_in = true; + return ret; + } + + ret.logged_in = false; + return ret; + }; + + _impl->websocket_server.clear_access_channels(websocketpp::log::alevel::all); +} + +EQ::Net::WebsocketServer::~WebsocketServer() +{ +} + +void EQ::Net::WebsocketServer::ReleaseConnection(WebsocketServerConnection *connection) +{ + //Clear any subscriptions here + + _impl->connections.erase(connection->GetWebsocketConnection()); +} + +Json::Value EQ::Net::WebsocketServer::HandleRequest(WebsocketServerConnection *connection, const std::string &method, const Json::Value ¶ms) +{ + Json::Value err; + if (method != "login") { + if (!connection->IsAuthorized()) { + throw WebsocketException("Not logged in"); + } + } + + auto iter = _impl->methods.find(method); + if (iter != _impl->methods.end()) { + auto &s = iter->second; + if (s.status > connection->GetStatus()) { + throw WebsocketException("Status too low"); + } + + return s.handler(connection, params); + } + + throw WebsocketException("Unknown Method"); +} + +void EQ::Net::WebsocketServer::SetMethodHandler(const std::string &method, MethodHandler handler, int required_status) +{ + //Reserved method names + if (method == "subscribe" || + method == "unsubscribe" || + method == "login") { + return; + } + + _impl->methods.insert_or_assign(method, MethodHandlerEntry(handler, required_status)); +} + +void EQ::Net::WebsocketServer::SetLoginHandler(LoginHandler handler) +{ + _impl->login_handler = handler; +} + +void EQ::Net::WebsocketServer::DispatchEvent(const std::string &evt, Json::Value data, int required_status) +{ + try { + Json::Value event_obj; + event_obj["type"] = "event"; + event_obj["event"] = evt; + event_obj["data"] = data; + + std::stringstream payload; + payload << event_obj; + + for (auto &iter : _impl->connections) { + auto &c = iter.second; + + //Might be better to get rid of subscriptions and just send everything and + //let the client sort out what they want idk + if (c->GetStatus() >= required_status && c->IsSubscribed(evt)) { + c->GetWebsocketConnection()->send(payload.str()); + } + } + } + catch (std::exception) { + } +} + +Json::Value EQ::Net::WebsocketServer::Login(WebsocketServerConnection *connection, const Json::Value ¶ms) +{ + Json::Value ret; + + try { + Json::Value ret; + + auto user = params[0].asString(); + auto pass = params[1].asString(); + + auto r = _impl->login_handler(connection, user, pass); + + if (r.logged_in) { + connection->SetAuthorized(true, r.account_name, r.account_id, 255); + ret["status"] = "Ok"; + } + else { + connection->SetAuthorized(false, "", 0, 0); + ret["status"] = "Not Authorized"; + } + + return ret; + } + catch (std::exception) { + throw WebsocketException("Unable to process login request"); + } +} + +Json::Value EQ::Net::WebsocketServer::Subscribe(WebsocketServerConnection *connection, const Json::Value ¶ms) +{ + Json::Value ret; + + try { + auto evt = params[0].asString(); + connection->Subscribe(evt); + ret["status"] = "Ok"; + return ret; + } + catch (std::exception) { + throw WebsocketException("Unable to process subscribe request"); + } +} + +Json::Value EQ::Net::WebsocketServer::Unsubscribe(WebsocketServerConnection *connection, const Json::Value ¶ms) +{ + Json::Value ret; + + try { + auto evt = params[0].asString(); + connection->Unsubscribe(evt); + ret["status"] = "Ok"; + return ret; + } + catch (std::exception) { + throw WebsocketException("Unable to process unsubscribe request"); + } +} diff --git a/common/net/websocket_server.h b/common/net/websocket_server.h new file mode 100644 index 000000000..7c2aafaf9 --- /dev/null +++ b/common/net/websocket_server.h @@ -0,0 +1,63 @@ +#pragma once + +#include "websocket_server_connection.h" + +#include "../json/json.h" +#include +#include +#include + +namespace EQ +{ + namespace Net + { + struct WebsocketLoginStatus + { + bool logged_in; + std::string account_name; + uint32 account_id; + int status; + }; + + class WebsocketException : public std::exception + { + public: + WebsocketException(const std::string &msg) + : _msg(msg.empty() ? "Unknown Error" : msg) { } + + ~WebsocketException() throw() {} + + virtual char const *what() const throw() { + return _msg.c_str(); + } + private: + const std::string _msg; + }; + + class WebsocketServer + { + public: + typedef std::function MethodHandler; + typedef std::function LoginHandler; + + WebsocketServer(const std::string &addr, int port); + ~WebsocketServer(); + + void SetMethodHandler(const std::string& method, MethodHandler handler, int required_status); + void SetLoginHandler(LoginHandler handler); + void DispatchEvent(const std::string& evt, Json::Value data = Json::Value(), int required_status = 0); + private: + void ReleaseConnection(WebsocketServerConnection *connection); + Json::Value HandleRequest(WebsocketServerConnection *connection, const std::string& method, const Json::Value ¶ms); + + Json::Value Login(WebsocketServerConnection *connection, const Json::Value ¶ms); + Json::Value Subscribe(WebsocketServerConnection *connection, const Json::Value ¶ms); + Json::Value Unsubscribe(WebsocketServerConnection *connection, const Json::Value ¶ms); + + struct Impl; + std::unique_ptr _impl; + + friend class WebsocketServerConnection; + }; + } +} diff --git a/common/net/websocket_server_connection.cpp b/common/net/websocket_server_connection.cpp new file mode 100644 index 000000000..d74700f51 --- /dev/null +++ b/common/net/websocket_server_connection.cpp @@ -0,0 +1,189 @@ +#include "websocket_server_connection.h" +#include "websocket_server.h" +#include "../timer.h" +#include "../util/uuid.h" +#include +#include +#include + +struct EQ::Net::WebsocketServerConnection::Impl { + WebsocketServer *parent; + std::shared_ptr connection; + std::shared_ptr ws_connection; + std::string id; + bool authorized; + std::string account_name; + uint32 account_id; + int status; + std::unordered_set subscribed; +}; + +EQ::Net::WebsocketServerConnection::WebsocketServerConnection(WebsocketServer *parent, + std::shared_ptr connection, + std::shared_ptr ws_connection) +{ + _impl.reset(new Impl()); + _impl->parent = parent; + _impl->connection = connection; + _impl->id = EQ::Util::UUID::Generate().ToString(); + _impl->authorized = false; + _impl->account_id = 0; + _impl->status = 0; + _impl->ws_connection = ws_connection; + _impl->ws_connection->set_message_handler(std::bind(&WebsocketServerConnection::OnMessage, this, std::placeholders::_1, std::placeholders::_2)); + _impl->ws_connection->start(); + + connection->OnDisconnect([this](EQ::Net::TCPConnection *connection) { + _impl->parent->ReleaseConnection(this); + }); + + connection->OnRead([this](EQ::Net::TCPConnection *c, const unsigned char *buffer, size_t buffer_size) { + _impl->ws_connection->read_all((const char*)buffer, buffer_size); + }); + + connection->Start(); +} + +EQ::Net::WebsocketServerConnection::~WebsocketServerConnection() +{ +} + +std::string EQ::Net::WebsocketServerConnection::GetID() const +{ + return _impl->id; +} + +bool EQ::Net::WebsocketServerConnection::IsAuthorized() const +{ + return _impl->authorized; +} + +std::string EQ::Net::WebsocketServerConnection::GetAccountName() const +{ + return _impl->account_name; +} + +uint32 EQ::Net::WebsocketServerConnection::GetAccountID() const +{ + return _impl->account_id; +} + +int EQ::Net::WebsocketServerConnection::GetStatus() const +{ + return _impl->status; +} + +std::string EQ::Net::WebsocketServerConnection::RemoteIP() const +{ + return _impl->connection->RemoteIP(); +} + +int EQ::Net::WebsocketServerConnection::RemotePort() const +{ + return _impl->connection->RemotePort(); +} + +std::shared_ptr EQ::Net::WebsocketServerConnection::GetWebsocketConnection() +{ + return _impl->ws_connection; +} + +std::shared_ptr EQ::Net::WebsocketServerConnection::GetTCPConnection() +{ + return _impl->connection; +} + +void EQ::Net::WebsocketServerConnection::OnMessage(websocketpp::connection_hdl hdl, websocket_message_ptr msg) +{ + BenchTimer timer; + timer.reset(); + + if (msg->get_opcode() == websocketpp::frame::opcode::text) { + try { + auto &payload = msg->get_payload(); + + std::stringstream ss(payload); + Json::Value root; + + ss >> root; + + auto method = root["method"].asString(); + auto params = root["params"]; + std::string id = ""; + + auto idNode = root["id"]; + if (!idNode.isNull() && idNode.isString()) { + id = idNode.asString(); + } + + Json::Value response; + response["type"] = "method"; + response["data"] = _impl->parent->HandleRequest(this, method, params); + response["method"] = method; + if(id != "") { + response["id"] = id; + } + + SendResponse(response, timer.elapsed()); + } + catch (std::exception &ex) { + Json::Value error; + error["type"] = "method"; + error["error"] = fmt::format("{0}", ex.what()); + SendResponse(error, timer.elapsed()); + } + } +} + +void EQ::Net::WebsocketServerConnection::SendResponse(const Json::Value &response, double time_elapsed) +{ + Json::Value root = response; + root["execution_time"] = std::to_string(time_elapsed); + + std::stringstream payload; + payload << root; + + _impl->ws_connection->send(payload.str()); +} + +void EQ::Net::WebsocketServerConnection::SetAuthorized(bool v, const std::string account_name, uint32 account_id, int status) +{ + _impl->authorized = v; + _impl->account_name = account_name; + _impl->account_id = account_id; + _impl->status = status; +} + +void EQ::Net::WebsocketServerConnection::Subscribe(const std::string &evt) +{ + if (evt == "") { + return; + } + + auto iter = _impl->subscribed.find(evt); + if (iter == _impl->subscribed.end()) { + _impl->subscribed.insert(evt); + } +} + +void EQ::Net::WebsocketServerConnection::Unsubscribe(const std::string &evt) +{ + if (evt == "") { + return; + } + + auto iter = _impl->subscribed.find(evt); + if (iter != _impl->subscribed.end()) { + _impl->subscribed.erase(iter); + } +} + +bool EQ::Net::WebsocketServerConnection::IsSubscribed(const std::string &evt) const +{ + auto iter = _impl->subscribed.find(evt); + if (iter != _impl->subscribed.end()) { + return true; + } + + return false; +} diff --git a/common/net/websocket_server_connection.h b/common/net/websocket_server_connection.h new file mode 100644 index 000000000..6ffb7f05a --- /dev/null +++ b/common/net/websocket_server_connection.h @@ -0,0 +1,49 @@ +#pragma once + +#include "tcp_server.h" +#include "../types.h" +#include "../json/json-forwards.h" +#include +#include + +namespace EQ +{ + namespace Net + { + typedef websocketpp::server websocket_server; + typedef websocketpp::connection websocket_connection; + typedef websocket_server::message_ptr websocket_message_ptr; + + class WebsocketServer; + class WebsocketServerConnection + { + public: + WebsocketServerConnection(WebsocketServer *parent, + std::shared_ptr connection, + std::shared_ptr ws_connection); + ~WebsocketServerConnection(); + + std::string GetID() const; + bool IsAuthorized() const; + std::string GetAccountName() const; + uint32 GetAccountID() const; + int GetStatus() const; + std::string RemoteIP() const; + int RemotePort() const; + private: + std::shared_ptr GetWebsocketConnection(); + std::shared_ptr GetTCPConnection(); + void OnMessage(websocketpp::connection_hdl hdl, websocket_message_ptr msg); + void SendResponse(const Json::Value &response, double time_elapsed); + void SetAuthorized(bool v, const std::string account_name, uint32 account_id, int status); + void Subscribe(const std::string &evt); + void Unsubscribe(const std::string &evt); + bool IsSubscribed(const std::string &evt) const; + + struct Impl; + std::unique_ptr _impl; + + friend class WebsocketServer; + }; + } +} diff --git a/submodules/websocketpp b/submodules/websocketpp new file mode 160000 index 000000000..c6d7e295b --- /dev/null +++ b/submodules/websocketpp @@ -0,0 +1 @@ +Subproject commit c6d7e295bf5a0ab9b5f896720cc1a0e0fdc397a7 diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 908a1cb02..3a897fda9 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -5,8 +5,9 @@ SET(zone_sources aa_ability.cpp aggro.cpp aggromanager.cpp - aura.cpp + api_service.cpp attack.cpp + aura.cpp beacon.cpp bonuses.cpp bot.cpp @@ -17,7 +18,6 @@ SET(zone_sources client_mods.cpp client_packet.cpp client_process.cpp - console.cpp command.cpp corpse.cpp data_bucket.cpp @@ -29,7 +29,6 @@ SET(zone_sources embxs.cpp encounter.cpp entity.cpp - eqemu_api_zone_data_service.cpp exp.cpp fastmath.cpp fearpath.cpp @@ -149,6 +148,7 @@ SET(zone_headers aa.h aa_ability.h aggromanager.h + api_service.h aura.h basic_functions.h beacon.h @@ -160,7 +160,6 @@ SET(zone_headers client_packet.h command.h common.h - console.h corpse.h data_bucket.h doors.h @@ -169,7 +168,6 @@ SET(zone_headers embxs.h encounter.h entity.h - eqemu_api_zone_data_service.h errmsg.h event_codes.h fastmath.h diff --git a/zone/eqemu_api_zone_data_service.cpp b/zone/api_service.cpp similarity index 89% rename from zone/eqemu_api_zone_data_service.cpp rename to zone/api_service.cpp index 6f9dd202b..5a0ab14c1 100644 --- a/zone/eqemu_api_zone_data_service.cpp +++ b/zone/api_service.cpp @@ -18,10 +18,14 @@ * */ +#include +#include "../common/net/websocket_server.h" +#include "../common/eqemu_logsys.h" +#include "zonedb.h" #include "client.h" #include "entity.h" #include "corpse.h" -#include "eqemu_api_zone_data_service.h" +#include "api_service.h" #include "npc.h" #include "object.h" #include "zone.h" @@ -30,8 +34,117 @@ extern Zone *zone; -void callGetNpcListDetail(Json::Value &response) -{ +EQ::Net::WebsocketLoginStatus CheckLogin(EQ::Net::WebsocketServerConnection *connection, const std::string &username, const std::string &password) { + EQ::Net::WebsocketLoginStatus ret; + ret.logged_in = false; + + ret.account_id = database.CheckLogin(username.c_str(), password.c_str()); + + if (ret.account_id == 0) { + return ret; + } + + char account_name[64]; + database.GetAccountName(static_cast(ret.account_id), account_name); + ret.account_name = account_name; + ret.logged_in = true; + ret.status = database.CheckStatus(ret.account_id); + return ret; +} + +Json::Value ApiGetPacketStatistics(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; + auto &list = entity_list.GetClientList(); + + for (auto &iter : list) { + auto client = iter.second; + auto connection = client->Connection(); + auto opts = connection->GetManager()->GetOptions(); + auto eqs_stats = connection->GetStats(); + auto &stats = eqs_stats.DaybreakStats; + auto now = EQ::Net::Clock::now(); + auto sec_since_stats_reset = std::chrono::duration_cast>( + now - stats.created + ).count(); + + Json::Value row; + + row["client_id"] = client->GetID(); + row["client_name"] = client->GetCleanName(); + row["seconds_since_reset"] = sec_since_stats_reset; + row["sent_bytes"] = stats.sent_bytes; + row["receive_bytes"] = stats.recv_bytes; + row["min_ping"] = stats.min_ping; + row["max_ping"] = stats.max_ping; + row["last_ping"] = stats.last_ping; + row["average_ping"] = stats.avg_ping; + row["realtime_receive_packets"] = stats.recv_packets; + row["realtime_sent_packets"] = stats.sent_packets; + row["sync_recv_packets"] = stats.sync_recv_packets; + row["sync_sent_packets"] = stats.sync_sent_packets; + row["sync_remote_recv_packets"] = stats.sync_remote_recv_packets; + row["sync_remote_sent_packets"] = stats.sync_remote_sent_packets; + row["packet_loss_in"] = (100.0 * (1.0 - static_cast(stats.sync_recv_packets) / + static_cast(stats.sync_remote_sent_packets))); + row["packet_loss_out"] = (100.0 * (1.0 - static_cast(stats.sync_remote_recv_packets) / + static_cast(stats.sync_sent_packets))); + row["resent_packets"] = stats.resent_packets; + row["resent_fragments"] = stats.resent_fragments; + row["resent_non_fragments"] = stats.resent_full; + row["dropped_datarate_packets"] = stats.dropped_datarate_packets; + + Json::Value sent_packet_types; + + for (auto i = 0; i < _maxEmuOpcode; ++i) { + auto count = eqs_stats.SentCount[i]; + if (count > 0) { + sent_packet_types[OpcodeNames[i]] = count; + } + } + + Json::Value receive_packet_types; + + for (auto i = 0; i < _maxEmuOpcode; ++i) { + auto count = eqs_stats.RecvCount[i]; + if (count > 0) { + receive_packet_types[OpcodeNames[i]] = count; + } + } + + row["sent_packet_types"] = sent_packet_types; + row["receive_packet_types"] = receive_packet_types; + + response.append(row); + } + + return response; +} + +Json::Value ApiGetOpcodeList(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; + for (auto i = 0; i < _maxEmuOpcode; ++i) { + Json::Value row = OpcodeNames[i]; + + response.append(row); + } + + return response; +} + +Json::Value ApiGetNpcListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &list = entity_list.GetNPCList(); for (auto &iter : list) { @@ -115,10 +228,16 @@ void callGetNpcListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetDoorListDetail(Json::Value &response) -{ +Json::Value ApiGetDoorListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &door_list = entity_list.GetDoorsList(); for (auto itr : door_list) { @@ -152,10 +271,16 @@ void callGetDoorListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetCorpseListDetail(Json::Value &response) -{ +Json::Value ApiGetCorpseListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &corpse_list = entity_list.GetCorpseList(); for (auto itr : corpse_list) { @@ -185,10 +310,16 @@ void callGetCorpseListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetObjectListDetail(Json::Value &response) -{ +Json::Value ApiGetObjectListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &list = entity_list.GetObjectList(); for (auto &iter : list) { @@ -214,10 +345,16 @@ void callGetObjectListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetMobListDetail(Json::Value &response) -{ +Json::Value ApiGetMobListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &list = entity_list.GetMobList(); for (auto &iter : list) { @@ -414,10 +551,16 @@ void callGetMobListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetClientListDetail(Json::Value &response) -{ +Json::Value ApiGetClientListDetail(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; auto &list = entity_list.GetClientList(); for (auto &iter : list) { @@ -607,10 +750,16 @@ void callGetClientListDetail(Json::Value &response) response.append(row); } + + return response; } -void callGetZoneAttributes(Json::Value &response) -{ +Json::Value ApiGetZoneAttributes(EQ::Net::WebsocketServerConnection *connection, Json::Value params) { + if (zone->GetZoneID() == 0) { + throw EQ::Net::WebsocketException("Zone must be loaded to invoke this call"); + } + + Json::Value response; Json::Value row; row["aggro_limit_reached"] = zone->AggroLimitReached(); @@ -648,129 +797,31 @@ void callGetZoneAttributes(Json::Value &response) row["zone_type"] = zone->GetZoneType(); response.append(row); + return response; } -void callGetPacketStatistics(Json::Value &response) +void RegisterApiLogEvent(std::unique_ptr &server) { - auto &list = entity_list.GetClientList(); - - for (auto &iter : list) { - auto client = iter.second; - auto connection = client->Connection(); - auto opts = connection->GetManager()->GetOptions(); - auto eqs_stats = connection->GetStats(); - auto &stats = eqs_stats.DaybreakStats; - auto now = EQ::Net::Clock::now(); - auto sec_since_stats_reset = std::chrono::duration_cast>( - now - stats.created - ).count(); - - Json::Value row; - - row["client_id"] = client->GetID(); - row["client_name"] = client->GetCleanName(); - row["seconds_since_reset"] = sec_since_stats_reset; - row["sent_bytes"] = stats.sent_bytes; - row["receive_bytes"] = stats.recv_bytes; - row["min_ping"] = stats.min_ping; - row["max_ping"] = stats.max_ping; - row["last_ping"] = stats.last_ping; - row["average_ping"] = stats.avg_ping; - row["realtime_receive_packets"] = stats.recv_packets; - row["realtime_sent_packets"] = stats.sent_packets; - row["sync_recv_packets"] = stats.sync_recv_packets; - row["sync_sent_packets"] = stats.sync_sent_packets; - row["sync_remote_recv_packets"] = stats.sync_remote_recv_packets; - row["sync_remote_sent_packets"] = stats.sync_remote_sent_packets; - row["packet_loss_in"] = (100.0 * (1.0 - static_cast(stats.sync_recv_packets) / - static_cast(stats.sync_remote_sent_packets))); - row["packet_loss_out"] = (100.0 * (1.0 - static_cast(stats.sync_remote_recv_packets) / - static_cast(stats.sync_sent_packets))); - row["resent_packets"] = stats.resent_packets; - row["resent_fragments"] = stats.resent_fragments; - row["resent_non_fragments"] = stats.resent_full; - row["dropped_datarate_packets"] = stats.dropped_datarate_packets; - - Json::Value sent_packet_types; - - for (auto i = 0; i < _maxEmuOpcode; ++i) { - auto count = eqs_stats.SentCount[i]; - if (count > 0) { - sent_packet_types[OpcodeNames[i]] = count; - } - } - - Json::Value receive_packet_types; - - for (auto i = 0; i < _maxEmuOpcode; ++i) { - auto count = eqs_stats.RecvCount[i]; - if (count > 0) { - receive_packet_types[OpcodeNames[i]] = count; - } - } - - row["sent_packet_types"] = sent_packet_types; - row["receive_packet_types"] = receive_packet_types; - - response.append(row); - } + LogSys.SetConsoleHandler([&](uint16 debug_level, uint16 log_category, const std::string &msg) { + Json::Value data; + data["debug_level"] = debug_level; + data["log_category"] = log_category; + data["msg"] = msg; + server->DispatchEvent("log", data, 50); + }); } +void RegisterApiService(std::unique_ptr &server) { + server->SetLoginHandler(CheckLogin); + server->SetMethodHandler("get_packet_statistics", &ApiGetPacketStatistics, 50); + server->SetMethodHandler("get_opcode_list", &ApiGetOpcodeList, 50); + server->SetMethodHandler("get_npc_list_detail", &ApiGetNpcListDetail, 50); + server->SetMethodHandler("get_door_list_detail", &ApiGetDoorListDetail, 50); + server->SetMethodHandler("get_corpse_list_detail", &ApiGetCorpseListDetail, 50); + server->SetMethodHandler("get_object_list_detail", &ApiGetObjectListDetail, 50); + server->SetMethodHandler("get_mob_list_detail", &ApiGetMobListDetail, 50); + server->SetMethodHandler("get_client_list_detail", &ApiGetClientListDetail, 50); + server->SetMethodHandler("get_zone_attributes", &ApiGetZoneAttributes, 50); -void callGetOpcodeList(Json::Value &response) -{ - for (auto i = 0; i < _maxEmuOpcode; ++i) { - Json::Value row = OpcodeNames[i]; - - response.append(row); - } -} - -void EQEmuApiZoneDataService::get(Json::Value &response, const std::vector &args) -{ - std::string method = args[0]; - - if (zone->GetZoneID() == 0) { - response["error"] = "Zone must be loaded to invoke calls"; - return; - } - - /** - * Packet statistics - */ - if (method == "get_packet_statistics") { - callGetPacketStatistics(response); - } - if (method == "get_opcode_list") { - callGetOpcodeList(response); - } - - /** - * List detail - */ - if (method == "get_npc_list_detail") { - callGetNpcListDetail(response); - } - if (method == "get_client_list_detail") { - callGetClientListDetail(response); - } - if (method == "get_mob_list_detail") { - callGetMobListDetail(response); - } - if (method == "get_door_list_detail") { - callGetDoorListDetail(response); - } - if (method == "get_corpse_list_detail") { - callGetCorpseListDetail(response); - } - if (method == "get_object_list_detail") { - callGetObjectListDetail(response); - } - - /** - * Zone attributes - */ - if (method == "get_zone_attributes") { - callGetZoneAttributes(response); - } + RegisterApiLogEvent(server); } diff --git a/zone/console.h b/zone/api_service.h similarity index 81% rename from zone/console.h rename to zone/api_service.h index 05dc29569..52018838d 100644 --- a/zone/console.h +++ b/zone/api_service.h @@ -1,6 +1,6 @@ /** * EQEmulator: Everquest Server Emulator - * Copyright (C) 2001-2018 EQEmulator Development Team (https://github.com/EQEmu/Server) + * Copyright (C) 2001-2019 EQEmulator Development Team (https://github.com/EQEmu/Server) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #pragma once -#include "../common/net/console_server.h" +#include +#include "../common/net/websocket_server.h" -void RegisterConsoleFunctions(std::unique_ptr &console); +void RegisterApiService(std::unique_ptr &server); diff --git a/zone/console.cpp b/zone/console.cpp deleted file mode 100644 index 0af4adeb5..000000000 --- a/zone/console.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/** - * EQEmulator: Everquest Server Emulator - * Copyright (C) 2001-2019 EQEmulator Development Team (https://github.com/EQEmu/Server) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 2 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY except by those people which sell it, which - * are required to give you total support for your newly bought product; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -#include "console.h" -#include "../common/string_util.h" -#include "../common/md5.h" -#include "../common/database.h" -#include "../common/json/json.h" -#include "zone.h" -#include "npc.h" -#include "entity.h" -#include "eqemu_api_zone_data_service.h" - -/** - * @param username - * @param password - * @return - */ -struct EQ::Net::ConsoleLoginStatus CheckLogin(const std::string &username, const std::string &password) -{ - struct EQ::Net::ConsoleLoginStatus ret; - ret.account_id = database.CheckLogin(username.c_str(), password.c_str()); - if (ret.account_id == 0) { - return ret; - } - - char account_name[64]; - database.GetAccountName(static_cast(ret.account_id), account_name); - - ret.account_name = account_name; - ret.status = database.CheckStatus(static_cast(ret.account_id)); - return ret; -} - -/** - * @param connection - * @param command - * @param args - */ -void ConsoleApi( - EQ::Net::ConsoleServerConnection *connection, - const std::string &command, - const std::vector &args -) -{ - Json::Value root; - Json::Value response; - - BenchTimer timer; - timer.reset(); - - EQEmuApiZoneDataService::get(response, args); - - std::string method = args[0]; - - root["execution_time"] = std::to_string(timer.elapsed()); - root["method"] = method; - root["data"] = response; - - std::stringstream payload; - payload << root; - - connection->SendLine(payload.str()); -} - -/** - * - * @param connection - * @param command - * @param args - */ -void ConsoleNull( - EQ::Net::ConsoleServerConnection *connection, - const std::string &command, - const std::vector &args -) -{ -} - -/** - * @param connection - * @param command - * @param args - */ -void ConsoleQuit( - EQ::Net::ConsoleServerConnection *connection, - const std::string &command, - const std::vector &args -) -{ - connection->SendLine("Exiting..."); - connection->Close(); -} - -/** - * @param console - */ -void RegisterConsoleFunctions(std::unique_ptr &console) -{ - console->RegisterLogin(std::bind(CheckLogin, std::placeholders::_1, std::placeholders::_2)); - console->RegisterCall("api", 200, "api", std::bind(ConsoleApi, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - console->RegisterCall("ping", 50, "ping", std::bind(ConsoleNull, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - console->RegisterCall("quit", 50, "quit", std::bind(ConsoleQuit, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); - console->RegisterCall("exit", 50, "exit", std::bind(ConsoleQuit, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); -} diff --git a/zone/eqemu_api_zone_data_service.h b/zone/eqemu_api_zone_data_service.h deleted file mode 100644 index b6ed250a9..000000000 --- a/zone/eqemu_api_zone_data_service.h +++ /dev/null @@ -1,32 +0,0 @@ -/** - * EQEmulator: Everquest Server Emulator - * Copyright (C) 2001-2019 EQEmulator Development Team (https://github.com/EQEmu/Server) - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; version 2 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY except by those people which sell it, which - * are required to give you total support for your newly bought product; - * without even the implied warranty of MERCHANTABILITY or FITNESS FOR - * A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -#ifndef EQEMU_API_ZONE_DATA_SERVICE_H -#define EQEMU_API_ZONE_DATA_SERVICE_H - -#include "../common/json/json.h" - -class EQEmuApiZoneDataService { -public: - static void get(Json::Value &response, const std::vector &args); -}; - - -#endif //EQEMU_API_ZONE_DATA_SERVICE_H diff --git a/zone/net.cpp b/zone/net.cpp index 868643bdf..e013978c7 100644 --- a/zone/net.cpp +++ b/zone/net.cpp @@ -43,8 +43,8 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "../common/spdat.h" #include "../common/eqemu_logsys.h" #include "../common/eqemu_logsys_fmt.h" -#include "../common/net/console_server.h" +#include "api_service.h" #include "zone_config.h" #include "masterentity.h" #include "worldserver.h" @@ -93,9 +93,6 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #else #include #include "../common/unix.h" -#include "../common/net/console_server.h" -#include "console.h" - #endif volatile bool RunLoops = true; @@ -121,7 +118,6 @@ double frame_time = 0.0; void Shutdown(); extern void MapOpcodes(); -void RegisterConsoleFunctions(std::unique_ptr &console); int main(int argc, char** argv) { RegisterExecutablePlatform(ExePlatformZone); @@ -255,7 +251,7 @@ int main(int argc, char** argv) { #endif /* Register Log System and Settings */ - LogSys.OnLogHookCallBackZone(&Zone::GMSayHookCallBackProcess); + LogSys.SetGMSayHandler(&Zone::GMSayHookCallBackProcess); database.LoadLogSettings(LogSys.log_settings); LogSys.StartFileLogs(); @@ -463,7 +459,7 @@ int main(int argc, char** argv) { EQStreamInterface *eqsi; std::unique_ptr eqsm; std::chrono::time_point frame_prev = std::chrono::system_clock::now(); - std::unique_ptr console; + std::unique_ptr ws_server; auto loop_fn = [&](EQ::Timer* t) { //Advance the timer to our current point in time @@ -488,8 +484,8 @@ int main(int argc, char** argv) { Config->TelnetIP.c_str(), Config->ZonePort ); - console.reset(new EQ::Net::ConsoleServer(Config->TelnetIP, Config->ZonePort)); - RegisterConsoleFunctions(console); + ws_server.reset(new EQ::Net::WebsocketServer(Config->TelnetIP, Config->ZonePort)); + RegisterApiService(ws_server); telnet_server_opened = true; } }