diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e86b6b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[Bug] Enter issue title here" +labels: bug +assignees: '' + +--- + +**Fill out general information** +OS (windows, linux, ...): +BeamMP-Server Version: + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Do x ... +2. Do y ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +Please attach the `Server.log` from the run in which the issue appeared, preferably with Debug turned on in the `ServerConfig.toml`. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..40f750b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature Request]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. For example: "I'm always frustrated when ...". + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. Also supply OS information if relevant, for example "*On Linux*, I would like to be able to...". + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/.github/workflows/cmake-linux.yml b/.github/workflows/cmake-linux.yml index 6180c19..098deaa 100644 --- a/.github/workflows/cmake-linux.yml +++ b/.github/workflows/cmake-linux.yml @@ -12,12 +12,15 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: 'true' + submodules: 'recursive' - name: Install Dependencies + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} run: | + echo ${#beammp_sentry_url} sudo apt-get update - sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev + sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev sudo add-apt-repository ppa:mhier/libboost-latest sudo apt-get install -y libboost1.70-dev libboost1.70 @@ -27,7 +30,9 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build-linux - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" - name: Build working-directory: ${{github.workspace}}/build-linux diff --git a/.github/workflows/cmake-windows.yml b/.github/workflows/cmake-windows.yml index 01a41f2..adbeb4b 100644 --- a/.github/workflows/cmake-windows.yml +++ b/.github/workflows/cmake-windows.yml @@ -12,13 +12,13 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: 'true' + submodules: 'recursive' - name: Restore artifacts, or run vcpkg, build and cache artifacts uses: lukka/run-vcpkg@main id: runvcpkg with: - vcpkgArguments: 'lua zlib rapidjson boost-beast boost-asio openssl websocketpp' + vcpkgArguments: 'lua zlib rapidjson boost-beast boost-asio openssl websocketpp curl' vcpkgDirectory: '${{ runner.workspace }}/b/vcpkg' vcpkgGitCommitId: '8dddc6c899ce6fdbeab38b525a31e7f23cb2d5bb' vcpkgTriplet: 'x64-windows-static' @@ -29,7 +29,9 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build-windows - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE='${{ runner.workspace }}/b/vcpkg/scripts/buildsystems/vcpkg.cmake' -DVCPKG_TARGET_TRIPLET=x64-windows-static + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: cmake $GITHUB_WORKSPACE -DSENTRY_BACKEND=breakpad -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE='${{ runner.workspace }}/b/vcpkg/scripts/buildsystems/vcpkg.cmake' -DVCPKG_TARGET_TRIPLET=x64-windows-static -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" - name: Build working-directory: ${{github.workspace}}/build-windows diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 45d562a..eab5ede 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -37,12 +37,12 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: 'true' + submodules: 'recursive' - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev + sudo apt-get install -y libz-dev rapidjson-dev liblua5.3 libssl-dev libwebsocketpp-dev libcurl4-openssl-dev sudo add-apt-repository ppa:mhier/libboost-latest sudo apt-get install -y libboost1.70-dev libboost1.70 @@ -52,14 +52,15 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build-linux - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_CXX_COMPILER=g++-10 -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" - name: Build working-directory: ${{github.workspace}}/build-linux shell: bash run: cmake --build . --config $BUILD_TYPE - - name: Upload Release Asset id: upload-release-asset uses: actions/upload-release-asset@v1 @@ -78,13 +79,13 @@ jobs: steps: - uses: actions/checkout@v2 with: - submodules: 'true' + submodules: 'recursive' - name: Restore artifacts, or run vcpkg, build and cache artifacts uses: lukka/run-vcpkg@main id: runvcpkg with: - vcpkgArguments: 'lua zlib rapidjson boost-beast boost-asio openssl websocketpp' + vcpkgArguments: 'lua zlib rapidjson boost-beast boost-asio openssl websocketpp curl' vcpkgDirectory: '${{ runner.workspace }}/b/vcpkg' vcpkgGitCommitId: '8dddc6c899ce6fdbeab38b525a31e7f23cb2d5bb' vcpkgTriplet: 'x64-windows-static' @@ -95,7 +96,9 @@ jobs: - name: Configure CMake shell: bash working-directory: ${{github.workspace}}/build-windows - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE='${{ runner.workspace }}/b/vcpkg/scripts/buildsystems/vcpkg.cmake' -DVCPKG_TARGET_TRIPLET=x64-windows-static + env: + beammp_sentry_url: ${{ secrets.BEAMMP_SECRET_SENTRY_URL }} + run: cmake $GITHUB_WORKSPACE -DSENTRY_BACKEND=breakpad -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE='${{ runner.workspace }}/b/vcpkg/scripts/buildsystems/vcpkg.cmake' -DVCPKG_TARGET_TRIPLET=x64-windows-static -DBEAMMP_SECRET_SENTRY_URL="$beammp_sentry_url" - name: Build working-directory: ${{github.workspace}}/build-windows diff --git a/.gitignore b/.gitignore index 21661a1..16e5721 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .idea/ +.sentry-native/ +*.orig *.toml boost_* Resources +run-in-env.sh ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## diff --git a/.gitmodules b/.gitmodules index 26aa89c..f2cf422 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,18 +1,19 @@ [submodule "include/commandline"] - path = deps/commandline + path = include/commandline url = https://github.com/lionkor/commandline -[submodule "socket.io-client-cpp"] - path = deps/socket.io-client-cpp - url = https://github.com/socketio/socket.io-client-cpp [submodule "asio"] - path = deps/asio + path = asio url = https://github.com/chriskohlhoff/asio [submodule "rapidjson"] - path = deps/rapidjson + path = rapidjson url = https://github.com/Tencent/rapidjson [submodule "include/toml11"] + path = include/toml11 + url = https://github.com/ToruNiina/toml11 +[submodule "include/sentry-native"] + path = include/sentry-native path = deps/toml11 url = https://github.com/ToruNiina/toml11 [submodule "deps/sol2"] path = deps/sol2 - url = https://github.com/ThePhD/sol2 + url = https://github.com/getsentry/sentry-native \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 14ddc34..0b72c1b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,17 @@ cmake_minimum_required(VERSION 3.0) -project(Server) + + + + + + +message(STATUS "You can find build instructions and a list of dependencies in the README at \ +https://github.com/BeamMP/BeamMP-Server") + +project(BeamMP-Server + DESCRIPTION "Server for BeamMP - The Multiplayer Mod for BeamNG.drive" + HOMEPAGE_URL https://beammp.com + LANGUAGES CXX C) include_directories("${PROJECT_SOURCE_DIR}/deps/asio/asio/include") include_directories("${PROJECT_SOURCE_DIR}/deps/rapidjson/include") @@ -9,9 +21,23 @@ include_directories("${PROJECT_SOURCE_DIR}/deps/sol2/include") include_directories("${PROJECT_SOURCE_DIR}/deps") if (WIN32) + # this has to happen before sentry, so that crashpad on windows links with these settings. message(STATUS "MSVC -> forcing use of statically-linked runtime.") STRING(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_RELEASE ${CMAKE_CXX_FLAGS_RELEASE}) STRING(REPLACE "/MDd" "/MTd" CMAKE_CXX_FLAGS_DEBUG ${CMAKE_CXX_FLAGS_DEBUG}) +endif() + +include_directories("include/sentry-native/include") +set(SENTRY_BUILD_SHARED_LIBS OFF) +if (MSVC) + set(SENTRY_BUILD_RUNTIMESTATIC ON) +endif() +set(SENTRY_BACKEND breakpad) +add_subdirectory("include/sentry-native") + +message(STATUS "Setting compiler flags") +if (WIN32) + #-DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-static set(VcpkgRoot ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}) include_directories(${VcpkgRoot}/include) @@ -22,17 +48,33 @@ elseif (UNIX) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 -s -fno-builtin") if (SANITIZE) message(STATUS "sanitize is ON") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined,thread") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLanAGS} -fsanitize=undefined,thread") endif (SANITIZE) endif () +message(STATUS "Checking for Sentry URL") +# this is set by the build system. +# IMPORTANT: if you're building from source, just leave this empty +if (NOT DEFINED BEAMMP_SECRET_SENTRY_URL) + message(WARNING "No sentry URL configured. Sentry logging is disabled for this build. \ + This is not an error, and if you're building the BeamMP-Server yourself, this is expected and can be ignored.") + set(BEAMMP_SECRET_SENTRY_URL "") +else() + string(LENGTH ${BEAMMP_SECRET_SENTRY_URL} URL_LEN) + message(STATUS "Sentry URL is length ${URL_LEN}") +endif() + +message(STATUS "Adding local source dependencies") # this has to happen before -DDEBUG since it wont compile properly with -DDEBUG add_subdirectory(deps) + + set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") +message(STATUS "Looking for Boost") find_package(Boost REQUIRED COMPONENTS system thread) add_executable(BeamMP-Server @@ -49,24 +91,57 @@ add_executable(BeamMP-Server include/TResourceManager.h src/TResourceManager.cpp include/THeartbeatThread.h src/THeartbeatThread.cpp include/Http.h src/Http.cpp + include/TSentry.h src/TSentry.cpp include/TPPSMonitor.h src/TPPSMonitor.cpp include/TNetwork.h src/TNetwork.cpp include/LuaAPI.h src/LuaAPI.cpp) +target_compile_definitions(BeamMP-Server PRIVATE SECRET_SENTRY_URL="${BEAMMP_SECRET_SENTRY_URL}") include_directories(BeamMP-Server PUBLIC ${Boost_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(BeamMP-Server PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/commandline") + +message(STATUS "Looking for Lua") find_package(Lua REQUIRED) -include_directories(${LUA_INCLUDE_DIR}) +target_include_directories(BeamMP-Server PUBLIC + ${Boost_INCLUDE_DIRS} + ${LUA_INCLUDE_DIR} + ${CURL_INCLUDE_DIRS} + "include/tomlplusplus" + "include/sentry-native/include" + "include/curl/include") + +message(STATUS "Looking for SSL") + find_package(OpenSSL REQUIRED) target_link_libraries(BeamMP-Server sol2::sol2 ${LUA_LIBRARIES}) +message(STATUS "CURL IS ${CURL_LIBRARIES}") if (UNIX) - target_link_libraries(BeamMP-Server z pthread stdc++fs crypto ${OPENSSL_LIBRARIES} commandline sioclient_tls) + target_link_libraries(BeamMP-Server + z + pthread + stdc++fs + ${LUA_LIBRARIES} + crypto + ${OPENSSL_LIBRARIES} + commandline + sentry) elseif (WIN32) include(FindLua) + message(STATUS "Looking for libz") find_package(ZLIB REQUIRED) + message(STATUS "Looking for RapidJSON") find_package(RapidJSON CONFIG REQUIRED) target_include_directories(BeamMP-Server PRIVATE ${RAPIDJSON_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS}) - target_link_libraries(BeamMP-Server ws2_32 ZLIB::ZLIB ${OPENSSL_LIBRARIES} commandline sioclient_tls) + target_link_libraries(BeamMP-Server PRIVATE + ws2_32 + ZLIB::ZLIB + ${LUA_LIBRARIES} + ${OPENSSL_LIBRARIES} + commandline + sentry) endif () diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..60e0daa --- /dev/null +++ b/Changelog.md @@ -0,0 +1,65 @@ +# v2.3.0 + +- ADDED version check - the server will now let you know when a new release is out +- ADDED logging of various errors, crashes and exceptions to the backend +- ADDED chat messages are now logged to the server console as [CHAT] +- ADDED debug message telling you when the server heartbeats to the backend +- REMOVED various [DEBUG] messages which were confusing (such as "breaking client loop") +- FIXED various crashes and issues with handling unexpected backend responses +- FIXED minor bugs due to code correctness + +# v2.2.0 + +- FIXED major security flaw +- FIXED minor bugs + +# v2.1.4 + +- ADDED debug heartbeat print +- ADDED kicking every player before shutdown +- FIXED rare bug which led to violent crash +- FIXED minor bugs + +# v2.1.3 + +- FIXED Lua events not cancelling properly on Linux + +# v2.1.2 + +- CHANGED default map to gridmap v2 +- FIXED version number display + +# v2.1.1 +# v2.1.0 (pre-v2.1.1) +# v2.0.4 (pre-v2.1.0) + +- REMOVED boost as a runtime dependency +- FIXED Lua plugins on Linux +- FIXED console history on Windows +- CHANGED to new config format TOML + +# v2.0.3 + +- WORKAROUND for timeout bug / ghost player bug +- FIXED 100% CPU spin when stdin is /dev/null. + +# v2.0.2 + +- ADDED fully new commandline +- ADDED new backend +- ADDED automated build system +- ADDED lua GetPlayerIdentifiers +- ADDED lots of debug info +- ADDED better POSTing and GETing +- ADDED a license +- FIXED ghost players in player list issue +- FIXED ghost vehicle after joining issue +- FIXED missing vehicle after joining issue +- FIXED a lot of desync issues +- FIXED some memory leaks +- FIXED various crashes +- FIXED various data-races +- FIXED some linux-specific crashes +- FIXED some linux-specific issues +- FIXED bug which caused kicking to be logged as leaving +- FIXED various internal developer quality-of-life things diff --git a/README.md b/README.md index cf90c3e..a138fa8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The server is the point throug which all clients communicate. You can write lua These values are guesstimated and are subject to change with each release. * RAM: 50+ MiB usable (not counting OS overhead) -* CPU: Any Hz, preferably multicore +* CPU: >1GHz, preferably multicore * OS: Windows, Linux (theoretically any POSIX) * GPU: None * HDD: 10 MiB + Mods/Plugins @@ -61,6 +61,7 @@ These package names are in the debian / ubuntu style. Feel free to PR your own g - `make` - `cmake` - `g++` +- `libcurl4-openssl-dev` or similar (search for `libcurl` and look for one with `-dev`) Must support ISO C++17. If your distro's `g++` doesn't support C++17, chances are that it has a `g++-8` or `g++-10` package that does. If this is the case. you just need to run CMake with `-DCMAKE_CXX_COMPILER=g++-10` (replace `g++-10` with your compiler's name). - `liblua5.3-dev` @@ -72,6 +73,13 @@ These package names are in the debian / ubuntu style. Feel free to PR your own g - `libopenssl-dev` or `libssl-dev` **If** you're building it from source, you'll need `libboost1.70-all-dev` or `libboost1.71-all-dev` or higher as well. +If you can't find this version of boost (only 1.6x, for example), you can either update to a newer version of your distro, build boost yourself, or use an unstable rolling release (like Debian `sid` aka `unstable`). + +In the end you should end up with a command something like this: + +```sh +sudo apt install git make cmake g++-10 liblua5.3 libz-dev rapidjson-dev libopenssl-dev libboost1.71-all-dev +``` In the end you should end up with a command something like this: @@ -81,14 +89,15 @@ sudo apt install git make cmake g++-10 liblua5.3 libz-dev rapidjson-dev libopens ### How to build -On windows. use git-bash for these commands. On linux, these should work in your shell. +On windows, use git-bash for these commands. On Linux, these should work in your shell. 1. Make sure you have all [prerequisites](#prerequisites) installed -2. Clone the repository in a location of your choice with `git clone --recursive https://github.com/BeamMP/BeamMP-Server`. Now change into the cloned directory by running `cd BeamMP-Server`. -3. Checkout the branch of the release you want to compile (`master` is often unstable), for example `git checkout tags/v1.20` for version 1.20. You can find the latest version [here](https://github.com/BeamMP/BeamMP-Server/tags). -4. Run `cmake .` (with `.`) -5. Run `make` -6. You will now have a `BeamMP-Server` file in your directory, which is executable with `./BeamMP-Server` (`.\BeamMP-Server.exe` for windows). Follow the (windows or linux, doesnt matter) instructions on the [wiki](https://wiki.beammp.com/en/home/Server_Mod) for further setup after installation (which we just did), such as port-forwarding and getting a key to actually run the server. +2. Clone the repository in a location of your choice with `git clone --recurse-submodules https://github.com/BeamMP/BeamMP-Server`. +3. Ensure that all submodules are initialized by running `git submodule update --init --recursive`. Then change into the cloned directory by running `cd BeamMP-Server`. +4. Checkout the branch of the release you want to compile (`master` is often unstable), for example `git checkout tags/v1.20` for version 1.20. You can find the latest version [here](https://github.com/BeamMP/BeamMP-Server/tags). +5. Run `cmake .` (with `.`) +6. Run `make` +7. You will now have a `BeamMP-Server` file in your directory, which is executable with `./BeamMP-Server` (`.\BeamMP-Server.exe` for windows). Follow the (windows or linux, doesnt matter) instructions on the [wiki](https://wiki.beammp.com/en/home/Server_Mod) for further setup after installation (which we just did), such as port-forwarding and getting a key to actually run the server. *tip: to run the server in the background, simply (in bash, zsh, etc) run:* `nohup ./BeamMP-Server &`*.* diff --git a/deps/asio b/deps/asio deleted file mode 160000 index 230c0d2..0000000 --- a/deps/asio +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 230c0d2ae035c5ce1292233fcab03cea0d341264 diff --git a/deps/commandline b/deps/commandline deleted file mode 160000 index c34703d..0000000 --- a/deps/commandline +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c34703df11ca00b7c4b04478bc566bd9744ac858 diff --git a/deps/rapidjson b/deps/rapidjson deleted file mode 160000 index 13dfc96..0000000 --- a/deps/rapidjson +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 13dfc96c9c2b104be7b0b09a9f6e06871ed3e81d diff --git a/deps/socket.io-client-cpp b/deps/socket.io-client-cpp deleted file mode 160000 index b196fa7..0000000 --- a/deps/socket.io-client-cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b196fa7537cd3f7bed626ead873a7b71d1293c0d diff --git a/include/Client.h b/include/Client.h index 9ac4296..0d66e50 100644 --- a/include/Client.h +++ b/include/Client.h @@ -13,6 +13,11 @@ class TServer; +#ifdef WIN32 +// for socklen_t +#include +#endif // WIN32 + struct TConnection final { SOCKET Socket; struct sockaddr SockAddr; diff --git a/include/Common.h b/include/Common.h index a7e8711..22420e1 100644 --- a/include/Common.h +++ b/include/Common.h @@ -1,5 +1,9 @@ #pragma once +#include "TSentry.h" +extern TSentry Sentry; + +#include #include #include #include @@ -17,6 +21,7 @@ struct Version { uint8_t minor; uint8_t patch; Version(uint8_t major, uint8_t minor, uint8_t patch); + Version(const std::array& v); std::string AsString(); }; @@ -67,13 +72,18 @@ public: static const Version& ServerVersion() { return mVersion; } static std::string ClientVersionString() { return "2.0"; } static std::string PPS() { return mPPS; } - static void SetPPS(std::string NewPPS) { mPPS = NewPPS; } + static void SetPPS(const std::string& NewPPS) { mPPS = NewPPS; } static inline TSettings Settings {}; static std::string GetBackendUrlForAuth() { return "auth.beammp.com"; } static std::string GetBackendHostname() { return "backend.beammp.com"; } + static std::string GetBackup1Hostname() { return "backup1.beammp.com"; } + static std::string GetBackup2Hostname() { return "backup2.beammp.com"; } static std::string GetBackendUrlForSocketIO() { return "https://backend.beammp.com"; } + static void CheckForUpdates(); + static std::array VersionStrToInts(const std::string& str); + static bool IsOutdated(const Version& Current, const Version& Newest); private: static inline std::string mPPS; @@ -81,11 +91,11 @@ private: static inline std::mutex mShutdownHandlersMutex {}; static inline std::deque mShutdownHandlers {}; - static inline Version mVersion { 2, 3, 0 }; + static inline Version mVersion { 2, 4, 0 }; }; -std::string ThreadName(); -void RegisterThread(const std::string str); +std::string ThreadName(bool DebugModeOverride = false); +void RegisterThread(const std::string& str); #define RegisterThreadAuto() RegisterThread(__func__) #define KB 1024 @@ -124,8 +134,11 @@ void RegisterThread(const std::string str); #define beammp_warn(x) Application::Console().Write(_this_location + std::string("[WARN] ") + (x)) #define beammp_info(x) Application::Console().Write(_this_location + std::string("[INFO] ") + (x)) -#define beammp_error(x) Application::Console().Write(_this_location + std::string("[ERROR] ") + (x)) -#define beammp_lua_error(x) Application::Console().Write(_this_location + std::string("[LUA ERROR] ") + (x)) +#define error(x) \ + do { \ + Application::Console().Write(_this_location + std::string("[ERROR] ") + (x)); \ + Sentry.AddErrorBreadcrumb((x), _file_basename, _line); \ + } while (false) #define luaprint(x) Application::Console().Write(_this_location + std::string("[LUA] ") + (x)) #define beammp_debug(x) \ do { \ @@ -136,6 +149,19 @@ void RegisterThread(const std::string str); // for those times when you just need to ignore something :^) // explicity disables a [[nodiscard]] warning #define beammp_ignore(x) (void)x +// trace() is a debug-build debug() +#if defined(DEBUG) +#define trace(x) \ + do { \ + if (Application::Settings.DebugModeEnabled) { \ + Application::Console().Write(_this_location + std::string("[TRACE] ") + (x)); \ + } \ + } while (false) +#else +#define trace(x) +#endif // defined(DEBUG) + +void LogChatMessage(const std::string& name, int id, const std::string& msg); #define Biggest 30000 std::string Comp(std::string Data); diff --git a/include/Compat.h b/include/Compat.h index c7b5e21..8c1b662 100644 --- a/include/Compat.h +++ b/include/Compat.h @@ -13,9 +13,9 @@ using DWORD = unsigned long; using PDWORD = unsigned long*; using LPDWORD = unsigned long*; char _getch(); -inline void CloseSocketProper(int socket) { - shutdown(socket, SHUT_RDWR); - close(socket); +inline void CloseSocketProper(int TheSocket) { + shutdown(TheSocket, SHUT_RDWR); + close(TheSocket); } #endif // unix @@ -24,10 +24,10 @@ inline void CloseSocketProper(int socket) { #ifdef WIN32 #include #include -#include -inline void CloseSocketProper(SOCKET socket) { - shutdown(socket, SD_BOTH); - closesocket(socket); +inline void CloseSocketProper(SOCKET TheSocket) { + shutdown(TheSocket, 2); // 2 == SD_BOTH + closesocket(TheSocket); + } #endif // WIN32 @@ -35,4 +35,4 @@ inline void CloseSocketProper(SOCKET socket) { #if !defined(WIN32) && !defined(__unix) #error "OS not supported" -#endif +#endif \ No newline at end of file diff --git a/include/CustomAssert.h b/include/CustomAssert.h index 78ff850..96e52e8 100644 --- a/include/CustomAssert.h +++ b/include/CustomAssert.h @@ -40,7 +40,7 @@ static const char* const ANSI_WHITE_BOLD = "\u001b[37;1m"; static const char* const ANSI_BOLD = "\u001b[1m"; static const char* const ANSI_UNDERLINE = "\u001b[4m"; -#if DEBUG +#ifdef DEBUG #include inline void _assert([[maybe_unused]] const char* file, [[maybe_unused]] const char* function, [[maybe_unused]] unsigned line, [[maybe_unused]] const char* condition_string, [[maybe_unused]] bool result) { @@ -60,9 +60,14 @@ inline void _assert([[maybe_unused]] const char* file, [[maybe_unused]] const ch #else // In release build, these macros turn into NOPs. The compiler will optimize these out. #define beammp_assert(x) \ - do { \ + do { \ + bool result = (cond); \ + if (!result) { \ + Sentry.LogAssert(#cond, _file_basename, _line, __func__); \ + } \ } while (false) #define beammp_assert_not_reachable() \ - do { \ + do { \ + Sentry.LogAssert("code is unreachable", _file_basename, _line, __func__); \ } while (false) #endif // DEBUG diff --git a/include/Defer.h b/include/Defer.h new file mode 100644 index 0000000..c037cac --- /dev/null +++ b/include/Defer.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +template +class Defer final { +public: + Defer(FnT fn) + : mFunction([&fn] { (void)fn(); }) { } + ~Defer() { + if (mFunction) { + mFunction(); + } + } + +private: + std::function mFunction; +}; diff --git a/include/Http.h b/include/Http.h index 7efba8f..daf38da 100644 --- a/include/Http.h +++ b/include/Http.h @@ -6,5 +6,8 @@ namespace Http { std::string GET(const std::string& host, int port, const std::string& target, unsigned int* status = nullptr); std::string POST(const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status = nullptr); -static inline const char* ErrorString = "!BeamMPHttpsError!"; +namespace Status { + std::string ToString(int code); +} +const std::string ErrorString = "-1"; } diff --git a/include/SocketIO.h b/include/SocketIO.h deleted file mode 100644 index 47c090e..0000000 --- a/include/SocketIO.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -/* - * We send relevant server events over socket.io to the backend. - * - * We send all events to `backend.beammp.com`, to the room `/key` - * where `key` is the currently active auth-key. - */ - -enum class SocketIOEvent { - ConsoleOut, - CPUUsage, - MemoryUsage, - NetworkUsage, - PlayerList, -}; - -enum class SocketIORoom { - None, - Stats, - Player, - Info, - Console, -}; - -class SocketIO final { -private: - struct Event; - -public: - enum class EventType { - }; - - // Singleton pattern - static SocketIO& Get(); - - void Emit(SocketIOEvent Event, const std::string& Data); - - ~SocketIO(); - - void SetAuthenticated(bool auth) { mAuthenticated = auth; } - -private: - SocketIO() noexcept; - - void ThreadMain(); - - struct Event { - std::string Name; - std::string Data; - }; - - bool mAuthenticated { false }; - sio::client mClient; - std::thread mThread; - std::atomic_bool mCloseThread { false }; - std::mutex mQueueMutex; - std::deque mQueue; - - friend std::unique_ptr std::make_unique(); -}; - diff --git a/include/TSentry.h b/include/TSentry.h new file mode 100644 index 0000000..0c68745 --- /dev/null +++ b/include/TSentry.h @@ -0,0 +1,38 @@ +#ifndef SENTRY_H +#define SENTRY_H + +#include +#include +#include +enum class SentryLevel { + Debug = -1, + Info = 0, + Warning = 1, + Error = 2, + Fatal = 3, +}; + +// singleton, dont make this twice +class TSentry final { +public: + TSentry(); + ~TSentry(); + + void PrintWelcome(); + void SetupUser(); + void Log(SentryLevel level, const std::string& logger, const std::string& text); + void LogError(const std::string& text, const std::string& file, const std::string& line); + void SetContext(const std::string& context_name, const std::unordered_map& map); + void LogException(const std::exception& e, const std::string& file, const std::string& line); + void LogAssert(const std::string& condition_string, const std::string& file, const std::string& line, const std::string& function); + void AddErrorBreadcrumb(const std::string& msg, const std::string& file, const std::string& line); + // cleared when Logged + void SetTransaction(const std::string& id); + [[nodiscard]] std::unique_lock CreateExclusiveContext(); + +private: + bool mValid { true }; + std::mutex mMutex; +}; + +#endif // SENTRY_H diff --git a/include/sentry-native b/include/sentry-native new file mode 160000 index 0000000..5218603 --- /dev/null +++ b/include/sentry-native @@ -0,0 +1 @@ +Subproject commit 521860373df1cc02607546c3474d78b42042b459 diff --git a/include/toml11 b/include/toml11 new file mode 160000 index 0000000..6473810 --- /dev/null +++ b/include/toml11 @@ -0,0 +1 @@ +Subproject commit 647381020ef04b5d41d540ec489eba45e82d90a7 diff --git a/src/Client.cpp b/src/Client.cpp index 36e3255..d5736fe 100644 --- a/src/Client.cpp +++ b/src/Client.cpp @@ -94,7 +94,6 @@ TClient::TClient(TServer& Server) void TClient::UpdatePingTime() { mLastPingTime = std::chrono::high_resolution_clock::now(); - //debug(GetName() + ": " + std::string("ping time updated!: ") + ((SecondsSinceLastPing() == 0) ? "OK" : "ERR")); } int TClient::SecondsSinceLastPing() { auto seconds = std::chrono::duration_cast( diff --git a/src/Common.cpp b/src/Common.cpp index 770da76..26e5f6b 100644 --- a/src/Common.cpp +++ b/src/Common.cpp @@ -2,11 +2,17 @@ #include "TConsole.h" #include +#include #include #include +#include +#include #include #include +#include "CustomAssert.h" +#include "Http.h" + std::unique_ptr Application::mConsole = std::make_unique(); void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) { @@ -17,7 +23,7 @@ void Application::RegisterShutdownHandler(const TShutdownHandler& Handler) { } void Application::GracefullyShutdown() { - beammp_info("please wait while all subsystems are shutting down..."); + info("please wait while all subsystems are shutting down..."); std::unique_lock Lock(mShutdownHandlersMutex); for (auto& Handler : mShutdownHandlers) { Handler(); @@ -28,6 +34,53 @@ std::string Application::ServerVersionString() { return mVersion.AsString(); } +std::array Application::VersionStrToInts(const std::string& str) { + std::array Version; + std::stringstream ss(str); + for (uint8_t& i : Version) { + std::string Part; + std::getline(ss, Part, '.'); + std::from_chars(&*Part.begin(), &*Part.begin() + Part.size(), i); + } + return Version; +} + +// FIXME: This should be used by operator< on Version +bool Application::IsOutdated(const Version& Current, const Version& Newest) { + if (Newest.major > Current.major) { + return true; + } else if (Newest.major == Current.major && Newest.minor > Current.minor) { + return true; + } else if (Newest.major == Current.major && Newest.minor == Current.minor && Newest.patch > Current.patch) { + return true; + } else { + return false; + } +} + +void Application::CheckForUpdates() { + // checks current version against latest version + std::regex VersionRegex { R"(\d+\.\d+\.\d+\n*)" }; + auto Response = Http::GET(GetBackendHostname(), 443, "/v/s"); + bool Matches = std::regex_match(Response, VersionRegex); + if (Matches) { + auto MyVersion = ServerVersion(); + auto RemoteVersion = Version(VersionStrToInts(Response)); + if (IsOutdated(MyVersion, RemoteVersion)) { + std::string RealVersionString = RemoteVersion.AsString(); + warn(std::string(ANSI_YELLOW_BOLD) + "NEW VERSION OUT! There's a new version (v" + RealVersionString + ") of the BeamMP-Server available! For info on how to update your server, visit https://wiki.beammp.com/en/home/server-maintenance#updating-the-server." + std::string(ANSI_RESET)); + } else { + info("Server up-to-date!"); + } + } else { + warn("Unable to fetch version from backend."); + trace("got " + Response); + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("get-response", { { "response", Response } }); + Sentry.LogError("failed to get server version", _file_basename, _line); + } +} + std::string Comp(std::string Data) { std::array C {}; // obsolete @@ -74,10 +127,12 @@ std::string DeComp(std::string Compressed) { // thread name stuff -std::map threadNameMap; +static std::map threadNameMap {}; +static std::mutex ThreadNameMapMutex {}; -std::string ThreadName() { - if (Application::Settings.DebugModeEnabled) { +std::string ThreadName(bool DebugModeOverride) { + auto Lock = std::unique_lock(ThreadNameMapMutex); + if (DebugModeOverride || Application::Settings.DebugModeEnabled) { auto id = std::this_thread::get_id(); if (threadNameMap.find(id) != threadNameMap.end()) { // found @@ -87,7 +142,8 @@ std::string ThreadName() { return ""; } -void RegisterThread(const std::string str) { +void RegisterThread(const std::string& str) { + auto Lock = std::unique_lock(ThreadNameMapMutex); threadNameMap[std::this_thread::get_id()] = str; } @@ -96,12 +152,27 @@ Version::Version(uint8_t major, uint8_t minor, uint8_t patch) , minor(minor) , patch(patch) { } +Version::Version(const std::array& v) + : Version(v[0], v[1], v[2]) { +} + std::string Version::AsString() { std::stringstream ss {}; ss << int(major) << "." << int(minor) << "." << int(patch); return ss.str(); } +void LogChatMessage(const std::string& name, int id, const std::string& msg) { + std::stringstream ss; + ss << "[CHAT] "; + if (id != -1) { + ss << "(" << id << ") <" << name << ">"; + } else { + ss << name << ""; + } + ss << msg; +} + std::string GetPlatformAgnosticErrorString() { #ifdef WIN32 // This will provide us with the error code and an error message, all in one. @@ -128,15 +199,3 @@ std::string GetPlatformAgnosticErrorString() { return std::strerror(errno); #endif } - -void LogChatMessage(const std::string& name, int id, const std::string& msg) { - std::stringstream ss; - ss << "[CHAT] "; - if (id != -1) { - ss << "(" << id << ") <" << name << ">"; - } else { - ss << name << ""; - } - ss << msg; - Application::Console().Write(ss.str()); -} diff --git a/src/Http.cpp b/src/Http.cpp index a97b833..d67f18e 100644 --- a/src/Http.cpp +++ b/src/Http.cpp @@ -1,12 +1,13 @@ #include "Http.h" #include "Common.h" -#undef beammp_error +#undef error #include #include #include #include +#include namespace beast = boost::beast; // from namespace http = beast::http; // from @@ -14,85 +15,7 @@ namespace net = boost::asio; // from namespace ssl = net::ssl; // from using tcp = net::ip::tcp; // from -std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) { - try { - // Check command line arguments. - int version = 11; - - // The io_context is required for all I/O - net::io_context ioc; - - // The SSL context is required, and holds certificates - ssl::context ctx(ssl::context::tlsv12_client); - - // This holds the root certificate used for verification - // we don't do / have this - // load_root_certificates(ctx); - - // Verify the remote server's certificate - ctx.set_verify_mode(ssl::verify_none); - - // These objects perform our I/O - tcp::resolver resolver(ioc); - beast::ssl_stream stream(ioc, ctx); - - // Set SNI Hostname (many hosts need this to handshake successfully) - if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) { - beast::error_code ec { static_cast(::ERR_get_error()), net::error::get_ssl_category() }; - throw beast::system_error { ec }; - } - - // Look up the domain name - auto const results = resolver.resolve(host.c_str(), std::to_string(port)); - - // Make the connection on the IP address we get from a lookup - beast::get_lowest_layer(stream).connect(results); - - // Perform the SSL handshake - stream.handshake(ssl::stream_base::client); - - // Set up an HTTP GET request message - http::request req { http::verb::get, target, version }; - req.set(http::field::host, host); - req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); - - // Send the HTTP request to the remote host - http::write(stream, req); - - // This buffer is used for reading and must be persisted - beast::flat_buffer buffer; - - // Declare a container to hold the response - http::response res; - - // Receive the HTTP response - http::read(stream, buffer, res); - - // Gracefully close the stream - beast::error_code ec; - stream.shutdown(ec); - if (ec == net::error::eof) { - // Rationale: - // http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error - ec = {}; - } - - if (status) { - *status = res.base().result_int(); - } - - if (ec) - throw beast::system_error { ec }; - - // If we get here then the connection is closed gracefully - return std::string(res.body()); - } catch (std::exception const& e) { - Application::Console().Write(__func__ + std::string(": ") + e.what()); - return ErrorString; - } -} - -std::string Http::POST(const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status) { +std::string GenericRequest(http::verb verb, const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status) { try { net::io_context io; @@ -125,17 +48,18 @@ std::string Http::POST(const std::string& host, int port, const std::string& tar //debug("IPv6 connect failed, trying IPv4"); bool ok = try_connect_with_protocol(tcp::v4()); if (!ok) { - //error("failed to resolve or connect in POST " + host + target); + Application::Console().Write("[ERROR] failed to resolve or connect in POST " + host + target); return "-1"; } //} stream.handshake(ssl::stream_base::client); - http::request req { http::verb::post, target, 11 /* http 1.1 */ }; + http::request req { verb, target, 11 /* http 1.1 */ }; req.set(http::field::host, host); if (!body.empty()) { req.set(http::field::content_type, ContentType); // "application/json" // "application/x-www-form-urlencoded" + req.set(http::field::content_length, std::to_string(body.size())); req.body() = body; // info("body is " + body + " (" + req.body() + ")"); @@ -146,6 +70,16 @@ std::string Http::POST(const std::string& host, int port, const std::string& tar req.set(pair.first, pair.second); } + std::unordered_map request_data; + for (const auto& header : req.base()) { + // need to do explicit casts to convert string_view to string + // since string_view may not be null-terminated (and in fact isn't, here) + std::string KeyString(header.name_string()); + std::string ValueString(header.value()); + request_data[KeyString] = ValueString; + } + Sentry.SetContext("https-post-request-data", request_data); + std::stringstream oss; oss << req; @@ -159,6 +93,20 @@ std::string Http::POST(const std::string& host, int port, const std::string& tar http::read(stream, buffer, response); + std::unordered_map response_data; + response_data["reponse-code"] = std::to_string(response.result_int()); + if (status) { + *status = response.result_int(); + } + for (const auto& header : response.base()) { + // need to do explicit casts to convert string_view to string + // since string_view may not be null-terminated (and in fact isn't, here) + std::string KeyString(header.name_string()); + std::string ValueString(header.value()); + response_data[KeyString] = ValueString; + } + Sentry.SetContext("https-post-response-data", response_data); + if (status) { *status = response.base().result_int(); } @@ -179,6 +127,88 @@ std::string Http::POST(const std::string& host, int port, const std::string& tar } catch (const std::exception& e) { Application::Console().Write(__func__ + std::string(": ") + e.what()); - return ErrorString; + return Http::ErrorString; + } +} + +std::string Http::GET(const std::string& host, int port, const std::string& target, unsigned int* status) { + return GenericRequest(http::verb::get, host, port, target, {}, {}, {}, status); +} + +std::string Http::POST(const std::string& host, int port, const std::string& target, const std::unordered_map& fields, const std::string& body, const std::string& ContentType, unsigned int* status) { + return GenericRequest(http::verb::post, host, port, target, fields, body, ContentType, status); +} + +// RFC 2616, RFC 7231 +static std::map Map = { + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, + { 103, "Early Hints" }, + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 226, "IM Used" }, + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "(Unused)" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 425, "Too Early" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 451, "Unavailable For Legal Reasons" }, + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, +}; + +std::string Http::Status::ToString(int code) { + if (Map.find(code) != Map.end()) { + return Map.at(code); + } else { + return "Unassigned"; } } diff --git a/src/SocketIO.cpp b/src/SocketIO.cpp deleted file mode 100644 index 2c10f6f..0000000 --- a/src/SocketIO.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "SocketIO.h" -#include "Common.h" -#include - - -//TODO Default disabled with config option -static std::unique_ptr SocketIOInstance = std::make_unique(); - -SocketIO& SocketIO::Get() { - return *SocketIOInstance; -} - -SocketIO::SocketIO() noexcept - : mThread([this] { ThreadMain(); }) { - - mClient.socket()->on("network", [&](sio::event&e) { - if(e.get_message()->get_string() == "Welcome"){ - info("SocketIO Authenticated!"); - mAuthenticated = true; - } - }); - - mClient.socket()->on("welcome", [&](sio::event&) { - info("Got welcome from backend! Authenticating SocketIO..."); - mClient.socket()->emit("onInitConnection", Application::Settings.Key); - }); - - mClient.set_logs_quiet(); - mClient.set_reconnect_delay(10000); - mClient.connect(Application::GetBackendUrlForSocketIO()); -} - -SocketIO::~SocketIO() { - mCloseThread.store(true); - mThread.join(); -} - - -static constexpr auto EventNameFromEnum(SocketIOEvent Event) { - switch (Event) { - case SocketIOEvent::CPUUsage: - return "cpu usage"; - case SocketIOEvent::MemoryUsage: - return "memory usage"; - case SocketIOEvent::ConsoleOut: - return "console out"; - case SocketIOEvent::NetworkUsage: - return "network usage"; - case SocketIOEvent::PlayerList: - return "player list"; - default: - error("unreachable code reached (developer error)"); - abort(); - } -} - -void SocketIO::Emit(SocketIOEvent Event, const std::string& Data) { - if (!mAuthenticated) { - debug("trying to emit a socket.io event when not yet authenticated"); - return; - } - std::string EventName = EventNameFromEnum(Event); - debug("emitting event \"" + EventName + "\" with data: \"" + Data); - std::unique_lock Lock(mQueueMutex); - mQueue.push_back({EventName, Data }); - debug("queue now has " + std::to_string(mQueue.size()) + " events"); -} - -void SocketIO::ThreadMain() { - while (!mCloseThread.load()) { - bool empty; - { // queue lock scope - std::unique_lock Lock(mQueueMutex); - empty = mQueue.empty(); - } // end queue lock scope - if (empty || !mClient.opened()) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; - } else { - Event TheEvent; - { // queue lock scope - std::unique_lock Lock(mQueueMutex); - TheEvent = mQueue.front(); - mQueue.pop_front(); - } // end queue lock scope - debug("sending \"" + TheEvent.Name + "\" event"); - mClient.socket()->emit(TheEvent.Name, TheEvent.Data); - debug("sent \"" + TheEvent.Name + "\" event"); - } - } - // using std::cout as this happens during static destruction and the logger might be dead already - std::cout << "closing " + std::string(__func__) << std::endl; - - mClient.sync_close(); - mClient.clear_con_listeners(); - - std::cout << "closed" << std::endl; -} diff --git a/src/THeartbeatThread.cpp b/src/THeartbeatThread.cpp index f049e94..e5d2855 100644 --- a/src/THeartbeatThread.cpp +++ b/src/THeartbeatThread.cpp @@ -26,7 +26,7 @@ void THeartbeatThread::operator()() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } - beammp_debug("heartbeat (after " + std::to_string(std::chrono::duration_cast(TimePassed).count()) + "s)"); + debug("heartbeat (after " + std::to_string(std::chrono::duration_cast(TimePassed).count()) + "s)"); Last = Body; LastNormalUpdateTime = Now; @@ -35,26 +35,48 @@ void THeartbeatThread::operator()() { Body += "&pps=" + Application::PPS(); - T = Http::POST(Application::GetBackendHostname(), 443, "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); + auto SentryReportError = [&](const std::string& transaction, int status) { + if (status < 0) { + status = 0; + } - if (T.substr(0, 2) != "20") { - //Backend system refused server startup! + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("heartbeat", + { { "response-body", T }, + { "request-body", Body } }); + Sentry.SetTransaction(transaction); + trace("sending log to sentry: " + std::to_string(status) + " for " + transaction); + Sentry.Log(SentryLevel::Error, "default", Http::Status::ToString(status) + " (" + std::to_string(status) + ")"); + }; + + auto Target = "/heartbeat"; + unsigned int ResponseCode = 0; + T = Http::POST(Application::GetBackendHostname(), 443, Target, {}, Body, "application/x-www-form-urlencoded", &ResponseCode); + + if (T.substr(0, 2) != "20" || ResponseCode != 200) { + trace("got " + T + " from backend"); + SentryReportError(Application::GetBackendHostname() + Target, ResponseCode); std::this_thread::sleep_for(std::chrono::milliseconds(500)); - T = Http::POST(Application::GetBackendHostname(), 443, "/heartbeat", {}, Body, "application/x-www-form-urlencoded"); - // TODO backup2 + HTTP flag (no TSL) - if (T.substr(0, 2) != "20") { - beammp_warn("Backend system refused server! Server might not show in the public list"); - beammp_debug("server returned \"" + T + "\""); - isAuth = false; + T = Http::POST(Application::GetBackup1Hostname(), 443, Target, {}, Body, "application/x-www-form-urlencoded", &ResponseCode); + if (T.substr(0, 2) != "20" || ResponseCode != 200) { + SentryReportError(Application::GetBackup1Hostname() + Target, ResponseCode); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + T = Http::POST(Application::GetBackup2Hostname(), 443, Target, {}, Body, "application/x-www-form-urlencoded", &ResponseCode); + if (T.substr(0, 2) != "20" || ResponseCode != 200) { + warn("Backend system refused server! Server will not show in the public server list."); + + isAuth = false; + SentryReportError(Application::GetBackup2Hostname() + Target, ResponseCode); + } } } if (!isAuth) { if (T == "2000") { - beammp_info(("Authenticated!")); + info(("Authenticated!")); isAuth = true; } else if (T == "200") { - beammp_info(("Resumed authenticated session!")); + info(("Resumed authenticated session!")); isAuth = true; } } @@ -62,6 +84,7 @@ void THeartbeatThread::operator()() { //SocketIO::Get().SetAuthenticated(isAuth); } } + std::string THeartbeatThread::GenerateCall() { std::stringstream Ret; @@ -86,10 +109,10 @@ THeartbeatThread::THeartbeatThread(TResourceManager& ResourceManager, TServer& S , mServer(Server) { Application::RegisterShutdownHandler([&] { if (mThread.joinable()) { - beammp_debug("shutting down Heartbeat"); + debug("shutting down Heartbeat"); mShutdown = true; mThread.join(); - beammp_debug("shut down Heartbeat"); + debug("shut down Heartbeat"); } }); Start(); diff --git a/src/TNetwork.cpp b/src/TNetwork.cpp index 8bc84ed..cf0625b 100644 --- a/src/TNetwork.cpp +++ b/src/TNetwork.cpp @@ -2,18 +2,15 @@ #include "Client.h" #include #include -#include #include #include -#include "LuaAPI.h" - TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& ResourceManager) : mServer(Server) , mPPSMonitor(PPSMonitor) , mResourceManager(ResourceManager) { Application::RegisterShutdownHandler([&] { - beammp_debug("Kicking all players due to shutdown"); + debug("Kicking all players due to shutdown"); Server.ForEachClient([&](std::weak_ptr client) -> bool { if (!client.expired()) { ClientKick(*client.lock(), "Server shutdown"); @@ -23,18 +20,18 @@ TNetwork::TNetwork(TServer& Server, TPPSMonitor& PPSMonitor, TResourceManager& R }); Application::RegisterShutdownHandler([&] { if (mUDPThread.joinable()) { - beammp_debug("shutting down TCPServer"); + debug("shutting down TCPServer"); mShutdown = true; mUDPThread.detach(); - beammp_debug("shut down TCPServer"); + debug("shut down TCPServer"); } }); Application::RegisterShutdownHandler([&] { if (mTCPThread.joinable()) { - beammp_debug("shutting down TCPServer"); + debug("shutting down TCPServer"); mShutdown = true; mTCPThread.detach(); - beammp_debug("shut down TCPServer"); + debug("shut down TCPServer"); } }); mTCPThread = std::thread(&TNetwork::TCPServerMain, this); @@ -46,7 +43,7 @@ void TNetwork::UDPServerMain() { #ifdef WIN32 WSADATA data; if (WSAStartup(514, &data)) { - beammp_error(("Can't start Winsock!")); + error(("Can't start Winsock!")); //return; } #endif // WIN32 @@ -59,13 +56,13 @@ void TNetwork::UDPServerMain() { // Try and bind the socket to the IP and port if (bind(mUDPSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) != 0) { - beammp_error("bind() failed: " + GetPlatformAgnosticErrorString()); + error("bind() failed: " + GetPlatformAgnosticErrorString()); std::this_thread::sleep_for(std::chrono::seconds(5)); exit(-1); //return; } - beammp_info(("Vehicle data network online on port ") + std::to_string(Application::Settings.Port) + (" with a Max of ") + info(("Vehicle data network online on port ") + std::to_string(Application::Settings.Port) + (" with a Max of ") + std::to_string(Application::Settings.MaxPlayers) + (" Clients")); while (!mShutdown) { try { @@ -97,7 +94,7 @@ void TNetwork::UDPServerMain() { return true; }); } catch (const std::exception& e) { - beammp_error(("fatal: ") + std::string(e.what())); + error(("fatal: ") + std::string(e.what())); } } } @@ -107,7 +104,7 @@ void TNetwork::TCPServerMain() { #ifdef WIN32 WSADATA wsaData; if (WSAStartup(514, &wsaData)) { - beammp_error("Can't start Winsock!"); + error("Can't start Winsock!"); return; } #endif // WIN32 @@ -115,54 +112,55 @@ void TNetwork::TCPServerMain() { SOCKET Listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); int optval = 1; #ifdef WIN32 - setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optval), sizeof(optval)); + const char* optval_ptr = reinterpret_cast(&optval); #else - setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optval), sizeof(optval)); + void* optval_ptr = reinterpret_cast(&optval); #endif + setsockopt(Listener, SOL_SOCKET, SO_REUSEADDR, optval_ptr, sizeof(optval)); // TODO: check optval or return value idk sockaddr_in addr {}; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_family = AF_INET; addr.sin_port = htons(uint16_t(Application::Settings.Port)); if (bind(Listener, (sockaddr*)&addr, sizeof(addr)) != 0) { - beammp_error("bind() failed: " + GetPlatformAgnosticErrorString()); + error("bind() failed: " + GetPlatformAgnosticErrorString()); std::this_thread::sleep_for(std::chrono::seconds(5)); exit(-1); } if (Listener == -1) { - beammp_error("Invalid listening socket"); + error("Invalid listening socket"); return; } if (listen(Listener, SOMAXCONN)) { - beammp_error("listen() failed: " + GetPlatformAgnosticErrorString()); + error("listen() failed: " + GetPlatformAgnosticErrorString()); // FIXME leak Listener return; } - beammp_info(("Vehicle event network online")); + info(("Vehicle event network online")); do { try { if (mShutdown) { - beammp_debug("shutdown during TCP wait for accept loop"); + debug("shutdown during TCP wait for accept loop"); break; } client.SockAddrLen = sizeof(client.SockAddr); client.Socket = accept(Listener, &client.SockAddr, &client.SockAddrLen); if (client.Socket == -1) { - beammp_warn(("Got an invalid client socket on connect! Skipping...")); + warn(("Got an invalid client socket on connect! Skipping...")); continue; } std::thread ID(&TNetwork::Identify, this, client); ID.detach(); // TODO: Add to a queue and attempt to join periodically } catch (const std::exception& e) { - beammp_error(("fatal: ") + std::string(e.what())); + error(("fatal: ") + std::string(e.what())); } } while (client.Socket); - beammp_debug("all ok, arrived at " + std::string(__func__) + ":" + std::to_string(__LINE__)); + debug("all ok, arrived at " + std::string(__func__) + ":" + std::to_string(__LINE__)); CloseSocketProper(client.Socket); #ifdef WIN32 - CloseSocketProper(client.Socket); + CloseSocketProper(client); WSACleanup(); #endif // WIN32 } @@ -215,7 +213,7 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { Client->SetIdentifier("ip", str); std::string Rc; - beammp_info("Identifying new ClientConnection..."); + info("Identifying new ClientConnection..."); Rc = TCPRcv(*Client); @@ -240,8 +238,12 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { return; } + auto RequestString = R"({"key":")" + Rc + "\"}"; + + auto Target = "/pkToUser"; + unsigned int ResponseCode = 0; if (!Rc.empty()) { - Rc = Http::POST(Application::GetBackendUrlForAuth(), 443, "/pkToUser", {}, R"({"key":")" + Rc + "\"}", "application/json"); + Rc = Http::POST(Application::GetBackendUrlForAuth(), 443, Target, {}, RequestString, "application/json", &ResponseCode); } json::Document AuthResponse; @@ -252,8 +254,23 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { } if (!AuthResponse.IsObject()) { - ClientKick(*Client, "Backend returned invalid auth response format."); - beammp_error("Backend returned invalid auth response format. This should never happen."); + if (Rc == "0") { + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("auth", + { { "response-body", Rc }, + { "key", RequestString } }); + Sentry.SetTransaction(Application::GetBackendUrlForAuth() + Target); + Sentry.Log(SentryLevel::Info, "default", "backend returned 0 instead of json (" + std::to_string(ResponseCode) + ")"); + } else { // Rc != "0" + ClientKick(*Client, "Backend returned invalid auth response format."); + error("Backend returned invalid auth response format. This should never happen."); + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("auth", + { { "response-body", Rc }, + { "key", RequestString } }); + Sentry.SetTransaction(Application::GetBackendUrlForAuth() + Target); + Sentry.Log(SentryLevel::Error, "default", "unexpected backend response (" + std::to_string(ResponseCode) + ")"); + } return; } @@ -273,8 +290,7 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { return; } - beammp_debug("Name -> " + Client->GetName() + ", Guest -> " + std::to_string(Client->IsGuest()) + ", Roles -> " + Client->GetRoles()); - beammp_debug("There are " + std::to_string(mServer.ClientCount()) + " known clients"); + debug("Name -> " + Client->GetName() + ", Guest -> " + std::to_string(Client->IsGuest()) + ", Roles -> " + Client->GetRoles()); mServer.ForEachClient([&](const std::weak_ptr& ClientPtr) -> bool { std::shared_ptr Cl; { @@ -284,10 +300,7 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { } else return true; } - beammp_info("Client Iteration: Name -> " + Cl->GetName() + ", Guest -> " + std::to_string(Cl->IsGuest()) + ", Roles -> " + Cl->GetRoles()); if (Cl->GetName() == Client->GetName() && Cl->IsGuest() == Client->IsGuest()) { - beammp_info("New ClientConnection matched with current iteration"); - beammp_info("Old ClientConnection (" + Cl->GetName() + ") kicked: Reconnecting"); CloseSocketProper(Cl->GetTCPSock()); Cl->SetStatus(-2); return false; @@ -295,32 +308,19 @@ void TNetwork::Authentication(const TConnection& ClientConnection) { return true; }); - auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerAuth", Client->GetName(), Client->GetRoles(), Client->IsGuest()); - TLuaEngine::WaitForAll(Futures); - bool NotAllowed = std::any_of(Futures.begin(), Futures.end(), - [](const std::shared_ptr& Result) { - return !Result->Error && Result->Result.is() && bool(Result->Result.as()); - }); - std::string Reason; - bool NotAllowedWithReason = std::any_of(Futures.begin(), Futures.end(), - [&Reason](const std::shared_ptr& Result) -> bool { - if (!Result->Error && Result->Result.is()) { - Reason = Result->Result.as(); - return true; - } - return false; - }); - if (NotAllowed) { + auto arg = std::make_unique(TLuaArg { { Client->GetName(), Client->GetRoles(), Client->IsGuest() } }); + std::any Res = TriggerLuaEvent("onPlayerAuth", false, nullptr, std::move(arg), true); + if (Res.type() == typeid(int) && std::any_cast(Res)) { ClientKick(*Client, "you are not allowed on the server!"); return; - } else if (NotAllowedWithReason) { - ClientKick(*Client, Reason); + } else if (Res.type() == typeid(std::string)) { + ClientKick(*Client, std::any_cast(Res)); return; } if (mServer.ClientCount() < size_t(Application::Settings.MaxPlayers)) { - beammp_info("Identification success"); + info("Identification success"); mServer.InsertClient(Client); TCPClient(Client); } else @@ -359,12 +359,12 @@ bool TNetwork::TCPSend(TClient& c, const std::string& Data, bool IsSync) { int32_t Temp = send(c.GetTCPSock(), &Send[Sent], Size - Sent, MSG_NOSIGNAL); #endif //WIN32 if (Temp == 0) { - beammp_debug("send() == 0: " + GetPlatformAgnosticErrorString()); + debug("send() == 0: " + GetPlatformAgnosticErrorString()); if (c.GetStatus() > -1) c.SetStatus(-1); return false; } else if (Temp < 0) { - beammp_debug("send() < 0: " + GetPlatformAgnosticErrorString()); //TODO fix it was spamming yet everyone stayed on the server + debug("send() < 0: " + GetPlatformAgnosticErrorString()); //TODO fix it was spamming yet everyone stayed on the server if (c.GetStatus() > -1) c.SetStatus(-1); CloseSocketProper(c.GetTCPSock()); @@ -378,15 +378,14 @@ bool TNetwork::TCPSend(TClient& c, const std::string& Data, bool IsSync) { bool TNetwork::CheckBytes(TClient& c, int32_t BytesRcv) { if (BytesRcv == 0) { - beammp_debug("(TCP) Connection closing..."); + trace("(TCP) Connection closing..."); if (c.GetStatus() > -1) c.SetStatus(-1); return false; } else if (BytesRcv < 0) { - beammp_debug("(TCP) recv() failed: " + GetPlatformAgnosticErrorString()); + debug("(TCP) recv() failed: " + GetPlatformAgnosticErrorString()); if (c.GetStatus() > -1) c.SetStatus(-1); - beammp_info(("Closing socket in CheckBytes, BytesRcv < 0")); CloseSocketProper(c.GetTCPSock()); return false; } @@ -402,63 +401,40 @@ std::string TNetwork::TCPRcv(TClient& c) { do { Temp = recv(c.GetTCPSock(), &Data[BytesRcv], 4 - BytesRcv, 0); if (!CheckBytes(c, Temp)) { -#ifdef DEBUG - beammp_error(std::string(__func__) + (": failed on CheckBytes in while(BytesRcv < 4)")); -#endif // DEBUG return ""; } BytesRcv += Temp; } while (size_t(BytesRcv) < sizeof(Header)); memcpy(&Header, &Data[0], sizeof(Header)); -#ifdef DEBUG - //debug(std::string(__func__) + (": expecting ") + std::to_string(Header) + (" bytes.")); -#endif // DEBUG if (!CheckBytes(c, BytesRcv)) { -#ifdef DEBUG - beammp_error(std::string(__func__) + (": failed on CheckBytes")); -#endif // DEBUG return ""; } if (Header < 100 * MB) { Data.resize(Header); } else { ClientKick(c, "Header size limit exceeded"); - beammp_warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header of >100MB - assuming malicious intent and disconnecting the client."); + warn("Client " + c.GetName() + " (" + std::to_string(c.GetID()) + ") sent header of >100MB - assuming malicious intent and disconnecting the client."); return ""; } BytesRcv = 0; do { Temp = recv(c.GetTCPSock(), &Data[BytesRcv], Header - BytesRcv, 0); if (!CheckBytes(c, Temp)) { -#ifdef DEBUG - beammp_error(std::string(__func__) + (": failed on CheckBytes in while(BytesRcv < Header)")); -#endif // DEBUG - return ""; } -#ifdef DEBUG - //debug(std::string(__func__) + (": Temp: ") + std::to_string(Temp) + (", BytesRcv: ") + std::to_string(BytesRcv)); -#endif // DEBUG BytesRcv += Temp; } while (BytesRcv < Header); -#ifdef DEBUG - //debug(std::string(__func__) + (": finished recv with Temp: ") + std::to_string(Temp) + (", BytesRcv: ") + std::to_string(BytesRcv)); -#endif // DEBUG std::string Ret(Data.data(), Header); if (Ret.substr(0, 4) == "ABG:") { Ret = DeComp(Ret.substr(4)); } -#ifdef DEBUG - //debug("Parsing from " + c->GetName() + " -> " +std::to_string(Ret.size())); -#endif - return Ret; } void TNetwork::ClientKick(TClient& c, const std::string& R) { - beammp_info("Client kicked: " + R); + info("Client kicked: " + R); if (!TCPSend(c, "E" + R)) { // TODO handle } @@ -474,7 +450,7 @@ void TNetwork::Looper(const std::weak_ptr& c) { while (!c.expired()) { auto Client = c.lock(); if (Client->GetStatus() < 0) { - beammp_debug("client status < 0, breaking client loop"); + debug("client status < 0, breaking client loop"); break; } if (!Client->IsSyncing() && Client->IsSynced() && Client->MissedPacketQueueSize() != 0) { @@ -524,13 +500,13 @@ void TNetwork::TCPClient(const std::weak_ptr& c) { break; auto Client = c.lock(); if (Client->GetStatus() < 0) { - beammp_debug("client status < 0, breaking client loop"); + debug("client status < 0, breaking client loop"); break; } auto res = TCPRcv(*Client); if (res == "") { - beammp_debug("TCPRcv error, break client loop"); + debug("TCPRcv error, break client loop"); break; } TServer::GlobalParser(c, res, mPPSMonitor, *this); @@ -542,7 +518,7 @@ void TNetwork::TCPClient(const std::weak_ptr& c) { auto Client = c.lock(); OnDisconnect(c, Client->GetStatus() == -2); } else { - beammp_warn("client expired in TCPClient, should never happen"); + warn("client expired in TCPClient, should never happen"); } } @@ -562,10 +538,10 @@ void TNetwork::UpdatePlayer(TClient& Client) { } void TNetwork::OnDisconnect(const std::weak_ptr& ClientPtr, bool kicked) { - beammp_assert(!ClientPtr.expired()); + Assert(!ClientPtr.expired()); auto LockedClientPtr = ClientPtr.lock(); TClient& c = *LockedClientPtr; - beammp_info(c.GetName() + (" Connection Terminated")); + info(c.GetName() + (" Connection Terminated")); std::string Packet; TClient::TSetOfVehicleData VehicleData; { // Vehicle Data Lock Scope @@ -582,8 +558,7 @@ void TNetwork::OnDisconnect(const std::weak_ptr& ClientPtr, bool kicked Packet = ("L") + c.GetName() + (" left the server!"); SendToAll(&c, Packet, false, true); Packet.clear(); - auto Futures = LuaAPI::MP::Engine->TriggerEvent("onPlayerDisconnect", c.GetID()); - beammp_ignore(Futures); + TriggerLuaEvent(("onPlayerDisconnect"), false, nullptr, std::make_unique(TLuaArg { { c.GetID() } }), false); if (c.GetTCPSock()) CloseSocketProper(c.GetTCPSock()); if (c.GetDownSock()) @@ -612,18 +587,18 @@ int TNetwork::OpenID() { } void TNetwork::OnConnect(const std::weak_ptr& c) { - beammp_assert(!c.expired()); - beammp_info("Client connected"); + Assert(!c.expired()); + info("Client connected"); auto LockedClient = c.lock(); LockedClient->SetID(OpenID()); - beammp_info("Assigned ID " + std::to_string(LockedClient->GetID()) + " to " + LockedClient->GetName()); - beammp_ignore(LuaAPI::MP::Engine->TriggerEvent("onPlayerConnecting", LockedClient->GetID())); + info("Assigned ID " + std::to_string(LockedClient->GetID()) + " to " + LockedClient->GetName()); + TriggerLuaEvent("onPlayerConnecting", false, nullptr, std::make_unique(TLuaArg { { LockedClient->GetID() } }), false); SyncResources(*LockedClient); if (LockedClient->GetStatus() < 0) return; (void)Respond(*LockedClient, "M" + Application::Settings.MapName, true); //Send the Map on connect - beammp_info(LockedClient->GetName() + " : Connected"); - beammp_ignore(LuaAPI::MP::Engine->TriggerEvent("onPlayerJoining", LockedClient->GetID())); + info(LockedClient->GetName() + " : Connected"); + TriggerLuaEvent("onPlayerJoining", false, nullptr, std::make_unique(TLuaArg { { LockedClient->GetID() } }), false); } void TNetwork::SyncResources(TClient& c) { @@ -642,7 +617,7 @@ void TNetwork::SyncResources(TClient& c) { } #ifndef DEBUG } catch (std::exception& e) { - beammp_error("Exception! : " + std::string(e.what())); + error("Exception! : " + std::string(e.what())); c.SetStatus(-1); } #endif @@ -660,7 +635,7 @@ void TNetwork::Parse(TClient& c, const std::string& Packet) { return; case 'S': if (SubCode == 'R') { - beammp_debug("Sending Mod Info"); + debug("Sending Mod Info"); std::string ToSend = mResourceManager.FileList() + mResourceManager.FileSizes(); if (ToSend.empty()) ToSend = "-"; @@ -675,13 +650,13 @@ void TNetwork::Parse(TClient& c, const std::string& Packet) { } void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { - beammp_info(c.GetName() + " requesting : " + UnsafeName.substr(UnsafeName.find_last_of('/'))); + info(c.GetName() + " requesting : " + UnsafeName.substr(UnsafeName.find_last_of('/'))); if (!fs::path(UnsafeName).has_filename()) { if (!TCPSend(c, "CO")) { // TODO: handle } - beammp_warn("File " + UnsafeName + " is not a file!"); + warn("File " + UnsafeName + " is not a file!"); return; } auto FileName = fs::path(UnsafeName).filename().string(); @@ -691,7 +666,7 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { if (!TCPSend(c, "CO")) { // TODO: handle } - beammp_warn("File " + UnsafeName + " could not be accessed!"); + warn("File " + UnsafeName + " could not be accessed!"); return; } @@ -707,7 +682,7 @@ void TNetwork::SendFile(TClient& c, const std::string& UnsafeName) { } if (c.GetDownSock() < 1) { - beammp_error("Client doesn't have a download socket!"); + error("Client doesn't have a download socket!"); if (c.GetStatus() > -1) c.SetStatus(-1); return; @@ -744,7 +719,7 @@ void TNetwork::SplitLoad(TClient& c, size_t Sent, size_t Size, bool D, const std TCPSock = c.GetDownSock(); else TCPSock = c.GetTCPSock(); - beammp_info("Split load Socket " + std::to_string(TCPSock)); + info("Split load Socket " + std::to_string(TCPSock)); while (c.GetStatus() > -1 && Sent < Size) { size_t Diff = Size - Sent; if (Diff > Split) { @@ -776,7 +751,7 @@ bool TNetwork::TCPSendRaw(TClient& C, SOCKET socket, char* Data, int32_t Size) { do { intmax_t Temp = send(socket, &Data[Sent], int(Size - Sent), 0); if (Temp < 1) { - beammp_info("Socket Closed! " + std::to_string(socket)); + info("Socket Closed! " + std::to_string(socket)); CloseSocketProper(socket); return false; } @@ -822,7 +797,7 @@ bool TNetwork::SyncClient(const std::weak_ptr& c) { // ignore error (void)SendToAll(LockedClient.get(), ("JWelcome ") + LockedClient->GetName() + "!", false, true); - beammp_ignore(LuaAPI::MP::Engine->TriggerEvent("onPlayerJoin", LockedClient->GetID())); + TriggerLuaEvent(("onPlayerJoin"), false, nullptr, std::make_unique(TLuaArg { { LockedClient->GetID() } }), false); LockedClient->SetIsSyncing(true); bool Return = false; bool res = true; @@ -858,13 +833,13 @@ bool TNetwork::SyncClient(const std::weak_ptr& c) { return res; } LockedClient->SetIsSynced(true); - beammp_info(LockedClient->GetName() + (" is now synced!")); + info(LockedClient->GetName() + (" is now synced!")); return true; } void TNetwork::SendToAll(TClient* c, const std::string& Data, bool Self, bool Rel) { if (!Self) - beammp_assert(c); + Assert(c); char C = Data.at(0); bool ret = true; mServer.ForEachClient([&](std::weak_ptr ClientPtr) -> bool { @@ -928,12 +903,12 @@ bool TNetwork::UDPSend(TClient& Client, std::string Data) const { sendOk = sendto(mUDPSock, Data.c_str(), len, 0, (sockaddr*)&Addr, int(AddrSize)); if (sendOk == -1) { - beammp_debug("(UDP) sendto() failed: " + GetPlatformAgnosticErrorString()); + debug("(UDP) sendto() failed: " + GetPlatformAgnosticErrorString()); if (Client.GetStatus() > -1) Client.SetStatus(-1); return false; } else if (sendOk == 0) { - beammp_debug(("(UDP) sendto() returned 0")); + debug(("(UDP) sendto() returned 0")); if (Client.GetStatus() > -1) Client.SetStatus(-1); return false; @@ -951,7 +926,7 @@ std::string TNetwork::UDPRcvFromClient(sockaddr_in& client) const { #endif // WIN32 if (Rcv == -1) { - beammp_error("(UDP) Error receiving from client! recvfrom() failed: " + GetPlatformAgnosticErrorString()); + error("(UDP) Error receiving from client! recvfrom() failed: " + GetPlatformAgnosticErrorString()); return ""; } return std::string(Ret.begin(), Ret.begin() + Rcv); diff --git a/src/TPPSMonitor.cpp b/src/TPPSMonitor.cpp index 11eeff3..9962107 100644 --- a/src/TPPSMonitor.cpp +++ b/src/TPPSMonitor.cpp @@ -44,7 +44,7 @@ void TPPSMonitor::operator()() { V += c->GetCarCount(); } // kick on "no ping" - if (c->SecondsSinceLastPing() > (5 * 60)) { + if (c->SecondsSinceLastPing() > (20 * 60)) { beammp_debug("client " + std::string("(") + std::to_string(c->GetID()) + ")" + c->GetName() + " timing out: " + std::to_string(c->SecondsSinceLastPing()) + ", pps: " + Application::PPS()); TimedOutClients.push_back(c); } @@ -52,7 +52,7 @@ void TPPSMonitor::operator()() { return true; }); for (auto& ClientToKick : TimedOutClients) { - Network().ClientKick(*ClientToKick, "Timeout (no ping for >5 min)"); + Network().ClientKick(*ClientToKick, "Timeout (no ping for way too long)"); } TimedOutClients.clear(); if (C == 0 || mInternalPPS == 0) { diff --git a/src/TSentry.cpp b/src/TSentry.cpp new file mode 100644 index 0000000..3986633 --- /dev/null +++ b/src/TSentry.cpp @@ -0,0 +1,125 @@ +#include "TSentry.h" +#include "Common.h" + +#include +#include + +// compile-time length of a string/array +template +constexpr size_t ConstexprLength(char const (&)[N]) { + return N - 1; +} + +TSentry::TSentry() { + if constexpr (ConstexprLength(SECRET_SENTRY_URL) == 0) { + mValid = false; + } else { + mValid = true; + sentry_options_t* options = sentry_options_new(); + sentry_options_set_dsn(options, SECRET_SENTRY_URL); + auto ReleaseString = "BeamMP-Server@" + Application::ServerVersionString(); + sentry_options_set_symbolize_stacktraces(options, true); + sentry_options_set_release(options, ReleaseString.c_str()); + sentry_options_set_max_breadcrumbs(options, 10); + sentry_init(options); + } +} + +TSentry::~TSentry() { + if (mValid) { + sentry_close(); + } +} + +void TSentry::PrintWelcome() { + if (mValid) { + info("Sentry started"); + } else { + info("Sentry disabled in unofficial build"); + } +} + +void TSentry::SetupUser() { + if (!mValid) { + return; + } + sentry_value_t user = sentry_value_new_object(); + if (Application::Settings.Key.size() == 36) { + sentry_value_set_by_key(user, "id", sentry_value_new_string(Application::Settings.Key.c_str())); + } else { + sentry_value_set_by_key(user, "id", sentry_value_new_string("unauthenticated")); + } + sentry_set_user(user); +} + +void TSentry::Log(SentryLevel level, const std::string& logger, const std::string& text) { + if (!mValid) { + return; + } + SetContext("threads", { { "thread-name", ThreadName(true) } }); + auto Msg = sentry_value_new_message_event(sentry_level_t(level), logger.c_str(), text.c_str()); + sentry_capture_event(Msg); + sentry_remove_transaction(); +} + +void TSentry::LogError(const std::string& text, const std::string& file, const std::string& line) { + if (!mValid) { + return; + } + SetTransaction(file + ":" + line); + Log(SentryLevel::Error, "default", file + ": " + text); +} + +void TSentry::SetContext(const std::string& context_name, const std::unordered_map& map) { + if (!mValid) { + return; + } + auto ctx = sentry_value_new_object(); + for (const auto& pair : map) { + std::string key = pair.first; + if (key == "type") { + // `type` is reserved + key = "_type"; + } + sentry_value_set_by_key(ctx, key.c_str(), sentry_value_new_string(pair.second.c_str())); + } + sentry_set_context(context_name.c_str(), ctx); +} + +void TSentry::LogException(const std::exception& e, const std::string& file, const std::string& line) { + if (!mValid) { + return; + } + SetTransaction(file + ":" + line); + Log(SentryLevel::Fatal, "exceptions", std::string(e.what()) + " @ " + file + ":" + line); +} + +void TSentry::LogAssert(const std::string& condition_string, const std::string& file, const std::string& line, const std::string& function) { + if (!mValid) { + return; + } + SetTransaction(file + ":" + line + ":" + function); + std::stringstream ss; + ss << "\"" << condition_string << "\" failed @ " << file << ":" << line; + Log(SentryLevel::Fatal, "asserts", ss.str()); +} + +void TSentry::AddErrorBreadcrumb(const std::string& msg, const std::string& file, const std::string& line) { + if (!mValid) { + return; + } + auto crumb = sentry_value_new_breadcrumb("default", (msg + " @ " + file + ":" + line).c_str()); + sentry_value_set_by_key(crumb, "level", sentry_value_new_string("error")); + sentry_add_breadcrumb(crumb); +} + +void TSentry::SetTransaction(const std::string& id) { + if (!mValid) { + return; + } + sentry_set_transaction(id.c_str()); +} + +std::unique_lock TSentry::CreateExclusiveContext() { + return std::unique_lock(mMutex); +} diff --git a/src/TServer.cpp b/src/TServer.cpp index 2eea8c0..147aa06 100644 --- a/src/TServer.cpp +++ b/src/TServer.cpp @@ -9,7 +9,7 @@ #include "LuaAPI.h" -#undef GetObject //Fixes Windows +#undef GetObject // Fixes Windows #include "Json.h" @@ -92,9 +92,8 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac } switch (Code) { case 'H': // initial connection -#ifdef DEBUG + trace(std::string("got 'H' packet: '") + Packet + "' (" + std::to_string(Packet.size()) + ")"); beammp_debug(std::string("got 'H' packet: '") + Packet + "' (" + std::to_string(Packet.size()) + ")"); -#endif if (!Network.SyncClient(Client)) { // TODO handle } @@ -116,19 +115,18 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac ParseVehicle(*LockedClient, Packet, Network); return; case 'J': -#ifdef DEBUG + trace(std::string(("got 'J' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); beammp_debug(std::string(("got 'J' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif Network.SendToAll(LockedClient.get(), Packet, false, true); return; case 'C': { -#ifdef DEBUG + trace(std::string(("got 'C' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); beammp_debug(std::string(("got 'C' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif if (Packet.length() < 4 || Packet.find(':', 3) == std::string::npos) break; auto Futures = LuaAPI::MP::Engine->TriggerEvent("onChatMessage", LockedClient->GetID(), LockedClient->GetName(), Packet.substr(Packet.find(':', 3) + 1)); TLuaEngine::WaitForAll(Futures); + LogChatMessage(LockedClient->GetName(), LockedClient->GetID(), Packet.substr(Packet.find(':', 3) + 1)); // FIXME: this needs to be adjusted once lua is merged if (std::any_of(Futures.begin(), Futures.end(), [](const std::shared_ptr& Elem) { return !Elem->Error @@ -141,14 +139,14 @@ void TServer::GlobalParser(const std::weak_ptr& Client, std::string Pac return; } case 'E': -#ifdef DEBUG + trace(std::string(("got 'E' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); beammp_debug(std::string(("got 'E' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif HandleEvent(*LockedClient, Packet); return; case 'N': - beammp_debug("got 'N' packet (" + std::to_string(Packet.size()) + ")"); + trace("got 'N' packet (" + std::to_string(Packet.size()) + ")"); Network.SendToAll(LockedClient.get(), Packet, false, true); + return; default: return; } @@ -208,9 +206,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ std::string Data = Packet.substr(3), pid, vid; switch (Code) { //Spawned Destroyed Switched/Moved NotFound Reset case 's': -#ifdef DEBUG - beammp_debug(std::string(("got 'Os' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif + trace(std::string(("got 'Os' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); if (Data.at(0) == '0') { int CarID = c.GetOpenCarID(); beammp_debug(c.GetName() + (" created a car with ID ") + std::to_string(CarID)); @@ -240,9 +236,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ } return; case 'c': -#ifdef DEBUG - beammp_debug(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif + trace(std::string(("got 'Oc' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); pid = Data.substr(0, Data.find('-')); vid = Data.substr(Data.find('-') + 1, Data.find(':', 1) - Data.find('-') - 1); if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { @@ -276,9 +270,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ } return; case 'd': -#ifdef DEBUG - beammp_debug(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif + trace(std::string(("got 'Od' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); pid = Data.substr(0, Data.find('-')); vid = Data.substr(Data.find('-') + 1); if (pid.find_first_not_of("0123456789") == std::string::npos && vid.find_first_not_of("0123456789") == std::string::npos) { @@ -296,9 +288,7 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ } return; case 'r': -#ifdef DEBUG - beammp_debug(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif + trace(std::string(("got 'Or' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); Pos = int(Data.find('-')); pid = Data.substr(0, Pos++); vid = Data.substr(Pos, Data.find(':') - Pos); @@ -315,15 +305,11 @@ void TServer::ParseVehicle(TClient& c, const std::string& Pckt, TNetwork& Networ } return; case 't': -#ifdef DEBUG - beammp_debug(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); -#endif + trace(std::string(("got 'Ot' packet: '")) + Packet + ("' (") + std::to_string(Packet.size()) + (")")); Network.SendToAll(&c, Packet, false, true); return; default: -#ifdef DEBUG - beammp_warn(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")"))); -#endif // DEBUG + trace(std::string(("possibly not implemented: '") + Packet + ("' (") + std::to_string(Packet.size()) + (")"))); return; } } @@ -338,13 +324,22 @@ void TServer::Apply(TClient& c, int VID, const std::string& pckt) { std::string VD = c.GetCarData(VID); if (VD.empty()) { beammp_error("Tried to apply change to vehicle that does not exist"); + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("vehicle-change", + { { "packet", Packet }, + { "vehicle-id", std::to_string(VID) }, + { "client-car-count", std::to_string(c.GetCarCount()) } }); + Sentry.LogError("attempt to apply change to nonexistent vehicle", _file_basename, _line); return; } std::string Header = VD.substr(0, VD.find('{')); FoundPos = VD.find('{'); if (FoundPos == std::string::npos) { - beammp_error("Malformed packet received, no '{' found"); + auto Lock = Sentry.CreateExclusiveContext(); + Sentry.SetContext("vehicle-change-packet", + { { "packet", VD } }); + Sentry.LogError("malformed packet", _file_basename, _line); return; } VD = VD.substr(FoundPos); diff --git a/src/VehicleData.cpp b/src/VehicleData.cpp index fd3deaa..d13bf68 100644 --- a/src/VehicleData.cpp +++ b/src/VehicleData.cpp @@ -1,18 +1,14 @@ #include "VehicleData.h" -#include #include "Common.h" +#include TVehicleData::TVehicleData(int ID, std::string Data) : mID(ID) , mData(std::move(Data)) { -#ifdef DEBUG - beammp_debug("vehicle " + std::to_string(mID) + " constructed"); -#endif + trace("vehicle " + std::to_string(mID) + " constructed"); } TVehicleData::~TVehicleData() { -#ifdef DEBUG - beammp_debug("vehicle " + std::to_string(mID) + " destroyed"); -#endif + trace("vehicle " + std::to_string(mID) + " destroyed"); } diff --git a/src/main.cpp b/src/main.cpp index fcbead8..e491f6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,7 @@ +#include "TSentry.h" + #include "Common.h" +#include "CustomAssert.h" #include "Http.h" #include "LuaAPI.h" #include "TConfig.h" @@ -8,6 +11,7 @@ #include "TPPSMonitor.h" #include "TResourceManager.h" #include "TServer.h" + #include #ifdef __unix @@ -33,18 +37,21 @@ void UnixSignalHandler(int sig) { } #endif // __unix -int main(int argc, char** argv) { +// this is provided by the build system, leave empty for source builds +// global, yes, this is ugly, no, it cant be done another way +TSentry Sentry {}; + +#include + +int main(int argc, char** argv) try { #ifdef __unix -#if DEBUG - beammp_info("registering handlers for SIGINT, SIGTERM, SIGPIPE"); -#endif // DEBUG + trace("registering handlers for SIGINT, SIGTERM, SIGPIPE"); signal(SIGPIPE, UnixSignalHandler); signal(SIGTERM, UnixSignalHandler); #ifndef DEBUG signal(SIGINT, UnixSignalHandler); #endif // DEBUG #endif // __unix - setlocale(LC_ALL, "C"); bool Shutdown = false; @@ -66,6 +73,12 @@ int main(int argc, char** argv) { } RegisterThread("Main"); + + trace("Running in debug mode on a debug build"); + + Sentry.SetupUser(); + Sentry.PrintWelcome(); + Application::CheckForUpdates(); TResourceManager ResourceManager; TPPSMonitor PPSMonitor(Server); THeartbeatThread Heartbeat(ResourceManager, Server); @@ -78,4 +91,7 @@ int main(int argc, char** argv) { while (!Shutdown) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } +} catch (const std::exception& e) { + error(e.what()); + Sentry.LogException(e, _file_basename, _line); }