This commit is contained in:
2023-05-26 16:09:29 +02:00
commit 32db868ae6
83 changed files with 5753 additions and 0 deletions

23
src/client.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include "utils/cryptography.hpp"
#include "game_server.hpp"
struct client
{
enum class state
{
can_authenticate = 0,
key_received,
challenge_sent,
};
uint64_t guid{0};
bool registered{false};
game_type game{game_type::unknown};
state state{state::can_authenticate};
utils::cryptography::ecc::key key{};
std::string challenge{};
std::string aes_key{};
std::chrono::high_resolution_clock::time_point heartbeat{};
};

38
src/client_list.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "std_include.hpp"
#include "client_list.hpp"
bool client_list::find_client(const uint64_t guid, const access_func& accessor)
{
auto found = false;
this->iterate([&](iteration_context& context)
{
auto& client = context.get();
if (client.guid == guid)
{
accessor(client, context.get_address());
context.stop_iterating();
found = true;
}
});
return found;
}
bool client_list::find_client(const uint64_t guid, const const_access_func& accessor) const
{
auto found = false;
this->iterate([&](const iteration_context& context)
{
const auto& client = context.get();
if (client.guid == guid)
{
accessor(client, context.get_address());
context.stop_iterating();
found = true;
}
});
return found;
}

13
src/client_list.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "client.hpp"
#include "network_list.hpp"
class client_list : public network_list<client, 0>
{
public:
bool find_client(uint64_t guid, const access_func& accessor);
bool find_client(uint64_t guid, const const_access_func& accessor) const;
using network_list::insert;
};

248
src/console.cpp Normal file
View File

@ -0,0 +1,248 @@
#include "std_include.hpp"
#include "console.hpp"
#ifdef _WIN32
#define COLOR_LOG_INFO 11//15
#define COLOR_LOG_WARN 14
#define COLOR_LOG_ERROR 12
#define COLOR_LOG_DEBUG 15//7
#else
#define COLOR_LOG_INFO "\033[0;36m"
#define COLOR_LOG_WARN "\033[0;33m"
#define COLOR_LOG_ERROR "\033[0;31m"
#define COLOR_LOG_DEBUG "\033[0m"
#endif
namespace console
{
namespace
{
std::mutex signal_mutex;
std::function<void()> signal_callback;
#ifdef _WIN32
#define COLOR(win, posix) win
using color_type = WORD;
#else
#define COLOR(win, posix) posix
using color_type = const char*;
#endif
const color_type color_array[] =
{
COLOR(0x8, "\033[0;90m"), // 0 - black
COLOR(0xC, "\033[0;91m"), // 1 - red
COLOR(0xA, "\033[0;92m"), // 2 - green
COLOR(0xE, "\033[0;93m"), // 3 - yellow
COLOR(0x9, "\033[0;94m"), // 4 - blue
COLOR(0xB, "\033[0;96m"), // 5 - cyan
COLOR(0xD, "\033[0;95m"), // 6 - pink
COLOR(0xF, "\033[0;97m"), // 7 - white
};
#ifdef _WIN32
BOOL WINAPI handler(const DWORD signal)
{
if (signal == CTRL_C_EVENT && signal_callback)
{
signal_callback();
}
return TRUE;
}
#else
void handler(int signal)
{
if (signal == SIGINT && signal_callback)
{
signal_callback();
}
}
#endif
std::string format(va_list* ap, const char* message)
{
static thread_local char buffer[0x1000];
#ifdef _WIN32
const int count = _vsnprintf_s(buffer, sizeof(buffer), sizeof(buffer), message, *ap);
#else
const int count = vsnprintf(buffer, sizeof(buffer), message, *ap);
#endif
if (count < 0) return {};
return {buffer, static_cast<size_t>(count)};
}
#ifdef _WIN32
HANDLE get_console_handle()
{
return GetStdHandle(STD_OUTPUT_HANDLE);
}
#endif
void set_color(const color_type color)
{
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), color);
#else
printf("%s", color);
#endif
}
bool apply_color(const std::string& data, const size_t index, const color_type base_color)
{
if (data[index] != '^' || (index + 1) >= data.size())
{
return false;
}
auto code = data[index + 1] - '0';
if (code < 0 || code > 11)
{
return false;
}
code = std::min(code, 7); // Everything above white is white
if (code == 7)
{
set_color(base_color);
}
else
{
set_color(color_array[code]);
}
return true;
}
void print_colored(const std::string& line, const color_type base_color)
{
lock _{};
set_color(base_color);
for (size_t i = 0; i < line.size(); ++i)
{
if (apply_color(line, i, base_color))
{
++i;
continue;
}
putchar(line[i]);
}
reset_color();
}
}
lock::lock()
{
#ifdef _WIN32
_lock_file(stdout);
#else
flockfile(stdout);
#endif
}
lock::~lock()
{
#ifdef _WIN32
_unlock_file(stdout);
#else
funlockfile(stdout);
#endif
}
void reset_color()
{
lock _{};
#ifdef _WIN32
SetConsoleTextAttribute(get_console_handle(), 7);
#else
printf("\033[0m");
#endif
fflush(stdout);
}
void info(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[+] " + data + "\n", COLOR_LOG_INFO);
va_end(ap);
}
void warn(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[!] " + data + "\n", COLOR_LOG_WARN);
va_end(ap);
}
void error(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[-] " + data + "\n", COLOR_LOG_ERROR);
va_end(ap);
}
void log(const char* message, ...)
{
va_list ap;
va_start(ap, message);
const auto data = format(&ap, message);
print_colored("[*] " + data + "\n", COLOR_LOG_DEBUG);
va_end(ap);
}
void set_title(const std::string& title)
{
lock _{};
#ifdef _WIN32
SetConsoleTitleA(title.data());
#else
printf("\033]0;%s\007", title.data());
fflush(stdout);
#endif
}
signal_handler::signal_handler(std::function<void()> callback)
: std::lock_guard<std::mutex>(signal_mutex)
{
signal_callback = std::move(callback);
#ifdef _WIN32
SetConsoleCtrlHandler(handler, TRUE);
#else
signal(SIGINT, handler);
#endif
}
signal_handler::~signal_handler()
{
#ifdef _WIN32
SetConsoleCtrlHandler(handler, FALSE);
#else
signal(SIGINT, SIG_DFL);
#endif
signal_callback = {};
}
}

32
src/console.hpp Normal file
View File

@ -0,0 +1,32 @@
#pragma once
namespace console
{
class lock
{
public:
lock();
~lock();
lock(lock&&) = delete;
lock(const lock&) = delete;
lock& operator=(lock&&) = delete;
lock& operator=(const lock&) = delete;
};
void reset_color();
void info(const char* message, ...);
void warn(const char* message, ...);
void error(const char* message, ...);
void log(const char* message, ...);
void set_title(const std::string& title);
class signal_handler : std::lock_guard<std::mutex>
{
public:
signal_handler(std::function<void()> callback);
~signal_handler();
};
}

74
src/crypto_key.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "std_include.hpp"
#include "crypto_key.hpp"
#include "console.hpp"
#include "utils/io.hpp"
namespace crypto_key
{
namespace
{
bool load_key(utils::cryptography::ecc::key& key)
{
std::string data{};
if (!utils::io::read_file("./private.key", &data))
{
return false;
}
key.deserialize(data);
if (!key.is_valid())
{
console::warn("Loaded key is invalid!");
return false;
}
return true;
}
utils::cryptography::ecc::key generate_key()
{
auto key = utils::cryptography::ecc::generate_key(512);
if (!key.is_valid())
{
throw std::runtime_error("Failed to generate server key!");
}
if (!utils::io::write_file("./private.key", key.serialize()))
{
throw std::runtime_error("Failed to write server key!");
}
console::info("Generated cryptographic key: %llX", key.get_hash());
return key;
}
utils::cryptography::ecc::key load_or_generate_key()
{
utils::cryptography::ecc::key key{};
if (load_key(key))
{
console::log("Loaded cryptographic key: %llX", key.get_hash());
return key;
}
return generate_key();
}
utils::cryptography::ecc::key get_key_internal()
{
auto key = load_or_generate_key();
if (!utils::io::write_file("./public.key", key.get_public_key()))
{
console::error("Failed to write public key!");
}
return key;
}
}
const utils::cryptography::ecc::key& get()
{
static auto key = get_key_internal();
return key;
}
}

8
src/crypto_key.hpp Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include <utils/cryptography.hpp>
namespace crypto_key
{
const utils::cryptography::ecc::key& get();
}

73
src/game_server.hpp Normal file
View File

@ -0,0 +1,73 @@
#pragma once
#include "utils/info_string.hpp"
enum class game_type
{
unknown = 0,
iw4,
iw6,
s1,
t7,
};
inline const std::string& resolve_game_type_name(const game_type game)
{
static const std::unordered_map<game_type, std::string> names =
{
{game_type::unknown, "Unknown"},
{game_type::iw4, "IW4"},
{game_type::iw6, "IW6"},
{game_type::s1, "S1"},
{game_type::t7, "T7"},
};
return names.at(game);
}
inline game_type resolve_game_type(const std::string& game_name)
{
if (game_name == "IW4")
{
return game_type::iw4;
}
if (game_name == "IW6")
{
return game_type::iw6;
}
if (game_name == "S1")
{
return game_type::s1;
}
if (game_name == "T7")
{
return game_type::t7;
}
return game_type::unknown;
}
struct game_server
{
enum class state
{
can_ping = 0,
needs_ping,
pinged,
dead,
};
state state{state::can_ping};
bool registered{false};
game_type game{game_type::unknown};
int protocol{};
uint32_t clients{};
std::string name{};
std::string challenge{};
utils::info_string info_string{};
std::chrono::high_resolution_clock::time_point heartbeat{};
};

47
src/main.cpp Normal file
View File

@ -0,0 +1,47 @@
#include <std_include.hpp>
#include "console.hpp"
#include "server.hpp"
#include "crypto_key.hpp"
namespace
{
void unsafe_main(const uint16_t port)
{
crypto_key::get();
console::log("Creating socket on port %hu", port);
network::address a{"0.0.0.0"};
a.set_port(port);
server s{a};
console::signal_handler handler([&s]()
{
s.stop();
});
s.run();
console::log("Terminating server...");
}
}
int main(const int argc, const char** argv)
{
console::set_title("X Labs Master");
console::log("Starting X Labs Master");
try
{
unsafe_main(argc > 1 ? static_cast<uint16_t>(atoi(argv[1])) : 20810);
}
catch (std::exception& e)
{
console::error("Fatal error: %s\n", e.what());
return -1;
}
return 0;
}

