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 @@
+
+
+