This commit is contained in:
Rim 2024-05-30 19:39:04 -04:00
commit 6048d9f1c2
9 changed files with 388 additions and 254 deletions

View File

@ -3,10 +3,12 @@ name: Build
on: on:
push: push:
branches: branches:
- "*" - "**"
tags:
- '[0-9]+.[0-9]+.[0-9]+'
pull_request: pull_request:
branches: branches:
- "*" - "**"
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
concurrency: concurrency:
@ -58,7 +60,11 @@ jobs:
uses: ammaraskar/msvc-problem-matcher@master uses: ammaraskar/msvc-problem-matcher@master
- name: Build ${{matrix.arch}} ${{matrix.configuration}} binaries - 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/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 - name: Upload ${{matrix.arch}} ${{matrix.configuration}} binaries
uses: actions/upload-artifact@main uses: actions/upload-artifact@main
@ -162,17 +168,28 @@ jobs:
with: with:
name: macos-${{matrix.arch}}-${{matrix.configuration}} name: macos-${{matrix.arch}}-${{matrix.configuration}}
path: | path: |
<<<<<<< HEAD
build/bin/${{matrix.arch}}/${{matrix.configuration}}/master-server build/bin/${{matrix.arch}}/${{matrix.configuration}}/master-server
=======
build/bin/${{matrix.arch}}/${{matrix.configuration}}/alterware-master
>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7
deploy: deploy:
name: Deploy artifacts name: Deploy artifacts
needs: [build-win, build-linux, build-macos] needs: [build-win, build-linux, build-macos]
runs-on: ubuntu-latest runs-on: ubuntu-latest
<<<<<<< HEAD
if: github.event_name == 'push' && github.ref == 'refs/heads/master' if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps: steps:
- name: Setup main environment - name: Setup main environment
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
run: echo "MASTER_SERVER_PATH=${{ secrets.MASTER_SERVER_SSH_PATH }}" >> $GITHUB_ENV 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 - name: Download Release binaries
uses: actions/download-artifact@main uses: actions/download-artifact@main
@ -182,6 +199,7 @@ jobs:
- name: Install SSH key - name: Install SSH key
uses: shimataro/ssh-key-action@v2.7.0 uses: shimataro/ssh-key-action@v2.7.0
with: with:
<<<<<<< HEAD
key: ${{ secrets.MASTER_SERVER_SSH_PRIVATE_KEY }} key: ${{ secrets.MASTER_SERVER_SSH_PRIVATE_KEY }}
known_hosts: 'just-a-placeholder-so-we-dont-get-errors' known_hosts: 'just-a-placeholder-so-we-dont-get-errors'
@ -193,3 +211,74 @@ jobs:
- name: Publish changes - name: Publish changes
run: ssh ${{ secrets.MASTER_SERVER_SSH_USER }}@${{ secrets.MASTER_SERVER_SSH_ADDRESS }} ${{ secrets.SSH_SERVER_PUBLISH_COMMAND }} 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

6
.gitmodules vendored
View File

@ -19,4 +19,8 @@
[submodule "deps/curl"] [submodule "deps/curl"]
path = deps/curl path = deps/curl
url = https://github.com/curl/curl.git url = https://github.com/curl/curl.git
branch = curl-8_7_1 <<<<<<< HEAD
branch = curl-8_7_1
=======
branch = curl-8_7_1
>>>>>>> 27c2b2b75b56e54dcfbec690dd086946a45587d7

13
Dockerfile Normal file
View File

@ -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"]

View File

@ -4,6 +4,13 @@
# Master Server # Master Server
This is the master server our clients use. It is based on the DP Master Server (ID Tech) protocol 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 ## Build
- Install [Premake5][premake5-link] and add it to your system PATH - Install [Premake5][premake5-link] and add it to your system PATH
- Clone this repository using [Git][git-link] - 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 - Customization: Modifications to the Premake5.lua script may be required
- Platform support: Details regarding supported platforms are available in [build.yml][build-link] - 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 [premake5-link]: https://premake.github.io
[git-link]: https://git-scm.com [git-link]: https://git-scm.com
[mold-link]: https://github.com/rui314/mold [mold-link]: https://github.com/rui314/mold

