diff --git a/CMakeLists.txt b/CMakeLists.txt index 50e426015..969113244 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ 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/Access/WhitelistManager.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" @@ -111,6 +112,7 @@ list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandStop.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandList.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandTp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandWhitelist.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandGamemode.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/FileUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Common/StringUtils.cpp" diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index 4c26cfa32..f22aa370b 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -16,6 +16,7 @@ #include "Settings.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" +#include "..\Minecraft.Server\Access\Access.h" #include "..\Minecraft.World\Socket.h" #endif // #ifdef __PS3__ @@ -213,6 +214,22 @@ void PendingConnection::handleLogin(shared_ptr packet) bannedXuid = server->getPlayers()->isXuidBanned(packet->m_onlineXuid); } + bool whitelistSatisfied = true; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (ServerRuntime::Access::IsWhitelistEnabled()) + { + whitelistSatisfied = false; + if (loginXuid != INVALID_XUID) + { + whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(loginXuid); + } + if (!whitelistSatisfied && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid) + { + whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(packet->m_onlineXuid); + } + } +#endif + if( sentDisconnect ) { // Do nothing @@ -221,6 +238,13 @@ void PendingConnection::handleLogin(shared_ptr packet) { #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid); +#endif + disconnect(DisconnectPacket::eDisconnect_Banned); + } + else if (!whitelistSatisfied) + { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_NotWhitelisted); #endif disconnect(DisconnectPacket::eDisconnect_Banned); } diff --git a/Minecraft.Server/Access/Access.cpp b/Minecraft.Server/Access/Access.cpp index b568e27e2..30b1cf9b6 100644 --- a/Minecraft.Server/Access/Access.cpp +++ b/Minecraft.Server/Access/Access.cpp @@ -2,10 +2,13 @@ #include "Access.h" +#include "..\Common\StringUtils.h" #include "..\ServerLogger.h" +#include #include #include +#include namespace ServerRuntime { @@ -25,6 +28,8 @@ namespace ServerRuntime std::mutex stateLock; std::mutex writeLock; std::shared_ptr banManager; + std::shared_ptr whitelistManager; + bool whitelistEnabled = false; }; AccessState g_accessState; @@ -48,6 +53,18 @@ namespace ServerRuntime std::lock_guard stateLock(g_accessState.stateLock); g_accessState.banManager = banManager; } + + static std::shared_ptr GetWhitelistManagerSnapshot() + { + std::lock_guard stateLock(g_accessState.stateLock); + return g_accessState.whitelistManager; + } + + static void PublishWhitelistManagerSnapshot(const std::shared_ptr &whitelistManager) + { + std::lock_guard stateLock(g_accessState.stateLock); + g_accessState.whitelistManager = whitelistManager; + } } std::string FormatXuid(PlayerUID xuid) @@ -62,70 +79,175 @@ namespace ServerRuntime return buffer; } - bool Initialize(const std::string &baseDirectory) + bool TryParseXuid(const std::string &text, PlayerUID *outXuid) + { + if (outXuid == nullptr) + { + return false; + } + + std::string trimmed = StringUtils::TrimAscii(text); + if (trimmed.empty()) + { + return false; + } + + errno = 0; + char *end = nullptr; + 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 || value == 0ULL) + { + return false; + } + + *outXuid = (PlayerUID)value; + return true; + } + + bool Initialize(const std::string &baseDirectory, bool whitelistEnabled) { std::lock_guard 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 = std::make_shared(baseDirectory); + std::shared_ptr whitelistManager = std::make_shared(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 playerEntries; std::vector ipEntries; + std::vector whitelistEntries; banManager->SnapshotBannedPlayers(&playerEntries); banManager->SnapshotBannedIps(&ipEntries); + whitelistManager->SnapshotWhitelistedPlayers(&whitelistEntries); PublishBanManagerSnapshot(banManager); + PublishWhitelistManagerSnapshot(whitelistManager); + { + std::lock_guard stateLock(g_accessState.stateLock); + g_accessState.whitelistEnabled = whitelistEnabled; + } LogInfof( "access", - "loaded %u player bans and %u ip bans", + "loaded %u player bans, %u ip bans, and %u whitelist entries (whitelist=%s)", (unsigned)playerEntries.size(), - (unsigned)ipEntries.size()); + (unsigned)ipEntries.size(), + (unsigned)whitelistEntries.size(), + whitelistEnabled ? "enabled" : "disabled"); return true; } void Shutdown() { std::lock_guard writeLock(g_accessState.writeLock); - PublishBanManagerSnapshot(std::shared_ptr()); + PublishBanManagerSnapshot(std::shared_ptr{}); + PublishWhitelistManagerSnapshot(std::shared_ptr{}); + std::lock_guard stateLock(g_accessState.stateLock); + g_accessState.whitelistEnabled = false; } bool Reload() { std::lock_guard writeLock(g_accessState.writeLock); std::shared_ptr current = GetBanManagerSnapshot(); - if (current == nullptr) + std::shared_ptr currentWhitelist = GetWhitelistManagerSnapshot(); + if (current == nullptr || currentWhitelist == nullptr) { return false; } std::shared_ptr banManager = std::make_shared(*current); + std::shared_ptr whitelistManager = std::make_shared(*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 writeLock(g_accessState.writeLock); + const auto current = GetWhitelistManagerSnapshot(); + if (current == nullptr) + { + return false; + } + + auto whitelistManager = std::make_shared(*current); + if (!whitelistManager->EnsureWhitelistFileExists()) + { + return false; + } + if (!whitelistManager->Reload()) + { + return false; + } + + PublishWhitelistManagerSnapshot(whitelistManager); return true; } bool IsInitialized() { - return GetBanManagerSnapshot() != nullptr; + return GetBanManagerSnapshot() != nullptr && GetWhitelistManagerSnapshot() != nullptr; + } + + bool IsWhitelistEnabled() + { + std::lock_guard stateLock(g_accessState.stateLock); + return g_accessState.whitelistEnabled; + } + + void SetWhitelistEnabled(bool enabled) + { + std::lock_guard writeLock(g_accessState.writeLock); + std::lock_guard stateLock(g_accessState.stateLock); + g_accessState.whitelistEnabled = enabled; } bool IsPlayerBanned(PlayerUID xuid) @@ -146,6 +268,18 @@ namespace ServerRuntime 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 = 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); @@ -241,6 +375,57 @@ namespace ServerRuntime 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 writeLock(g_accessState.writeLock); + const auto current = GetWhitelistManagerSnapshot(); + if (current == nullptr) + { + return false; + } + + auto whitelistManager = std::make_shared(*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 writeLock(g_accessState.writeLock); + const auto current = GetWhitelistManagerSnapshot(); + if (current == nullptr) + { + return false; + } + + auto whitelistManager = std::make_shared(*current); + if (!whitelistManager->RemovePlayerByXuid(formatted)) + { + return false; + } + + PublishWhitelistManagerSnapshot(whitelistManager); + return true; + } + bool SnapshotBannedPlayers(std::vector *outEntries) { if (outEntries == nullptr) @@ -274,5 +459,22 @@ namespace ServerRuntime return banManager->SnapshotBannedIps(outEntries); } + + bool SnapshotWhitelistedPlayers(std::vector *outEntries) + { + if (outEntries == nullptr) + { + return false; + } + + const auto whitelistManager = GetWhitelistManagerSnapshot(); + if (whitelistManager == nullptr) + { + outEntries->clear(); + return false; + } + + return whitelistManager->SnapshotWhitelistedPlayers(outEntries); + } } -} \ No newline at end of file +} diff --git a/Minecraft.Server/Access/Access.h b/Minecraft.Server/Access/Access.h index e4866025f..80e61e551 100644 --- a/Minecraft.Server/Access/Access.h +++ b/Minecraft.Server/Access/Access.h @@ -1,6 +1,7 @@ #pragma once #include "BanManager.h" +#include "WhitelistManager.h" namespace ServerRuntime { @@ -9,18 +10,24 @@ namespace ServerRuntime */ namespace Access { - bool Initialize(const std::string &baseDirectory = "."); + 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 @@ -32,7 +39,9 @@ namespace ServerRuntime * 現在のIP BAN一覧を複製取得 */ bool SnapshotBannedIps(std::vector *outEntries); + bool SnapshotWhitelistedPlayers(std::vector *outEntries); std::string FormatXuid(PlayerUID xuid); + bool TryParseXuid(const std::string &text, PlayerUID *outXuid); } } diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp new file mode 100644 index 000000000..6dbc39a3b --- /dev/null +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -0,0 +1,439 @@ +#include "stdafx.h" + +#include "WhitelistManager.h" + +#include "..\Common\FileUtils.h" +#include "..\Common\StringUtils.h" +#include "..\ServerLogger.h" +#include "..\vendor\nlohmann\json.hpp" + +#include +#include +#include +#include + +namespace ServerRuntime +{ + namespace Access + { + using OrderedJson = nlohmann::ordered_json; + + namespace + { + static const char *kWhitelistFileName = "whitelist.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); + } + + 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; + 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; + } + + const auto it = object.find(key); + if (it == object.end() || !it->is_string()) + { + return false; + } + + *outValue = it->get(); + return true; + } + } + + WhitelistManager::WhitelistManager(const std::string &baseDirectory) + : m_baseDirectory(baseDirectory.empty() ? "." : baseDirectory) + { + } + + bool WhitelistManager::EnsureWhitelistFileExists() const + { + const std::string path = GetWhitelistFilePath(); + if (!EnsureJsonListFile(path)) + { + LogErrorf("access", "failed to create %s", path.c_str()); + return false; + } + return true; + } + + bool WhitelistManager::Reload() + { + std::vector players; + if (!LoadPlayers(&players)) + { + return false; + } + + m_whitelistedPlayers.swap(players); + return true; + } + + bool WhitelistManager::Save() const + { + std::vector players; + return SnapshotWhitelistedPlayers(&players) && SavePlayers(players); + } + + bool WhitelistManager::LoadPlayers(std::vector *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 = "[]"; + } + + OrderedJson root; + try + { + 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; + } + + for (const auto &object : root) + { + if (!object.is_object()) + { + LogWarnf("access", "skipping whitelist entry that is not an object in %s", path.c_str()); + continue; + } + + std::string rawXuid; + if (!TryGetStringField(object, "xuid", &rawXuid)) + { + LogWarnf("access", "skipping whitelist entry without xuid in %s", path.c_str()); + continue; + } + + WhitelistedPlayerEntry entry; + entry.xuid = NormalizeXuid(rawXuid); + if (entry.xuid.empty()) + { + LogWarnf("access", "skipping whitelist 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); + NormalizeMetadata(&entry.metadata); + + outEntries->push_back(entry); + } + + return true; + } + + bool WhitelistManager::SavePlayers(const std::vector &entries) const + { + OrderedJson root = OrderedJson::array(); + for (const auto &entry : entries) + { + OrderedJson object = OrderedJson::object(); + object["xuid"] = NormalizeXuid(entry.xuid); + object["name"] = entry.name; + object["created"] = entry.metadata.created; + object["source"] = entry.metadata.source; + root.push_back(object); + } + + const std::string path = GetWhitelistFilePath(); + 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 &WhitelistManager::GetWhitelistedPlayers() const + { + return m_whitelistedPlayers; + } + + bool WhitelistManager::SnapshotWhitelistedPlayers(std::vector *outEntries) const + { + if (outEntries == nullptr) + { + return false; + } + + *outEntries = m_whitelistedPlayers; + return true; + } + + bool WhitelistManager::IsPlayerWhitelistedByXuid(const std::string &xuid) const + { + const auto normalized = 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 updatedEntries; + if (!SnapshotWhitelistedPlayers(&updatedEntries)) + { + return false; + } + + auto normalized = entry; + normalized.xuid = 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 = NormalizeXuid(xuid); + if (normalized.empty()) + { + return false; + } + + std::vector 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; + + 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"; + return metadata; + } + + std::string WhitelistManager::NormalizeXuid(const std::string &xuid) + { + std::string trimmed = StringUtils::TrimAscii(xuid); + if (trimmed.empty()) + { + return ""; + } + + unsigned long long numericXuid = 0; + 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); + } + + 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 + { + 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); + } + + const wchar_t last = wideBaseDirectory[wideBaseDirectory.size() - 1]; + if (last == L'\\' || last == L'/') + { + return StringUtils::WideToUtf8(wideBaseDirectory + wideFileName); + } + + return StringUtils::WideToUtf8(wideBaseDirectory + L"\\" + wideFileName); + } + } +} diff --git a/Minecraft.Server/Access/WhitelistManager.h b/Minecraft.Server/Access/WhitelistManager.h new file mode 100644 index 000000000..dbdc40eef --- /dev/null +++ b/Minecraft.Server/Access/WhitelistManager.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include + +namespace ServerRuntime +{ + namespace Access + { + /** + * Information stored with dedicated-server whitelist entries + * ホワイトリスト項目に保存する補助情報 + */ + struct WhitelistMetadata + { + std::string created; + std::string source; + }; + + struct WhitelistedPlayerEntry + { + std::string xuid; + std::string name; + WhitelistMetadata metadata; + }; + + /** + * Dedicated server whitelist file 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 *outEntries) const; + bool SavePlayers(const std::vector &entries) const; + + const std::vector &GetWhitelistedPlayers() const; + bool SnapshotWhitelistedPlayers(std::vector *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 std::string NormalizeXuid(const std::string &xuid); + static void NormalizeMetadata(WhitelistMetadata *metadata); + + std::string BuildPath(const char *fileName) const; + + private: + std::string m_baseDirectory; + std::vector m_whitelistedPlayers; + }; + } +} diff --git a/Minecraft.Server/Common/StringUtils.cpp b/Minecraft.Server/Common/StringUtils.cpp index 82646eeb5..a7c1bb493 100644 --- a/Minecraft.Server/Common/StringUtils.cpp +++ b/Minecraft.Server/Common/StringUtils.cpp @@ -101,6 +101,36 @@ namespace ServerRuntime return lowered; } + std::string JoinTokens(const std::vector &tokens, size_t startIndex, const char *separator) + { + if (startIndex >= tokens.size()) + { + return std::string(); + } + + const auto joinSeparator = std::string((separator != nullptr) ? separator : " "); + size_t totalLength = 0; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + totalLength += tokens[i].size(); + } + + totalLength += (tokens.size() - startIndex - 1) * joinSeparator.size(); + std::string joined; + joined.reserve(totalLength); + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) + { + joined += joinSeparator; + } + + joined += tokens[i]; + } + + return joined; + } + bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix) { if (prefix.size() > value.size()) diff --git a/Minecraft.Server/Common/StringUtils.h b/Minecraft.Server/Common/StringUtils.h index a2585b732..7720cc443 100644 --- a/Minecraft.Server/Common/StringUtils.h +++ b/Minecraft.Server/Common/StringUtils.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace ServerRuntime { @@ -13,6 +14,7 @@ namespace ServerRuntime std::string TrimAscii(const std::string &value); std::string ToLowerAscii(const std::string &value); + std::string JoinTokens(const std::vector &tokens, size_t startIndex = 0, const char *separator = " "); bool StartsWithIgnoreCase(const std::string &value, const std::string &prefix); } } diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp index b72ac11a5..c629b765f 100644 --- a/Minecraft.Server/Console/ServerCliEngine.cpp +++ b/Minecraft.Server/Console/ServerCliEngine.cpp @@ -15,6 +15,7 @@ #include "commands\CliCommandPardonIp.h" #include "commands\CliCommandStop.h" #include "commands\CliCommandTp.h" +#include "commands\CliCommandWhitelist.h" #include "..\Common\StringUtils.h" #include "..\ServerShutdown.h" #include "..\ServerLogger.h" @@ -49,6 +50,7 @@ namespace ServerRuntime m_registry->Register(std::unique_ptr(new CliCommandPardon())); m_registry->Register(std::unique_ptr(new CliCommandPardonIp())); m_registry->Register(std::unique_ptr(new CliCommandBanList())); + m_registry->Register(std::unique_ptr(new CliCommandWhitelist())); m_registry->Register(std::unique_ptr(new CliCommandTp())); m_registry->Register(std::unique_ptr(new CliCommandGamemode())); } diff --git a/Minecraft.Server/Console/commands/CliCommandWhitelist.cpp b/Minecraft.Server/Console/commands/CliCommandWhitelist.cpp new file mode 100644 index 000000000..9ac8ec70e --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandWhitelist.cpp @@ -0,0 +1,284 @@ +#include "stdafx.h" + +#include "CliCommandWhitelist.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" +#include "..\..\ServerProperties.h" + +#include +#include + +namespace ServerRuntime +{ + namespace + { + static const char *kWhitelistUsage = "whitelist [...]"; + + static bool CompareWhitelistEntries(const ServerRuntime::Access::WhitelistedPlayerEntry &left, const ServerRuntime::Access::WhitelistedPlayerEntry &right) + { + const auto leftName = StringUtils::ToLowerAscii(left.name); + const auto rightName = StringUtils::ToLowerAscii(right.name); + if (leftName != rightName) + { + return leftName < rightName; + } + + return StringUtils::ToLowerAscii(left.xuid) < StringUtils::ToLowerAscii(right.xuid); + } + + static bool PersistWhitelistToggle(bool enabled) + { + auto config = LoadServerPropertiesConfig(); + config.whiteListEnabled = enabled; + return SaveServerPropertiesConfig(config); + } + + static std::string BuildWhitelistEntryRow(const ServerRuntime::Access::WhitelistedPlayerEntry &entry) + { + std::string row = " "; + row += entry.xuid; + if (!entry.name.empty()) + { + row += " - "; + row += entry.name; + } + return row; + } + + static void LogWhitelistMode(ServerCliEngine *engine) + { + engine->LogInfo(std::string("Whitelist is ") + (ServerRuntime::Access::IsWhitelistEnabled() ? "enabled." : "disabled.")); + } + + static bool LogWhitelistEntries(ServerCliEngine *engine) + { + std::vector entries; + if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries)) + { + engine->LogError("Failed to read whitelist entries."); + return false; + } + + std::sort(entries.begin(), entries.end(), CompareWhitelistEntries); + LogWhitelistMode(engine); + engine->LogInfo("There are " + std::to_string(entries.size()) + " whitelisted player(s)."); + for (const auto &entry : entries) + { + engine->LogInfo(BuildWhitelistEntryRow(entry)); + } + return true; + } + + static bool TryParseWhitelistXuid(const std::string &text, ServerCliEngine *engine, PlayerUID *outXuid) + { + if (ServerRuntime::Access::TryParseXuid(text, outXuid)) + { + return true; + } + + engine->LogWarn("Invalid XUID: " + text); + return false; + } + + static void SuggestLiteral(const std::string &candidate, const ServerCliCompletionContext &context, std::vector *out) + { + if (out == nullptr) + { + return; + } + + if (StringUtils::StartsWithIgnoreCase(candidate, context.prefix)) + { + out->push_back(context.linePrefix + candidate); + } + } + } + + const char *CliCommandWhitelist::Name() const + { + return "whitelist"; + } + + const char *CliCommandWhitelist::Usage() const + { + return kWhitelistUsage; + } + + const char *CliCommandWhitelist::Description() const + { + return "Manage the dedicated-server XUID whitelist."; + } + + bool CliCommandWhitelist::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() < 2) + { + engine->LogWarn(std::string("Usage: ") + kWhitelistUsage); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + const auto subcommand = StringUtils::ToLowerAscii(line.tokens[1]); + if (subcommand == "on" || subcommand == "off") + { + if (line.tokens.size() != 2) + { + engine->LogWarn("Usage: whitelist "); + return false; + } + + const bool enabled = (subcommand == "on"); + if (!PersistWhitelistToggle(enabled)) + { + engine->LogError("Failed to persist whitelist mode to server.properties."); + return false; + } + + ServerRuntime::Access::SetWhitelistEnabled(enabled); + engine->LogInfo(std::string("Whitelist ") + (enabled ? "enabled." : "disabled.")); + return true; + } + + if (subcommand == "list") + { + if (line.tokens.size() != 2) + { + engine->LogWarn("Usage: whitelist list"); + return false; + } + + return LogWhitelistEntries(engine); + } + + if (subcommand == "reload") + { + if (line.tokens.size() != 2) + { + engine->LogWarn("Usage: whitelist reload"); + return false; + } + if (!ServerRuntime::Access::ReloadWhitelist()) + { + engine->LogError("Failed to reload whitelist."); + return false; + } + + const auto config = LoadServerPropertiesConfig(); + ServerRuntime::Access::SetWhitelistEnabled(config.whiteListEnabled); + engine->LogInfo("Reloaded whitelist from disk."); + LogWhitelistMode(engine); + return true; + } + + if (subcommand == "add") + { + if (line.tokens.size() < 3) + { + engine->LogWarn("Usage: whitelist add [name ...]"); + return false; + } + + PlayerUID xuid = INVALID_XUID; + if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid)) + { + return false; + } + + if (ServerRuntime::Access::IsPlayerWhitelisted(xuid)) + { + engine->LogWarn("That XUID is already whitelisted."); + return false; + } + + const auto metadata = ServerRuntime::Access::WhitelistManager::BuildDefaultMetadata("Console"); + const auto name = StringUtils::JoinTokens(line.tokens, 3); + if (!ServerRuntime::Access::AddWhitelistedPlayer(xuid, name, metadata)) + { + engine->LogError("Failed to write whitelist entry."); + return false; + } + + std::string message = "Whitelisted XUID " + ServerRuntime::Access::FormatXuid(xuid) + "."; + if (!name.empty()) + { + message += " Name: " + name; + } + engine->LogInfo(message); + return true; + } + + if (subcommand == "remove") + { + if (line.tokens.size() != 3) + { + engine->LogWarn("Usage: whitelist remove "); + return false; + } + + PlayerUID xuid = INVALID_XUID; + if (!TryParseWhitelistXuid(line.tokens[2], engine, &xuid)) + { + return false; + } + + if (!ServerRuntime::Access::IsPlayerWhitelisted(xuid)) + { + engine->LogWarn("That XUID is not whitelisted."); + return false; + } + + if (!ServerRuntime::Access::RemoveWhitelistedPlayer(xuid)) + { + engine->LogError("Failed to remove whitelist entry."); + return false; + } + + engine->LogInfo("Removed XUID " + ServerRuntime::Access::FormatXuid(xuid) + " from the whitelist."); + return true; + } + + engine->LogWarn(std::string("Usage: ") + kWhitelistUsage); + return false; + } + + void CliCommandWhitelist::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + (void)engine; + if (out == nullptr) + { + return; + } + + if (context.currentTokenIndex == 1) + { + SuggestLiteral("on", context, out); + SuggestLiteral("off", context, out); + SuggestLiteral("list", context, out); + SuggestLiteral("add", context, out); + SuggestLiteral("remove", context, out); + SuggestLiteral("reload", context, out); + return; + } + + if (context.currentTokenIndex == 2 && context.parsed.tokens.size() >= 2 && StringUtils::ToLowerAscii(context.parsed.tokens[1]) == "remove") + { + std::vector entries; + if (!ServerRuntime::Access::SnapshotWhitelistedPlayers(&entries)) + { + return; + } + + for (const auto &entry : entries) + { + SuggestLiteral(entry.xuid, context, out); + } + } + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandWhitelist.h b/Minecraft.Server/Console/commands/CliCommandWhitelist.h new file mode 100644 index 000000000..aba96247a --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandWhitelist.h @@ -0,0 +1,16 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + class CliCommandWhitelist : public IServerCliCommand + { + public: + const char *Name() const override; + const char *Usage() const override; + const char *Description() const override; + bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) override; + void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const override; + }; +} diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index 23854c364..e9ffa47d9 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -126,6 +126,7 @@ + @@ -662,6 +663,7 @@ + @@ -684,6 +686,7 @@ + @@ -696,6 +699,7 @@ + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index 87a66e205..d151cd3e8 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -60,6 +60,9 @@ Server\Access + + Server\Access + Server\Common @@ -588,6 +591,9 @@ Server\Console\Commands + + Server\Console\Commands + @@ -608,6 +614,9 @@ Server\Access + + Server\Access + Server\Common @@ -660,6 +669,9 @@ Server\Console\Commands + + Server\Console\Commands + Server\Console\Commands diff --git a/Minecraft.Server/ServerLogManager.cpp b/Minecraft.Server/ServerLogManager.cpp index d4204af80..84805f7e4 100644 --- a/Minecraft.Server/ServerLogManager.cpp +++ b/Minecraft.Server/ServerLogManager.cpp @@ -157,6 +157,7 @@ namespace ServerRuntime switch (reason) { case eLoginRejectReason_BannedXuid: return "banned-xuid"; + case eLoginRejectReason_NotWhitelisted: return "not-whitelisted"; case eLoginRejectReason_DuplicateXuid: return "duplicate-xuid"; case eLoginRejectReason_DuplicateName: return "duplicate-name"; default: return "unknown"; diff --git a/Minecraft.Server/ServerLogManager.h b/Minecraft.Server/ServerLogManager.h index 989553caa..1d4abfb5b 100644 --- a/Minecraft.Server/ServerLogManager.h +++ b/Minecraft.Server/ServerLogManager.h @@ -27,6 +27,7 @@ namespace ServerRuntime enum ELoginRejectReason { eLoginRejectReason_BannedXuid = 0, + eLoginRejectReason_NotWhitelisted, eLoginRejectReason_DuplicateXuid, eLoginRejectReason_DuplicateName }; diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index b06fb4531..c93abc6ea 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -71,6 +71,7 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { "server-ip", "0.0.0.0" }, { "server-name", "DedicatedServer" }, { "server-port", "25565" }, + { "white-list", "false" }, { "lan-advertise", "false" }, { "spawn-animals", "true" }, { "spawn-monsters", "true" }, @@ -691,6 +692,7 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.serverPort = ReadNormalizedIntProperty(&merged, "server-port", kDefaultServerPort, 1, 65535, &shouldWrite); config.serverIp = ReadNormalizedStringProperty(&merged, "server-ip", "0.0.0.0", 255, &shouldWrite); config.lanAdvertise = ReadNormalizedBoolProperty(&merged, kLanAdvertisePropertyKey, false, &shouldWrite); + config.whiteListEnabled = ReadNormalizedBoolProperty(&merged, "white-list", false, &shouldWrite); config.serverName = ReadNormalizedStringProperty(&merged, "server-name", "DedicatedServer", 16, &shouldWrite); config.maxPlayers = ReadNormalizedIntProperty(&merged, "max-players", kDefaultMaxPlayers, 1, kMaxDedicatedPlayers, &shouldWrite); config.seed = 0; @@ -750,7 +752,7 @@ ServerPropertiesConfig LoadServerPropertiesConfig() * * Saves world identity fields while preserving as many other settings as possible * - Reads existing file and merges including unknown keys - * - Updates only `level-name` and `level-id` before writing back + * - Updates `level-name`, `level-id`, and `white-list` before writing back * ワールド識別情報の保存処理 */ bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config) @@ -787,6 +789,7 @@ bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config) merged["level-name"] = worldName; merged["level-id"] = worldSaveId; + merged["white-list"] = BoolToString(config.whiteListEnabled); return WriteServerPropertiesFile(kServerPropertiesPath, merged); } diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 57d7fa8ed..50db845c7 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -21,6 +21,8 @@ namespace ServerRuntime std::string serverIp; /** `lan-advertise` */ bool lanAdvertise; + /** `white-list` */ + bool whiteListEnabled; /** `server-name` (max 16 chars at runtime) */ std::string serverName; /** `max-players` */ @@ -85,9 +87,10 @@ namespace ServerRuntime * server.properties saver * * - `level-name` と `level-id` を更新 + * - `white-list` を更新 * - それ以外の既存キーは極力保持 * - * @param config 保存するワールド識別情報 + * @param config 保存するワールド識別情報と永続化対象設定 * @return 書き込み成功時 `true` */ bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config); diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index 638358042..251bcc121 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -389,13 +389,14 @@ int main(int argc, char **argv) LogStartupStep("initializing server log manager"); ServerRuntime::ServerLogManager::Initialize(); LogStartupStep("initializing dedicated access control"); - if (!ServerRuntime::Access::Initialize(".")) + if (!ServerRuntime::Access::Initialize(".", serverProperties.whiteListEnabled)) { LogError("startup", "Failed to initialize dedicated server access control."); return 2; } accessShutdownGuard.Activate(); LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled"); + LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled"); LogStartupStep("registering hidden window class"); HINSTANCE hInstance = GetModuleHandle(NULL);