160
src/network/address.cpp Normal file
View File

@ -0,0 +1,160 @@
#include "std_include.hpp"
#include "network/address.hpp"
namespace network
{
address::address()
{
ZeroMemory(&this->address_, sizeof(this->address_));
}
address::address(const std::string& addr)
: address()
{
this->parse(addr);
}
address::address(sockaddr_in& addr)
{
this->address_ = addr;
}
bool address::operator==(const address& obj) const
{
//return !std::memcmp(&this->address_, &obj.address_, sizeof(this->address_));
return this->address_.sin_family == obj.address_.sin_family //
&& this->address_.sin_addr.s_addr == obj.address_.sin_addr.s_addr //
&& this->address_.sin_port == obj.address_.sin_port;
}
void address::set_ipv4(const in_addr addr)
{
this->address_.sin_family = AF_INET;
this->address_.sin_addr = addr;
}
void address::set_port(const unsigned short port)
{
this->address_.sin_port = htons(port);
}
unsigned short address::get_port() const
{
return ntohs(this->address_.sin_port);
}
std::string address::to_string(bool with_port) const
{
char buffer[1000] = {0};
inet_ntop(this->address_.sin_family, &this->address_.sin_addr, buffer, sizeof(buffer));
auto address = std::string(buffer);
if (with_port)
{
address += ":"s + std::to_string(this->get_port());
}
return address;
}
bool address::is_local() const
{
// According to: https://en.wikipedia.org/wiki/Private_network
uint8_t bytes[4];
*reinterpret_cast<uint32_t*>(&bytes) = this->address_.sin_addr.s_addr;
// 10.X.X.X
if (bytes[0] == 10)
{
return true;
}
// 192.168.X.X
if (bytes[0] == 192
&& bytes[1] == 168)
{
return true;
}
// 172.16.X.X - 172.31.X.X
if (bytes[0] == 172
&& bytes[1] >= 16
&& bytes[1] < 32)
{
return true;
}
// 127.0.0.1
if (this->address_.sin_addr.s_addr == 0x0100007F)
{
return true;
}
return false;
}
sockaddr& address::get_addr()
{
return reinterpret_cast<sockaddr&>(this->get_in_addr());
}
const sockaddr& address::get_addr() const
{
return reinterpret_cast<const sockaddr&>(this->get_in_addr());
}
sockaddr_in& address::get_in_addr()
{
return this->address_;
}
const sockaddr_in& address::get_in_addr() const
{
return this->address_;
}
void address::parse(std::string addr)
{
const auto pos = addr.find_last_of(':');
if (pos != std::string::npos)
{
auto port = addr.substr(pos + 1);
this->set_port(uint16_t(atoi(port.data())));
addr = addr.substr(0, pos);
}
this->resolve(addr);
}
void address::resolve(const std::string& hostname)
{
addrinfo* result = nullptr;
if (!getaddrinfo(hostname.data(), nullptr, nullptr, &result))
{
for (auto* i = result; i; i = i->ai_next)
{
if (i->ai_addr->sa_family == AF_INET)
{
const auto port = this->get_port();
std::memcpy(&this->address_, i->ai_addr, sizeof(this->address_));
this->set_port(port);
break;
}
}
freeaddrinfo(result);
}
}
}
std::size_t std::hash<network::address>::operator()(const network::address& a) const noexcept
{
const auto h1 = std::hash<decltype(a.get_in_addr().sin_family)>{}(a.get_in_addr().sin_family);
const auto h2 = std::hash<decltype(a.get_in_addr().sin_addr.s_addr)>{}(a.get_in_addr().sin_addr.s_addr);
const auto h3 = std::hash<decltype(a.get_in_addr().sin_port)>{}(a.get_in_addr().sin_port);
return h1 ^ (h2 << 1) ^ (h3 << 2);
}

46
src/network/address.hpp Normal file
View File

@ -0,0 +1,46 @@
#pragma once
namespace network
{
class address
{
public:
address();
address(const std::string& addr);
address(sockaddr_in& addr);
void set_ipv4(in_addr addr);
void set_port(unsigned short port);
[[nodiscard]] unsigned short get_port() const;
sockaddr& get_addr();
const sockaddr& get_addr() const;
sockaddr_in& get_in_addr();
const sockaddr_in& get_in_addr() const;
[[nodiscard]] bool is_local() const;
[[nodiscard]] std::string to_string(bool with_port = true) const;
bool operator==(const address& obj) const;
bool operator!=(const address& obj) const
{
return !(*this == obj);
}
private:
sockaddr_in address_{};
void parse(std::string addr);
void resolve(const std::string& hostname);
};
}
namespace std
{
template <>
struct hash<network::address>
{
std::size_t operator()(const network::address& a) const noexcept;
};
}

129
src/network/socket.cpp Normal file
View File

@ -0,0 +1,129 @@
#include "std_include.hpp"
#include "network/socket.hpp"
namespace network
{
namespace
{
#ifdef _WIN32
[[maybe_unused]] class wsa_initializer
{
public:
wsa_initializer()
{
WSADATA wsa_data;
if (WSAStartup(MAKEWORD(2, 2), &wsa_data))
{
throw std::runtime_error("Unable to initialize WSA");
}
}
~wsa_initializer()
{
WSACleanup();
}
} _;
#endif
}
socket::socket()
{
this->socket_ = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
}
socket::~socket()
{
if (this->socket_ != INVALID_SOCKET)
{
#ifdef _WIN32
closesocket(this->socket_);
#else
close(this->socket_);
#endif
}
}
socket::socket(socket&& obj) noexcept
{
this->operator=(std::move(obj));
}
socket& socket::operator=(socket&& obj) noexcept
{
if (this != &obj)
{
this->~socket();
this->socket_ = obj.socket_;
obj.socket_ = INVALID_SOCKET;
}
return *this;
}
bool socket::bind(const address& target)
{
return ::bind(this->socket_, &target.get_addr(), sizeof(target.get_addr())) == 0;
}
void socket::send(const address& target, const std::string& data) const
{
sendto(this->socket_, data.data(), static_cast<int>(data.size()), 0, &target.get_addr(),
sizeof(target.get_addr()));
}
bool socket::receive(address& source, std::string& data) const
{
char buffer[0x2000];
socklen_t len = sizeof(source.get_in_addr());
const auto result = recvfrom(this->socket_, buffer, sizeof(buffer), 0, &source.get_addr(), &len);
if (result == SOCKET_ERROR) // Probably WSAEWOULDBLOCK
{
return false;
}
data.assign(buffer, buffer + result);
return true;
}
bool socket::set_blocking(const bool blocking)
{
#ifdef _WIN32
unsigned long mode = blocking ? 0 : 1;
return ioctlsocket(this->socket_, FIONBIO, &mode) == 0;
#else
int flags = fcntl(this->socket_, F_GETFL, 0);
if (flags == -1) return false;
flags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
return fcntl(this->socket_, F_SETFL, flags) == 0;
#endif
}
bool socket::sleep(const std::chrono::milliseconds timeout) const
{
fd_set fdr;
FD_ZERO(&fdr);
FD_SET(this->socket_, &fdr);
const auto msec = timeout.count();
timeval tv{};
tv.tv_sec = static_cast<long>(msec / 1000ll);
tv.tv_usec = static_cast<long>((msec % 1000) * 1000);
const auto retval = select(static_cast<int>(this->socket_) + 1, &fdr, nullptr, nullptr, &tv);
if (retval == SOCKET_ERROR)
{
std::this_thread::sleep_for(1ms);
return socket_is_ready;
}
if (retval > 0)
{
return socket_is_ready;
}
return !socket_is_ready;
}
}

40
src/network/socket.hpp Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include "network/address.hpp"
namespace network
{
class socket
{
public:
socket();
~socket();
socket(const socket& obj) = delete;
socket& operator=(const socket& obj) = delete;
socket(socket&& obj) noexcept;
socket& operator=(socket&& obj) noexcept;
bool bind(const address& target);
void send(const address& target, const std::string& data) const;
bool receive(address& source, std::string& data) const;
bool set_blocking(bool blocking);
static const bool socket_is_ready = true;
bool sleep(std::chrono::milliseconds timeout) const;
private:
#ifdef _WIN32
using socklen_t = int;
#else
using SOCKET = int;
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
#endif
SOCKET socket_ = INVALID_SOCKET;
};
}

172
src/network_list.hpp Normal file
View File

@ -0,0 +1,172 @@
#pragma once
#include <functional>
#include "console.hpp"
#include "network/address.hpp"
#include "utils/concurrency.hpp"
template <typename T, size_t IPLimit = 0>
class network_list
{
public:
class iteration_context
{
public:
friend network_list;
const network::address& get_address() const { return *this->address_; }
T& get() { return *this->element_; }
const T& get() const { return *this->element_; }
void remove() { this->remove_ = true; }
void stop_iterating() const { this->stop_iterating_ = true; }
private:
T* element_{};
const network::address* address_{};
bool remove_{false};
mutable bool stop_iterating_{false};
};
using iterate_func = std::function<void (iteration_context&)>;
using const_iterate_func = std::function<void (const iteration_context&)>;
using access_func = std::function<void(T&, const network::address&)>;
using const_access_func = std::function<void(const T&, const network::address&)>;
using insert_func = std::function<void(T&)>;
bool find(const network::address& address, const access_func& accessor)
{
return this->elements_.template access<bool>([&](list_type& list)
{
const auto i = list.find(address);
if (i == list.end())
{
return false;
}
accessor(i->second, i->first);
return true;
});
}
bool find(const network::address& address, const const_access_func& accessor) const
{
return this->elements_.template access<bool>([&](const list_type& list)
{
const auto i = list.find(address);
if (i == list.end())
{
return false;
}
accessor(i->second, i->first);
return true;
});
}
void iterate(const iterate_func& iterator)
{
this->elements_.access([&](list_type& list)
{
iteration_context context{};
for (auto i = list.begin(); i != list.end();)
{
context.element_ = &i->second;
context.address_ = &i->first;
context.remove_ = false;
iterator(context);
if (context.remove_)
{
i = list.erase(i);
}
else
{
++i;
}
if (context.stop_iterating_)
{
break;
}
}
});
}
void iterate(const const_iterate_func& iterator) const
{
this->elements_.access([&](const list_type& list)
{
iteration_context context{};
for (const auto& server : list)
{
// const_cast is ok here
context.element_ = const_cast<T*>(&server.second);
context.address_ = &server.first;
iterator(context);
if (context.stop_iterating_)
{
break;
}
}
});
}
protected:
void insert(const network::address& address, const insert_func& callback)
{
this->elements_.access([&](list_type& list)
{
auto entry = list.find(address);
if (entry != list.end())
{
callback(entry->second);
return;
}
if (!is_insertion_allowed(list, address))
{
console::log("Insertion rejected for target:\t%s", address.to_string().data());
return;
}
callback(list[address]);
});
}
private:
using list_type = std::unordered_map<network::address, T>;
utils::concurrency::container<list_type> elements_;
static bool is_insertion_allowed(const list_type& list, const network::address& address)
{
if constexpr (IPLimit == 0)
{
return true;
}
const auto target_ip = address.get_in_addr().sin_addr.s_addr;
size_t occurrences = 0;
for (const auto& entry : list)
{
const auto entry_ip = entry.first.get_in_addr().sin_addr.s_addr;
if (entry_ip == target_ip)
{
if (++occurrences >= IPLimit)
{
return false;
}
}
}
return true;
}
};