View File

@ -36,7 +36,7 @@ namespace crypto_key
if (!utils::io::write_file("./private.key", key.serialize())) 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()); console::info("Generated cryptographic key: %llX", key.get_hash());

View File

@ -30,6 +30,7 @@ void elimination_handler::run_frame()
(server.state == game_server::state::can_ping && diff > 15min)) (server.state == game_server::state::can_ping && diff > 15min))
{ {
context.remove(); context.remove();
return;
} }
if (server.game == game_type::unknown) 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); console::log("Removing T7 server '%s' because they are using an outdated protocol (%i)", context.get_address().to_string().data(), server.protocol);
#endif #endif
context.remove(); context.remove();
return;
} }
++server_count[server.game][context.get_address().to_string(false)]; ++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()); console::log("Removing server '%s' because it exceeds MAX_SERVERS_PER_GAME", context.get_address().to_string().data());
context.remove(); 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); const auto name = utils::string::to_lower(server.name);

View File

@ -1,153 +1,159 @@
#include "std_include.hpp" #include "std_include.hpp"
#include "kill_list.hpp" #include "kill_list.hpp"
#include <utils/io.hpp> #include <utils/io.hpp>
constexpr auto* kill_file = "./kill.txt"; constexpr auto* kill_file = "./kill.txt";
kill_list::kill_list_entry::kill_list_entry(std::string ip_address, std::string reason) 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)) : ip_address_(std::move(ip_address)), reason_(std::move(reason))
{ {
} }
bool kill_list::contains(const network::address& address, std::string& reason) bool kill_list::contains(const network::address& address, std::string& reason)
{ {
auto str_address = address.to_string(false); auto str_address = address.to_string(false);
return this->entries_container_.access<bool>([&str_address, &reason](const kill_list_entries& entries) return this->entries_container_.access<bool>([&str_address, &reason](const kill_list_entries& entries)
{ {
if (const auto itr = entries.find(str_address); itr != entries.end()) if (const auto itr = entries.find(str_address); itr != entries.end())
{ {
reason = itr->second.reason_; reason = itr->second.reason_;
return true; return true;
} }
return false; return false;
}); });
} }
void kill_list::add_to_kill_list(const kill_list_entry& add) void kill_list::add_to_kill_list(const kill_list_entry& add)
{ {
const auto any_change = this->entries_container_.access<bool>([&add](kill_list_entries& entries) const auto any_change = this->entries_container_.access<bool>([&add](kill_list_entries& entries)
{ {
const auto existing_entry = entries.find(add.ip_address_); const auto existing_entry = entries.find(add.ip_address_);
if (existing_entry == entries.end() || existing_entry->second.reason_ != add.reason_) 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()); console::info("Added %s to kill list (reason: %s)", add.ip_address_.data(), add.reason_.data());
entries[add.ip_address_] = add; entries[add.ip_address_] = add;
return true; return true;
} }
return false; return false;
}); });
if (!any_change) if (!any_change)
{ {
console::info("%s already in kill list, doing nothing", add.ip_address_.data()); console::info("%s already in kill list, doing nothing", add.ip_address_.data());
return; return;
} }
this->write_to_disk(); this->write_to_disk();
} }
void kill_list::remove_from_kill_list(const network::address& remove) void kill_list::remove_from_kill_list(const network::address& remove)
{ {
this->remove_from_kill_list(remove.to_string()); this->remove_from_kill_list(remove.to_string());
} }
void kill_list::remove_from_kill_list(const std::string& remove) void kill_list::remove_from_kill_list(const std::string& remove)
{ {
const auto any_change = this->entries_container_.access<bool>([&remove](kill_list_entries& entries) const auto any_change = this->entries_container_.access<bool>([&remove](kill_list_entries& entries)
{ {
if (entries.erase(remove)) if (entries.erase(remove))
{ {
console::info("Removed %s from kill list", remove.data()); console::info("Removed %s from kill list", remove.data());
return true; return true;
} }
return false; return false;
}); });
if (!any_change) if (!any_change)
{ {
console::info("%s not in kill list, doing nothing", remove.data()); console::info("%s not in kill list, doing nothing", remove.data());
return; return;
} }
this->write_to_disk(); this->write_to_disk();
} }
void kill_list::reload_from_disk() void kill_list::reload_from_disk()
{ {
std::string contents; std::string contents;
if (!utils::io::read_file(kill_file, &contents)) if (!utils::io::read_file(kill_file, &contents))
{ {
console::info("Could not find %s, kill list will not be loaded.", kill_file); console::info("Could not find %s, kill list will not be loaded.", kill_file);
return; return;
} }
std::istringstream string_stream(contents); std::istringstream string_stream(contents);
std::string line; std::string line;
this->entries_container_.access([&string_stream, &line](kill_list_entries& entries) this->entries_container_.access([&string_stream, &line](kill_list_entries& entries)
{ {
entries.clear(); entries.clear();
while (std::getline(string_stream, line)) while (std::getline(string_stream, line))
{ {
if (line[0] == '#') if (line[0] == '#')
{ {
// comments or ignored line // comments or ignored line
continue; continue;
} }
std::string ip; std::string ip;
std::string comment; std::string comment;
const auto index = line.find(' '); const auto index = line.find(' ');
if (index != std::string::npos) if (index != std::string::npos)
{ {
ip = line.substr(0, index); ip = line.substr(0, index);
comment = line.substr(index + 1); comment = line.substr(index + 1);
} }
else else
{ {
ip = line; ip = line;
} }
if (ip.empty()) if (ip.empty())
{ {
continue; continue;
} }
// Double line breaks from windows' \r\n // Double line breaks from windows' \r\n
if (ip[ip.size() - 1] == '\r') if (ip[ip.size() - 1] == '\r')
{ {
ip.pop_back(); ip.pop_back();
} }
entries.emplace(ip, kill_list_entry(ip, comment)); entries.emplace(ip, kill_list_entry(ip, comment));
} }
console::info("Loaded %zu kill list entries from %s", entries.size(), kill_file); console::info("Loaded %zu kill list entries from %s", entries.size(), kill_file);
}); });
} }
void kill_list::write_to_disk() void kill_list::write_to_disk()
{ {
std::ostringstream stream; std::ostringstream stream;
this->entries_container_.access([&stream](const kill_list_entries& entries) this->entries_container_.access([&stream](const kill_list_entries& entries)
{ {
for (const auto& [ip, entry] : entries) for (const auto& [ip, entry] : entries)
{ {
stream << entry.ip_address_ << " " << entry.reason_ << "\n"; stream << entry.ip_address_ << " " << entry.reason_ << "\n";
} }
utils::io::write_file(kill_file, stream.str(), false); if (utils::io::write_file(kill_file, stream.str(), false))
console::info("Wrote %s to disk (%zu entries)", kill_file, entries.size()); {
}); console::info("Wrote %s to disk (%zu entries)", kill_file, entries.size());
} }
else
kill_list::kill_list(server& server) : service(server) {
{ console::error("Failed to write %s!", kill_file);
this->reload_from_disk(); }
} });
}
kill_list::kill_list(server& server) : service(server)
{
this->reload_from_disk();
}

