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:
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

6
.gitmodules vendored
View File

@ -19,4 +19,8 @@
[submodule "deps/curl"]
path = deps/curl
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
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

View File

@ -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());

View File

@ -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);

View File

@ -1,153 +1,159 @@
#include "std_include.hpp"
#include "kill_list.hpp"
#include <utils/io.hpp>
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<bool>([&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<bool>([&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<bool>([&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 <utils/io.hpp>
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<bool>([&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<bool>([&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<bool>([&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();
}

View File

@ -1,32 +1,32 @@
#pragma once
#include <network/address.hpp>
#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<std::string, kill_list_entry>;
utils::concurrency::container<kill_list_entries> entries_container_;
void reload_from_disk();
void write_to_disk();
};
#pragma once
#include <network/address.hpp>
#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<std::string, kill_list_entry>;
utils::concurrency::container<kill_list_entries> entries_container_;
void reload_from_disk();
void write_to_disk();
};

View File

@ -1,65 +1,65 @@
#include <std_include.hpp>
#include "patch_kill_list_command.hpp"
#include "crypto_key.hpp"
#include "services/kill_list.hpp"
#include <utils/parameters.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
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::seconds>(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<kill_list>();
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 <std_include.hpp>
#include "patch_kill_list_command.hpp"
#include "crypto_key.hpp"
#include "services/kill_list.hpp"
#include <utils/parameters.hpp>
#include <utils/io.hpp>
#include <utils/string.hpp>
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::seconds>(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<kill_list>();
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));
}
}