105
src/server.cpp Normal file
View File

@ -0,0 +1,105 @@
#include "std_include.hpp"
#include "server.hpp"
#include "service.hpp"
#include "console.hpp"
#include "services/getbots_command.hpp"
#include "services/getservers_command.hpp"
#include "services/heartbeat_command.hpp"
#include "services/info_response_command.hpp"
#include "services/patch_kill_list_command.hpp"
#include "services/ping_handler.hpp"
#include "services/elimination_handler.hpp"
#include "services/statistics_handler.hpp"
#include "services/kill_list.hpp"
server::server(const network::address& bind_addr)
: server_base(bind_addr)
{
this->register_service<getbots_command>();
this->register_service<getservers_command>();
this->register_service<heartbeat_command>();
this->register_service<info_response_command>();
this->register_service<patch_kill_list_command>();
this->register_service<ping_handler>();
this->register_service<elimination_handler>();
this->register_service<statistics_handler>();
this->register_service<kill_list>();
}
server_list& server::get_server_list()
{
return this->server_list_;
}
const server_list& server::get_server_list() const
{
return this->server_list_;
}
client_list& server::get_client_list()
{
return this->client_list_;
}
const client_list& server::get_client_list() const
{
return this->client_list_;
}
void server::run_frame()
{
for (const auto& service : services_)
{
try
{
service->run_frame();
}
catch (const service::execution_exception& e)
{
console::warn("Execption in service: %s", e.what());
}
catch (const std::exception& e)
{
console::error("Fatal execption in service: %s", e.what());
}
}
}
void server::handle_command(const network::address& target, const std::string_view& command,
const std::string_view& data)
{
const auto handler = this->command_services_.find(std::string{command});
if (handler == this->command_services_.end())
{
console::warn("Unhandled command (%s): %.*s", target.to_string().data(), command.size(), command.data());
return;
}
std::string ban_reason;
if (this->get_service<kill_list>()->contains(target, ban_reason))
{
console::log("Refused command from server %s - target is on the kill list (%s)", target.to_string().data(), ban_reason.data());
return;
}
#ifdef DEBUG
console::log("Handling command (%s): %.*s - %.*s", target.to_string().data(), command.size(), command.data(),
data.size(), data.data());
#endif
try
{
handler->second->handle_command(target, data);
}
catch (const service::execution_exception& e)
{
console::warn("Execption in command %.*s (%s): %s", command.size(), command.data(), target.to_string().data(),
e.what());
}
catch (const std::exception& e)
{
console::error("Fatal execption in command %.*s (%s): %s", command.size(), command.data(),
target.to_string().data(), e.what());
}
}

61
src/server.hpp Normal file
View File

@ -0,0 +1,61 @@
#pragma once
#include "server_base.hpp"
#include "server_list.hpp"
#include "client_list.hpp"
#include "service.hpp"
class server : public server_base
{
public:
server(const network::address& bind_addr);
server_list& get_server_list();
const server_list& get_server_list() const;
client_list& get_client_list();
const client_list& get_client_list() const;
template <typename T>
T* get_service()
{
static_assert(std::is_base_of_v<service, T>, "Type must be a service!");
for (auto& service : this->services_)
{
const auto& service_ref = *service;
if (typeid(service_ref) == typeid(T))
{
return reinterpret_cast<T*>(service.get());
}
}
return nullptr;
}
private:
server_list server_list_;
client_list client_list_;
std::vector<std::unique_ptr<service>> services_;
std::unordered_map<std::string, service*> command_services_;
template <typename T, typename... Args>
void register_service(Args&&... args)
{
static_assert(std::is_base_of_v<service, T>, "Type must be a service!");
auto service = std::make_unique<T>(*this, std::forward<Args>(args)...);
auto* const command = service->get_command();
if (command)
{
command_services_[command] = service.get();
}
services_.emplace_back(std::move(service));
}
void run_frame() override;
void handle_command(const network::address& target, const std::string_view& command,
const std::string_view& data) override;
};

114
src/server_base.cpp Normal file
View File

@ -0,0 +1,114 @@
#include "std_include.hpp"
#include "server_base.hpp"
#include "console.hpp"
namespace
{
bool is_command(const std::string_view& data)
{
return data.size() > 4 && *reinterpret_cast<const int32_t*>(data.data()) == -1;
}
int find_separator(const std::string_view& data)
{
for (size_t i = 4; i < data.size(); ++i)
{
const auto& chr = data[i];
if (chr == ' ' || chr == '\n' || chr == '\0')
{
return static_cast<int>(i);
}
}
return -1;
}
}
server_base::server_base(const network::address& bind_addr)
{
if (!this->socket_.bind(bind_addr))
{
throw std::runtime_error("Failed to bind socket!");
}
this->socket_.set_blocking(false);
}
void server_base::run()
{
this->stopped_ = false;
std::thread thread = std::thread([this]
{
std::this_thread::sleep_for(30ms);
this->run_socket();
});
while (!this->stopped_)
{
this->run_frame();
std::this_thread::sleep_for(100ms);
}
if (thread.joinable())
{
thread.join();
}
}
void server_base::stop()
{
stopped_ = true;
}
void server_base::send(const network::address& target, const std::string& command, const std::string& data,
const std::string& separator) const
{
this->socket_.send(target, "\xFF\xFF\xFF\xFF" + command + separator + data);
}
void server_base::run_socket()
{
while (!this->stopped_)
{
if (!this->receive_data())
{
(void)this->socket_.sleep(100ms);
}
}
}
bool server_base::receive_data()
{
std::string data{};
network::address address{};
if (!this->socket_.receive(address, data))
{
return false;
}
if (!is_command(data))
{
console::warn("Received invalid data from: %s", address.to_string().data());
return false;
}
this->parse_data(address, std::string_view{data.data() + 4, data.size() - 4});
return true;
}
void server_base::parse_data(const network::address& target, const std::string_view& data)
{
const auto separator = find_separator(data);
if (separator <= 0)
{
this->handle_command(target, data, {});
}
else
{
this->handle_command(target, std::string_view{data.data(), static_cast<size_t>(separator)},
std::string_view{data.data() + (separator + 1), data.size() - (separator + 1)});
}
}

30
src/server_base.hpp Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include "network/socket.hpp"
class server_base
{
public:
server_base(const network::address& bind_addr);
virtual ~server_base() = default;
void run();
void stop();
void send(const network::address& target, const std::string& command, const std::string& data,
const std::string& separator = " ") const;
protected:
virtual void run_frame() = 0;
virtual void handle_command(const network::address& target, const std::string_view& command,
const std::string_view& data) = 0;
private:
network::socket socket_{};
volatile bool stopped_ = false;
void run_socket();
[[nodiscard]] bool receive_data();
void parse_data(const network::address& target, const std::string_view& data);
};

39
src/server_list.cpp Normal file
View File

@ -0,0 +1,39 @@
#include "std_include.hpp"
#include "server_list.hpp"
void server_list::find_registered_servers(const game_type game, const int protocol, const access_func& accessor)
{
this->iterate([&](iteration_context& context)
{
auto& server = context.get();
if (server.registered && server.game == game && server.protocol == protocol)
{
accessor(server, context.get_address());
}
});
}
void server_list::find_registered_servers(const game_type game, const int protocol,
const const_access_func& accessor) const
{
this->iterate([&](const iteration_context& context)
{
const auto& server = context.get();
if (server.registered && server.game == game && server.protocol == protocol)
{
accessor(server, context.get_address());
}
});
}
void server_list::heartbeat(const network::address& address)
{
this->insert(address, [](game_server& server)
{
if (server.state == game_server::state::can_ping)
{
server.heartbeat = std::chrono::high_resolution_clock::now();
server.state = game_server::state::needs_ping;
}
});
}

13
src/server_list.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "game_server.hpp"
#include "network_list.hpp"
class server_list : public network_list<game_server, 30>
{
public:
void find_registered_servers(game_type game, int protocol, const access_func& accessor);
void find_registered_servers(game_type game, int protocol, const const_access_func& accessor) const;
void heartbeat(const network::address& address);
};

56
src/service.hpp Normal file
View File

@ -0,0 +1,56 @@
#pragma once
#include "network/address.hpp"
class server;
class service
{
public:
class execution_exception : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};
service(server& server)
: server_(server)
{
}
virtual ~service() = default;
service(service&&) = delete;
service(const service&) = delete;
service& operator=(service&&) = delete;
service& operator=(const service&) = delete;
[[nodiscard]] virtual const char* get_command() const { return nullptr; }
virtual void handle_command([[maybe_unused]] const network::address& target,
[[maybe_unused]] const std::string_view& data)
{
}
virtual void run_frame()
{
}
protected:
[[nodiscard]] const server& get_server() const
{
return server_;
}
server& get_server()
{
return server_;
}
private:
server& server_;
};
// server and service have a cycle, but fuck, it's easier that way
// include guards should handle that
#include "server.hpp"

View File

