diff options
author | Jan200101 <sentrycraft123@gmail.com> | 2023-10-15 18:20:11 +0200 |
---|---|---|
committer | Jan200101 <sentrycraft123@gmail.com> | 2023-10-15 18:20:11 +0200 |
commit | 3583254d59ecc7ca5534c7759224a806de1ff5a8 (patch) | |
tree | fa093c8f93069f2ed15c4575aa4d97ddd1ae0869 /src | |
parent | fd6eae1b446fa5164a01e61384d01b7bb0565c1a (diff) | |
download | SouthRPC-3583254d59ecc7ca5534c7759224a806de1ff5a8.tar.gz SouthRPC-3583254d59ecc7ca5534c7759224a806de1ff5a8.zip |
rewrite HTTP server
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 26 | ||||
-rw-r--r-- | src/handler.cpp | 113 | ||||
-rw-r--r-- | src/handler.h | 41 | ||||
-rw-r--r-- | src/helper.cpp | 62 | ||||
-rw-r--r-- | src/helper.h | 14 | ||||
-rw-r--r-- | src/http_server.cpp | 212 | ||||
-rw-r--r-- | src/http_server.h | 52 | ||||
-rw-r--r-- | src/init.cpp | 1 | ||||
-rw-r--r-- | src/plugin.cpp | 105 | ||||
-rw-r--r-- | src/plugin.h | 6 | ||||
-rw-r--r-- | src/rpc_server.cpp | 160 | ||||
-rw-r--r-- | src/rpc_server.h | 46 | ||||
-rw-r--r-- | src/server.cpp | 384 | ||||
-rw-r--r-- | src/server.h | 74 |
14 files changed, 821 insertions, 475 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6e2ad25..a68c423 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,21 +10,27 @@ plugin_manifest(SouthRPC run_on_server ON) # Insecure but sure plugin_manifest(SouthRPC run_on_client ON) add_library(SouthRPC SHARED - ${CMAKE_CURRENT_SOURCE_DIR}/init.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/plugin.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/plugin.h - ${CMAKE_CURRENT_SOURCE_DIR}/server.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/server.h + "${CMAKE_CURRENT_SOURCE_DIR}/init.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/plugin.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/plugin.h" + "${CMAKE_CURRENT_SOURCE_DIR}/handler.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/handler.h" + "${CMAKE_CURRENT_SOURCE_DIR}/helper.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/helper.h" + "${CMAKE_CURRENT_SOURCE_DIR}/http_server.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/http_server.h" + "${CMAKE_CURRENT_SOURCE_DIR}/rpc_server.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/rpc_server.h" ) -target_include_directories(SouthRPC PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(SouthRPC PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(SouthRPC rapidjson_header) target_link_libraries(SouthRPC ws2_32) -target_precompile_headers(SouthRPC PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/ns_plugin.h) +target_precompile_headers(SouthRPC PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/ns_plugin.h") plugin_link(SouthRPC) plugin_thunderstore(SouthRPC - "https://github.com/Jan200101/SouthRPC" - "${PROJECT_SOURCE_DIR}/README.md" - "${PROJECT_SOURCE_DIR}/icon.png" + "https://github.com/Jan200101/SouthRPC" + "${PROJECT_SOURCE_DIR}/README.md" + "${PROJECT_SOURCE_DIR}/icon.png" ) diff --git a/src/handler.cpp b/src/handler.cpp new file mode 100644 index 0000000..f82573c --- /dev/null +++ b/src/handler.cpp @@ -0,0 +1,113 @@ +#include <winsock2.h> + +#include "handler.h" +#include "plugin.h" +#include "internal/convarproxy.h" + +#define SLEEP_DURATION 5000 + +ServerHandler::ServerHandler(Plugin* plugin): + plugin(plugin) +{ + this->Convar_Port = plugin->ConVar("southrpc_port", DEFAULT_PORT, FCVAR_ARCHIVE, "South RPC HTTP Port (requires restart, default: " DEFAULT_PORT ")"); + + if (WSAStartup(MAKEWORD(2, 2), &this->wsaData) != 0) { + spdlog::error("Failed to open Windows Socket"); + return; + } + + if (LOBYTE(this->wsaData.wVersion) != 2 || + HIBYTE(this->wsaData.wVersion) != 2) + { + spdlog::error("Incorrect Winsock version loaded"); + WSACleanup(); + return; + } + + spdlog::info("Initialized handler"); + + this->init = true; + + /* + @TODO + this->register_callback( + "list_methods", + [=](rapidjson::Value& params) -> rapidjson::Value + { + rapidjson::MemoryPoolAllocator<>& allocator = this->body.GetAllocator(); + + return rapidjson::Value(1); + } + ); + */ + +} + +static DWORD WINAPI static_server_run(void* param) +{ + ServerHandler* server = (ServerHandler*)param; + + server->run(); + + return 0; +} + +void ServerHandler::start() +{ + if (!this->init || this->running) + return; + + DWORD thread_id; + this->thread = CreateThread(NULL, 0, static_server_run, (void*)this, 0, &thread_id); +} + +void ServerHandler::stop() +{ + if (!this->thread) + return; + + this->running = false; + TerminateThread(this->thread, 0); + this->thread = nullptr; +} + +void ServerHandler::run() +{ + this->running = true; + + spdlog::info("Running Handler"); + + // The engine won't have loaded convar data until after the entry + //Sleep(SLEEP_DURATION); + + //int port = this->Convar_Port->GetInt(); + int port = 26503; + RPCServer* server = new RPCServer(INADDR_ANY, port); + Sleep(SLEEP_DURATION); + + while (this->running) + { + RPCRequest* request = server->receive_request(); + spdlog::debug("received request"); + if (!request) + continue; + + std::string method_name = request->get_method(); + + callback_list::const_iterator method_pos = this->methods.find(method_name); + if (method_pos != this->methods.end()) + { + spdlog::info("Invoked method '{}'", method_name); + request->result(method_pos->second(request->get_allocator(), request->get_params())); + } + else + { + spdlog::error("Attempted to invoke unknown method '{}'", method_name); + request->error(rapidjson::Value("Unknown method")); + } + + delete request; + } + + delete server; +} diff --git a/src/handler.h b/src/handler.h new file mode 100644 index 0000000..32d26dc --- /dev/null +++ b/src/handler.h @@ -0,0 +1,41 @@ +#ifndef HANDLER_H +#define HANDLER_H + +#include <functional> + +#include <rapidjson/document.h> + +#include "rpc_server.h" +#include "plugin.h" +#include "internal/convarproxy.h" + +#define DEFAULT_PORT "26503" + +typedef std::function<rapidjson::Value(rapidjson::MemoryPoolAllocator<>&, rapidjson::Value&)> method_callback; +typedef std::map<std::string, method_callback> callback_list; + +class ServerHandler +{ + private: + bool init = false; + Plugin* plugin; + + WSADATA wsaData; + + bool running = false; + HANDLE thread = nullptr; + + ConVarProxy* Convar_Port = nullptr; + + callback_list methods; + public: + ServerHandler(Plugin* parent); + + void register_callback(std::string name, method_callback callback) { this->methods.try_emplace(name, callback); } + + void start(); + void stop(); + void run(); +}; + +#endif diff --git a/src/helper.cpp b/src/helper.cpp new file mode 100644 index 0000000..bb09b2b --- /dev/null +++ b/src/helper.cpp @@ -0,0 +1,62 @@ +#include <rapidjson/document.h> + +#include "ns_plugin.h" + +#include "helper.h" + +void SquirrelToJSON( + rapidjson::Value* out_val, + rapidjson::MemoryPoolAllocator<>& allocator, + SQObject* obj_ptr +) { + + if (obj_ptr) + { + switch (obj_ptr->_Type) + { + case OT_BOOL: + out_val->SetBool(obj_ptr->_VAL.asInteger); + break; + + case OT_INTEGER: + out_val->SetInt(obj_ptr->_VAL.asInteger); + break; + + case OT_STRING: + out_val->SetString(obj_ptr->_VAL.asString->_val, obj_ptr->_VAL.asString->length); + break; + + case OT_ARRAY: + out_val->SetArray(); + + for (int i = 0; i < obj_ptr->_VAL.asArray->_usedSlots; ++i) + { + rapidjson::Value n; + SquirrelToJSON(&n, allocator, &obj_ptr->_VAL.asArray->_values[i]); + out_val->PushBack(n, allocator); + } + break; + + case OT_TABLE: + out_val->SetObject(); + + for (int i = 0; i < obj_ptr->_VAL.asTable->_numOfNodes; ++i) + { + auto node = &obj_ptr->_VAL.asTable->_nodes[i]; + if (node->key._Type != OT_STRING) + continue; + + rapidjson::Value k; + SquirrelToJSON(&k, allocator, &node->key); + + rapidjson::Value v; + SquirrelToJSON(&v, allocator, &node->val); + out_val->AddMember(k, v, allocator); + } + break; + + default: + break; + } + } +} diff --git a/src/helper.h b/src/helper.h new file mode 100644 index 0000000..f3ce275 --- /dev/null +++ b/src/helper.h @@ -0,0 +1,14 @@ +#ifndef HELPER_H +#define HELPER_H + +#include <rapidjson/document.h> + +#include "ns_plugin.h" + +void SquirrelToJSON( + rapidjson::Value* out_val, + rapidjson::MemoryPoolAllocator<>& allocator, + SQObject* obj_ptr +); + +#endif
\ No newline at end of file diff --git a/src/http_server.cpp b/src/http_server.cpp new file mode 100644 index 0000000..cc8e3b4 --- /dev/null +++ b/src/http_server.cpp @@ -0,0 +1,212 @@ +#include <sstream> +#include <ws2tcpip.h> + +#include "http_server.h" +#include "ns_plugin.h" + +#define BUFFER_SIZE 128 +#define HTTP_LF "\r\n" +#define BODY_SEP HTTP_LF HTTP_LF + +HTTPRequest::HTTPRequest(SOCKET socket): + socket(socket) +{ +} + +HTTPRequest::~HTTPRequest() +{ + this->close(); +} + +size_t HTTPRequest::content_length() +{ + auto& map = this->headers; + header_map::const_iterator pos = map.find("Content-Length"); + if (pos == map.end()) + return 0; + + size_t result; + std::istringstream sstream(pos->second); + sstream >> result; + + return result; +} + +void HTTPRequest::parse_headers(std::string raw) +{ + // @TODO validate first line for correct method and path + std::istringstream iss(raw); + + for (std::string line; std::getline(iss, line, HTTP_LF[1]); ) + { + if (line[line.size()-1] != HTTP_LF[0]) + break; + + line = line.substr(0, line.size()-1); + if (line.empty()) + break; + + std::string::size_type sep_pos = line.find(":"); + + if (line.size() <= sep_pos) + continue; + + std::string key = line.substr(0, sep_pos); + std::string value = line.substr(sep_pos+1); + + this->headers.try_emplace(key, value); + } +} + +void HTTPRequest::respond(std::string status_code, header_map response_headers, std::string response_body) +{ + if (this->socket == -1) + { + spdlog::error("Attempted to send response when socket is already closed"); + return; + } + + std::ostringstream response; + + response << "HTTP/1.1 " << status_code << HTTP_LF; + + for (auto const& [key, val] : response_headers) + { + response << key << ": " << val << HTTP_LF; + } + + response << HTTP_LF << response_body; + + std::string response_data = response.str(); + send(this->socket, response_data.c_str(), response_data.size(), 0); +} + +void HTTPRequest::close() +{ + if (this->socket != -1) + { + closesocket(this->socket); + this->socket = -1; + } +} + +HTTPServer::HTTPServer(unsigned long addr, unsigned short port) +{ + this->sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) + { + spdlog::error("Failed to create socket"); + return; + } + + struct sockaddr_in local = { 0 }; + local.sin_family = AF_INET; + local.sin_addr.s_addr = addr; + local.sin_port = htons(port); + + if (bind(this->sock, (struct sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) + { + spdlog::error("Failed to bindsocket ({})", WSAGetLastError()); + this->close(); + return; + } + + if (listen(this->sock, 10) == SOCKET_ERROR) + { + spdlog::error("Failed to listen to socket"); + this->close(); + return; + } + + spdlog::info("Initialized HTTPServer"); +} + +HTTPServer::~HTTPServer() +{ + this->close(); +} + +void HTTPServer::close() +{ + if (this->sock != -1) + { + closesocket(this->sock); + this->sock = -1; + } +} + +HTTPRequest* HTTPServer::receive_request() +{ + if (this->sock == -1) + { + spdlog::error("Attempted to receive request without running web server"); + return nullptr; + } + + struct sockaddr_in addr; + int addr_len = sizeof(addr); + + spdlog::debug("awaiting HTTP request"); + + SOCKET msg = accept(this->sock, (struct sockaddr*)&addr, &addr_len); + if (msg == INVALID_SOCKET || msg == -1) + { + spdlog::error("Failed to receive packet ({})", WSAGetLastError()); + return nullptr; + }; + + spdlog::info("Connection opened by {}", inet_ntoa(addr.sin_addr)); + + std::string content; + char buffer[BUFFER_SIZE]; + memset(buffer, 0, sizeof(buffer)); + + bool parsed_header = false; + std::string::size_type header_end; + std::string::size_type expected_size; + HTTPRequest* req = new HTTPRequest(msg); + do + { + spdlog::debug("receiving buffer"); + int len = recv(msg, buffer, BUFFER_SIZE-1, 0); + spdlog::debug("received buffer ({})", len); + + if (len == SOCKET_ERROR || len == 0) + break; + + buffer[len] = '\0'; + + content += buffer; + + if (!parsed_header && strstr(buffer, BODY_SEP)) + { + parsed_header = true; + req->parse_headers(content); + + header_end = content.find(BODY_SEP); + expected_size = header_end + req->content_length() + strlen(BODY_SEP); + spdlog::debug("Expecting size {}", expected_size); + } + + if (parsed_header) + { + if (expected_size <= content.length()) + { + req->set_body(content.substr(header_end+strlen(HTTP_LF))); + break; + } + } + } + while(1); + + if (content.empty()) + { + spdlog::error("Received no data ({})", WSAGetLastError()); + delete req; + return nullptr; + } + + spdlog::debug("Received Data ({})", content.length()); + + return req; +}
\ No newline at end of file diff --git a/src/http_server.h b/src/http_server.h new file mode 100644 index 0000000..0c4dafe --- /dev/null +++ b/src/http_server.h @@ -0,0 +1,52 @@ +#ifndef HTTP_SERVER +#define HTTP_SERVER + +#include <string> +#include <map> +#include <ws2tcpip.h> + +#include "http_server.h" + +#define HTTP_OK "200 OK" + +typedef std::map<std::string, std::string> header_map; + +class HTTPRequest { + private: + SOCKET socket; + + std::string request_type; + std::string path; + header_map headers; + std::string body; + + + public: + HTTPRequest(SOCKET socket); + ~HTTPRequest(); + + void parse_headers(std::string raw); + void set_body(std::string body) { this->body = body; } + size_t content_length(); + + std::string get_body() { return this->body; } + + void respond(std::string status_code, header_map response_headers, std::string response_body); + void close(); +}; + +class HTTPServer { + private: + SOCKET sock = -1; + + void parse_header(); + + public: + HTTPServer(unsigned long addr, unsigned short port); + ~HTTPServer(); + + void close(); + HTTPRequest* receive_request(); +}; + +#endif diff --git a/src/init.cpp b/src/init.cpp index d0005c0..7ecc84a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -1,7 +1,6 @@ #include "ns_plugin.h" #include "internal/logging.h" -#include "server.h" #include "plugin.h" Plugin* plugin = nullptr; diff --git a/src/plugin.cpp b/src/plugin.cpp index 1e3f63b..273b41f 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -3,9 +3,12 @@ #include <stdio.h> #include <psapi.h> +#include <rapidjson/document.h> + #include "ns_plugin.h" #include "plugin.h" -#include "server.h" +#include "handler.h" +#include "helper.h" #include "internal/types.h" #include "internal/concommandproxy.h" #include "internal/convarproxy.h" @@ -13,10 +16,13 @@ Plugin::Plugin(PluginInitFuncs* funcs, PluginNorthstarData* data) { - this->funcs = *funcs; - this->data = *data; + if (funcs) + this->funcs = *funcs; + if (data) + this->data = *data; - this->server = new rpc_server(this); + this->server = new ServerHandler(this); + this->register_server_callbacks(); spdlog::info(PLUGIN_NAME " initialised!"); } @@ -39,6 +45,94 @@ Plugin::~Plugin() delete server; } +void Plugin::register_server_callbacks() +{ + /* execute_squirrel */ + this->server->register_callback( + "execute_squirrel", + [this](rapidjson::MemoryPoolAllocator<>& allocator, rapidjson::Value& params) -> rapidjson::Value + { + if (!params.IsObject()) + { + return rapidjson::Value("method 'execute_command' only supports object parameters"); + } + + const char* code = params["code"].GetString(); + ScriptContext context = ScriptContext::UI; + + if (params.HasMember("context")) + { + if (params["context"] == "client") + { + context = ScriptContext::CLIENT; + } + else if (params["context"] == "server") + { + context = ScriptContext::SERVER; + } + else if (params["context"] == "ui") + { + context = ScriptContext::UI; + } + else + { + return rapidjson::Value("method 'execute_command' received invalid value for parameter 'context'"); + } + } + + SQObject obj_ptr; + this->RunSquirrelCode(context, code, &obj_ptr); + + rapidjson::Value result; + SquirrelToJSON(&result, allocator, &obj_ptr); + return result; + } + ); + + /* execute_squirrel */ + this->server->register_callback( + "execute_command", + [this](rapidjson::MemoryPoolAllocator<>& allocator, rapidjson::Value& params) -> rapidjson::Value + { + const char* cmd = nullptr; + if (params.IsObject()) + { + if (!params.HasMember("command")) + { + return rapidjson::Value("method 'execute_command' is missing parameter 'command'"); + } + else if (!params["command"].IsString()) + { + return rapidjson::Value("method 'execute_command' received invalid type for parameter 'command'"); + } + + cmd = params["command"].GetString(); + } + else if (params.IsArray()) + { + if (params.Size() < 1) + { + return rapidjson::Value("method 'execute_command' received 0 position arguments, 1 required"); + } + else if (!params[0].IsString()) + { + return rapidjson::Value("method 'execute_command' received invalid type for parameter"); + } + + cmd = params[0].GetString(); + } + else + { + assert(0); + } + + this->RunCommand(cmd); + + return rapidjson::Value(); + } + ); +} + HMODULE Plugin::GetModuleByName(const char* name) { HMODULE hMods[1024]; @@ -180,6 +274,9 @@ void Plugin::StartServer() void Plugin::RunCommand(const char* cmd) { + if (!cmd) + return; + this->engine_funcs.Cbuf_AddText(this->engine_funcs.Cbuf_GetCurrentPlayer(), cmd, cmd_source_t::kCommandSrcCode); this->engine_funcs.Cbuf_Execute(); } diff --git a/src/plugin.h b/src/plugin.h index b913412..b55cb40 100644 --- a/src/plugin.h +++ b/src/plugin.h @@ -8,7 +8,7 @@ #include "internal/convarproxy.h" #include "internal/sqfuncregistrationproxy.h" -class rpc_server; +class ServerHandler; class Plugin { private: @@ -30,16 +30,18 @@ class Plugin { Cbuf_ExecuteType Cbuf_Execute; } engine_funcs = { 0 }; - rpc_server* server = nullptr; std::vector<ConCommandProxy*> commands; std::vector<ConVarProxy*> variables; std::vector<SQFuncRegistrationProxy*> squirrel_functions; + void register_server_callbacks(); HMODULE GetModuleByName(const char* name); public: + ServerHandler* server = nullptr; + Plugin(PluginInitFuncs* funcs, PluginNorthstarData* data); ~Plugin(); diff --git a/src/rpc_server.cpp b/src/rpc_server.cpp new file mode 100644 index 0000000..20b55cf --- /dev/null +++ b/src/rpc_server.cpp @@ -0,0 +1,160 @@ + +#include <rapidjson/document.h> +#include <rapidjson/writer.h> +#include <rapidjson/stringbuffer.h> + +#include "rpc_server.h" + +#define RPC_PREFIX "rpc." + +RPCRequest::RPCRequest(HTTPRequest* request): + request(request) +{ + if (!request) + return; + + this->body.Parse(request->get_body()); + + if (!this->body.HasMember("jsonrpc")) + { + spdlog::error("Request is missing 'jsonrpc' field"); + this->error(rapidjson::Value("Request is missing 'jsonrpc' field")); + return; + } + else if (this->body["jsonrpc"] != "2.0") + { + spdlog::error("Request has invalid value for 'jsonrpc' field"); + this->error(rapidjson::Value("Request has invalid value for 'jsonrpc' field")); + return; + } + + if (this->body.HasMember("id")) + { + this->id = this->body["id"]; + + if (!this->id.IsInt() && !this->id.IsString() && !this->id.IsNull()) + { + spdlog::error("Request has invalid type for 'id' field"); + this->error(rapidjson::Value("Request has invalid type for 'id' field")); + return; + } + } + + if (!this->body.HasMember("method")) + { + spdlog::error("Request is missing 'method' field"); + this->error(rapidjson::Value("Request is missing 'method' field")); + return; + } + else if (!this->body["method"].IsString()) + { + spdlog::error("Request has invalid type for 'method' field"); + this->error(rapidjson::Value("Request has invalid type for 'method' field")); + return; + } + + this->method = this->body["method"].GetString(); + + if (!strncmp(this->method.c_str(), RPC_PREFIX, strlen(RPC_PREFIX))) + { + spdlog::error("Request uses reserved value for 'method' field"); + this->error(rapidjson::Value("Request uses reserved value for 'method' field")); + return; + } + + if (this->body.HasMember("params")) + { + this->params = this->body["params"]; + + if (!this->params.IsArray() && !this->params.IsObject()) + { + spdlog::error("Request has invalid type for 'params' field"); + this->error(rapidjson::Value("Request has invalid type for 'params' field")); + return; + } + } + + spdlog::debug("Valid request"); +} + +RPCRequest::~RPCRequest() +{ + delete this->request; +} + +void RPCRequest::response(rapidjson::Value& response) +{ + // a request without an ID is a Notification. + // Nothing we can do about it + if (!this->body.HasMember("id")) + return; + + rapidjson::StringBuffer buffer; + rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); + + response.Accept(writer); + + this->request->respond(HTTP_OK, { + {"Content-Type", "application/json"} + }, buffer.GetString()); +} + +void RPCRequest::result(rapidjson::Value result) +{ + if (!this->body.HasMember("id")) + return; + + rapidjson::MemoryPoolAllocator<>& allocator = this->body.GetAllocator(); + + rapidjson::Value error_resp; + error_resp.SetObject(); + + error_resp.AddMember("jsonrpc", "2.0", allocator); + error_resp.AddMember("id", this->id, allocator); + error_resp.AddMember("result", result, allocator); + + this->response(error_resp); +} + +void RPCRequest::error(rapidjson::Value error) +{ + if (!this->body.HasMember("id")) + return; + + rapidjson::MemoryPoolAllocator<>& allocator = this->body.GetAllocator(); + + rapidjson::Value error_resp; + error_resp.SetObject(); + + error_resp.AddMember("jsonrpc", "2.0", allocator); + error_resp.AddMember("id", this->id, allocator); + error_resp.AddMember("error", error, allocator); + + this->response(error_resp); +} + +void RPCRequest::close() +{ + if (this->request) + { + this->request->close(); + } +} + +RPCServer::RPCServer(unsigned long addr, unsigned short port): + http_server(addr, port) +{ +} + +RPCRequest* RPCServer::receive_request() +{ + spdlog::debug("awaiting JSON-RPC request"); + + HTTPRequest* http_req = this->http_server.receive_request(); + if (!http_req) + return nullptr; + + RPCRequest* rpc_req = new RPCRequest(http_req); + + return rpc_req; +}
\ No newline at end of file diff --git a/src/rpc_server.h b/src/rpc_server.h new file mode 100644 index 0000000..afa72fe --- /dev/null +++ b/src/rpc_server.h @@ -0,0 +1,46 @@ +#ifndef RPC_SERVER +#define RPC_SERVER + +#include <map> +#include <string> + +#include <rapidjson/document.h> + +#include "http_server.h" + +class RPCRequest { + private: + HTTPRequest* request; + + rapidjson::Document body; + + rapidjson::Value id; + std::string method; + rapidjson::Value params; + + void response(rapidjson::Value& result); + + public: + RPCRequest(HTTPRequest* request); + ~RPCRequest(); + + rapidjson::MemoryPoolAllocator<>& get_allocator() { return this->body.GetAllocator(); }; + std::string get_method() { return this->method; } + rapidjson::Value& get_params() { return this->params; } + + void result(rapidjson::Value result); + void error(rapidjson::Value error); + void close(); +}; + +class RPCServer { + private: + HTTPServer http_server; + + public: + RPCServer(unsigned long addr, unsigned short port); + + RPCRequest* receive_request(); +}; + +#endif diff --git a/src/server.cpp b/src/server.cpp deleted file mode 100644 index 9156260..0000000 --- a/src/server.cpp +++ /dev/null @@ -1,384 +0,0 @@ -#include <winsock2.h> -#include <ws2tcpip.h> - -#include <spdlog/spdlog.h> - -#include <rapidjson/document.h> -#include <rapidjson/writer.h> -#include <rapidjson/stringbuffer.h> - -#include "server.h" -#include "plugin.h" - -ConVar* Cvar_southrpc_port; - -void ConCommand_southrpc_status(const CCommand& args) -{ - spdlog::info("Yes"); -} - -SQRESULT Script_test(HSquirrelVM* sqvm) -{ - spdlog::info("Script_test invoked"); - return SQRESULT_NOTNULL; -} - -rpc_server::rpc_server(Plugin* plugin) - : parent(plugin) -{ - plugin->ConCommand("southrpc_status", ConCommand_southrpc_status, "", 0); - - this->Convar_Port = plugin->ConVar("southrpc_port", DEFAULT_PORT, FCVAR_ARCHIVE, "South RPC HTTP Port (requires restart, default: " DEFAULT_PORT ")"); - - plugin->AddNativeSquirrelFunction("void", "south_test", "", "", ScriptContext::UI, Script_test); - - if (WSAStartup(MAKEWORD(2, 2), &this->wsaData) != 0) { - spdlog::error("Failed to open Windows Socket"); - return; - } - - if (LOBYTE(this->wsaData.wVersion) != 2 || - HIBYTE(this->wsaData.wVersion) != 2) - { - spdlog::error("Incorrect Winsock version loaded"); - WSACleanup(); - return; - } - - initialized = true; -} - -rpc_server::~rpc_server() -{ - this->stop(); -} - -static DWORD WINAPI static_server_run(void* param) -{ - rpc_server* server = (rpc_server*)param; - - return server->run(); -} - -void rpc_server::start() -{ - if (!initialized) - { - return; - } - - this->running = true; - - DWORD thread_id; - this->thread = CreateThread(NULL, 0, static_server_run, (void*)this, 0, &thread_id); -} - -void rpc_server::stop() -{ - if (!this->thread) - return; - - this->running = false; - this->thread = nullptr; -} - -DWORD rpc_server::run() -{ - // Waiting for engine to init so we can actually use convar values from the archive - Sleep(SLEEP_DURATION); - - int port = this->Convar_Port->GetInt(); - - struct sockaddr_in local = { 0 }; - local.sin_family = AF_INET; - local.sin_addr.s_addr = INADDR_ANY; - local.sin_port = htons(port); - - SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock == INVALID_SOCKET) - { - spdlog::error("Failed to create socket"); - goto run_error; - } - - if (bind(sock, (struct sockaddr*)&local, sizeof(local)) == SOCKET_ERROR) - { - spdlog::error("Failed to bindsocket"); - goto run_error; - } - - if (listen(sock, 10) == SOCKET_ERROR) - { - spdlog::error("Failed to listen to socket"); - goto run_error; - } - - spdlog::info("Running under {}", port); - - SOCKET msg; - struct sockaddr_in addr; - int addr_len; - - char buf[REQUEST_SIZE]; - char http_method[10]; - char protocol[10]; - - this->running = true; - while (this->running) - { - addr_len = sizeof(addr); - msg = accept(sock, (struct sockaddr*)&addr, &addr_len); - if (msg == INVALID_SOCKET || msg == -1) - { - spdlog::error("Received invalid request ({})", WSAGetLastError()); - continue; - }; - - spdlog::info("Connection opened by {}", inet_ntoa(addr.sin_addr)); - - memset(buf, 0, sizeof(buf)); - if (recv(msg, buf, sizeof(buf), 0) == SOCKET_ERROR) - { - spdlog::error("Received no data ({})", WSAGetLastError()); - closesocket(msg); - continue; - } - - buf[REQUEST_SIZE - 1] = '\0'; - - memset(http_method, 0, sizeof(http_method)); - if (sscanf(buf, "%s /rpc %s" HTTP_LF, &http_method, &protocol) < 2) - { - spdlog::error("Request is not to the correct endpoint"); - send(msg, RESP_INVALID_ENDPOINT, strlen(RESP_INVALID_ENDPOINT), 0); - closesocket(msg); - continue; - } - - if (strncmp(http_method, METHOD_POST, strlen(METHOD_POST)+1)) - { - spdlog::error("Request is not a " METHOD_POST " request"); - send(msg, RESP_INVALID_METHOD, strlen(RESP_INVALID_METHOD), 0); - closesocket(msg); - continue; - } - - char* body = nullptr; - char* ptr = buf; - while (*ptr) - { - if (!memcmp(ptr, BODY_SEP, 4)) - { - body = ptr + 4; - break; - } - ++ptr; - } - - if (!body || !*body) - { - spdlog::error("No request body found"); - send(msg, RESP_INVALID_RPC, strlen(RESP_INVALID_RPC), 0); - closesocket(msg); - continue; - } - - rapidjson::Document json_body; - json_body.Parse(body); - - if (json_body.HasParseError()) - { - spdlog::error("Failed to parse request"); - send(msg, RESP_FAIL_PARSE, strlen(RESP_FAIL_PARSE), 0); - } - else if (!json_body.IsObject() || - !(json_body.HasMember("jsonrpc") && json_body["jsonrpc"] == "2.0") || - !(json_body.HasMember("id") && (json_body["id"].IsInt() || json_body["id"].IsNull())) || - !(json_body.HasMember("method") && json_body["method"].IsString()) || - (json_body.HasMember("params") && !(json_body["params"].IsObject() ))) - { - spdlog::error("Request is not valid JSON-RPC 2.0"); - send(msg, RESP_INVALID_RPC, strlen(RESP_INVALID_RPC), 0); - } - else - { - const char* method = json_body["method"].GetString(); - auto params = json_body["params"].GetObject(); - - spdlog::info("Received request for method \"{}\"", method); - - if (!strcmp(method, "execute_command")) - { - if (!params.HasMember("command")) - { - send(msg, RESP_RPC_MISSING_PARAM, strlen(RESP_RPC_MISSING_PARAM), 0); - closesocket(msg); - continue; - } - - const char* cmd = params["command"].GetString(); - this->parent->RunCommand(cmd); - - send(msg, RESP_JSON, strlen(RESP_JSON), 0); - if (!json_body["id"].IsNull()) - { - rapidjson::Document doc; - rapidjson::MemoryPoolAllocator<>& allocator = doc.GetAllocator(); - doc.SetObject(); - - rapidjson::Value result; - - doc.AddMember("jsonrpc", "2.0", allocator); - doc.AddMember("id", json_body["id"], allocator); - doc.AddMember("result", result, allocator); - - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - - doc.Accept(writer); - - const char* json_str = buffer.GetString(); - - send(msg, json_str, strlen(json_str), 0); - } - } - else if (!strcmp(method, "execute_squirrel")) - { - if (!params.HasMember("code")) - { - send(msg, RESP_RPC_MISSING_PARAM, strlen(RESP_RPC_MISSING_PARAM), 0); - closesocket(msg); - continue; - } - - const char* code = params["code"].GetString(); - ScriptContext context = ScriptContext::UI; - - if (params.HasMember("context")) - { - if (params["context"] == "client") - { - context = ScriptContext::CLIENT; - } - else if (params["context"] == "server") - { - context = ScriptContext::SERVER; - } - else if (params["context"] == "ui") - { - context = ScriptContext::UI; - } - else - { - send(msg, RESP_SQUIRREL_INVALID_CONTEXT, strlen(RESP_SQUIRREL_INVALID_CONTEXT), 0); - closesocket(msg); - continue; - } - } - SQObject obj_ptr; - - this->parent->RunSquirrelCode(context, code, &obj_ptr); - - send(msg, RESP_JSON, strlen(RESP_JSON), 0); - - if (!json_body["id"].IsNull()) - { - rapidjson::Document doc; - rapidjson::MemoryPoolAllocator<>& allocator = doc.GetAllocator(); - doc.SetObject(); - - rapidjson::Value result; - SquirrelToJSON(&result, allocator, &obj_ptr); - - doc.AddMember("jsonrpc", "2.0", allocator); - doc.AddMember("id", json_body["id"], allocator); - doc.AddMember("result", result, allocator); - - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - - doc.Accept(writer); - - const char* json_str = buffer.GetString(); - - send(msg, json_str, strlen(json_str), 0); - } - - if (ISREFCOUNTED(obj_ptr._Type)) - { - // pray - obj_ptr._VAL.asString->uiRef--; - } - } - else - { - send(msg, RESP_RPC_INVALID_METHOD, strlen(RESP_RPC_INVALID_METHOD), 0); - } - - } - - closesocket(msg); - } - -run_error: - this->running = false; - - return 0; -} - -void rpc_server::SquirrelToJSON( - rapidjson::Value* out_val, - rapidjson::MemoryPoolAllocator<>& allocator, - SQObject* obj_ptr -) { - - if (obj_ptr) - { - switch (obj_ptr->_Type) - { - case OT_BOOL: - out_val->SetBool(obj_ptr->_VAL.asInteger); - break; - - case OT_INTEGER: - out_val->SetInt(obj_ptr->_VAL.asInteger); - break; - - case OT_STRING: - out_val->SetString(obj_ptr->_VAL.asString->_val, obj_ptr->_VAL.asString->length); - break; - - case OT_ARRAY: - out_val->SetArray(); - - for (int i = 0; i < obj_ptr->_VAL.asArray->_usedSlots; ++i) - { - rapidjson::Value n; - SquirrelToJSON(&n, allocator, &obj_ptr->_VAL.asArray->_values[i]); - out_val->PushBack(n, allocator); - } - break; - - case OT_TABLE: - out_val->SetObject(); - - for (int i = 0; i < obj_ptr->_VAL.asTable->_numOfNodes; ++i) - { - auto node = &obj_ptr->_VAL.asTable->_nodes[i]; - if (node->key._Type != OT_STRING) - continue; - - rapidjson::Value k; - SquirrelToJSON(&k, allocator, &node->key); - - rapidjson::Value v; - SquirrelToJSON(&v, allocator, &node->val); - out_val->AddMember(k, v, allocator); - } - break; - - default: - break; - } - } -} diff --git a/src/server.h b/src/server.h deleted file mode 100644 index d45ef15..0000000 --- a/src/server.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef SERVER_H -#define SERVER_H - -#include <winsock2.h> -#include <ws2tcpip.h> - -#include "rapidjson/error/en.h" -#include "rapidjson/document.h" -#include "rapidjson/writer.h" -#include "rapidjson/allocators.h" - -#include "ns_plugin.h" -#include "internal/convarproxy.h" - -#define SLEEP_DURATION 5000 -#define DEFAULT_PORT "26503" - -#define METHOD_POST "POST" - -#define REQUEST_SIZE 4096 -#define HTTP_LF "\r\n" -#define BODY_SEP HTTP_LF HTTP_LF -#define RESP(STATUS, TYPE, BODY) "HTTP/1.1 " STATUS HTTP_LF "Content-Type: " TYPE BODY_SEP BODY - -#define RESP_200(TYPE, BODY) RESP("200 OK", TYPE, BODY) -#define RESP_400(TYPE, BODY) RESP("400 Bad Request", TYPE, BODY) -#define RESP_404(TYPE, BODY) RESP("404 Not Found", TYPE, BODY) - -#define RESP_OK RESP_200("text/plain", "") -#define RESP_JSON RESP_200("application/json", "") -#define RESP_FAIL_PARSE RESP_400("text/plain", "Failed to parse request") - -#define RESP_RPC_PARAMS_ARR RESP_400("text/plain", "SouthRPC cannot handle parameters in an array") -#define RESP_RPC_INVALID_METHOD RESP_400("text/plain", "Invalid RPC Method") -#define RESP_RPC_MISSING_PARAM RESP_400("text/plain", "Missing RPC Parameter") - -#define RESP_INVALID_RPC RESP_400("text/plain", "Request is invalid JSON-RPC 2.0") -#define RESP_INVALID_METHOD RESP_400("text/plain", "Invalid HTTP Method") -#define RESP_INVALID_ENDPOINT RESP_404("text/plain", "Invalid Endpoint") - -#define RESP_SQUIRREL_ERROR RESP_400("text/plain", "Failed to execute squirrel code") -#define RESP_SQUIRREL_INVALID_CONTEXT RESP_400("text/plain", "Invalid Squirrel Context") - - -class Plugin; - -class rpc_server { - private: - Plugin* parent; - bool initialized = false; - - WSADATA wsaData; - ConVarProxy* Convar_Port = nullptr; - ConVarProxy* Convar_Connections = nullptr; - - bool running = false; - HANDLE thread = nullptr; - - void SquirrelToJSON( - rapidjson::Value* out_val, - rapidjson::MemoryPoolAllocator<>& allocator, - SQObject* obj_ptr - ); - - public: - rpc_server(Plugin* plugin); - ~rpc_server(); - - void start(); - void stop(); - DWORD run(); -}; - -#endif
\ No newline at end of file |