aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan200101 <sentrycraft123@gmail.com>2023-10-15 18:20:11 +0200
committerJan200101 <sentrycraft123@gmail.com>2023-10-15 18:20:11 +0200
commit3583254d59ecc7ca5534c7759224a806de1ff5a8 (patch)
treefa093c8f93069f2ed15c4575aa4d97ddd1ae0869
parentfd6eae1b446fa5164a01e61384d01b7bb0565c1a (diff)
downloadSouthRPC-3583254d59ecc7ca5534c7759224a806de1ff5a8.tar.gz
SouthRPC-3583254d59ecc7ca5534c7759224a806de1ff5a8.zip
rewrite HTTP server
-rw-r--r--src/CMakeLists.txt26
-rw-r--r--src/handler.cpp113
-rw-r--r--src/handler.h41
-rw-r--r--src/helper.cpp62
-rw-r--r--src/helper.h14
-rw-r--r--src/http_server.cpp212
-rw-r--r--src/http_server.h52
-rw-r--r--src/init.cpp1
-rw-r--r--src/plugin.cpp105
-rw-r--r--src/plugin.h6
-rw-r--r--src/rpc_server.cpp160
-rw-r--r--src/rpc_server.h46
-rw-r--r--src/server.cpp384
-rw-r--r--src/server.h74
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