@ -0,0 +1,60 @@
#include <std_include.hpp>
#include "elimination_handler.hpp"
constexpr auto T7_PROTOCOL = 7;
constexpr size_t MAX_SERVERS_PER_GAME = 15;
void elimination_handler::run_frame()
{
std::unordered_map<game_type, std::unordered_map<network::address, size_t>> server_count;
auto now = std::chrono::high_resolution_clock::now();
this->get_server().get_server_list().iterate([&](server_list::iteration_context& context)
{
auto& server = context.get();
const auto diff = now - server.heartbeat;
if ((server.state == game_server::state::pinged && diff > 2min) ||
(server.state == game_server::state::can_ping && diff > 15min))
{
context.remove();
}
if (server.game == game_type::unknown)
{
return;
}
if (server.game == game_type::t7 && server.protocol < T7_PROTOCOL)
{
#ifdef _DEBUG
console::info("Removing T7 server '%s' because protocol %i is less than %i\n",
context.get_address().to_string().data(), server.protocol, T7_PROTOCOL);
#endif
context.remove();
}
++server_count[server.game][context.get_address()];
if (server_count[server.game][context.get_address()] >= MAX_SERVERS_PER_GAME)
{
#ifdef _DEBUG
console::info("Removing server '%s' because it exceeds MAX_SERVERS_PER_GAME\n",
context.get_address().to_string().data());
#endif
context.remove();
}
});
now = std::chrono::high_resolution_clock::now();
this->get_server().get_client_list().iterate([&](client_list::iteration_context& context)
{
auto& client = context.get();
const auto diff = now - client.heartbeat;
if (diff > 5min || (!client.registered && diff > 20s))
{
context.remove();
}
});
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "../service.hpp"
class elimination_handler : public service
{
public:
using service::service;
void run_frame() override;
};

View File

@ -0,0 +1,56 @@
#include <std_include.hpp>
#include "getbots_command.hpp"
#include "../console.hpp"
const char* getbots_command::get_command() const
{
return "getbots";
}
void getbots_command::handle_command(const network::address& target, const std::string_view&)
{
static const std::vector<std::string> bot_names
{
"aerosoul",
"Eldor",
"FutureRave",
"Girl",
"INeedBots",
"INeedGames",
"Infamous",
"Jebus3211",
"Joel",
"JTAG",
"Laupetin",
"Louvenarde",
"OneFourOne",
"PeterG",
"quaK",
"RezTech",
"sass",
"Slykuiper",
"st0rm",
"xensik",
"xoxor4d",
"Diamante",
"Dsso",
"Evan",
"FragsAreUs",
"FryTechTip",
"H3X1C",
"homura",
"Jimbo",
"RektInator",
"Squirrel",
};
std::stringstream stream{};
for (const auto& bot : bot_names)
{
stream << bot << std::endl;
}
this->get_server().send(target, "getbotsResponse", stream.str());
console::log("Sent bot names: %s", target.to_string().data());
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class getbots_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,96 @@
#include <std_include.hpp>
#include "getservers_command.hpp"
#include "../console.hpp"
#include <utils/parameters.hpp>
namespace
{
struct prepared_server
{
uint32_t address;
uint16_t port;
};
}
constexpr auto MTU = 1400; // Real UDP MTU is more like 1500 bytes, but we keep a little wiggle room just in case
constexpr auto DPM_PROTOCOL_ADDRESS_LENGTH = sizeof prepared_server::address;
constexpr auto DPM_PROTOCOL_PORT_LENGTH = sizeof prepared_server::port;
static_assert(DPM_PROTOCOL_ADDRESS_LENGTH == 4);
static_assert(DPM_PROTOCOL_PORT_LENGTH == 2);
const char* getservers_command::get_command() const
{
return "getservers";
}
void getservers_command::handle_command(const network::address& target, const std::string_view& data)
{
const utils::parameters params(data);
if (params.size() < 2)
{
throw execution_exception("Invalid parameter count");
}
const auto& game = params[0];
const auto* p = params[1].data();
char* end;
const auto protocol = strtol(params[1].data(), &end, 10);
if (p == end)
{
throw execution_exception("Invalid protocol");
}
const auto game_type = resolve_game_type(game);
if (game_type == game_type::unknown)
{
throw execution_exception("Invalid game type: " + game);
}
std::queue<prepared_server> prepared_servers{};
this->get_server().get_server_list() //
.find_registered_servers(game_type, protocol,
[&prepared_servers](const game_server&, const network::address& address)
{
const auto addr = address.get_in_addr().sin_addr.s_addr;
const auto port = htons(address.get_port());
prepared_servers.push({ addr, port });
});
size_t packet_count = 0;
std::string response{};
while (!prepared_servers.empty())
{
const auto& server = prepared_servers.front();
response.push_back('\\');
response.append(reinterpret_cast<const char*>(&server.address), DPM_PROTOCOL_ADDRESS_LENGTH);
response.append(reinterpret_cast<const char*>(&server.port), DPM_PROTOCOL_PORT_LENGTH);
prepared_servers.pop();
if (response.size() >= MTU || prepared_servers.empty())
{
// Only send EOT if the queue is empty (last packet)
if (prepared_servers.empty())
{
response.push_back('\\');
response.append("EOT");
response.push_back('\0');
response.push_back('\0');
response.push_back('\0');
}
this->get_server().send(target, "getserversResponse", response);
packet_count++;
response.clear();
}
}
console::log("Sent %zu servers in %zu parts for game %s:\t%s", prepared_servers.size(), packet_count, game.data(), target.to_string().data());
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class getservers_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,12 @@
#include <std_include.hpp>
#include "heartbeat_command.hpp"
const char* heartbeat_command::get_command() const
{
return "heartbeat";
}
void heartbeat_command::handle_command(const network::address& target, [[maybe_unused]] const std::string_view& data)
{
this->get_server().get_server_list().heartbeat(target);
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class heartbeat_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,60 @@
#include <std_include.hpp>
#include "info_response_command.hpp"
#include "../console.hpp"
const char* info_response_command::get_command() const
{
return "infoResponse";
}
void info_response_command::handle_command(const network::address& target, const std::string_view& data)
{
const auto found = this->get_server().get_server_list().find(
target, [&data](game_server& server, const network::address& address)
{
utils::info_string info{data};
const auto game = info.get("gamename");
const auto challenge = info.get("challenge");
const auto game_type = resolve_game_type(game);
if (game_type == game_type::unknown)
{
server.state = game_server::state::dead;
throw execution_exception{"Invalid game type: " + game};
}
if (server.state != game_server::state::pinged)
{
throw execution_exception{"Stray info response"};
}
if (challenge != server.challenge)
{
throw execution_exception{"Invalid challenge"};
}
const auto player_count = atoi(info.get("clients").data());
const auto bot_count = atoi(info.get("bots").data());
auto real_player_count = player_count - bot_count;
real_player_count = std::clamp(real_player_count, 0, 18);
server.registered = true;
server.game = game_type;
server.state = game_server::state::can_ping;
server.protocol = atoi(info.get("protocol").data());
server.clients = static_cast<unsigned int>(real_player_count);
server.name = info.get("hostname");
server.heartbeat = std::chrono::high_resolution_clock::now();
server.info_string = std::move(info);
console::log("Server registered for game %s (%i):\t%s\t- %s", game.data(), server.protocol,
address.to_string().data(), server.name.data());
});
if (!found)
{
throw execution_exception{"infoResponse without server!"};
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class info_response_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

153
src/services/kill_list.cpp Normal file
View File

@ -0,0 +1,153 @@
#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();
}

View File

@ -0,0 +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();
};

View File

@ -0,0 +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));
}
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "../service.hpp"
class patch_kill_list_command : public service
{
public:
using service::service;
const char* get_command() const override;
void handle_command(const network::address& target, const std::string_view& data) override;
};

View File

@ -0,0 +1,26 @@
#include <std_include.hpp>
#include "ping_handler.hpp"
#include <utils/cryptography.hpp>
void ping_handler::run_frame()
{
auto count = 0;
this->get_server().get_server_list().iterate([&](server_list::iteration_context& context)
{
auto& server = context.get();
if (server.state == game_server::state::needs_ping)
{
server.state = game_server::state::pinged;
server.challenge = utils::cryptography::random::get_challenge();
server.heartbeat = std::chrono::high_resolution_clock::now();
this->get_server().send(context.get_address(), "getinfo", server.challenge);
if (20 >= ++count)
{
context.stop_iterating();
}
}
});
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "../service.hpp"
class ping_handler : public service
{
public:
using service::service;
void run_frame() override;
};

View File

@ -0,0 +1,86 @@
#include <std_include.hpp>
#include "statistics_handler.hpp"
#include "../console.hpp"
#include <utils/io.hpp>
#include <utils/string.hpp>
namespace
{
void write_stats(const std::map<game_type, std::vector<std::pair<std::string, network::address>>>& servers,
std::map<game_type, uint32_t>& players)
{
rapidjson::Document root{};
root.SetObject();
for (const auto& game_servers : servers)
{
const auto server_count = static_cast<uint32_t>(game_servers.second.size());
const auto player_count = players[game_servers.first];
rapidjson::Value game{};
game.SetObject();
game.AddMember("servers", server_count, root.GetAllocator());
game.AddMember("players", player_count, root.GetAllocator());
auto game_name = resolve_game_type_name(game_servers.first);
game_name = utils::string::to_lower(game_name);
rapidjson::Value game_name_object(game_name, root.GetAllocator());
root.AddMember(game_name_object, game, root.GetAllocator());
}
rapidjson::StringBuffer root_buffer{};
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::Document::EncodingType, rapidjson::ASCII<>>
root_writer(root_buffer);
root.Accept(root_writer);
std::string root_data(root_buffer.GetString(), root_buffer.GetLength());
utils::io::write_file("/var/www/master.xlabs.dev/stats.json", root_data);
}
}
statistics_handler::statistics_handler(server& server)
: service(server)
, last_print(std::chrono::high_resolution_clock::now())
{
}
void statistics_handler::run_frame()
{
const auto now = std::chrono::high_resolution_clock::now();
if (now - this->last_print < 5min)
{
return;
}
std::map<game_type, uint32_t> players{};
std::map<game_type, std::vector<std::pair<std::string, network::address>>> servers;
this->last_print = std::chrono::high_resolution_clock::now();
this->get_server().get_server_list().iterate([&servers, &players](const server_list::iteration_context& context)
{
const auto& server = context.get();
if (server.registered)
{
servers[server.game].emplace_back(server.name, context.get_address());
players[server.game] += server.clients;
}
});
console::lock _{};
for (const auto& game_servers : servers)
{
console::log("%s (%d):", resolve_game_type_name(game_servers.first).data(),
static_cast<uint32_t>(game_servers.second.size()));
for (const auto& server : game_servers.second)
{
console::log("\t%s\t%s", server.second.to_string().data(), server.first.data());
}
}
write_stats(servers, players);
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "../service.hpp"
class statistics_handler : public service
{
public:
statistics_handler(server& server);
void run_frame() override;
private:
std::chrono::high_resolution_clock::time_point last_print;
};

33
src/std_include.cpp Normal file
View File

@ -0,0 +1,33 @@
#include <std_include.hpp>
extern "C"
{
int s_read_arc4random(void*, size_t)
{
return -1;
}
#if !defined(__linux__)
int s_read_getrandom(void*, size_t)
{
return -1;
}
#endif
#ifdef _WIN32
int s_read_urandom(void*, size_t)
{
return -1;
}
int s_read_ltm_rng(void*, size_t)
{
return -1;
}
#else
int s_read_wincsp(void*, size_t)
{
return -1;
}
#endif
}

71
src/std_include.hpp Normal file
View File

@ -0,0 +1,71 @@
#ifdef _WIN32
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <fcntl.h>
#define ZeroMemory(x, y) std::memset(x, 0, y)
#endif
// min and max is required by gdi, therefore NOMINMAX won't work
#ifdef max
#undef max
#endif
#ifdef min
#undef min
#endif
#include <cassert>
#include <csignal>
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <ctime>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <map>
#include <mutex>
#include <optional>
#include <queue>
#include <regex>
#include <sstream>
#include <thread>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gsl/gsl>
#include <rapidjson/document.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/stringbuffer.h>
#ifdef _WIN32
#pragma comment(lib, "ntdll.lib")
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "urlmon.lib" )
#pragma comment(lib, "iphlpapi.lib")
#endif
using namespace std::literals;

170
src/utils/compression.cpp Normal file
View File

@ -0,0 +1,170 @@
#include <std_include.hpp>
#include "compression.hpp"
#include <zlib.h>
#include <zip.h>
#include <gsl/gsl>
#include "io.hpp"
namespace utils::compression
{
namespace zlib
{
namespace
{
class zlib_stream
{
public:
zlib_stream()
{
memset(&stream_, 0, sizeof(stream_));
valid_ = inflateInit(&stream_) == Z_OK;
}
zlib_stream(zlib_stream&&) = delete;
zlib_stream(const zlib_stream&) = delete;
zlib_stream& operator=(zlib_stream&&) = delete;
zlib_stream& operator=(const zlib_stream&) = delete;
~zlib_stream()
{
if (valid_)
{
inflateEnd(&stream_);
}
}
z_stream& get()
{
return stream_; //
}
bool is_valid() const
{
return valid_;
}
private:
bool valid_{false};
z_stream stream_{};
};
}
std::string decompress(const std::string& data)
{
std::string buffer{};
zlib_stream stream_container{};
if (!stream_container.is_valid())
{
return {};
}
int ret{};
size_t offset = 0;
static thread_local uint8_t dest[CHUNK] = {0};
auto& stream = stream_container.get();
do
{
const auto input_size = std::min(sizeof(dest), data.size() - offset);
stream.avail_in = static_cast<uInt>(input_size);
stream.next_in = reinterpret_cast<const Bytef*>(data.data()) + offset;
offset += stream.avail_in;
do
{
stream.avail_out = sizeof(dest);
stream.next_out = dest;
ret = inflate(&stream, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END)
{
return {};
}
buffer.insert(buffer.end(), dest, dest + sizeof(dest) - stream.avail_out);
}
while (stream.avail_out == 0);
}
while (ret != Z_STREAM_END);
return buffer;
}
std::string compress(const std::string& data)
{
std::string result{};
auto length = compressBound(static_cast<uLong>(data.size()));
result.resize(length);
if (compress2(reinterpret_cast<Bytef*>(result.data()), &length,
reinterpret_cast<const Bytef*>(data.data()), static_cast<uLong>(data.size()),
Z_BEST_COMPRESSION) != Z_OK)
{
return {};
}
result.resize(length);
return result;
}
}
namespace zip
{
namespace
{
bool add_file(zipFile& zip_file, const std::string& filename, const std::string& data)
{
const auto zip_64 = data.size() > 0xffffffff ? 1 : 0;
if (ZIP_OK != zipOpenNewFileInZip64(zip_file, filename.data(), nullptr, nullptr, 0, nullptr, 0, nullptr,
Z_DEFLATED, Z_BEST_COMPRESSION, zip_64))
{
return false;
}
const auto _ = gsl::finally([&zip_file]()
{
zipCloseFileInZip(zip_file);
});
return ZIP_OK == zipWriteInFileInZip(zip_file, data.data(), static_cast<unsigned>(data.size()));
}
}
void archive::add(std::string filename, std::string data)
{
this->files_[std::move(filename)] = std::move(data);
}
bool archive::write(const std::string& filename, const std::string& comment)
{
// Hack to create the directory :3
io::write_file(filename, {});
io::remove_file(filename);
auto* zip_file = zipOpen64(filename.data(), 0);
if (!zip_file)
{
return false;
}
const auto _ = gsl::finally([&zip_file, &comment]()
{
zipClose(zip_file, comment.empty() ? nullptr : comment.data());
});
for (const auto& file : this->files_)
{
if (!add_file(zip_file, file.first, file.second))
{
return false;
}
}
return true;
}
}
}

28
src/utils/compression.hpp Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <string>
#include <unordered_map>
#define CHUNK 16384u
namespace utils::compression
{
namespace zlib
{
std::string compress(const std::string& data);
std::string decompress(const std::string& data);
}
namespace zip
{
class archive
{
public:
void add(std::string filename, std::string data);
bool write(const std::string& filename, const std::string& comment = {});
private:
std::unordered_map<std::string, std::string> files_;
};
}
};

46
src/utils/concurrency.hpp Normal file
View File

@ -0,0 +1,46 @@
#pragma once
#include <mutex>
namespace utils::concurrency
{
template <typename T, typename MutexType = std::mutex>
class container
{
public:
template <typename R = void, typename F>
R access(F&& accessor) const
{
std::lock_guard<MutexType> _{mutex_};
return accessor(object_);
}
template <typename R = void, typename F>
R access(F&& accessor)
{
std::lock_guard<MutexType> _{mutex_};
return accessor(object_);
}
template <typename R = void, typename F>
R access_with_lock(F&& accessor) const
{
std::unique_lock<MutexType> lock{mutex_};
return accessor(object_, lock);
}
template <typename R = void, typename F>
R access_with_lock(F&& accessor)
{
std::unique_lock<MutexType> lock{mutex_};
return accessor(object_, lock);
}
T& get_raw() { return object_; }
const T& get_raw() const { return object_; }
private:
mutable MutexType mutex_{};
T object_{};
};
}

649
src/utils/cryptography.cpp Normal file
View File

@ -0,0 +1,649 @@
#include <std_include.hpp>
#include <random>
#include "string.hpp"
#include "cryptography.hpp"
/// http://www.opensource.apple.com/source/CommonCrypto/CommonCrypto-55010/Source/libtomcrypt/doc/libTomCryptDoc.pdf
namespace utils::cryptography
{
namespace
{
struct __
{
__()
{
ltc_mp = ltm_desc;
register_cipher(&aes_desc);
register_cipher(&des3_desc);
register_prng(&sprng_desc);
register_prng(&fortuna_desc);
register_prng(&yarrow_desc);
register_hash(&sha1_desc);
register_hash(&sha256_desc);
register_hash(&sha512_desc);
}
} ___;
[[maybe_unused]] const char* cs(const uint8_t* data)
{
return reinterpret_cast<const char*>(data);
}
[[maybe_unused]] char* cs(uint8_t* data)
{
return reinterpret_cast<char*>(data);
}
[[maybe_unused]] const uint8_t* cs(const char* data)
{
return reinterpret_cast<const uint8_t*>(data);
}
[[maybe_unused]] uint8_t* cs(char* data)
{
return reinterpret_cast<uint8_t*>(data);
}
[[maybe_unused]] unsigned long ul(const size_t value)
{
return static_cast<unsigned long>(value);
}
class prng
{
public:
prng(const ltc_prng_descriptor& descriptor, const bool autoseed = true)
: state_(std::make_unique<prng_state>())
, descriptor_(descriptor)
{
this->id_ = register_prng(&descriptor);
if (this->id_ == -1)
{
throw std::runtime_error("PRNG "s + this->descriptor_.name + " could not be registered!");
}
if (autoseed)
{
this->auto_seed();
}
else
{
this->descriptor_.start(this->state_.get());
}
}
~prng()
{
this->descriptor_.done(this->state_.get());
}
prng_state* get_state() const
{
this->descriptor_.ready(this->state_.get());
return this->state_.get();
}
int get_id() const
{
return this->id_;
}
void add_entropy(const void* data, const size_t length) const
{
this->descriptor_.add_entropy(static_cast<const uint8_t*>(data), ul(length), this->state_.get());
}
void read(void* data, const size_t length) const
{
this->descriptor_.read(static_cast<unsigned char*>(data), ul(length), this->get_state());
}
private:
int id_;
std::unique_ptr<prng_state> state_;
const ltc_prng_descriptor& descriptor_;
void auto_seed() const
{
rng_make_prng(128, this->id_, this->state_.get(), nullptr);
int i[4]; // uninitialized data
auto* i_ptr = &i;
this->add_entropy(&i, sizeof(i));
this->add_entropy(&i_ptr, sizeof(i_ptr));
auto t = std::time(nullptr);
this->add_entropy(&t, sizeof(t));
std::random_device rd{};
for (auto j = 0; j < 4; ++j)
{
const auto x = rd();
this->add_entropy(&x, sizeof(x));
}
}
};
const prng prng_(fortuna_desc);
}
ecc::key::key()
{
ZeroMemory(&this->key_storage_, sizeof(this->key_storage_));
}
ecc::key::~key()
{
this->free();
}
ecc::key::key(key&& obj) noexcept
: key()
{
this->operator=(std::move(obj));
}
ecc::key::key(const key& obj)
: key()
{
this->operator=(obj);
}
ecc::key& ecc::key::operator=(key&& obj) noexcept
{
if (this != &obj)
{
std::memmove(&this->key_storage_, &obj.key_storage_, sizeof(this->key_storage_));
ZeroMemory(&obj.key_storage_, sizeof(obj.key_storage_));
}
return *this;
}
ecc::key& ecc::key::operator=(const key& obj)
{
if (this != &obj && obj.is_valid())
{
this->deserialize(obj.serialize(obj.key_storage_.type));
}
return *this;
}
bool ecc::key::is_valid() const
{
return (!memory::is_set(&this->key_storage_, 0, sizeof(this->key_storage_)));
}
ecc_key& ecc::key::get()
{
return this->key_storage_;
}
const ecc_key& ecc::key::get() const
{
return this->key_storage_;
}
std::string ecc::key::get_public_key() const
{
uint8_t buffer[512] = {0};
unsigned long length = sizeof(buffer);
if (ecc_ansi_x963_export(&this->key_storage_, buffer, &length) == CRYPT_OK)
{
return std::string(cs(buffer), length);
}
return {};
}
void ecc::key::set(const std::string& pub_key_buffer)
{
this->free();
if (ecc_ansi_x963_import(cs(pub_key_buffer.data()),
ul(pub_key_buffer.size()),
&this->key_storage_) != CRYPT_OK)
{
ZeroMemory(&this->key_storage_, sizeof(this->key_storage_));
}
}
void ecc::key::deserialize(const std::string& key)
{
this->free();
if (ecc_import(cs(key.data()), ul(key.size()),
&this->key_storage_) != CRYPT_OK
)
{
ZeroMemory(&this->key_storage_, sizeof(this->key_storage_));
}
}
std::string ecc::key::serialize(const int type) const
{
uint8_t buffer[4096] = {0};
unsigned long length = sizeof(buffer);
if (ecc_export(buffer, &length, type, &this->key_storage_) == CRYPT_OK)
{
return std::string(cs(buffer), length);
}
return {};
}
void ecc::key::free()
{
if (this->is_valid())
{
ecc_free(&this->key_storage_);
}
ZeroMemory(&this->key_storage_, sizeof(this->key_storage_));
}
bool ecc::key::operator==(key& key) const
{
return (this->is_valid() && key.is_valid() && this->serialize(PK_PUBLIC) == key.serialize(PK_PUBLIC));
}
uint64_t ecc::key::get_hash() const
{
const auto hash = sha1::compute(this->get_public_key());
if (hash.size() >= 8)
{
return *reinterpret_cast<const uint64_t*>(hash.data());
}
return 0;
}
ecc::key ecc::generate_key(const int bits)
{
key key;
ecc_make_key(prng_.get_state(), prng_.get_id(), bits / 8, &key.get());
return key;
}
ecc::key ecc::generate_key(const int bits, const std::string& entropy)
{
key key{};
const prng yarrow(yarrow_desc, false);
yarrow.add_entropy(entropy.data(), entropy.size());
ecc_make_key(yarrow.get_state(), yarrow.get_id(), bits / 8, &key.get());
return key;
}
std::string ecc::sign_message(const key& key, const std::string& message)
{
if (!key.is_valid()) return {};
uint8_t buffer[512];
unsigned long length = sizeof(buffer);
const auto hash = sha512::compute(message);
ecc_sign_hash(cs(hash.data()), ul(hash.size()), buffer, &length, prng_.get_state(), prng_.get_id(),
&key.get());
return std::string(cs(buffer), length);
}
bool ecc::verify_message(const key& key, const std::string& message, const std::string& signature)
{
if (!key.is_valid()) return false;
const auto hash = sha512::compute(message);
auto result = 0;
return (ecc_verify_hash(cs(signature.data()),
ul(signature.size()),
cs(hash.data()),
ul(hash.size()), &result,
&key.get()) == CRYPT_OK && result != 0);
}
bool ecc::encrypt(const key& key, std::string& data)
{
std::string out_data{};
out_data.resize(std::max(ul(data.size() * 3), ul(0x100)));
auto out_len = ul(out_data.size());
auto crypt = [&]()
{
return ecc_encrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len,
prng_.get_state(), prng_.get_id(), find_hash("sha512"), &key.get());
};
auto res = crypt();
if (res == CRYPT_BUFFER_OVERFLOW)
{
out_data.resize(out_len);
res = crypt();
}
if (res != CRYPT_OK)
{
return false;
}
out_data.resize(out_len);
data = std::move(out_data);
return true;
}
bool ecc::decrypt(const key& key, std::string& data)
{
std::string out_data{};
out_data.resize(std::max(ul(data.size() * 3), ul(0x100)));
auto out_len = ul(out_data.size());
auto crypt = [&]()
{
return ecc_decrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, &key.get());
};
auto res = crypt();
if (res == CRYPT_BUFFER_OVERFLOW)
{
out_data.resize(out_len);
res = crypt();
}
if (res != CRYPT_OK)
{
return false;
}
out_data.resize(out_len);
data = std::move(out_data);
return true;
}
std::string rsa::encrypt(const std::string& data, const std::string& hash, const std::string& key)
{
rsa_key new_key;
rsa_import(cs(key.data()), ul(key.size()), &new_key);
const auto _ = gsl::finally([&]()
{
rsa_free(&new_key);
});
std::string out_data{};
out_data.resize(std::max(ul(data.size() * 3), ul(0x100)));
auto out_len = ul(out_data.size());
auto crypt = [&]()
{
return rsa_encrypt_key(cs(data.data()), ul(data.size()), cs(out_data.data()), &out_len, cs(hash.data()),
ul(hash.size()), prng_.get_state(), prng_.get_id(), find_hash("sha512"), &new_key);
};
auto res = crypt();
if (res == CRYPT_BUFFER_OVERFLOW)
{
out_data.resize(out_len);
res = crypt();
}
if (res == CRYPT_OK)
{
out_data.resize(out_len);
return out_data;
}
return {};
}
std::string des3::encrypt(const std::string& data, const std::string& iv, const std::string& key)
{
std::string enc_data;
enc_data.resize(data.size());
symmetric_CBC cbc;
const auto des3 = find_cipher("3des");
cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast<int>(key.size()), 0, &cbc);
cbc_encrypt(cs(data.data()), cs(enc_data.data()), ul(data.size()), &cbc);
cbc_done(&cbc);
return enc_data;
}
std::string des3::decrypt(const std::string& data, const std::string& iv, const std::string& key)
{
std::string dec_data;
dec_data.resize(data.size());
symmetric_CBC cbc;
const auto des3 = find_cipher("3des");
cbc_start(des3, cs(iv.data()), cs(key.data()), static_cast<int>(key.size()), 0, &cbc);
cbc_decrypt(cs(data.data()), cs(dec_data.data()), ul(data.size()), &cbc);
cbc_done(&cbc);
return dec_data;
}
std::string tiger::compute(const std::string& data, const bool hex)
{
return compute(cs(data.data()), data.size(), hex);
}
std::string tiger::compute(const uint8_t* data, const size_t length, const bool hex)
{
uint8_t buffer[24] = {0};
hash_state state;
tiger_init(&state);
tiger_process(&state, data, ul(length));
tiger_done(&state, buffer);
std::string hash(cs(buffer), sizeof(buffer));
if (!hex) return hash;
return string::dump_hex(hash, {});
}
std::string aes::encrypt(const std::string& data, const std::string& iv, const std::string& key)
{
std::string enc_data;
enc_data.resize(data.size());
symmetric_CBC cbc;
const auto aes = find_cipher("aes");
cbc_start(aes, cs(iv.data()), cs(key.data()),
static_cast<int>(key.size()), 0, &cbc);
cbc_encrypt(cs(data.data()),
cs(enc_data.data()),
ul(data.size()), &cbc);
cbc_done(&cbc);
return enc_data;
}
std::string aes::decrypt(const std::string& data, const std::string& iv, const std::string& key)
{
std::string dec_data;
dec_data.resize(data.size());
symmetric_CBC cbc;
const auto aes = find_cipher("aes");
cbc_start(aes, cs(iv.data()), cs(key.data()),
static_cast<int>(key.size()), 0, &cbc);
cbc_decrypt(cs(data.data()),
cs(dec_data.data()),
ul(data.size()), &cbc);
cbc_done(&cbc);
return dec_data;
}
std::string hmac_sha1::compute(const std::string& data, const std::string& key)
{
std::string buffer;
buffer.resize(20);
hmac_state state;
hmac_init(&state, find_hash("sha1"), cs(key.data()), ul(key.size()));
hmac_process(&state, cs(data.data()), static_cast<int>(data.size()));
auto out_len = ul(buffer.size());
hmac_done(&state, cs(buffer.data()), &out_len);
buffer.resize(out_len);
return buffer;
}
std::string sha1::compute(const std::string& data, const bool hex)
{
return compute(cs(data.data()), data.size(), hex);
}
std::string sha1::compute(const uint8_t* data, const size_t length, const bool hex)
{
uint8_t buffer[20] = {0};
hash_state state;
sha1_init(&state);
sha1_process(&state, data, ul(length));
sha1_done(&state, buffer);
std::string hash(cs(buffer), sizeof(buffer));
if (!hex) return hash;
return string::dump_hex(hash, {});
}
std::string sha256::compute(const std::string& data, const bool hex)
{
return compute(cs(data.data()), data.size(), hex);
}
std::string sha256::compute(const uint8_t* data, const size_t length, const bool hex)
{
uint8_t buffer[32] = {0};
hash_state state;
sha256_init(&state);
sha256_process(&state, data, ul(length));
sha256_done(&state, buffer);
std::string hash(cs(buffer), sizeof(buffer));
if (!hex) return hash;
return string::dump_hex(hash, {});
}
std::string sha512::compute(const std::string& data, const bool hex)
{
return compute(cs(data.data()), data.size(), hex);
}
std::string sha512::compute(const uint8_t* data, const size_t length, const bool hex)
{
uint8_t buffer[64] = {0};
hash_state state;
sha512_init(&state);
sha512_process(&state, data, ul(length));
sha512_done(&state, buffer);
std::string hash(cs(buffer), sizeof(buffer));
if (!hex) return hash;
return string::dump_hex(hash, {});
}
std::string base64::encode(const uint8_t* data, const size_t len)
{
std::string result;
result.resize((len + 2) * 2);
auto out_len = ul(result.size());
if (base64_encode(data, ul(len), result.data(), &out_len) != CRYPT_OK)
{
return {};
}
result.resize(out_len);
return result;
}
std::string base64::encode(const std::string& data)
{
return base64::encode(cs(data.data()), static_cast<unsigned>(data.size()));
}
std::string base64::decode(const std::string& data)
{
std::string result;
result.resize((data.size() + 2) * 2);
auto out_len = ul(result.size());
if (base64_decode(data.data(), ul(data.size()), cs(result.data()), &out_len) != CRYPT_OK)
{
return {};
}
result.resize(out_len);
return result;
}
unsigned int jenkins_one_at_a_time::compute(const std::string& data)
{
return compute(data.data(), data.size());
}
unsigned int jenkins_one_at_a_time::compute(const char* key, const size_t len)
{
unsigned int hash, i;
for (hash = i = 0; i < len; ++i)
{
hash += key[i];
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return hash;
}
uint32_t random::get_integer()
{
uint32_t result;
random::get_data(&result, sizeof(result));
return result;
}
std::string random::get_challenge()
{
std::string result;
result.resize(sizeof(uint32_t));
random::get_data(result.data(), result.size());
return string::dump_hex(result, {});
}
void random::get_data(void* data, const size_t size)
{
prng_.read(data, size);
}
}