View File

@ -1,32 +1,32 @@
#pragma once #pragma once
#include <network/address.hpp> #include <network/address.hpp>
#include "../service.hpp" #include "../service.hpp"
class kill_list : public service class kill_list : public service
{ {
public: public:
class kill_list_entry class kill_list_entry
{ {
public: public:
kill_list_entry() = default; kill_list_entry() = default;
kill_list_entry(std::string ip_address, std::string reason); kill_list_entry(std::string ip_address, std::string reason);
std::string ip_address_; std::string ip_address_;
std::string reason_; std::string reason_;
}; };
kill_list(server& server); kill_list(server& server);
bool contains(const network::address& address, std::string& reason); bool contains(const network::address& address, std::string& reason);
void add_to_kill_list(const kill_list_entry& add); 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 network::address& remove);
void remove_from_kill_list(const std::string& remove); void remove_from_kill_list(const std::string& remove);
private: private:
using kill_list_entries = std::unordered_map<std::string, kill_list_entry>; using kill_list_entries = std::unordered_map<std::string, kill_list_entry>;
utils::concurrency::container<kill_list_entries> entries_container_; utils::concurrency::container<kill_list_entries> entries_container_;
void reload_from_disk(); void reload_from_disk();
void write_to_disk(); void write_to_disk();
}; };

