diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87d8c62..5746966 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,12 @@ name: Build on: push: branches: - - "*" + - "**" + tags: + - '[0-9]+.[0-9]+.[0-9]+' pull_request: branches: - - "*" + - "**" types: [opened, synchronize, reopened] concurrency: @@ -58,7 +60,11 @@ jobs: uses: ammaraskar/msvc-problem-matcher@master - name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries +<<<<<<< HEAD run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:Platform=${{matrix.platform}} build/master-server.sln +======= + run: msbuild /m /v:minimal /p:Configuration=${{matrix.configuration}} /p:Platform=${{matrix.platform}} build/alterware-master.sln +>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7 - name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries uses: actions/upload-artifact@main @@ -162,17 +168,28 @@ jobs: with: name: macos-${{matrix.arch}}-${{matrix.configuration}} path: | +<<<<<<< HEAD build/bin/${{matrix.arch}}/${{matrix.configuration}}/master-server +======= + build/bin/${{matrix.arch}}/${{matrix.configuration}}/alterware-master +>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7 deploy: name: Deploy artifacts needs: [build-win, build-linux, build-macos] runs-on: ubuntu-latest +<<<<<<< HEAD if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Setup main environment if: github.ref == 'refs/heads/master' run: echo "MASTER_SERVER_PATH=${{ secrets.MASTER_SERVER_SSH_PATH }}" >> $GITHUB_ENV +======= + if: github.ref_type == 'tag' + steps: + - name: Setup main environment + run: echo "ALTERWARE_MASTER_SERVER_PATH=${{ secrets.ALTERWARE_MASTER_SERVER_SSH_PATH }}" >> $GITHUB_ENV +>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7 - name: Download Release binaries uses: actions/download-artifact@main @@ -182,6 +199,7 @@ jobs: - name: Install SSH key uses: shimataro/ssh-key-action@v2.7.0 with: +<<<<<<< HEAD key: ${{ secrets.MASTER_SERVER_SSH_PRIVATE_KEY }} known_hosts: 'just-a-placeholder-so-we-dont-get-errors' @@ -193,3 +211,74 @@ jobs: - name: Publish changes run: ssh ${{ secrets.MASTER_SERVER_SSH_USER }}@${{ secrets.MASTER_SERVER_SSH_ADDRESS }} ${{ secrets.SSH_SERVER_PUBLISH_COMMAND }} +======= + key: ${{ secrets.ALTERWARE_MASTER_SERVER_SSH_PRIVATE_KEY }} + known_hosts: 'just-a-placeholder-so-we-dont-get-errors' + + - name: Add known hosts + run: ssh-keyscan -H ${{ secrets.ALTERWARE_MASTER_SERVER_SSH_ADDRESS }} >> ~/.ssh/known_hosts + + - name: Upload release binary + run: rsync -avz alterware-master ${{ secrets.ALTERWARE_MASTER_SERVER_SSH_USER }}@${{ secrets.ALTERWARE_MASTER_SERVER_SSH_ADDRESS }}:${{ env.ALTERWARE_MASTER_SERVER_PATH }}/ + + - name: Publish changes + run: ssh ${{ secrets.ALTERWARE_MASTER_SERVER_SSH_USER }}@${{ secrets.ALTERWARE_MASTER_SERVER_SSH_ADDRESS }} ${{ secrets.ALTERWARE_SSH_SERVER_PUBLISH_COMMAND }} + + docker: + name: Create Docker Image + needs: [build-win, build-linux, build-macos] + runs-on: ubuntu-latest + if: github.ref_type == 'tag' + steps: + - name: Check out files + uses: actions/checkout@main + with: + sparse-checkout: | + Dockerfile + README.md + sparse-checkout-cone-mode: false + + - name: Download Release binaries + uses: actions/download-artifact@main + + - name: Compress Binaries + run: | + for dir in */; do + if [[ $dir == *"windows"* ]]; then + cd "$dir" && zip -r "../${dir%/}.zip" . && cd .. + else + tar -czvf "${dir%/}.tar.gz" -C "$dir" . + fi + done + shell: bash + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3.2.0 + + - name: Login to DockerHub + uses: docker/login-action@v3.1.0 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - id: meta + uses: docker/metadata-action@v5.5.1 + with: + images: | + alterware/master-server + tags: | + ${{ github.ref_name }} + latest + + - name: Build and Push Docker Image + id: build-and-push + uses: docker/build-push-action@v5.1.0 + with: + context: . + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max +>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7 diff --git a/.gitmodules b/.gitmodules index 80688d5..1bd5840 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,4 +19,8 @@ [submodule "deps/curl"] path = deps/curl url = https://github.com/curl/curl.git - branch = curl-8_7_1 \ No newline at end of file +<<<<<<< HEAD + branch = curl-8_7_1 +======= + branch = curl-8_7_1 +>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..571d90b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:latest + +RUN apt-get update +RUN apt-get install -y libc++-dev libcurl4-gnutls-dev + +COPY --chmod=755 ./linux-x64-release/alterware-master /usr/local/bin/ + +RUN groupadd alterware-master && useradd -r -g alterware-master alterware-master +USER alterware-master + +EXPOSE 20810/udp + +ENTRYPOINT ["/usr/local/bin/alterware-master"] diff --git a/README.md b/README.md index ff786b3..7ed0810 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,13 @@ # Master Server This is the master server our clients use. It is based on the DP Master Server (ID Tech) protocol +## Usage +Run using [Docker][docker-link] + +```bash +docker run -p 20810:20810/udp alterware/master-server:latest +``` + ## Build - Install [Premake5][premake5-link] and add it to your system PATH - Clone this repository using [Git][git-link] @@ -18,6 +25,7 @@ Requirements for Unix systems: - Customization: Modifications to the Premake5.lua script may be required - Platform support: Details regarding supported platforms are available in [build.yml][build-link] +[docker-link]: https://www.docker.com [premake5-link]: https://premake.github.io [git-link]: https://git-scm.com [mold-link]: https://github.com/rui314/mold diff --git a/src/crypto_key.cpp b/src/crypto_key.cpp index d2fe2b1..ed6bff9 100644 --- a/src/crypto_key.cpp +++ b/src/crypto_key.cpp @@ -36,7 +36,7 @@ namespace crypto_key if (!utils::io::write_file("./private.key", key.serialize())) { - throw std::runtime_error("Failed to write server key!"); + console::error("Failed to write server key!"); } console::info("Generated cryptographic key: %llX", key.get_hash()); diff --git a/src/services/elimination_handler.cpp b/src/services/elimination_handler.cpp index 9bc35bb..4e70be7 100644 --- a/src/services/elimination_handler.cpp +++ b/src/services/elimination_handler.cpp @@ -30,6 +30,7 @@ void elimination_handler::run_frame() (server.state == game_server::state::can_ping && diff > 15min)) { context.remove(); + return; } if (server.game == game_type::unknown) @@ -43,6 +44,7 @@ void elimination_handler::run_frame() console::log("Removing T7 server '%s' because they are using an outdated protocol (%i)", context.get_address().to_string().data(), server.protocol); #endif context.remove(); + return; } ++server_count[server.game][context.get_address().to_string(false)]; @@ -50,6 +52,18 @@ void elimination_handler::run_frame() { console::log("Removing server '%s' because it exceeds MAX_SERVERS_PER_GAME", context.get_address().to_string().data()); context.remove(); + return; + } + + const auto name = utils::string::to_lower(server.name); + for (const auto& entry : bad_names) + { + if (const auto pos = name.find(entry); pos != std::string::npos) + { + console::log("Removing server '%s' (%s) because it contains a bad name", server.name.data(), context.get_address().to_string().data()); + context.remove(); + return; + } } const auto name = utils::string::to_lower(server.name); diff --git a/src/services/kill_list.cpp b/src/services/kill_list.cpp index a349d95..3fcd360 100644 --- a/src/services/kill_list.cpp +++ b/src/services/kill_list.cpp @@ -1,153 +1,159 @@ -#include "std_include.hpp" -#include "kill_list.hpp" - -#include - -constexpr auto* kill_file = "./kill.txt"; - -kill_list::kill_list_entry::kill_list_entry(std::string ip_address, std::string reason) - : ip_address_(std::move(ip_address)), reason_(std::move(reason)) -{ -} - -bool kill_list::contains(const network::address& address, std::string& reason) -{ - auto str_address = address.to_string(false); - - return this->entries_container_.access([&str_address, &reason](const kill_list_entries& entries) - { - if (const auto itr = entries.find(str_address); itr != entries.end()) - { - reason = itr->second.reason_; - return true; - } - - return false; - }); -} - -void kill_list::add_to_kill_list(const kill_list_entry& add) -{ - const auto any_change = this->entries_container_.access([&add](kill_list_entries& entries) - { - const auto existing_entry = entries.find(add.ip_address_); - if (existing_entry == entries.end() || existing_entry->second.reason_ != add.reason_) - { - console::info("Added %s to kill list (reason: %s)", add.ip_address_.data(), add.reason_.data()); - entries[add.ip_address_] = add; - return true; - } - - return false; - }); - - if (!any_change) - { - console::info("%s already in kill list, doing nothing", add.ip_address_.data()); - return; - } - - this->write_to_disk(); -} - -void kill_list::remove_from_kill_list(const network::address& remove) -{ - this->remove_from_kill_list(remove.to_string()); -} - -void kill_list::remove_from_kill_list(const std::string& remove) -{ - const auto any_change = this->entries_container_.access([&remove](kill_list_entries& entries) - { - if (entries.erase(remove)) - { - console::info("Removed %s from kill list", remove.data()); - return true; - } - - return false; - }); - - if (!any_change) - { - console::info("%s not in kill list, doing nothing", remove.data()); - return; - } - - this->write_to_disk(); -} - -void kill_list::reload_from_disk() -{ - std::string contents; - if (!utils::io::read_file(kill_file, &contents)) - { - console::info("Could not find %s, kill list will not be loaded.", kill_file); - return; - } - - std::istringstream string_stream(contents); - std::string line; - - this->entries_container_.access([&string_stream, &line](kill_list_entries& entries) - { - entries.clear(); - while (std::getline(string_stream, line)) - { - if (line[0] == '#') - { - // comments or ignored line - continue; - } - - std::string ip; - std::string comment; - - const auto index = line.find(' '); - if (index != std::string::npos) - { - ip = line.substr(0, index); - comment = line.substr(index + 1); - } - else - { - ip = line; - } - - if (ip.empty()) - { - continue; - } - - // Double line breaks from windows' \r\n - if (ip[ip.size() - 1] == '\r') - { - ip.pop_back(); - } - - entries.emplace(ip, kill_list_entry(ip, comment)); - } - - console::info("Loaded %zu kill list entries from %s", entries.size(), kill_file); - }); -} - -void kill_list::write_to_disk() -{ - std::ostringstream stream; - this->entries_container_.access([&stream](const kill_list_entries& entries) - { - for (const auto& [ip, entry] : entries) - { - stream << entry.ip_address_ << " " << entry.reason_ << "\n"; - } - - utils::io::write_file(kill_file, stream.str(), false); - console::info("Wrote %s to disk (%zu entries)", kill_file, entries.size()); - }); -} - -kill_list::kill_list(server& server) : service(server) -{ - this->reload_from_disk(); -} +#include "std_include.hpp" +#include "kill_list.hpp" + +#include + +constexpr auto* kill_file = "./kill.txt"; + +kill_list::kill_list_entry::kill_list_entry(std::string ip_address, std::string reason) + : ip_address_(std::move(ip_address)), reason_(std::move(reason)) +{ +} + +bool kill_list::contains(const network::address& address, std::string& reason) +{ + auto str_address = address.to_string(false); + + return this->entries_container_.access([&str_address, &reason](const kill_list_entries& entries) + { + if (const auto itr = entries.find(str_address); itr != entries.end()) + { + reason = itr->second.reason_; + return true; + } + + return false; + }); +} + +void kill_list::add_to_kill_list(const kill_list_entry& add) +{ + const auto any_change = this->entries_container_.access([&add](kill_list_entries& entries) + { + const auto existing_entry = entries.find(add.ip_address_); + if (existing_entry == entries.end() || existing_entry->second.reason_ != add.reason_) + { + console::info("Added %s to kill list (reason: %s)", add.ip_address_.data(), add.reason_.data()); + entries[add.ip_address_] = add; + return true; + } + + return false; + }); + + if (!any_change) + { + console::info("%s already in kill list, doing nothing", add.ip_address_.data()); + return; + } + + this->write_to_disk(); +} + +void kill_list::remove_from_kill_list(const network::address& remove) +{ + this->remove_from_kill_list(remove.to_string()); +} + +void kill_list::remove_from_kill_list(const std::string& remove) +{ + const auto any_change = this->entries_container_.access([&remove](kill_list_entries& entries) + { + if (entries.erase(remove)) + { + console::info("Removed %s from kill list", remove.data()); + return true; + } + + return false; + }); + + if (!any_change) + { + console::info("%s not in kill list, doing nothing", remove.data()); + return; + } + + this->write_to_disk(); +} + +void kill_list::reload_from_disk() +{ + std::string contents; + if (!utils::io::read_file(kill_file, &contents)) + { + console::info("Could not find %s, kill list will not be loaded.", kill_file); + return; + } + + std::istringstream string_stream(contents); + std::string line; + + this->entries_container_.access([&string_stream, &line](kill_list_entries& entries) + { + entries.clear(); + while (std::getline(string_stream, line)) + { + if (line[0] == '#') + { + // comments or ignored line + continue; + } + + std::string ip; + std::string comment; + + const auto index = line.find(' '); + if (index != std::string::npos) + { + ip = line.substr(0, index); + comment = line.substr(index + 1); + } + else + { + ip = line; + } + + if (ip.empty()) + { + continue; + } + + // Double line breaks from windows' \r\n + if (ip[ip.size() - 1] == '\r') + { + ip.pop_back(); + } + + entries.emplace(ip, kill_list_entry(ip, comment)); + } + + console::info("Loaded %zu kill list entries from %s", entries.size(), kill_file); + }); +} + +void kill_list::write_to_disk() +{ + std::ostringstream stream; + this->entries_container_.access([&stream](const kill_list_entries& entries) + { + for (const auto& [ip, entry] : entries) + { + stream << entry.ip_address_ << " " << entry.reason_ << "\n"; + } + + if (utils::io::write_file(kill_file, stream.str(), false)) + { + console::info("Wrote %s to disk (%zu entries)", kill_file, entries.size()); + } + else + { + console::error("Failed to write %s!", kill_file); + } + }); +} + +kill_list::kill_list(server& server) : service(server) +{ + this->reload_from_disk(); +} diff --git a/src/services/kill_list.hpp b/src/services/kill_list.hpp index 97308ae..2626d19 100644 --- a/src/services/kill_list.hpp +++ b/src/services/kill_list.hpp @@ -1,32 +1,32 @@ -#pragma once -#include - -#include "../service.hpp" - -class kill_list : public service -{ -public: - class kill_list_entry - { - public: - kill_list_entry() = default; - kill_list_entry(std::string ip_address, std::string reason); - - std::string ip_address_; - std::string reason_; - }; - - kill_list(server& server); - - bool contains(const network::address& address, std::string& reason); - void add_to_kill_list(const kill_list_entry& add); - void remove_from_kill_list(const network::address& remove); - void remove_from_kill_list(const std::string& remove); - -private: - using kill_list_entries = std::unordered_map; - utils::concurrency::container entries_container_; - - void reload_from_disk(); - void write_to_disk(); -}; +#pragma once +#include + +#include "../service.hpp" + +class kill_list : public service +{ +public: + class kill_list_entry + { + public: + kill_list_entry() = default; + kill_list_entry(std::string ip_address, std::string reason); + + std::string ip_address_; + std::string reason_; + }; + + kill_list(server& server); + + bool contains(const network::address& address, std::string& reason); + void add_to_kill_list(const kill_list_entry& add); + void remove_from_kill_list(const network::address& remove); + void remove_from_kill_list(const std::string& remove); + +private: + using kill_list_entries = std::unordered_map; + utils::concurrency::container entries_container_; + + void reload_from_disk(); + void write_to_disk(); +}; diff --git a/src/services/patch_kill_list_command.cpp b/src/services/patch_kill_list_command.cpp index e8ed862..bab2374 100644 --- a/src/services/patch_kill_list_command.cpp +++ b/src/services/patch_kill_list_command.cpp @@ -1,65 +1,65 @@ -#include -#include "patch_kill_list_command.hpp" - -#include "crypto_key.hpp" -#include "services/kill_list.hpp" - -#include -#include -#include - -const char* patch_kill_list_command::get_command() const -{ - return "patchkill"; -} - -// patchkill timestamp signature add/remove target_ip (ban_reason) -void patch_kill_list_command::handle_command([[maybe_unused]] const network::address& target, const std::string_view& data) -{ - const utils::parameters params(data); - if (params.size() < 3) - { - throw execution_exception("Invalid parameter count"); - } - - const auto supplied_timestamp = std::chrono::seconds(std::stoul(params[0])); - const auto current_timestamp = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); - - // Abs the duration so that the client can be ahead or behind - const auto time_stretch = std::chrono::abs(current_timestamp - supplied_timestamp); - - // not offset by more than 5 minutes in either direction - if (time_stretch > 5min) - { - throw execution_exception(utils::string::va("Invalid timestamp supplied - expected %llu, got %llu, which is more than 5 minutes apart", current_timestamp.count(), supplied_timestamp.count())); - } - - const auto& signature = utils::cryptography::base64::decode(params[1]); - const auto should_remove = params[2] == "remove"s; - - if (!should_remove && params[2] != "add"s) - { - throw execution_exception("Invalid parameter #2: should be 'add' or 'remove'"); - } - - const auto supplied_reason = params.join(4); - const auto& crypto_key = crypto_key::get(); - const auto signature_candidate = std::to_string(supplied_timestamp.count()); - - if (!utils::cryptography::ecc::verify_message(crypto_key, signature_candidate, signature)) - { - throw execution_exception("Signature verification of the kill list patch key failed"); - } - - const auto kill_list_service = this->get_server().get_service(); - const auto& supplied_address = params[3]; - - if (should_remove) - { - kill_list_service->remove_from_kill_list(supplied_address); - } - else - { - kill_list_service->add_to_kill_list(kill_list::kill_list_entry(supplied_address, supplied_reason)); - } -} +#include +#include "patch_kill_list_command.hpp" + +#include "crypto_key.hpp" +#include "services/kill_list.hpp" + +#include +#include +#include + +const char* patch_kill_list_command::get_command() const +{ + return "patchkill"; +} + +// patchkill timestamp signature add/remove target_ip (ban_reason) +void patch_kill_list_command::handle_command([[maybe_unused]] const network::address& target, const std::string_view& data) +{ + const utils::parameters params(data); + if (params.size() < 3) + { + throw execution_exception("Invalid parameter count"); + } + + const auto supplied_timestamp = std::chrono::seconds(std::stoul(params[0])); + const auto current_timestamp = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()); + + // Abs the duration so that the client can be ahead or behind + const auto time_stretch = std::chrono::abs(current_timestamp - supplied_timestamp); + + // not offset by more than 5 minutes in either direction + if (time_stretch > 5min) + { + throw execution_exception(utils::string::va("Invalid timestamp supplied - expected %llu, got %llu, which is more than 5 minutes apart", current_timestamp.count(), supplied_timestamp.count())); + } + + const auto& signature = utils::cryptography::base64::decode(params[1]); + const auto should_remove = params[2] == "remove"s; + + if (!should_remove && params[2] != "add"s) + { + throw execution_exception("Invalid parameter #2: should be 'add' or 'remove'"); + } + + const auto supplied_reason = params.join(4); + const auto& crypto_key = crypto_key::get(); + const auto signature_candidate = std::to_string(supplied_timestamp.count()); + + if (!utils::cryptography::ecc::verify_message(crypto_key, signature_candidate, signature)) + { + throw execution_exception("Signature verification of the kill list patch key failed"); + } + + const auto kill_list_service = this->get_server().get_service(); + const auto& supplied_address = params[3]; + + if (should_remove) + { + kill_list_service->remove_from_kill_list(supplied_address); + } + else + { + kill_list_service->add_to_kill_list(kill_list::kill_list_entry(supplied_address, supplied_reason)); + } +}