118
src/utils/cryptography.hpp Normal file
View File

@ -0,0 +1,118 @@
#pragma once
#include <string>
#include <tomcrypt.h>
namespace utils::cryptography
{
namespace ecc
{
class key final
{
public:
key();
~key();
key(key&& obj) noexcept;
key(const key& obj);
key& operator=(key&& obj) noexcept;
key& operator=(const key& obj);
bool is_valid() const;
ecc_key& get();
const ecc_key& get() const;
std::string get_public_key() const;
void set(const std::string& pub_key_buffer);
void deserialize(const std::string& key);
std::string serialize(int type = PK_PRIVATE) const;
void free();
bool operator==(key& key) const;
uint64_t get_hash() const;
private:
ecc_key key_storage_{};
};
key generate_key(int bits);
key generate_key(int bits, const std::string& entropy);
std::string sign_message(const key& key, const std::string& message);
bool verify_message(const key& key, const std::string& message, const std::string& signature);
bool encrypt(const key& key, std::string& data);
bool decrypt(const key& key, std::string& data);
}
namespace rsa
{
std::string encrypt(const std::string& data, const std::string& hash, const std::string& key);
}
namespace des3
{
std::string encrypt(const std::string& data, const std::string& iv, const std::string& key);
std::string decrypt(const std::string& data, const std::string& iv, const std::string& key);
}
namespace tiger
{
std::string compute(const std::string& data, bool hex = false);
std::string compute(const uint8_t* data, size_t length, bool hex = false);
}
namespace aes
{
std::string encrypt(const std::string& data, const std::string& iv, const std::string& key);
std::string decrypt(const std::string& data, const std::string& iv, const std::string& key);
}
namespace hmac_sha1
{
std::string compute(const std::string& data, const std::string& key);
}
namespace sha1
{
std::string compute(const std::string& data, bool hex = false);
std::string compute(const uint8_t* data, size_t length, bool hex = false);
}
namespace sha256
{
std::string compute(const std::string& data, bool hex = false);
std::string compute(const uint8_t* data, size_t length, bool hex = false);
}
namespace sha512
{
std::string compute(const std::string& data, bool hex = false);
std::string compute(const uint8_t* data, size_t length, bool hex = false);
}
namespace base64
{
std::string encode(const uint8_t* data, size_t len);
std::string encode(const std::string& data);
std::string decode(const std::string& data);
}
namespace jenkins_one_at_a_time
{
unsigned int compute(const std::string& data);
unsigned int compute(const char* key, size_t len);
};
namespace random
{
uint32_t get_integer();
std::string get_challenge();
void get_data(void* data, size_t size);
}
}