View File

@ -1,65 +1,65 @@
#include <std_include.hpp> #include <std_include.hpp>
#include "patch_kill_list_command.hpp" #include "patch_kill_list_command.hpp"
#include "crypto_key.hpp" #include "crypto_key.hpp"
#include "services/kill_list.hpp" #include "services/kill_list.hpp"
#include <utils/parameters.hpp> #include <utils/parameters.hpp>
#include <utils/io.hpp> #include <utils/io.hpp>
#include <utils/string.hpp> #include <utils/string.hpp>
const char* patch_kill_list_command::get_command() const const char* patch_kill_list_command::get_command() const
{ {
return "patchkill"; return "patchkill";
} }
// patchkill timestamp signature add/remove target_ip (ban_reason) // 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) void patch_kill_list_command::handle_command([[maybe_unused]] const network::address& target, const std::string_view& data)
{ {
const utils::parameters params(data); const utils::parameters params(data);
if (params.size() < 3) if (params.size() < 3)
{ {
throw execution_exception("Invalid parameter count"); throw execution_exception("Invalid parameter count");
} }
const auto supplied_timestamp = std::chrono::seconds(std::stoul(params[0])); const auto supplied_timestamp = std::chrono::seconds(std::stoul(params[0]));
const auto current_timestamp = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()); const auto current_timestamp = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch());
// Abs the duration so that the client can be ahead or behind // Abs the duration so that the client can be ahead or behind
const auto time_stretch = std::chrono::abs(current_timestamp - supplied_timestamp); const auto time_stretch = std::chrono::abs(current_timestamp - supplied_timestamp);
// not offset by more than 5 minutes in either direction // not offset by more than 5 minutes in either direction
if (time_stretch > 5min) 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())); 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& signature = utils::cryptography::base64::decode(params[1]);
const auto should_remove = params[2] == "remove"s; const auto should_remove = params[2] == "remove"s;
if (!should_remove && params[2] != "add"s) if (!should_remove && params[2] != "add"s)
{ {
throw execution_exception("Invalid parameter #2: should be 'add' or 'remove'"); throw execution_exception("Invalid parameter #2: should be 'add' or 'remove'");
} }
const auto supplied_reason = params.join(4); const auto supplied_reason = params.join(4);
const auto& crypto_key = crypto_key::get(); const auto& crypto_key = crypto_key::get();
const auto signature_candidate = std::to_string(supplied_timestamp.count()); const auto signature_candidate = std::to_string(supplied_timestamp.count());
if (!utils::cryptography::ecc::verify_message(crypto_key, signature_candidate, signature)) if (!utils::cryptography::ecc::verify_message(crypto_key, signature_candidate, signature))
{ {
throw execution_exception("Signature verification of the kill list patch key failed"); throw execution_exception("Signature verification of the kill list patch key failed");
} }
const auto kill_list_service = this->get_server().get_service<kill_list>(); const auto kill_list_service = this->get_server().get_service<kill_list>();
const auto& supplied_address = params[3]; const auto& supplied_address = params[3];
if (should_remove) if (should_remove)
{ {
kill_list_service->remove_from_kill_list(supplied_address); kill_list_service->remove_from_kill_list(supplied_address);
} }
else else
{ {
kill_list_service->add_to_kill_list(kill_list::kill_list_entry(supplied_address, supplied_reason)); kill_list_service->add_to_kill_list(kill_list::kill_list_entry(supplied_address, supplied_reason));
} }
} }