From 60e88d1a01c78f3072f096402454013415461d2d Mon Sep 17 00:00:00 2001 From: Edgar Date: Wed, 8 Jan 2025 16:44:26 +0100 Subject: [PATCH] :tada: Reimplemented webserver with POCO --- .clang-format | 4 + CMakeLists.txt | 5 +- cmake/PageCompiler.cmake | 20 ++++ conanfile.py | 22 +++- source/server/CMakeLists.txt | 30 ++++++ source/server/config.cpp | 14 +++ source/server/config.h | 8 ++ source/server/macros.h | 28 +++++ source/server/rorserver.cpp | 20 ++++ source/server/webserver.cpp | 176 ++++++++++++++++++++++++++++++++ source/server/webserver.h | 10 ++ source/web/footer.cpsp | 4 + source/web/header.cpsp | 28 +++++ source/web/index.cpsp | 40 ++++++++ source/web/playerlist.cpsp | 37 +++++++ source/web/playerlist_test.cpsp | 61 +++++++++++ 16 files changed, 503 insertions(+), 4 deletions(-) create mode 100644 .clang-format create mode 100644 cmake/PageCompiler.cmake create mode 100644 source/server/macros.h create mode 100644 source/server/webserver.cpp create mode 100644 source/server/webserver.h create mode 100644 source/web/footer.cpsp create mode 100644 source/web/header.cpsp create mode 100644 source/web/index.cpsp create mode 100644 source/web/playerlist.cpsp create mode 100644 source/web/playerlist_test.cpsp diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..897542b9 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +--- +BasedOnStyle: Microsoft + +... diff --git a/CMakeLists.txt b/CMakeLists.txt index 9464c6ee..c132c375 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,13 +13,16 @@ set(CMAKE_PREFIX_PATH ${ROR_DEPENDENCY_DIR} ${CMAKE_PREFIX_PATH}) set(CMAKE_THREAD_PREFER_PTHREAD YES) find_package(Threads REQUIRED) -find_package(Angelscript) find_package(jsoncpp REQUIRED) find_package(SocketW REQUIRED) find_package(CURL) +find_package(Angelscript) cmake_dependent_option(RORSERVER_WITH_ANGELSCRIPT "Adds scripting support" ON "TARGET Angelscript::angelscript" OFF) cmake_dependent_option(RORSERVER_WITH_CURL "Adds CURL request support (needs AngelScript)" ON "TARGET CURL::libcurl" OFF) +find_package(Poco) +cmake_dependent_option(RORSERVER_WITH_WEBSERVER "Adds the webserver" ON "TARGET Poco::Poco" OFF) + # setup paths SET(RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/") SET(LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/") diff --git a/cmake/PageCompiler.cmake b/cmake/PageCompiler.cmake new file mode 100644 index 00000000..776d7077 --- /dev/null +++ b/cmake/PageCompiler.cmake @@ -0,0 +1,20 @@ +find_program(PAGECOMPILER_EXE cpspc HINTS ${PAGECOMPILER_DIR} REQUIRED) + + + +macro(PageCompiler file outfile) + get_filename_component(out ${outfile} DIRECTORY) + +if (WIN32) + set(PC_COMMAND ${PAGECOMPILER_EXE} /output-dir ${out} ${file}) +else () + set(PC_COMMAND ${PAGECOMPILER_EXE} --output-dir ${out} ${file}) +endif () + + add_custom_command( + OUTPUT ${outfile} + COMMENT "Compiling ${file}" + DEPENDS ${file} + COMMAND ${PC_COMMAND} + ) +endmacro() \ No newline at end of file diff --git a/conanfile.py b/conanfile.py index 2865c957..06742901 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,12 +1,16 @@ import os from conan import ConanFile -from conan.tools.files import copy +from conan.tools.cmake import CMakeToolchain, CMakeDeps +from conan.tools.files import copy, save class RoRServer(ConanFile): name = "RoRServer" settings = "os", "compiler", "build_type", "arch" - generators = "CMakeToolchain", "CMakeDeps" + default_options = { + "poco*:enable_pagecompiler": True, + "poco*:enable_data_mysql": False, + } def layout(self): self.folders.generators = os.path.join(self.folders.build, "generators") @@ -14,6 +18,18 @@ def layout(self): def requirements(self): self.requires("angelscript/2.37.0") self.requires("jsoncpp/1.9.5") + self.requires("poco/1.13.3") self.requires("openssl/3.3.2", override=True) self.requires("socketw/3.11.0@anotherfoxguy/stable") - self.requires("libcurl/8.10.1") \ No newline at end of file + self.requires("libcurl/8.10.1") + + def generate(self): + pc_exe = self.dependencies["poco"].cpp_info.bindirs[0].replace("\\", "/") + tc = CMakeToolchain(self) + tc.variables["PAGECOMPILER_DIR"] = pc_exe + tc.generate() + deps = CMakeDeps(self) + deps.generate() + if self.settings.build_type == "Release": + deps.configuration = "RelWithDebInfo" + deps.generate() diff --git a/source/server/CMakeLists.txt b/source/server/CMakeLists.txt index f4e8348d..23e8bf22 100644 --- a/source/server/CMakeLists.txt +++ b/source/server/CMakeLists.txt @@ -1,5 +1,21 @@ FILE(GLOB_RECURSE server_src CONFIGURE_DEPENDS *.cpp *.c *.h *.rc) +if (RORSERVER_WITH_WEBSERVER) + include(PageCompiler) + + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/web) + + PageCompiler("${CMAKE_SOURCE_DIR}/source/web/index.cpsp" "${CMAKE_CURRENT_BINARY_DIR}/web/index.cpp") + PageCompiler("${CMAKE_SOURCE_DIR}/source/web/playerlist.cpsp" "${CMAKE_CURRENT_BINARY_DIR}/web/playerlist.cpp") + PageCompiler("${CMAKE_SOURCE_DIR}/source/web/playerlist_test.cpsp" "${CMAKE_CURRENT_BINARY_DIR}/web/playerlist_test.cpp") + set( + server_src ${server_src} + "${CMAKE_CURRENT_BINARY_DIR}/web/index.cpp" + "${CMAKE_CURRENT_BINARY_DIR}/web/playerlist.cpp" + "${CMAKE_CURRENT_BINARY_DIR}/web/playerlist_test.cpp" + ) +endif (RORSERVER_WITH_WEBSERVER) + # the final lib add_executable(${PROJECT_NAME} ${server_src}) @@ -22,6 +38,12 @@ endif () target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads SocketW::SocketW jsoncpp_lib) +if (RORSERVER_WITH_WEBSERVER) + target_compile_definitions(${PROJECT_NAME} PRIVATE WITH_WEBSERVER) + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + target_link_libraries(${PROJECT_NAME} PRIVATE Poco::Poco) +endif (RORSERVER_WITH_WEBSERVER) + IF (WIN32) target_compile_definitions(${PROJECT_NAME} PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) ELSEIF (UNIX) @@ -42,6 +64,14 @@ ELSEIF (UNIX) LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) + # install the files required for the runtime + INSTALL( + DIRECTORY ${CMAKE_SOURCE_DIR}/bin/resources/ + DESTINATION share/rorserver/ + FILES_MATCHING PATTERN "*" + PATTERN ".svn" EXCLUDE + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ + ) # configure and install init script CONFIGURE_FILE(${CMAKE_SOURCE_DIR}/contrib/rorserver-initscript.in diff --git a/source/server/config.cpp b/source/server/config.cpp index e713aeb9..65d1fa57 100644 --- a/source/server/config.cpp +++ b/source/server/config.cpp @@ -63,6 +63,7 @@ static std::string s_serverlist_host("api.rigsofrods.org"); static std::string s_serverlist_path(""); static std::string s_resourcedir(RESOURCE_DIR); +static unsigned int s_webserver_port(0); static unsigned int s_listen_port(0); static unsigned int s_max_clients(16); static unsigned int s_heartbeat_retry_count(5); @@ -73,6 +74,7 @@ static bool s_print_stats(false); static bool s_foreground(false); static bool s_show_version(false); static bool s_show_help(false); +static bool s_webserver_enabled(false); // Vehicle spawn limits static size_t s_max_vehicles(20); @@ -263,8 +265,10 @@ namespace Config { HANDLE_ARG_VALUE("max-clients", { setMaxClients(atoi(value)); }); HANDLE_ARG_VALUE("vehicle-limit", { setMaxVehicles(atoi(value)); }); HANDLE_ARG_VALUE("port", { setListenPort(atoi(value)); }); + HANDLE_ARG_VALUE("webserver-port", { setWebserverPort(atoi(value)); }); HANDLE_ARG_FLAG ("print-stats", { setPrintStats(true); }); + HANDLE_ARG_FLAG ("use-webserver", { setWebserverEnabled(true); }); HANDLE_ARG_FLAG ("foreground", { setForeground(true); }); HANDLE_ARG_FLAG ("fg", { setForeground(true); }); HANDLE_ARG_FLAG ("inet", { setServerMode(SERVER_INET); }); @@ -310,6 +314,10 @@ namespace Config { ServerType getServerMode() { return s_server_mode; } + bool getWebserverEnabled() { return s_webserver_enabled; } + + unsigned int getWebserverPort() { return s_webserver_port; } + bool getPrintStats() { return s_print_stats; } bool getForeground() { return s_foreground; } @@ -407,6 +415,10 @@ namespace Config { return true; } + void setWebserverPort(unsigned int port) { s_webserver_port = port; } + + void setWebserverEnabled(bool webserver) { s_webserver_enabled = webserver; } + void setPrintStats(bool value) { s_print_stats = value; } void setAuthFile(const std::string &file) { s_authfile = file; } @@ -494,6 +506,8 @@ namespace Config { else if (strcmp(key, "port") == 0) { setListenPort(VAL_INT (value)); } else if (strcmp(key, "mode") == 0) { SetConfServerMode(VAL_STR (value)); } else if (strcmp(key, "printstats") == 0) { setPrintStats(VAL_BOOL(value)); } + else if (strcmp(key, "webserver") == 0) { setWebserverEnabled(VAL_BOOL(value)); } + else if (strcmp(key, "webserverport") == 0) { setWebserverPort(VAL_INT (value)); } else if (strcmp(key, "foreground") == 0) { setForeground(VAL_BOOL(value)); } else if (strcmp(key, "resdir") == 0) { setResourceDir(VAL_STR (value)); } else if (strcmp(key, "logfilename") == 0) { Logger::SetOutputFile(VAL_STR (value)); } diff --git a/source/server/config.h b/source/server/config.h index 0d098de8..da7d5865 100644 --- a/source/server/config.h +++ b/source/server/config.h @@ -65,6 +65,10 @@ namespace Config { bool getEnableScripting(); + bool getWebserverEnabled(); + + unsigned int getWebserverPort(); + bool getForeground(); const std::string &getResourceDir(); @@ -130,6 +134,10 @@ namespace Config { bool setServerMode(ServerType mode); + void setWebserverEnabled(bool value); + + void setWebserverPort(unsigned int port); + void setPrintStats(bool value); void setHeartbeatIntervalSec(unsigned sec); diff --git a/source/server/macros.h b/source/server/macros.h new file mode 100644 index 00000000..3aba151c --- /dev/null +++ b/source/server/macros.h @@ -0,0 +1,28 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" 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, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; 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 Foobar. If not, see . +*/ + +#pragma once + + +#define BEGIN_HTML_ITERATOR(_BEGIN, _END) for (auto it = _BEGIN; it != _END; it++){ +#define END_HTML_ITERATOR } + +#define BEGIN_HTML_LOOP(_max) for (int i = 0; i < _max; i++){ +#define END_HTML_LOOP } \ No newline at end of file diff --git a/source/server/rorserver.cpp b/source/server/rorserver.cpp index 62de763f..7c15c588 100644 --- a/source/server/rorserver.cpp +++ b/source/server/rorserver.cpp @@ -239,6 +239,10 @@ void daemonize() { #endif // ! _WIN32 +#ifdef WITH_WEBSERVER +#include "webserver.h" +#endif + int main(int argc, char *argv[]) { // set default verbose levels Logger::SetLogLevel(LOGTYPE_DISPLAY, LOG_INFO); @@ -331,6 +335,16 @@ int main(int argc, char *argv[]) { } } +#ifdef WITH_WEBSERVER + // start webserver if used + if (Config::getWebserverEnabled()) { + int port = Config::getWebserverPort(); + Logger::Log(LOG_INFO, "starting webserver on port %d ...", port); + StartWebserver(port, &s_sequencer, s_master_server.IsRegistered(), s_master_server.GetTrustLevel()); + Logger::Log(LOG_INFO, "Done"); + } +#endif //WITH_WEBSERVER + // start the main program loop // if we need to communiate to the master user the notifier routine if (server_mode != SERVER_LAN) { @@ -387,6 +401,12 @@ int main(int argc, char *argv[]) { } s_sequencer.Close(); +#ifdef WITH_WEBSERVER + // start webserver if used + if (Config::getWebserverEnabled()) { + StopWebserver(); + } +#endif //WITH_WEBSERVER return 0; } diff --git a/source/server/webserver.cpp b/source/server/webserver.cpp new file mode 100644 index 00000000..5c6980f6 --- /dev/null +++ b/source/server/webserver.cpp @@ -0,0 +1,176 @@ +/* +This file is part of "Rigs of Rods Server" (Relay mode) + +Copyright 2007 Pierre-Michel Ricordel +Copyright 2014+ Rigs of Rods Community + +"Rigs of Rods Server" 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, either version 3 +of the License, or (at your option) any later version. + +"Rigs of Rods Server" is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; 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 "Rigs of Rods Server". +If not, see . +*/ + +#ifdef WITH_WEBSERVER + +#include "webserver.h" + +#include "config.h" +#include "sequencer.h" +#include "userauth.h" + +#include "Poco/JSON/Array.h" +#include "Poco/JSON/Object.h" +#include "Poco/JSON/Stringifier.h" +#include "Poco/Net/HTTPRequestHandler.h" +#include "Poco/Net/HTTPRequestHandlerFactory.h" +#include "Poco/Net/HTTPServer.h" +#include "Poco/Net/HTTPServerRequest.h" +#include "Poco/Net/HTTPServerResponse.h" +#include + +#include "web/index.h" +#include "web/playerlist.h" +#include "web/playerlist_test.h" + +using namespace Poco; +using namespace Poco::Net; +using namespace Poco::JSON; + +static bool s_is_advertised; +static int s_trust_level; +static Sequencer *s_sequencer; +static HTTPServer *srv; + +class APIConfigHandler : public HTTPRequestHandler +{ + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) + { + response.setContentType("text/json"); + + Object root; + root.set("Max Clients", Config::getMaxClients()); + root.set("Server Name", Config::getServerName()); + root.set("Terrain Name", Config::getTerrainName()); + root.set("Password Protected", Config::getPublicPassword().empty() ? "No" : "Yes"); + root.set("IP Address", Config::getIPAddr() == "0.0.0.0" ? "0.0.0.0 (Any)" : Config::getIPAddr()); + root.set("Listening Port", Config::getListenPort()); + root.set("Protocol Version", std::string(RORNET_VERSION)); + + std::stringstream s; + Stringifier::stringify(root, s); + response.send() << s.str(); + } +}; + +class APIPlayerListHandler : public HTTPRequestHandler +{ + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) + { + response.setContentType("text/json"); + + Array root; + + std::vector clients = s_sequencer->GetClientListCopy(); + for (auto it = clients.begin(); it != clients.end(); it++) + { + Object row; + + if (it->GetStatus() == Client::STATUS_FREE) + { + row.set("status", "FREE"); + root.add(row); + } + else if (it->GetStatus() == Client::STATUS_BUSY) + { + row.set("status", "BUSY"); + root.add(row); + } + else if (it->GetStatus() == Client::STATUS_USED) + { + // some auth identifiers + std::string authst = "none"; + if (it->user.authstatus & RoRnet::AUTH_BANNED) + authst = "banned"; + if (it->user.authstatus & RoRnet::AUTH_BOT) + authst = "bot"; + if (it->user.authstatus & RoRnet::AUTH_RANKED) + authst = "ranked"; + if (it->user.authstatus & RoRnet::AUTH_MOD) + authst = "moderator"; + if (it->user.authstatus & RoRnet::AUTH_ADMIN) + authst = "admin"; + + row.set("status", "USED"); + row.set("uid", it->user.uniqueid); + row.set("ip", it->GetIpAddress()); + row.set("name", it->user.username); + row.set("auth", authst); + + root.add(row); + } + } + + std::stringstream s; + Stringifier::stringify(root, s, 1); + response.send() << s.str(); + } +}; + +class NotFoundHandler : public HTTPRequestHandler +{ + void handleRequest(HTTPServerRequest &request, HTTPServerResponse &response) + { + response.setContentType("text/html"); + + response.setStatus(Poco::Net::HTTPResponse::HTTP_NOT_FOUND); + + response.send() << "Not found: " << request.getURI(); + } +}; + +class RequestHandlerFactory : public HTTPRequestHandlerFactory +{ + HTTPRequestHandler *createRequestHandler(const HTTPServerRequest &request) + { + if (request.getURI() == "/") + return new IndexHandler(); + else if (request.getURI() == "/playerlist") + return new PlayerlistHandler(); + else if (request.getURI() == "/playerlist-test") + return new Playerlist_testHandler(s_sequencer->GetClientListCopy()); + else if (request.getURI() == "/api/playerlist") + return new APIPlayerListHandler(); + else if (request.getURI() == "/api/config") + return new APIConfigHandler(); + else + return new NotFoundHandler(); + } +}; + +int StartWebserver(int port, Sequencer *sequencer, bool is_advertised, int trust_level) +{ + s_is_advertised = is_advertised; + s_trust_level = trust_level; + s_sequencer = sequencer; + + srv = new HTTPServer(new RequestHandlerFactory, port); + srv->start(); + + return 0; +} + +int StopWebserver() +{ + srv->stop(); + return 0; +} +#endif \ No newline at end of file diff --git a/source/server/webserver.h b/source/server/webserver.h new file mode 100644 index 00000000..7f898511 --- /dev/null +++ b/source/server/webserver.h @@ -0,0 +1,10 @@ +#pragma once + +#ifdef WITH_WEBSERVER +class Sequencer; // Forward decl... + +int StartWebserver(int port, Sequencer *sequencer, bool is_advertised, int trust_level); + +int StopWebserver(); + +#endif diff --git a/source/web/footer.cpsp b/source/web/footer.cpsp new file mode 100644 index 00000000..55fa644d --- /dev/null +++ b/source/web/footer.cpsp @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/source/web/header.cpsp b/source/web/header.cpsp new file mode 100644 index 00000000..e1ffa002 --- /dev/null +++ b/source/web/header.cpsp @@ -0,0 +1,28 @@ + + + + + Rigs of Rods Server + + + + + + + + + + +
\ No newline at end of file diff --git a/source/web/index.cpsp b/source/web/index.cpsp new file mode 100644 index 00000000..f208aa74 --- /dev/null +++ b/source/web/index.cpsp @@ -0,0 +1,40 @@ +<%@ include file="header.cpsp" %> + +<%!! +#include "../server/config.h" +#include "rornet.h" +%> + + + + + + + + + + + + + +<% +#ifdef WITH_ANGELSCRIPT +%> + + + <% if (Config::getEnableScripting()){ %> + + <% } %> +<% +#else // WITH_ANGELSCRIPT +%> + +<% +#endif // WITH_ANGELSCRIPT +%> + + +
Server settings
Server Name<%= Config::getServerName() %>
Max Clients<%= Config::getMaxClients() %>
Terrain Name<%= Config::getTerrainName() %>
IP Address<%= Config::getIPAddr() == "0.0.0.0" ? "0.0.0.0 (Any)" : Config::getIPAddr() %>
Password Protected<%= Config::getPublicPassword().empty() ? "No" : "Yes" %>
Listening Port<%= Config::getListenPort() %>
Protocol Version<%= RORNET_VERSION %>
Scripting supportYes
Scripting enabled<%= Config::getEnableScripting() ? "Yes" : "No" %>
Script Name in use<%= Config::getScriptName() %>
Scripting supportNo
Webserver Port<%= Config::getWebserverPort() %>
+ + +<%@ include file="footer.cpsp" %> \ No newline at end of file diff --git a/source/web/playerlist.cpsp b/source/web/playerlist.cpsp new file mode 100644 index 00000000..0eab5712 --- /dev/null +++ b/source/web/playerlist.cpsp @@ -0,0 +1,37 @@ +<%@ include file="header.cpsp" %> + + + + + + + + + + + + +
StatusUidNameIP
+ + + +<%@ include file="footer.cpsp" %> \ No newline at end of file diff --git a/source/web/playerlist_test.cpsp b/source/web/playerlist_test.cpsp new file mode 100644 index 00000000..bba789bf --- /dev/null +++ b/source/web/playerlist_test.cpsp @@ -0,0 +1,61 @@ +<%@ include file="header.cpsp" %> + +<%@ page context="std::vector" %> + +<%!! +#include +#include "../server/sequencer.h" +#include "../server/macros.h" +%> + + + + + + + + + + + +<% BEGIN_HTML_ITERATOR(_context.begin(), _context.end()) %> + + + + + + +<% END_HTML_ITERATOR %> + +
StatusUidNameIP
USED<%= it->user.uniqueid %><%= it->user.username %><%= it->GetIpAddress() %>
+ +
+ +
+ + + +<%@ include file="footer.cpsp" %> \ No newline at end of file