59
src/utils/http.cpp Normal file
View File

@ -0,0 +1,59 @@
#include <std_include.hpp>
#include "http.hpp"
#include <curl/curl.h>
namespace utils::http
{
namespace
{
size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp)
{
static_cast<std::string*>(userp)->append(static_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
}
std::optional<std::string> get_data(const std::string& url, const headers& headers)
{
curl_slist* header_list = nullptr;
auto* curl = curl_easy_init();
if (!curl)
{
return {};
}
auto _ = gsl::finally([&]()
{
curl_slist_free_all(header_list);
curl_easy_cleanup(curl);
});
for(const auto& header : headers)
{
auto data = header.first + ": "s + header.second;
header_list = curl_slist_append(header_list, data.data());
}
std::string buffer{};
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
curl_easy_setopt(curl, CURLOPT_URL, url.data());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer);
if (curl_easy_perform(curl) == CURLE_OK)
{
return {std::move(buffer)};
}
return {};
}
std::future<std::optional<std::string>> get_data_async(const std::string& url, const headers& headers)
{
return std::async(std::launch::async, [url, headers]()
{
return get_data(url, headers);
});
}
}

14
src/utils/http.hpp Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <string>
#include <optional>
#include <future>
#include <unordered_map>
namespace utils::http
{
using headers = std::unordered_map<std::string, std::string>;
std::optional<std::string> get_data(const std::string& url, const headers& headers = {});
std::future<std::optional<std::string>> get_data_async(const std::string& url, const headers& headers = {});
}

