mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-08-20 09:57:09 +00:00
add: Dedicated Server BAN access manager with persistent player and IP bans
- add Access frontend that publishes thread-safe ban manager snapshots for dedicated server use - add BanManager storage for banned-players.json and banned-ips.json with load/save/update flows - add persistent player and IP ban checks during dedicated server connection handling - add UTF-8 BOM-safe JSON parsing and shared file helpers backed by nlohmann/json - add Unicode-safe ban file read/write and safer atomic replacement behavior on Windows - add active-ban snapshot APIs and expiry-aware filtering for expires metadata - add RAII-based dedicated access shutdown handling during server startup and teardown
This commit is contained in:
parent
da284c1387
commit
b535baa7c6
|
|
@ -91,6 +91,8 @@ target_link_libraries(MinecraftClient PRIVATE
|
|||
set(MINECRAFT_SERVER_SOURCES ${MINECRAFT_CLIENT_SOURCES})
|
||||
list(APPEND MINECRAFT_SERVER_SOURCES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Windows64/ServerMain.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Access/Access.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Access/BanManager.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCli.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliInput.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliParser.cpp"
|
||||
|
|
@ -115,8 +117,8 @@ target_include_directories(MinecraftServer PRIVATE
|
|||
"${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Windows64"
|
||||
)
|
||||
target_compile_definitions(MinecraftServer PRIVATE
|
||||
$<$<CONFIG:Debug>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64>
|
||||
$<$<NOT:$<CONFIG:Debug>>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64>
|
||||
$<$<CONFIG:Debug>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD>
|
||||
$<$<NOT:$<CONFIG:Debug>>:_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD>
|
||||
)
|
||||
if(MSVC)
|
||||
configure_msvc_target(MinecraftServer)
|
||||
|
|
@ -217,4 +219,3 @@ add_custom_command(TARGET MinecraftServer POST_BUILD
|
|||
)
|
||||
|
||||
set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT MinecraftServer)
|
||||
|
||||
|
|
|
|||
|
|
@ -180,11 +180,21 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
|
|||
duplicateXuid = true;
|
||||
}
|
||||
|
||||
bool bannedXuid = false;
|
||||
if (loginXuid != INVALID_XUID)
|
||||
{
|
||||
bannedXuid = server->getPlayers()->isXuidBanned(loginXuid);
|
||||
}
|
||||
if (!bannedXuid && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid)
|
||||
{
|
||||
bannedXuid = server->getPlayers()->isXuidBanned(packet->m_onlineXuid);
|
||||
}
|
||||
|
||||
if( sentDisconnect )
|
||||
{
|
||||
// Do nothing
|
||||
}
|
||||
else if( server->getPlayers()->isXuidBanned( packet->m_onlineXuid ) )
|
||||
else if (bannedXuid)
|
||||
{
|
||||
disconnect(DisconnectPacket::eDisconnect_Banned);
|
||||
}
|
||||
|
|
@ -323,4 +333,4 @@ bool PendingConnection::isServerPacketListener()
|
|||
bool PendingConnection::isDisconnected()
|
||||
{
|
||||
return done;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@
|
|||
#include "Common\Network\Sony\NetworkPlayerSony.h"
|
||||
#endif
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\Minecraft.Server\Access\Access.h"
|
||||
extern bool g_Win64DedicatedServer;
|
||||
#endif
|
||||
|
||||
// 4J - this class is fairly substantially altered as there didn't seem any point in porting code for banning, whitelisting, ops etc.
|
||||
|
||||
PlayerList::PlayerList(MinecraftServer *server)
|
||||
|
|
@ -1633,6 +1638,13 @@ bool PlayerList::isXuidBanned(PlayerUID xuid)
|
|||
}
|
||||
}
|
||||
|
||||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||||
if (!banned && g_Win64DedicatedServer)
|
||||
{
|
||||
banned = ServerRuntime::Access::IsPlayerBanned(xuid);
|
||||
}
|
||||
#endif
|
||||
|
||||
return banned;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,20 @@
|
|||
#include "WinsockNetLayer.h"
|
||||
#include "..\..\Common\Network\PlatformNetworkManagerStub.h"
|
||||
#include "..\..\..\Minecraft.World\Socket.h"
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
#include "..\..\..\Minecraft.Server\Access\Access.h"
|
||||
#endif
|
||||
#include "..\..\..\Minecraft.World\DisconnectPacket.h"
|
||||
#include "..\..\Minecraft.h"
|
||||
#include "..\4JLibs\inc\4J_Profile.h"
|
||||
|
||||
static bool RecvExact(SOCKET sock, BYTE* buf, int len);
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp);
|
||||
#endif
|
||||
|
||||
SOCKET WinsockNetLayer::s_listenSocket = INVALID_SOCKET;
|
||||
SOCKET WinsockNetLayer::s_hostConnectionSocket = INVALID_SOCKET;
|
||||
HANDLE WinsockNetLayer::s_acceptThread = NULL;
|
||||
|
|
@ -475,6 +483,27 @@ static bool RecvExact(SOCKET sock, BYTE* buf, int len)
|
|||
return true;
|
||||
}
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
static bool TryGetNumericRemoteIp(const sockaddr_in &remoteAddress, std::string *outIp)
|
||||
{
|
||||
if (outIp == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
outIp->clear();
|
||||
char ipBuffer[64] = {};
|
||||
const char *ip = inet_ntop(AF_INET, (void *)&remoteAddress.sin_addr, ipBuffer, sizeof(ipBuffer));
|
||||
if (ip == NULL || ip[0] == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*outIp = ip;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void WinsockNetLayer::HandleDataReceived(BYTE fromSmallId, BYTE toSmallId, unsigned char* data, unsigned int dataSize)
|
||||
{
|
||||
INetworkPlayer* pPlayerFrom = g_NetworkManager.GetPlayerBySmallId(fromSmallId);
|
||||
|
|
@ -500,7 +529,10 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
|
|||
{
|
||||
while (s_active)
|
||||
{
|
||||
SOCKET clientSocket = accept(s_listenSocket, NULL, NULL);
|
||||
sockaddr_in remoteAddress;
|
||||
ZeroMemory(&remoteAddress, sizeof(remoteAddress));
|
||||
int remoteAddressLength = sizeof(remoteAddress);
|
||||
SOCKET clientSocket = accept(s_listenSocket, (sockaddr*)&remoteAddress, &remoteAddressLength);
|
||||
if (clientSocket == INVALID_SOCKET)
|
||||
{
|
||||
if (s_active)
|
||||
|
|
@ -511,6 +543,20 @@ DWORD WINAPI WinsockNetLayer::AcceptThreadProc(LPVOID param)
|
|||
int noDelay = 1;
|
||||
setsockopt(clientSocket, IPPROTO_TCP, TCP_NODELAY, (const char*)&noDelay, sizeof(noDelay));
|
||||
|
||||
#if defined(MINECRAFT_SERVER_BUILD)
|
||||
if (g_Win64DedicatedServer)
|
||||
{
|
||||
std::string remoteIp;
|
||||
if (TryGetNumericRemoteIp(remoteAddress, &remoteIp) && ServerRuntime::Access::IsIpBanned(remoteIp))
|
||||
{
|
||||
app.DebugPrintf("Win64 LAN: Rejecting banned ip %s\n", remoteIp.c_str());
|
||||
SendRejectWithReason(clientSocket, DisconnectPacket::eDisconnect_Banned);
|
||||
closesocket(clientSocket);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extern QNET_STATE _iQNetStubState;
|
||||
if (_iQNetStubState != QNET_STATE_GAME_PLAY)
|
||||
{
|
||||
|
|
|
|||
278
Minecraft.Server/Access/Access.cpp
Normal file
278
Minecraft.Server/Access/Access.cpp
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
#include "stdafx.h"
|
||||
|
||||
#include "Access.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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 Initialize(const std::string &baseDirectory)
|
||||
{
|
||||
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);
|
||||
if (!banManager->EnsureBanFilesExist())
|
||||
{
|
||||
LogError("access", "failed to ensure dedicated server ban files exist");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!banManager->Reload())
|
||||
{
|
||||
LogError("access", "failed to load dedicated server ban files");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<BannedPlayerEntry> playerEntries;
|
||||
std::vector<BannedIpEntry> ipEntries;
|
||||
banManager->SnapshotBannedPlayers(&playerEntries);
|
||||
banManager->SnapshotBannedIps(&ipEntries);
|
||||
PublishBanManagerSnapshot(banManager);
|
||||
|
||||
LogInfof(
|
||||
"access",
|
||||
"loaded %u player bans and %u ip bans",
|
||||
(unsigned)playerEntries.size(),
|
||||
(unsigned)ipEntries.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
std::lock_guard<std::mutex> writeLock(g_accessState.writeLock);
|
||||
PublishBanManagerSnapshot(std::shared_ptr<BanManager>());
|
||||
}
|
||||
|
||||
bool Reload()
|
||||
{
|
||||
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->EnsureBanFilesExist())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!banManager->Reload())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PublishBanManagerSnapshot(banManager);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsInitialized()
|
||||
{
|
||||
return GetBanManagerSnapshot() != nullptr;
|
||||
}
|
||||
|
||||
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 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Minecraft.Server/Access/Access.h
Normal file
38
Minecraft.Server/Access/Access.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include "BanManager.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 = ".");
|
||||
void Shutdown();
|
||||
bool Reload();
|
||||
bool IsInitialized();
|
||||
|
||||
bool IsPlayerBanned(PlayerUID xuid);
|
||||
bool IsIpBanned(const std::string &ip);
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
std::string FormatXuid(PlayerUID xuid);
|
||||
}
|
||||
}
|
||||
781
Minecraft.Server/Access/BanManager.cpp
Normal file
781
Minecraft.Server/Access/BanManager.cpp
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
#include "stdafx.h"
|
||||
|
||||
#include "BanManager.h"
|
||||
|
||||
#include "..\Common\FileUtils.h"
|
||||
#include "..\Common\StringUtils.h"
|
||||
#include "..\ServerLogger.h"
|
||||
#include "..\vendor\nlohmann\json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace ServerRuntime
|
||||
{
|
||||
namespace Access
|
||||
{
|
||||
using OrderedJson = nlohmann::ordered_json;
|
||||
|
||||
namespace
|
||||
{
|
||||
static const char *kBannedPlayersFileName = "banned-players.json";
|
||||
static const char *kBannedIpsFileName = "banned-ips.json";
|
||||
|
||||
static bool FileExists(const std::string &path)
|
||||
{
|
||||
const std::wstring widePath = StringUtils::Utf8ToWide(path);
|
||||
if (widePath.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD attrs = GetFileAttributesW(widePath.c_str());
|
||||
return (attrs != INVALID_FILE_ATTRIBUTES) && ((attrs & FILE_ATTRIBUTE_DIRECTORY) == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty array file for access lists that do not exist yet
|
||||
* 未作成のアクセス一覧に空配列ファイルを作る
|
||||
*/
|
||||
static bool EnsureJsonListFile(const std::string &path)
|
||||
{
|
||||
if (FileExists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return FileUtils::WriteTextFileAtomic(path, "[]\n");
|
||||
}
|
||||
|
||||
static bool TryParseNumericXuid(const std::string &text, unsigned long long *outValue)
|
||||
{
|
||||
if (outValue == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string trimmed = StringUtils::TrimAscii(text);
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
char *end = nullptr;
|
||||
// Accept both decimal and hexadecimal XUID text so manual edits and tool output stay compatible.
|
||||
unsigned long long value = _strtoui64(trimmed.c_str(), &end, 0);
|
||||
if (end == trimmed.c_str() || errno != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')
|
||||
{
|
||||
++end;
|
||||
}
|
||||
|
||||
if (*end != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*outValue = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryGetStringField(const OrderedJson &object, const char *key, std::string *outValue)
|
||||
{
|
||||
if (key == nullptr || outValue == nullptr || !object.is_object())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OrderedJson::const_iterator it = object.find(key);
|
||||
if (it == object.end() || !it->is_string())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*outValue = it->get<std::string>();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
|
||||
bool playersOk = EnsureJsonListFile(playersPath);
|
||||
bool ipsOk = EnsureJsonListFile(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 = "[]";
|
||||
}
|
||||
|
||||
OrderedJson root;
|
||||
try
|
||||
{
|
||||
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
|
||||
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
|
||||
}
|
||||
catch (const nlohmann::json::exception &e)
|
||||
{
|
||||
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!root.is_array())
|
||||
{
|
||||
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 (size_t i = 0; i < root.size(); ++i)
|
||||
{
|
||||
const OrderedJson &object = root[i];
|
||||
if (!object.is_object())
|
||||
{
|
||||
LogWarnf("access", "skipping banned player entry that is not an object in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string rawXuid;
|
||||
if (!TryGetStringField(object, "xuid", &rawXuid))
|
||||
{
|
||||
LogWarnf("access", "skipping banned player entry without xuid in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
BannedPlayerEntry entry;
|
||||
entry.xuid = NormalizeXuid(rawXuid);
|
||||
if (entry.xuid.empty())
|
||||
{
|
||||
LogWarnf("access", "skipping banned player entry with empty xuid in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
TryGetStringField(object, "name", &entry.name);
|
||||
TryGetStringField(object, "created", &entry.metadata.created);
|
||||
TryGetStringField(object, "source", &entry.metadata.source);
|
||||
TryGetStringField(object, "expires", &entry.metadata.expires);
|
||||
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 = "[]";
|
||||
}
|
||||
|
||||
OrderedJson root;
|
||||
try
|
||||
{
|
||||
// Strip an optional UTF-8 BOM because some editors prepend it when rewriting JSON files.
|
||||
root = OrderedJson::parse(StringUtils::StripUtf8Bom(text));
|
||||
}
|
||||
catch (const nlohmann::json::exception &e)
|
||||
{
|
||||
LogErrorf("access", "failed to parse %s: %s", path.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!root.is_array())
|
||||
{
|
||||
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 (size_t i = 0; i < root.size(); ++i)
|
||||
{
|
||||
const OrderedJson &object = root[i];
|
||||
if (!object.is_object())
|
||||
{
|
||||
LogWarnf("access", "skipping banned ip entry that is not an object in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string rawIp;
|
||||
if (!TryGetStringField(object, "ip", &rawIp))
|
||||
{
|
||||
LogWarnf("access", "skipping banned ip entry without ip in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
BannedIpEntry entry;
|
||||
entry.ip = NormalizeIp(rawIp);
|
||||
if (entry.ip.empty())
|
||||
{
|
||||
LogWarnf("access", "skipping banned ip entry with empty ip in %s", path.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
TryGetStringField(object, "created", &entry.metadata.created);
|
||||
TryGetStringField(object, "source", &entry.metadata.source);
|
||||
TryGetStringField(object, "expires", &entry.metadata.expires);
|
||||
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
|
||||
{
|
||||
OrderedJson root = OrderedJson::array();
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
OrderedJson object = OrderedJson::object();
|
||||
object["xuid"] = NormalizeXuid(entries[i].xuid);
|
||||
object["name"] = entries[i].name;
|
||||
object["created"] = entries[i].metadata.created;
|
||||
object["source"] = entries[i].metadata.source;
|
||||
object["expires"] = entries[i].metadata.expires;
|
||||
object["reason"] = entries[i].metadata.reason;
|
||||
root.push_back(object);
|
||||
}
|
||||
|
||||
const std::string path = GetBannedPlayersFilePath();
|
||||
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\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
|
||||
{
|
||||
OrderedJson root = OrderedJson::array();
|
||||
for (size_t i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
OrderedJson object = OrderedJson::object();
|
||||
object["ip"] = NormalizeIp(entries[i].ip);
|
||||
object["created"] = entries[i].metadata.created;
|
||||
object["source"] = entries[i].metadata.source;
|
||||
object["expires"] = entries[i].metadata.expires;
|
||||
object["reason"] = entries[i].metadata.reason;
|
||||
root.push_back(object);
|
||||
}
|
||||
|
||||
const std::string path = GetBannedIpsFilePath();
|
||||
const std::string json = root.empty() ? std::string("[]\n") : (root.dump(2) + "\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 (size_t i = 0; i < m_bannedPlayers.size(); ++i)
|
||||
{
|
||||
if (!IsMetadataExpired(m_bannedPlayers[i].metadata, nowFileTime))
|
||||
{
|
||||
outEntries->push_back(m_bannedPlayers[i]);
|
||||
}
|
||||
}
|
||||
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 (size_t i = 0; i < m_bannedIps.size(); ++i)
|
||||
{
|
||||
if (!IsMetadataExpired(m_bannedIps[i].metadata, nowFileTime))
|
||||
{
|
||||
outEntries->push_back(m_bannedIps[i]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool BanManager::IsPlayerBannedByXuid(const std::string &xuid) const
|
||||
{
|
||||
const std::string normalized = NormalizeXuid(xuid);
|
||||
if (normalized.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
|
||||
for (size_t i = 0; i < m_bannedPlayers.size(); ++i)
|
||||
{
|
||||
if (m_bannedPlayers[i].xuid == normalized && !IsMetadataExpired(m_bannedPlayers[i].metadata, nowFileTime))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BanManager::IsIpBanned(const std::string &ip) const
|
||||
{
|
||||
const std::string normalized = NormalizeIp(ip);
|
||||
if (normalized.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsigned long long nowFileTime = FileUtils::GetCurrentUtcFileTime();
|
||||
for (size_t i = 0; i < m_bannedIps.size(); ++i)
|
||||
{
|
||||
if (m_bannedIps[i].ip == normalized && !IsMetadataExpired(m_bannedIps[i].metadata, nowFileTime))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BanManager::AddPlayerBan(const BannedPlayerEntry &entry)
|
||||
{
|
||||
std::vector<BannedPlayerEntry> updatedEntries;
|
||||
if (!SnapshotBannedPlayers(&updatedEntries))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BannedPlayerEntry normalized = entry;
|
||||
normalized.xuid = NormalizeXuid(normalized.xuid);
|
||||
NormalizeMetadata(&normalized.metadata);
|
||||
if (normalized.xuid.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < updatedEntries.size(); ++i)
|
||||
{
|
||||
// Update the existing entry in place so the stored list remains unique by canonical XUID.
|
||||
if (updatedEntries[i].xuid == normalized.xuid)
|
||||
{
|
||||
updatedEntries[i] = 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 = NormalizeIp(normalized.ip);
|
||||
NormalizeMetadata(&normalized.metadata);
|
||||
if (normalized.ip.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < updatedEntries.size(); ++i)
|
||||
{
|
||||
// Update the existing entry in place so the stored list remains unique by normalized IP.
|
||||
if (updatedEntries[i].ip == normalized.ip)
|
||||
{
|
||||
updatedEntries[i] = 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 = 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 = NormalizeIp(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;
|
||||
|
||||
SYSTEMTIME utc;
|
||||
GetSystemTime(&utc);
|
||||
|
||||
char created[64] = {};
|
||||
sprintf_s(
|
||||
created,
|
||||
sizeof(created),
|
||||
"%04u-%02u-%02uT%02u:%02u:%02uZ",
|
||||
(unsigned)utc.wYear,
|
||||
(unsigned)utc.wMonth,
|
||||
(unsigned)utc.wDay,
|
||||
(unsigned)utc.wHour,
|
||||
(unsigned)utc.wMinute,
|
||||
(unsigned)utc.wSecond);
|
||||
metadata.created = created;
|
||||
metadata.source = (source != nullptr) ? source : "Server";
|
||||
metadata.expires = "";
|
||||
metadata.reason = "";
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
||||
std::string BanManager::NormalizeXuid(const std::string &xuid)
|
||||
{
|
||||
std::string trimmed = StringUtils::TrimAscii(xuid);
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
unsigned long long numericXuid = 0;
|
||||
// Canonicalize numeric XUID input into a lowercase hexadecimal string so lookups stay stable.
|
||||
if (TryParseNumericXuid(trimmed, &numericXuid))
|
||||
{
|
||||
if (numericXuid == 0ULL)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
char buffer[32] = {};
|
||||
sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return StringUtils::ToLowerAscii(trimmed);
|
||||
}
|
||||
|
||||
|
||||
std::string BanManager::NormalizeIp(const std::string &ip)
|
||||
{
|
||||
return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip));
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
{
|
||||
if (fileName == nullptr || fileName[0] == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::wstring wideFileName = StringUtils::Utf8ToWide(fileName);
|
||||
if (wideFileName.empty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (m_baseDirectory.empty() || m_baseDirectory == ".")
|
||||
{
|
||||
return StringUtils::WideToUtf8(wideFileName);
|
||||
}
|
||||
|
||||
const std::wstring wideBaseDirectory = StringUtils::Utf8ToWide(m_baseDirectory);
|
||||
if (wideBaseDirectory.empty())
|
||||
{
|
||||
return StringUtils::WideToUtf8(wideFileName);
|
||||
}
|
||||
|
||||
// Join Unicode-aware path strings after conversion so non-ASCII dedicated-server directories remain supported.
|
||||
const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1];
|
||||
if (last == L'\\' || last == L'/')
|
||||
{
|
||||
return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName);
|
||||
}
|
||||
|
||||
return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
Minecraft.Server/Access/BanManager.h
Normal file
108
Minecraft.Server/Access/BanManager.h
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#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 std::string NormalizeXuid(const std::string &xuid);
|
||||
static std::string NormalizeIp(const std::string &ip);
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
<RuntimeTypeInfo>true</RuntimeTypeInfo>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<PreprocessorDefinitions>_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_DEBUG;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Windows64\Iggy\include;..\Minecraft.Client\Xbox\Sentient\Include;..\Minecraft.World\x64headers;$(ProjectDir)Windows64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<MASM>
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
<ExceptionHandling>Sync</ExceptionHandling>
|
||||
<RuntimeTypeInfo>true</RuntimeTypeInfo>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
<PreprocessorDefinitions>_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>_LARGE_WORLDS;_DEBUG_MENUS_ENABLED;_CRT_NON_CONFORMING_SWPRINTFS;_CRT_SECURE_NO_WARNINGS;_WINDOWS64;MINECRAFT_SERVER_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>..\Minecraft.Client;..\Minecraft.Client\Windows64\Iggy\include;..\Minecraft.Client\Xbox\Sentient\Include;..\Minecraft.World\x64headers;$(ProjectDir)Windows64;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<MASM>
|
||||
|
|
@ -124,6 +124,8 @@
|
|||
</PostBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Access\Access.cpp" />
|
||||
<ClCompile Include="Access\BanManager.cpp" />
|
||||
<ClCompile Include="..\Minecraft.Client\AbstractTexturePack.cpp" />
|
||||
<ClCompile Include="..\Minecraft.Client\AchievementPopup.cpp" />
|
||||
<ClCompile Include="..\Minecraft.Client\AchievementScreen.cpp" />
|
||||
|
|
@ -674,6 +676,8 @@
|
|||
<ClCompile Include="Windows64\ServerMain.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Access\Access.h" />
|
||||
<ClInclude Include="Access\BanManager.h" />
|
||||
<ClInclude Include="Console\ServerCli.h" />
|
||||
<ClInclude Include="Console\ServerCliInput.h" />
|
||||
<ClInclude Include="Console\commands\CliCommandGamemode.h" />
|
||||
|
|
@ -709,4 +713,3 @@
|
|||
<Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
<Filter Include="Server\Console\Commands">
|
||||
<UniqueIdentifier>{7C28D123-0DA3-4B17-84C0-E326F5A75740}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Server\Access">
|
||||
<UniqueIdentifier>{29AB58D1-E8A9-465A-B3EA-BC5E9110A7A1}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="Server\Common">
|
||||
<UniqueIdentifier>{BC6FD58B-1A40-45FE-B8D9-1A087C25126D}</UniqueIdentifier>
|
||||
</Filter>
|
||||
|
|
@ -48,6 +51,12 @@
|
|||
<ClCompile Include="Windows64\ServerMain.cpp">
|
||||
<Filter>Server</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Access\Access.cpp">
|
||||
<Filter>Server\Access</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Access\BanManager.cpp">
|
||||
<Filter>Server\Access</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Common\FileUtils.cpp">
|
||||
<Filter>Server\Common</Filter>
|
||||
</ClCompile>
|
||||
|
|
@ -575,6 +584,12 @@
|
|||
<ClInclude Include="Console\ServerCliRegistry.h">
|
||||
<Filter>Server\Console</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Access\Access.h">
|
||||
<Filter>Server\Access</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Access\BanManager.h">
|
||||
<Filter>Server\Access</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Common\FileUtils.h">
|
||||
<Filter>Server\Common</Filter>
|
||||
</ClInclude>
|
||||
|
|
@ -620,4 +635,3 @@
|
|||
<MASM Include="..\Minecraft.Client\iob_shim.asm" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "Input.h"
|
||||
#include "Minecraft.h"
|
||||
#include "MinecraftServer.h"
|
||||
#include "..\Access\Access.h"
|
||||
#include "..\Common\StringUtils.h"
|
||||
#include "..\ServerLogger.h"
|
||||
#include "..\ServerProperties.h"
|
||||
|
|
@ -66,6 +67,34 @@ static volatile bool g_shutdownRequested = false;
|
|||
static const DWORD kDefaultAutosaveIntervalMs = 60 * 1000;
|
||||
static const int kServerActionPad = 0;
|
||||
|
||||
/**
|
||||
* Calls Access::Shutdown automatically once dedicated access control was initialized successfully
|
||||
* アクセス制御初期化後のShutdownを自動化する
|
||||
*/
|
||||
class AccessShutdownGuard
|
||||
{
|
||||
public:
|
||||
AccessShutdownGuard()
|
||||
: m_active(false)
|
||||
{
|
||||
}
|
||||
|
||||
void Activate()
|
||||
{
|
||||
m_active = true;
|
||||
}
|
||||
|
||||
~AccessShutdownGuard()
|
||||
{
|
||||
if (m_active)
|
||||
{
|
||||
ServerRuntime::Access::Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_active;
|
||||
};
|
||||
static BOOL WINAPI ConsoleCtrlHandlerProc(DWORD ctrlType)
|
||||
{
|
||||
switch (ctrlType)
|
||||
|
|
@ -324,6 +353,7 @@ int main(int argc, char **argv)
|
|||
|
||||
SetServerLogLevel(config.logLevel);
|
||||
LogStartupStep("initializing process state");
|
||||
AccessShutdownGuard accessShutdownGuard;
|
||||
|
||||
g_iScreenWidth = 1280;
|
||||
g_iScreenHeight = 720;
|
||||
|
|
@ -339,6 +369,13 @@ int main(int argc, char **argv)
|
|||
g_Win64DedicatedServerPort = config.port;
|
||||
strncpy_s(g_Win64DedicatedServerBindIP, sizeof(g_Win64DedicatedServerBindIP), config.bindIP, _TRUNCATE);
|
||||
g_Win64DedicatedServerLanAdvertise = serverProperties.lanAdvertise;
|
||||
LogStartupStep("initializing dedicated access control");
|
||||
if (!ServerRuntime::Access::Initialize("."))
|
||||
{
|
||||
LogError("startup", "Failed to initialize dedicated server access control.");
|
||||
return 2;
|
||||
}
|
||||
accessShutdownGuard.Activate();
|
||||
LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled");
|
||||
|
||||
LogStartupStep("registering hidden window class");
|
||||
|
|
@ -349,6 +386,7 @@ int main(int argc, char **argv)
|
|||
if (!InitInstance(hInstance, SW_HIDE))
|
||||
{
|
||||
LogError("startup", "Failed to create window instance.");
|
||||
|
||||
return 2;
|
||||
}
|
||||
ShowWindow(g_hWnd, SW_HIDE);
|
||||
|
|
@ -358,6 +396,7 @@ int main(int argc, char **argv)
|
|||
{
|
||||
LogError("startup", "Failed to initialize D3D device.");
|
||||
CleanupDevice();
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
|
@ -414,6 +453,7 @@ int main(int argc, char **argv)
|
|||
{
|
||||
LogError("startup", "Minecraft initialization failed.");
|
||||
CleanupDevice();
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
|
@ -479,6 +519,7 @@ int main(int argc, char **argv)
|
|||
WinsockNetLayer::Shutdown();
|
||||
g_NetworkManager.Terminate();
|
||||
CleanupDevice();
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
|
|
@ -514,6 +555,7 @@ int main(int argc, char **argv)
|
|||
WinsockNetLayer::Shutdown();
|
||||
g_NetworkManager.Terminate();
|
||||
CleanupDevice();
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
|
|
@ -612,6 +654,7 @@ int main(int argc, char **argv)
|
|||
WinsockNetLayer::Shutdown();
|
||||
g_NetworkManager.Terminate();
|
||||
CleanupDevice();
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue