feat: add smartcmd dedicated server

major bug: when connecting to the server a second time, it gets softlocked and keeps repeating ticking connection A\n in debug logs until it times out.
This commit is contained in:
2026-08-29 14:01:24 +03:00
parent a1541c590d
commit 4b962fcb21
146 changed files with 32837 additions and 272 deletions
+460
View File
@@ -0,0 +1,460 @@
#include "stdafx.h"
#include "Access.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include <memory>
#include <mutex>
namespace ServerRuntime
{
namespace Access
{
namespace
{
/**
* **Access State**
*
* These features are used extensively from various parts of the code, so safe read/write handling is implemented
* Stores the published BAN manager snapshot plus a writer gate for clone-and-publish updates
* 公開中のBanManagerスナップショットと更新直列化用ロックを保持する
*/
struct AccessState
{
std::mutex stateLock;
std::mutex writeLock;
std::shared_ptr<BanManager> banManager;
std::shared_ptr<WhitelistManager> whitelistManager;
bool whitelistEnabled;
};
AccessState g_accessState;
/**
* Copies the currently published manager pointer so readers can work without holding the publish mutex
* 公開中のBanManager共有ポインタを複製取得する
*/
static std::shared_ptr<BanManager> GetBanManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.banManager;
}
/**
* Replaces the shared manager pointer with a fully prepared snapshot in one short critical section
* 準備完了したBanManagerスナップショットを短いロックで公開する
*/
static void PublishBanManagerSnapshot(const std::shared_ptr<BanManager> &banManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.banManager = banManager;
}
static std::shared_ptr<WhitelistManager> GetWhitelistManagerSnapshot()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistManager;
}
static void PublishWhitelistManagerSnapshot(const std::shared_ptr<WhitelistManager> &whitelistManager)
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistManager = whitelistManager;
}
}
std::string FormatXuid(PlayerUID xuid)
{
if (xuid == INVALID_XUID)
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", (unsigned long long)xuid);
return buffer;
}
bool TryParseXuid(const std::string &text, PlayerUID *outXuid)
{
if (outXuid == nullptr)
{
return false;
}
unsigned long long parsed = 0;
if (!StringUtils::TryParseUnsignedLongLong(text, &parsed) || parsed == 0ULL)
{
return false;
}
*outXuid = (PlayerUID)parsed;
return true;
}
bool Initialize(const std::string &baseDirectory, bool whitelistEnabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
// Build the replacement manager privately so readers keep using the last published snapshot during disk I/O.
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(baseDirectory);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(baseDirectory);
if (!banManager->EnsureBanFilesExist())
{
LogError("access", "failed to ensure dedicated server ban files exist");
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
LogError("access", "failed to ensure dedicated server whitelist file exists");
return false;
}
if (!banManager->Reload())
{
LogError("access", "failed to load dedicated server ban files");
return false;
}
if (!whitelistManager->Reload())
{
LogError("access", "failed to load dedicated server whitelist file");
return false;
}
std::vector<BannedPlayerEntry> playerEntries;
std::vector<BannedIpEntry> ipEntries;
std::vector<WhitelistedPlayerEntry> whitelistEntries;
banManager->SnapshotBannedPlayers(&playerEntries);
banManager->SnapshotBannedIps(&ipEntries);
whitelistManager->SnapshotWhitelistedPlayers(&whitelistEntries);
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = whitelistEnabled;
}
LogInfof(
"access",
"loaded %u player bans, %u ip bans, and %u whitelist entries (whitelist=%s)",
(unsigned)playerEntries.size(),
(unsigned)ipEntries.size(),
(unsigned)whitelistEntries.size(),
whitelistEnabled ? "enabled" : "disabled");
return true;
}
void Shutdown()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>());
PublishWhitelistManagerSnapshot(std::shared_ptr<WhitelistManager>());
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = false;
}
bool Reload()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
std::shared_ptr<WhitelistManager> currentWhitelist = GetWhitelistManagerSnapshot();
if (current == nullptr || currentWhitelist == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
std::shared_ptr<WhitelistManager> whitelistManager = std::make_shared<WhitelistManager>(*currentWhitelist);
if (!banManager->EnsureBanFilesExist())
{
return false;
}
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!banManager->Reload())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishBanManagerSnapshot(banManager);
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool ReloadWhitelist()
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->EnsureWhitelistFileExists())
{
return false;
}
if (!whitelistManager->Reload())
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool IsInitialized()
{
return GetBanManagerSnapshot() != nullptr && GetWhitelistManagerSnapshot() != nullptr;
}
bool IsWhitelistEnabled()
{
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.whitelistEnabled;
}
void SetWhitelistEnabled(bool enabled)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
g_accessState.whitelistEnabled = enabled;
}
bool IsPlayerBanned(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsPlayerBannedByXuid(formatted) : false;
}
bool IsIpBanned(const std::string &ip)
{
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
return (banManager != nullptr) ? banManager->IsIpBanned(ip) : false;
}
bool IsPlayerWhitelisted(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::shared_ptr<WhitelistManager> whitelistManager = GetWhitelistManagerSnapshot();
return (whitelistManager != nullptr) ? whitelistManager->IsPlayerWhitelistedByXuid(formatted) : false;
}
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedPlayerEntry entry;
entry.xuid = formatted;
entry.name = name;
entry.metadata = metadata;
if (!banManager->AddPlayerBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddIpBan(const std::string &ip, const BanMetadata &metadata)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
BannedIpEntry entry;
entry.ip = ip;
entry.metadata = metadata;
if (!banManager->AddIpBan(entry))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemovePlayerBan(PlayerUID xuid)
{
const std::string formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemovePlayerBanByXuid(formatted))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool RemoveIpBan(const std::string &ip)
{
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
std::shared_ptr<BanManager> current = GetBanManagerSnapshot();
if (current == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = std::make_shared<BanManager>(*current);
if (!banManager->RemoveIpBan(ip))
{
return false;
}
PublishBanManagerSnapshot(banManager);
return true;
}
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
const WhitelistedPlayerEntry entry = { formatted, name, metadata };
if (!whitelistManager->AddPlayer(entry))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool RemoveWhitelistedPlayer(PlayerUID xuid)
{
const auto formatted = FormatXuid(xuid);
if (formatted.empty())
{
return false;
}
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
const auto current = GetWhitelistManagerSnapshot();
if (current == nullptr)
{
return false;
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
if (!whitelistManager->RemovePlayerByXuid(formatted))
{
return false;
}
PublishWhitelistManagerSnapshot(whitelistManager);
return true;
}
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedPlayers(outEntries);
}
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
std::shared_ptr<BanManager> banManager = GetBanManagerSnapshot();
if (banManager == nullptr)
{
outEntries->clear();
return false;
}
return banManager->SnapshotBannedIps(outEntries);
}
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries)
{
if (outEntries == nullptr)
{
return false;
}
const auto whitelistManager = GetWhitelistManagerSnapshot();
if (whitelistManager == nullptr)
{
outEntries->clear();
return false;
}
return whitelistManager->SnapshotWhitelistedPlayers(outEntries);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "BanManager.h"
#include "WhitelistManager.h"
namespace ServerRuntime
{
/**
* A frontend that will be general-purpose, assuming the implementation of whitelists and ops in the future.
*/
namespace Access
{
bool Initialize(const std::string &baseDirectory = ".", bool whitelistEnabled = false);
void Shutdown();
bool Reload();
bool ReloadWhitelist();
bool IsInitialized();
bool IsWhitelistEnabled();
void SetWhitelistEnabled(bool enabled);
bool IsPlayerBanned(PlayerUID xuid);
bool IsIpBanned(const std::string &ip);
bool IsPlayerWhitelisted(PlayerUID xuid);
bool AddPlayerBan(PlayerUID xuid, const std::string &name, const BanMetadata &metadata);
bool AddIpBan(const std::string &ip, const BanMetadata &metadata);
bool RemovePlayerBan(PlayerUID xuid);
bool RemoveIpBan(const std::string &ip);
bool AddWhitelistedPlayer(PlayerUID xuid, const std::string &name, const WhitelistMetadata &metadata);
bool RemoveWhitelistedPlayer(PlayerUID xuid);
/**
* Copies the current cached player bans for inspection or command output
* 現在のプレイヤーBAN一覧を複製取得
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries);
/**
* Copies the current cached IP bans for inspection or command output
* 現在のIP BAN一覧を複製取得
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries);
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries);
std::string FormatXuid(PlayerUID xuid);
bool TryParseXuid(const std::string &text, PlayerUID *outXuid);
}
}
+682
View File
@@ -0,0 +1,682 @@
#include "stdafx.h"
#include "BanManager.h"
#include "../Common/AccessStorageUtils.h"
#include "../Common/FileUtils.h"
#include "../Common/NetworkUtils.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include "../vendor/rapidjson/document.h"
#include "../vendor/rapidjson/error/en.h"
#include "../vendor/rapidjson/prettywriter.h"
#include "../vendor/rapidjson/stringbuffer.h"
#include <algorithm>
#include <stdio.h>
namespace ServerRuntime
{
namespace Access
{
namespace
{
static const char *kBannedPlayersFileName = "banned-players.json";
static const char *kBannedIpsFileName = "banned-ips.json";
static bool TryParseUtcTimestamp(const std::string &text, unsigned long long *outFileTime)
{
if (outFileTime == nullptr)
{
return false;
}
std::string trimmed = StringUtils::TrimAscii(text);
if (trimmed.empty())
{
return false;
}
unsigned year = 0;
unsigned month = 0;
unsigned day = 0;
unsigned hour = 0;
unsigned minute = 0;
unsigned second = 0;
if (sscanf_s(trimmed.c_str(), "%4u-%2u-%2uT%2u:%2u:%2uZ", &year, &month, &day, &hour, &minute, &second) != 6)
{
return false;
}
SYSTEMTIME utc = {};
utc.wYear = (WORD)year;
utc.wMonth = (WORD)month;
utc.wDay = (WORD)day;
utc.wHour = (WORD)hour;
utc.wMinute = (WORD)minute;
utc.wSecond = (WORD)second;
FILETIME fileTime = {};
if (!SystemTimeToFileTime(&utc, &fileTime))
{
return false;
}
ULARGE_INTEGER value = {};
value.LowPart = fileTime.dwLowDateTime;
value.HighPart = fileTime.dwHighDateTime;
*outFileTime = value.QuadPart;
return true;
}
static bool IsMetadataExpired(const BanMetadata &metadata, unsigned long long nowFileTime)
{
if (metadata.expires.empty())
{
return false;
}
unsigned long long expiresFileTime = 0;
if (!TryParseUtcTimestamp(metadata.expires, &expiresFileTime))
{
// Keep malformed metadata active instead of silently unbanning a player or address.
return false;
}
return expiresFileTime <= nowFileTime;
}
}
BanManager::BanManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool BanManager::EnsureBanFilesExist() const
{
const std::string playersPath = GetBannedPlayersFilePath();
const std::string ipsPath = GetBannedIpsFilePath();
const bool playersOk = AccessStorageUtils::EnsureJsonListFileExists(playersPath);
const bool ipsOk = AccessStorageUtils::EnsureJsonListFileExists(ipsPath);
if (!playersOk)
{
LogErrorf("access", "failed to create %s", playersPath.c_str());
}
if (!ipsOk)
{
LogErrorf("access", "failed to create %s", ipsPath.c_str());
}
return playersOk && ipsOk;
}
bool BanManager::Reload()
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
if (!LoadPlayers(&players))
{
return false;
}
if (!LoadIps(&ips))
{
return false;
}
m_bannedPlayers.swap(players);
m_bannedIps.swap(ips);
return true;
}
bool BanManager::Save() const
{
std::vector<BannedPlayerEntry> players;
std::vector<BannedIpEntry> ips;
return SnapshotBannedPlayers(&players) &&
SnapshotBannedIps(&ips) &&
SavePlayers(players) &&
SaveIps(ips);
}
bool BanManager::LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedPlayersFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping banned player entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping banned player entry without xuid in %s", path.c_str());
continue;
}
BannedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping banned player entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::LoadIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetBannedIpsFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping banned ip entry that is not an object in %s", path.c_str());
continue;
}
std::string rawIp;
if (!AccessStorageUtils::TryGetStringField(object, "ip", &rawIp))
{
LogWarnf("access", "skipping banned ip entry without ip in %s", path.c_str());
continue;
}
BannedIpEntry entry;
entry.ip = NetworkUtils::NormalizeIpToken(rawIp);
if (entry.ip.empty())
{
LogWarnf("access", "skipping banned ip entry with empty ip in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires);
AccessStorageUtils::TryGetStringField(object, "reason", &entry.metadata.reason);
NormalizeMetadata(&entry.metadata);
// Ignore entries that already expired before reload so the in-memory cache starts from the active set.
if (IsMetadataExpired(entry.metadata, nowFileTime))
{
continue;
}
outEntries->push_back(entry);
}
return true;
}
bool BanManager::SavePlayers(const std::vector<BannedPlayerEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedXuid = AccessStorageUtils::NormalizeXuid(entry.xuid);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"xuid", rapidjson::Value(normalizedXuid.c_str(), static_cast<rapidjson::SizeType>(normalizedXuid.size()), allocator), allocator);
object.AddMember(
"name", rapidjson::Value(entry.name.c_str(), static_cast<rapidjson::SizeType>(entry.name.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
object.AddMember(
"expires",
rapidjson::Value(entry.metadata.expires.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.expires.size()), allocator),
allocator);
object.AddMember(
"reason",
rapidjson::Value(entry.metadata.reason.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.reason.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetBannedPlayersFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
bool BanManager::SaveIps(const std::vector<BannedIpEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedIp = NetworkUtils::NormalizeIpToken(entry.ip);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"ip", rapidjson::Value(normalizedIp.c_str(), static_cast<rapidjson::SizeType>(normalizedIp.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
object.AddMember(
"expires",
rapidjson::Value(entry.metadata.expires.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.expires.size()), allocator),
allocator);
object.AddMember(
"reason",
rapidjson::Value(entry.metadata.reason.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.reason.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetBannedIpsFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<BannedPlayerEntry> &BanManager::GetBannedPlayers() const
{
return m_bannedPlayers;
}
const std::vector<BannedIpEntry> &BanManager::GetBannedIps() const
{
return m_bannedIps;
}
bool BanManager::SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedPlayers.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedPlayers)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
outEntries->reserve(m_bannedIps.size());
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
for (const auto &entry : m_bannedIps)
{
if (!IsMetadataExpired(entry.metadata, nowFileTime))
{
outEntries->push_back(entry);
}
}
return true;
}
bool BanManager::IsPlayerBannedByXuid(const std::string &xuid) const
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedPlayers.begin(),
m_bannedPlayers.end(),
[&normalized, nowFileTime](const BannedPlayerEntry &entry)
{
return entry.xuid == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::IsIpBanned(const std::string &ip) const
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
return std::any_of(
m_bannedIps.begin(),
m_bannedIps.end(),
[&normalized, nowFileTime](const BannedIpEntry &entry)
{
return entry.ip == normalized && !IsMetadataExpired(entry.metadata, nowFileTime);
});
}
bool BanManager::AddPlayerBan(const BannedPlayerEntry &entry)
{
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
BannedPlayerEntry normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by canonical XUID.
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::AddIpBan(const BannedIpEntry &entry)
{
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
BannedIpEntry normalized = entry;
normalized.ip = NetworkUtils::NormalizeIpToken(normalized.ip);
NormalizeMetadata(&normalized.metadata);
if (normalized.ip.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &candidate)
{
return candidate.ip == normalized.ip;
});
if (existing != updatedEntries.end())
{
// Update the existing entry in place so the stored list remains unique by normalized IP.
*existing = normalized;
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
bool BanManager::RemovePlayerBanByXuid(const std::string &xuid)
{
const std::string normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<BannedPlayerEntry> updatedEntries;
if (!SnapshotBannedPlayers(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_bannedPlayers.swap(updatedEntries);
return true;
}
bool BanManager::RemoveIpBan(const std::string &ip)
{
const std::string normalized = NetworkUtils::NormalizeIpToken(ip);
if (normalized.empty())
{
return false;
}
std::vector<BannedIpEntry> updatedEntries;
if (!SnapshotBannedIps(&updatedEntries))
{
return false;
}
size_t oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const BannedIpEntry &entry) { return entry.ip == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SaveIps(updatedEntries))
{
return false;
}
m_bannedIps.swap(updatedEntries);
return true;
}
std::string BanManager::GetBannedPlayersFilePath() const
{
return BuildPath(kBannedPlayersFileName);
}
std::string BanManager::GetBannedIpsFilePath() const
{
return BuildPath(kBannedIpsFileName);
}
BanMetadata BanManager::BuildDefaultMetadata(const char *source)
{
BanMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
metadata.expires = "";
metadata.reason = "";
return metadata;
}
void BanManager::NormalizeMetadata(BanMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
metadata->expires = StringUtils::TrimAscii(metadata->expires);
metadata->reason = StringUtils::TrimAscii(metadata->reason);
}
std::string BanManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
/**
* Information shared with player bans and IP bans
* プレイヤーBANとIP BANで共有する情報
*/
struct BanMetadata
{
std::string created;
std::string source;
std::string expires;
std::string reason;
};
struct BannedPlayerEntry
{
std::string xuid;
std::string name;
BanMetadata metadata;
};
struct BannedIpEntry
{
std::string ip;
BanMetadata metadata;
};
/**
* Dedicated server BAN file manager.
*
* Files:
* - banned-players.json
* - banned-ips.json
*
* This class only handles storage/caching.
* Connection-time hooks are wired separately.
*/
class BanManager
{
public:
/**
* **Create Ban Manager**
*
* Binds the manager to the directory that stores the dedicated-server access files
* Dedicated Server のアクセスファイル配置先を設定する
*/
explicit BanManager(const std::string &baseDirectory = ".");
/**
* Creates empty JSON array files when the dedicated server starts without persisted access data
* BANファイルが無い初回起動時に空JSONを用意する
*/
bool EnsureBanFilesExist() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
bool LoadIps(std::vector<BannedIpEntry> *outEntries) const;
bool SavePlayers(const std::vector<BannedPlayerEntry> &entries) const;
bool SaveIps(const std::vector<BannedIpEntry> &entries) const;
const std::vector<BannedPlayerEntry> &GetBannedPlayers() const;
const std::vector<BannedIpEntry> &GetBannedIps() const;
/**
* Copies only currently active player BAN entries so expired metadata does not leak into command output
* 期限切れを除いた有効なプレイヤーBAN一覧を複製取得する
*/
bool SnapshotBannedPlayers(std::vector<BannedPlayerEntry> *outEntries) const;
/**
* Copies only currently active IP BAN entries so expired metadata does not leak into command output
* 期限切れを除いた有効なIP BAN一覧を複製取得する
*/
bool SnapshotBannedIps(std::vector<BannedIpEntry> *outEntries) const;
bool IsPlayerBannedByXuid(const std::string &xuid) const;
bool IsIpBanned(const std::string &ip) const;
bool AddPlayerBan(const BannedPlayerEntry &entry);
bool AddIpBan(const BannedIpEntry &entry);
bool RemovePlayerBanByXuid(const std::string &xuid);
bool RemoveIpBan(const std::string &ip);
std::string GetBannedPlayersFilePath() const;
std::string GetBannedIpsFilePath() const;
static BanMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(BanMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<BannedPlayerEntry> m_bannedPlayers;
std::vector<BannedIpEntry> m_bannedIps;
};
}
}
@@ -0,0 +1,318 @@
#include "stdafx.h"
#include "WhitelistManager.h"
#include "../Common/AccessStorageUtils.h"
#include "../Common/FileUtils.h"
#include "../Common/StringUtils.h"
#include "../ServerLogger.h"
#include "../vendor/rapidjson/document.h"
#include "../vendor/rapidjson/error/en.h"
#include "../vendor/rapidjson/prettywriter.h"
#include "../vendor/rapidjson/stringbuffer.h"
#include <algorithm>
namespace ServerRuntime
{
namespace Access
{
namespace
{
static const char *kWhitelistFileName = "whitelist.json";
}
WhitelistManager::WhitelistManager(const std::string &baseDirectory)
: m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory)
{
}
bool WhitelistManager::EnsureWhitelistFileExists() const
{
const std::string path = GetWhitelistFilePath();
if (!AccessStorageUtils::EnsureJsonListFileExists(path))
{
LogErrorf("access", "failed to create %s", path.c_str());
return false;
}
return true;
}
bool WhitelistManager::Reload()
{
std::vector<WhitelistedPlayerEntry> players;
if (!LoadPlayers(&players))
{
return false;
}
m_whitelistedPlayers.swap(players);
return true;
}
bool WhitelistManager::Save() const
{
std::vector<WhitelistedPlayerEntry> players;
return SnapshotWhitelistedPlayers(&players) && SavePlayers(players);
}
bool WhitelistManager::LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
outEntries->clear();
std::string text;
const std::string path = GetWhitelistFilePath();
if (!FileUtils::ReadTextFile(path, &text))
{
LogErrorf("access", "failed to read %s", path.c_str());
return false;
}
if (text.empty())
{
text = "[]";
}
const std::string stripped = StringUtils::StripUtf8Bom(text);
rapidjson::Document root;
const rapidjson::ParseResult parseResult = root.Parse(stripped.c_str());
if (!parseResult)
{
LogErrorf(
"access",
"failed to parse %s: %s (offset %zu)",
path.c_str(),
rapidjson::GetParseError_En(parseResult.Code()),
parseResult.Offset());
return false;
}
if (!root.IsArray())
{
LogErrorf("access", "failed to parse %s: root json value is not an array", path.c_str());
return false;
}
for (const auto &object : root.GetArray())
{
if (!object.IsObject())
{
LogWarnf("access", "skipping whitelist entry that is not an object in %s", path.c_str());
continue;
}
std::string rawXuid;
if (!AccessStorageUtils::TryGetStringField(object, "xuid", &rawXuid))
{
LogWarnf("access", "skipping whitelist entry without xuid in %s", path.c_str());
continue;
}
WhitelistedPlayerEntry entry;
entry.xuid = AccessStorageUtils::NormalizeXuid(rawXuid);
if (entry.xuid.empty())
{
LogWarnf("access", "skipping whitelist entry with empty xuid in %s", path.c_str());
continue;
}
AccessStorageUtils::TryGetStringField(object, "name", &entry.name);
AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created);
AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source);
NormalizeMetadata(&entry.metadata);
outEntries->push_back(entry);
}
return true;
}
bool WhitelistManager::SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const
{
rapidjson::Document root(rapidjson::kArrayType);
auto &allocator = root.GetAllocator();
for (const auto &entry : entries)
{
const std::string normalizedXuid = AccessStorageUtils::NormalizeXuid(entry.xuid);
rapidjson::Value object(rapidjson::kObjectType);
object.AddMember(
"xuid", rapidjson::Value(normalizedXuid.c_str(), static_cast<rapidjson::SizeType>(normalizedXuid.size()), allocator), allocator);
object.AddMember(
"name", rapidjson::Value(entry.name.c_str(), static_cast<rapidjson::SizeType>(entry.name.size()), allocator), allocator);
object.AddMember(
"created",
rapidjson::Value(entry.metadata.created.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.created.size()), allocator),
allocator);
object.AddMember(
"source",
rapidjson::Value(entry.metadata.source.c_str(), static_cast<rapidjson::SizeType>(entry.metadata.source.size()), allocator),
allocator);
root.PushBack(object, allocator);
}
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
writer.SetIndent(' ', 2);
root.Accept(writer);
const std::string path = GetWhitelistFilePath();
const std::string json = root.Empty() ? std::string("[]\n") : (std::string(buffer.GetString(), buffer.GetSize()) + "\n");
if (!FileUtils::WriteTextFileAtomic(path, json))
{
LogErrorf("access", "failed to write %s", path.c_str());
return false;
}
return true;
}
const std::vector<WhitelistedPlayerEntry> &WhitelistManager::GetWhitelistedPlayers() const
{
return m_whitelistedPlayers;
}
bool WhitelistManager::SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const
{
if (outEntries == nullptr)
{
return false;
}
*outEntries = m_whitelistedPlayers;
return true;
}
bool WhitelistManager::IsPlayerWhitelistedByXuid(const std::string &xuid) const
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
return std::any_of(
m_whitelistedPlayers.begin(),
m_whitelistedPlayers.end(),
[&normalized](const WhitelistedPlayerEntry &entry)
{
return entry.xuid == normalized;
});
}
bool WhitelistManager::AddPlayer(const WhitelistedPlayerEntry &entry)
{
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
auto normalized = entry;
normalized.xuid = AccessStorageUtils::NormalizeXuid(normalized.xuid);
NormalizeMetadata(&normalized.metadata);
if (normalized.xuid.empty())
{
return false;
}
const auto existing = std::find_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &candidate)
{
return candidate.xuid == normalized.xuid;
});
if (existing != updatedEntries.end())
{
*existing = normalized;
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
updatedEntries.push_back(normalized);
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
bool WhitelistManager::RemovePlayerByXuid(const std::string &xuid)
{
const auto normalized = AccessStorageUtils::NormalizeXuid(xuid);
if (normalized.empty())
{
return false;
}
std::vector<WhitelistedPlayerEntry> updatedEntries;
if (!SnapshotWhitelistedPlayers(&updatedEntries))
{
return false;
}
const auto oldSize = updatedEntries.size();
updatedEntries.erase(
std::remove_if(
updatedEntries.begin(),
updatedEntries.end(),
[&normalized](const WhitelistedPlayerEntry &entry) { return entry.xuid == normalized; }),
updatedEntries.end());
if (updatedEntries.size() == oldSize)
{
return false;
}
if (!SavePlayers(updatedEntries))
{
return false;
}
m_whitelistedPlayers.swap(updatedEntries);
return true;
}
std::string WhitelistManager::GetWhitelistFilePath() const
{
return BuildPath(kWhitelistFileName);
}
WhitelistMetadata WhitelistManager::BuildDefaultMetadata(const char *source)
{
WhitelistMetadata metadata;
metadata.created = StringUtils::GetCurrentUtcTimestampIso8601();
metadata.source = (source != nullptr) ? source : "Server";
return metadata;
}
void WhitelistManager::NormalizeMetadata(WhitelistMetadata *metadata)
{
if (metadata == nullptr)
{
return;
}
metadata->created = StringUtils::TrimAscii(metadata->created);
metadata->source = StringUtils::TrimAscii(metadata->source);
}
std::string WhitelistManager::BuildPath(const char *fileName) const
{
return AccessStorageUtils::BuildPathFromBaseDirectory(m_baseDirectory, fileName);
}
}
}
@@ -0,0 +1,64 @@
#pragma once
#include <string>
#include <vector>
namespace ServerRuntime
{
namespace Access
{
struct WhitelistMetadata
{
std::string created;
std::string source;
};
struct WhitelistedPlayerEntry
{
std::string xuid;
std::string name;
WhitelistMetadata metadata;
};
/**
* whitelist manager
*
* Files:
* - whitelist.json
*
* Stores and normalizes XUID-based allow entries.
*/
class WhitelistManager
{
public:
explicit WhitelistManager(const std::string &baseDirectory = ".");
bool EnsureWhitelistFileExists() const;
bool Reload();
bool Save() const;
bool LoadPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool SavePlayers(const std::vector<WhitelistedPlayerEntry> &entries) const;
const std::vector<WhitelistedPlayerEntry> &GetWhitelistedPlayers() const;
bool SnapshotWhitelistedPlayers(std::vector<WhitelistedPlayerEntry> *outEntries) const;
bool IsPlayerWhitelistedByXuid(const std::string &xuid) const;
bool AddPlayer(const WhitelistedPlayerEntry &entry);
bool RemovePlayerByXuid(const std::string &xuid);
std::string GetWhitelistFilePath() const;
static WhitelistMetadata BuildDefaultMetadata(const char *source = "Server");
private:
static void NormalizeMetadata(WhitelistMetadata *metadata);
std::string BuildPath(const char *fileName) const;
private:
std::string m_baseDirectory;
std::vector<WhitelistedPlayerEntry> m_whitelistedPlayers;
};
}
}