67
src/utils/info_string.cpp Normal file
View File

@ -0,0 +1,67 @@
#include <std_include.hpp>
#include "info_string.hpp"
#include "string.hpp"
namespace utils
{
info_string::info_string(const std::string& buffer)
{
this->parse(buffer);
}
info_string::info_string(const std::string_view& buffer)
: info_string(std::string{buffer})
{
}
void info_string::set(const std::string& key, const std::string& value)
{
this->key_value_pairs_[key] = value;
}
std::string info_string::get(const std::string& key) const
{
const auto value = this->key_value_pairs_.find(key);
if (value != this->key_value_pairs_.end())
{
return value->second;
}
return {};
}
void info_string::parse(std::string buffer)
{
if (buffer[0] == '\\')
{
buffer = buffer.substr(1);
}
auto key_values = string::split(buffer, '\\');
for (size_t i = 0; !key_values.empty() && i < (key_values.size() - 1); i += 2)
{
const auto& key = key_values[i];
const auto& value = key_values[i + 1];
this->key_value_pairs_[key] = value;
}
}
std::string info_string::build() const
{
//auto first = true;
std::string info_string;
for (auto i = this->key_value_pairs_.begin(); i != this->key_value_pairs_.end(); ++i)
{
//if (first) first = false;
/*else*/
info_string.append("\\");
info_string.append(i->first); // Key
info_string.append("\\");
info_string.append(i->second); // Value
}
return info_string;
}
}

24
src/utils/info_string.hpp Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <unordered_map>
namespace utils
{
class info_string
{
public:
info_string() = default;
info_string(const std::string& buffer);
info_string(const std::string_view& buffer);
void set(const std::string& key, const std::string& value);
std::string get(const std::string& key) const;
std::string build() const;
private:
std::unordered_map<std::string, std::string> key_value_pairs_{};
void parse(std::string buffer);
};
}

132
src/utils/io.cpp Normal file
View File

@ -0,0 +1,132 @@
#include <std_include.hpp>
#include "io.hpp"
#include <fstream>
#include <ios>
namespace utils::io
{
bool remove_file(const std::string& file)
{
return remove(file.data()) == 0;
}
bool move_file(const std::string& src, const std::string& target)
{
return rename(src.data(), target.data()) == 0;
}
bool file_exists(const std::string& file)
{
return std::ifstream(file).good();
}
bool write_file(const std::string& file, const std::string& data, const bool append)
{
const auto pos = file.find_last_of("/\\");
if (pos != std::string::npos)
{
create_directory(file.substr(0, pos));
}
auto mode = std::ios::binary | std::ofstream::out;
if (append)
{
mode |= std::ofstream::app;
}
std::ofstream stream(file, mode);
if (stream.is_open())
{
stream.write(data.data(), data.size());
stream.close();
return true;
}
return false;
}
std::string read_file(const std::string& file)
{
std::string data;
read_file(file, &data);
return data;
}
bool read_file(const std::string& file, std::string* data)
{
if (!data) return false;
data->clear();
if (file_exists(file))
{
std::ifstream stream(file, std::ios::binary);
if (!stream.is_open()) return false;
stream.seekg(0, std::ios::end);
const std::streamsize size = stream.tellg();
stream.seekg(0, std::ios::beg);
if (size > -1)
{
data->resize(static_cast<uint32_t>(size));
stream.read(const_cast<char*>(data->data()), size);
stream.close();
return true;
}
}
return false;
}
size_t file_size(const std::string& file)
{
if (file_exists(file))
{
std::ifstream stream(file, std::ios::binary);
if (stream.good())
{
stream.seekg(0, std::ios::end);
return static_cast<size_t>(stream.tellg());
}
}
return 0;
}
bool create_directory(const std::string& directory)
{
return std::filesystem::create_directories(directory);
}
bool directory_exists(const std::string& directory)
{
return std::filesystem::is_directory(directory);
}
bool directory_is_empty(const std::string& directory)
{
return std::filesystem::is_empty(directory);
}
std::vector<std::string> list_files(const std::string& directory)
{
std::vector<std::string> files;
for (auto& file : std::filesystem::directory_iterator(directory))
{
files.push_back(file.path().generic_string());
}
return files;
}
void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target)
{
std::filesystem::copy(src, target,
std::filesystem::copy_options::overwrite_existing |
std::filesystem::copy_options::recursive);
}
}

21
src/utils/io.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <vector>
#include <filesystem>
namespace utils::io
{
bool remove_file(const std::string& file);
bool move_file(const std::string& src, const std::string& target);
bool file_exists(const std::string& file);
bool write_file(const std::string& file, const std::string& data, bool append = false);
bool read_file(const std::string& file, std::string* data);
std::string read_file(const std::string& file);
size_t file_size(const std::string& file);
bool create_directory(const std::string& directory);
bool directory_exists(const std::string& directory);
bool directory_is_empty(const std::string& directory);
std::vector<std::string> list_files(const std::string& directory);
void copy_folder(const std::filesystem::path& src, const std::filesystem::path& target);
}

107
src/utils/memory.cpp Normal file
View File

@ -0,0 +1,107 @@
#include <std_include.hpp>
#include "memory.hpp"
namespace utils
{
memory::allocator memory::mem_allocator_;
memory::allocator::~allocator()
{
this->clear();
}
void memory::allocator::clear()
{
std::lock_guard _(this->mutex_);
for (auto& data : this->pool_)
{
memory::free(data);
}
this->pool_.clear();
}
void memory::allocator::free(void* data)
{
std::lock_guard _(this->mutex_);
const auto j = std::find(this->pool_.begin(), this->pool_.end(), data);
if (j != this->pool_.end())
{
memory::free(data);
this->pool_.erase(j);
}
}
void memory::allocator::free(const void* data)
{
this->free(const_cast<void*>(data));
}
void* memory::allocator::allocate(const size_t length)
{
std::lock_guard _(this->mutex_);
const auto data = memory::allocate(length);
this->pool_.push_back(data);
return data;
}
bool memory::allocator::empty() const
{
return this->pool_.empty();
}
char* memory::allocator::duplicate_string(const std::string& string)
{
std::lock_guard _(this->mutex_);
const auto data = memory::duplicate_string(string);
this->pool_.push_back(data);
return data;
}
void* memory::allocate(const size_t length)
{
return ::calloc(length, 1);
}
char* memory::duplicate_string(const std::string& string)
{
const auto new_string = allocate_array<char>(string.size() + 1);
std::memcpy(new_string, string.data(), string.size());
return new_string;
}
void memory::free(void* data)
{
::free(data);
}
void memory::free(const void* data)
{
free(const_cast<void*>(data));
}
bool memory::is_set(const void* mem, const char chr, const size_t length)
{
auto* const mem_arr = static_cast<const char*>(mem);
for (size_t i = 0; i < length; ++i)
{
if (mem_arr[i] != chr)
{
return false;
}
}
return true;
}
memory::allocator* memory::get_allocator()
{
return &memory::mem_allocator_;
}
}

71
src/utils/memory.hpp Normal file
View File

@ -0,0 +1,71 @@
#pragma once
#include <mutex>
#include <vector>
namespace utils
{
class memory final
{
public:
class allocator final
{
public:
~allocator();
void clear();
void free(void* data);
void free(const void* data);
void* allocate(size_t length);
template <typename T>
inline T* allocate()
{
return this->allocate_array<T>(1);
}
template <typename T>
inline T* allocate_array(const size_t count = 1)
{
return static_cast<T*>(this->allocate(count * sizeof(T)));
}
bool empty() const;
char* duplicate_string(const std::string& string);
private:
std::mutex mutex_;
std::vector<void*> pool_;
};
static void* allocate(size_t length);
template <typename T>
static inline T* allocate()
{
return allocate_array<T>(1);
}
template <typename T>
static inline T* allocate_array(const size_t count = 1)
{
return static_cast<T*>(allocate(count * sizeof(T)));
}
static char* duplicate_string(const std::string& string);
static void free(void* data);
static void free(const void* data);
static bool is_set(const void* mem, char chr, size_t length);
static allocator* get_allocator();
private:
static allocator mem_allocator_;
};
}

92
src/utils/parameters.cpp Normal file
View File

@ -0,0 +1,92 @@
#include <std_include.hpp>
#include "parameters.hpp"
#include "string.hpp"
namespace utils
{
parameters::parameters(std::string buffer)
{
while (!buffer.empty() && (buffer.back() == '\0' || isspace(buffer.back())))
{
buffer.pop_back();
}
this->arguments_ = string::split(buffer, ' ');
}
parameters::parameters(const std::string_view& buffer)
: parameters(std::string{buffer.data(), buffer.size()})
{
}
void parameters::add(std::string value)
{
this->arguments_.emplace_back(std::move(value));
}
size_t parameters::size() const
{
return this->arguments_.size();
}
const std::string& parameters::get(const size_t index) const
{
return this->arguments_.at(index);
}
std::string parameters::join(const size_t index, const std::string& separator) const
{
std::string buffer{};
for (auto i = index; i < this->size(); ++i)
{
if (i != 0)
{
buffer.append(separator);
}
buffer.append(this->get(i));
}
return buffer;
}
const std::string& parameters::operator[](const size_t index) const
{
return this->get(index);
}
parameters::list_type::iterator parameters::begin()
{
return this->arguments_.begin();
}
parameters::list_type::const_iterator parameters::begin() const
{
return this->arguments_.begin();
}
parameters::list_type::iterator parameters::end()
{
return this->arguments_.end();
}
parameters::list_type::const_iterator parameters::end() const
{
return this->arguments_.end();
}
bool parameters::has(const std::string& value) const
{
for (const auto& val : this->arguments_)
{
if (val == value)
{
return true;
}
}
return false;
}
}

35
src/utils/parameters.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <unordered_map>
namespace utils
{
class parameters
{
public:
using list_type = std::vector<std::string>;
parameters() = default;
parameters(std::string buffer);
parameters(const std::string_view& buffer);
void add(std::string value);
size_t size() const;
std::string join(size_t index = 0, const std::string& separator = " ") const;
const std::string& operator [](size_t index) const;
const std::string& get(size_t index) const;
list_type::iterator begin();
list_type::const_iterator begin() const;
list_type::iterator end();
list_type::const_iterator end() const;
bool has(const std::string& value) const;
private:
list_type arguments_{};
};
}

161
src/utils/string.cpp Normal file
View File

@ -0,0 +1,161 @@
#include <std_include.hpp>
#include "string.hpp"
#include <sstream>
#include <cstdarg>
#include <algorithm>
namespace utils::string
{
const char* va(const char* fmt, ...)
{
static thread_local va_provider<8, 256> provider;
va_list ap;
va_start(ap, fmt);
const char* result = provider.get(fmt, ap);
va_end(ap);
return result;
}
std::vector<std::string> split(const std::string& s, const char delim)
{
std::stringstream ss(s);
std::string item;
std::vector<std::string> elems;
while (std::getline(ss, item, delim))
{
elems.push_back(std::move(item));
item = std::string{};
}
return elems;
}
std::string to_lower(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input)
{
return static_cast<char>(tolower(input));
});
return text;
}
std::string to_upper(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(), [](const unsigned char input)
{
return static_cast<char>(toupper(input));
});
return text;
}
bool starts_with(const std::string& text, const std::string& substring)
{
return text.find(substring) == 0;
}
bool ends_with(const std::string& text, const std::string& substring)
{
if (substring.size() > text.size()) return false;
return std::equal(substring.rbegin(), substring.rend(), text.rbegin());
}
std::string dump_hex(const std::string& data, const std::string& separator)
{
std::string result;
for (unsigned int i = 0; i < data.size(); ++i)
{
if (i > 0)
{
result.append(separator);
}
result.append(va("%02X", data[i] & 0xFF));
}
return result;
}
void strip(const char* in, char* out, int max)
{
if (!in || !out) return;
max--;
auto current = 0;
while (*in != 0 && current < max)
{
const auto color_index = (*(in + 1) - 48) >= 0xC ? 7 : (*(in + 1) - 48);
if (*in == '^' && (color_index != 7 || *(in + 1) == '7'))
{
++in;
}
else
{
*out = *in;
++out;
++current;
}
++in;
}
*out = '\0';
}
#ifdef _WIN32
#pragma warning(push)
#pragma warning(disable: 4100)
#endif
std::string convert(const std::wstring& wstr)
{
std::string result;
result.reserve(wstr.size());
for (const auto& chr : wstr)
{
result.push_back(static_cast<char>(chr));
}
return result;
}
std::wstring convert(const std::string& str)
{
std::wstring result;
result.reserve(str.size());
for (const auto& chr : str)
{
result.push_back(static_cast<wchar_t>(chr));
}
return result;
}
#ifdef _WIN32
#pragma warning(pop)
#endif
std::string replace(std::string str, const std::string& from, const std::string& to)
{
if (from.empty())
{
return str;
}
size_t start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != std::string::npos)
{
str.replace(start_pos, from.length(), to);
start_pos += to.length();
}
return str;
}
}

105
src/utils/string.hpp Normal file
View File

@ -0,0 +1,105 @@
#pragma once
#include "memory.hpp"
#include <cstdint>
#ifndef ARRAYSIZE
template <class Type, size_t n>
size_t ARRAYSIZE(Type (&)[n]) { return n; }
#endif
namespace utils::string
{
template <size_t Buffers, size_t MinBufferSize>
class va_provider final
{
public:
static_assert(Buffers != 0 && MinBufferSize != 0, "Buffers and MinBufferSize mustn't be 0");
va_provider() : current_buffer_(0)
{
}
char* get(const char* format, va_list ap)
{
++this->current_buffer_ %= ARRAYSIZE(this->string_pool_);
auto entry = &this->string_pool_[this->current_buffer_];
if (!entry->size || !entry->buffer)
{
throw std::runtime_error("String pool not initialized");
}
while (true)
{
#ifdef _WIN32
const int res = vsnprintf_s(entry->buffer, entry->size, _TRUNCATE, format, ap);
#else
const int res = vsnprintf(entry->buffer, entry->size, format, ap);
#endif
if (res > 0) break; // Success
if (res == 0) return nullptr; // Error
entry->double_size();
}
return entry->buffer;
}
private:
class entry final
{
public:
explicit entry(const size_t _size = MinBufferSize) : size(_size), buffer(nullptr)
{
if (this->size < MinBufferSize) this->size = MinBufferSize;
this->allocate();
}
~entry()
{
if (this->buffer) memory::get_allocator()->free(this->buffer);
this->size = 0;
this->buffer = nullptr;
}
void allocate()
{
if (this->buffer) memory::get_allocator()->free(this->buffer);
this->buffer = memory::get_allocator()->allocate_array<char>(this->size + 1);
}
void double_size()
{
this->size *= 2;
this->allocate();
}
size_t size;
char* buffer;
};
size_t current_buffer_;
entry string_pool_[Buffers];
};
const char* va(const char* fmt, ...);
std::vector<std::string> split(const std::string& s, char delim);
std::vector<std::string> split(const std::string_view& s, char delim);
std::string to_lower(std::string text);
std::string to_upper(std::string text);
bool starts_with(const std::string& text, const std::string& substring);
bool ends_with(const std::string& text, const std::string& substring);
std::string dump_hex(const std::string& data, const std::string& separator = " ");
void strip(const char* in, char* out, int max);
std::string convert(const std::wstring& wstr);
std::wstring convert(const std::string& str);
std::string replace(std::string str, const std::string& from, const std::string& to);
}