diff --git a/CMakeLists.txt b/CMakeLists.txt index dea11a747..50e426015 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,7 +102,12 @@ list(APPEND MINECRAFT_SERVER_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliParser.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliEngine.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/ServerCliRegistry.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandBan.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandBanIp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandBanList.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandHelp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandPardon.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp" "${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" diff --git a/Minecraft.Server/Console/ServerCliEngine.cpp b/Minecraft.Server/Console/ServerCliEngine.cpp index 1a8c3a5b3..9885a3762 100644 --- a/Minecraft.Server/Console/ServerCliEngine.cpp +++ b/Minecraft.Server/Console/ServerCliEngine.cpp @@ -5,9 +5,14 @@ #include "ServerCliParser.h" #include "ServerCliRegistry.h" #include "commands\IServerCliCommand.h" +#include "commands\CliCommandBan.h" +#include "commands\CliCommandBanIp.h" +#include "commands\CliCommandBanList.h" #include "commands\CliCommandGamemode.h" #include "commands\CliCommandHelp.h" #include "commands\CliCommandList.h" +#include "commands\CliCommandPardon.h" +#include "commands\CliCommandPardonIp.h" #include "commands\CliCommandStop.h" #include "commands\CliCommandTp.h" #include "..\Common\StringUtils.h" @@ -38,6 +43,11 @@ namespace ServerRuntime m_registry->Register(std::unique_ptr(new CliCommandHelp())); m_registry->Register(std::unique_ptr(new CliCommandStop())); m_registry->Register(std::unique_ptr(new CliCommandList())); + m_registry->Register(std::unique_ptr(new CliCommandBan())); + m_registry->Register(std::unique_ptr(new CliCommandBanIp())); + 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 CliCommandTp())); m_registry->Register(std::unique_ptr(new CliCommandGamemode())); } diff --git a/Minecraft.Server/Console/commands/CliCommandBan.cpp b/Minecraft.Server/Console/commands/CliCommandBan.cpp new file mode 100644 index 000000000..a8b26cb61 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBan.cpp @@ -0,0 +1,163 @@ +#include "stdafx.h" + +#include "CliCommandBan.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" +#include "..\..\..\Minecraft.Client\PlayerConnection.h" +#include "..\..\..\Minecraft.Client\ServerPlayer.h" +#include "..\..\..\Minecraft.World\DisconnectPacket.h" + +#include + +namespace ServerRuntime +{ + namespace + { + static std::string JoinTokens(const std::vector &tokens, size_t startIndex) + { + std::string joined; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) + { + joined.push_back(' '); + } + joined += tokens[i]; + } + return joined; + } + + static void AppendUniqueXuid(PlayerUID xuid, std::vector *out) + { + if (out == NULL || xuid == INVALID_XUID) + { + return; + } + + if (std::find(out->begin(), out->end(), xuid) == out->end()) + { + out->push_back(xuid); + } + } + + static void CollectPlayerBanXuids(const std::shared_ptr &player, std::vector *out) + { + if (player == NULL || out == NULL) + { + return; + } + + // Keep both identity variants because the dedicated server checks login and online XUIDs separately. + AppendUniqueXuid(player->getXuid(), out); + AppendUniqueXuid(player->getOnlineXuid(), out); + } + } + + const char *CliCommandBan::Name() const + { + return "ban"; + } + + const char *CliCommandBan::Usage() const + { + return "ban [reason ...]"; + } + + const char *CliCommandBan::Description() const + { + return "Ban an online player."; + } + + /** + * Resolves the live player, writes one or more Access ban entries, and disconnects the target with the banned reason + * 対象プレイヤーを解決してBANを保存し切断する + */ + bool CliCommandBan::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() < 2) + { + engine->LogWarn("Usage: ban [reason ...]"); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + std::shared_ptr target = engine->FindPlayerByNameUtf8(line.tokens[1]); + if (target == NULL) + { + engine->LogWarn("Unknown player: " + line.tokens[1] + " (this server build can only ban players that are currently online)."); + return false; + } + + std::vector xuids; + CollectPlayerBanXuids(target, &xuids); + if (xuids.empty()) + { + engine->LogWarn("Cannot ban that player because no valid XUID is available."); + return false; + } + + bool hasUnbannedIdentity = false; + for (size_t i = 0; i < xuids.size(); ++i) + { + if (!ServerRuntime::Access::IsPlayerBanned(xuids[i])) + { + hasUnbannedIdentity = true; + break; + } + } + if (!hasUnbannedIdentity) + { + engine->LogWarn("That player is already banned."); + return false; + } + + ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console"); + metadata.reason = JoinTokens(line.tokens, 2); + if (metadata.reason.empty()) + { + metadata.reason = "Banned by an operator."; + } + + const std::string playerName = StringUtils::WideToUtf8(target->getName()); + for (size_t i = 0; i < xuids.size(); ++i) + { + if (ServerRuntime::Access::IsPlayerBanned(xuids[i])) + { + continue; + } + + if (!ServerRuntime::Access::AddPlayerBan(xuids[i], playerName, metadata)) + { + engine->LogError("Failed to write player ban."); + return false; + } + } + + if (target->connection != NULL) + { + target->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + } + + engine->LogInfo("Banned player " + playerName + "."); + return true; + } + + /** + * Suggests currently connected player names for the Java-style player argument + * プレイヤー引数の補完候補を返す + */ + void CliCommandBan::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + if (context.currentTokenIndex == 1) + { + engine->SuggestPlayers(context.prefix, context.linePrefix, out); + } + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandBan.h b/Minecraft.Server/Console/commands/CliCommandBan.h new file mode 100644 index 000000000..0135d9c00 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBan.h @@ -0,0 +1,20 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + /** + * Applies a dedicated-server player ban using Java Edition style syntax and Access-backed persistence + * Java Edition 風の ban コマンドで永続プレイヤーBANを行う + */ + class CliCommandBan : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual const char *Usage() const; + virtual const char *Description() const; + virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); + virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + }; +} \ No newline at end of file diff --git a/Minecraft.Server/Console/commands/CliCommandBanIp.cpp b/Minecraft.Server/Console/commands/CliCommandBanIp.cpp new file mode 100644 index 000000000..7c72a3433 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBanIp.cpp @@ -0,0 +1,206 @@ +#include "stdafx.h" + +#include "CliCommandBanIp.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" +#include "..\..\ServerLogManager.h" +#include "..\..\..\Minecraft.Client\MinecraftServer.h" +#include "..\..\..\Minecraft.Client\PlayerConnection.h" +#include "..\..\..\Minecraft.Client\PlayerList.h" +#include "..\..\..\Minecraft.Client\ServerPlayer.h" +#include "..\..\..\Minecraft.World\Connection.h" +#include "..\..\..\Minecraft.World\DisconnectPacket.h" + +#include + +namespace ServerRuntime +{ + namespace + { + static std::string JoinTokens(const std::vector &tokens, size_t startIndex) + { + std::string joined; + for (size_t i = startIndex; i < tokens.size(); ++i) + { + if (!joined.empty()) + { + joined.push_back(' '); + } + joined += tokens[i]; + } + return joined; + } + + // Compare IPs in a canonical lowercase form so literal input and cached values match reliably. + static std::string NormalizeIpToken(const std::string &ip) + { + return StringUtils::ToLowerAscii(StringUtils::TrimAscii(ip)); + } + + // Accept both IPv4 and IPv6 literals because Java Edition style ban-ip can target either form. + static bool IsIpLiteral(const std::string &text) + { + const std::string trimmed = StringUtils::TrimAscii(text); + if (trimmed.empty()) + { + return false; + } + + IN_ADDR ipv4 = {}; + IN6_ADDR ipv6 = {}; + return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1; + } + + // The dedicated server keeps the accepted remote IP in ServerLogManager, keyed by connection smallId. + // It's a bit strange from a responsibility standpoint, so we'll need to implement it separately. + static bool TryGetPlayerRemoteIp(const std::shared_ptr &player, std::string *outIp) + { + if (outIp == NULL || player == NULL || player->connection == NULL || player->connection->connection == NULL || player->connection->connection->getSocket() == NULL) + { + return false; + } + + const unsigned char smallId = player->connection->connection->getSocket()->getSmallId(); + if (smallId == 0) + { + return false; + } + + return ServerRuntime::ServerLogManager::TryGetConnectionRemoteIp(smallId, outIp); + } + + // After persisting the ban, walk a snapshot of current players so every matching session is removed. + static int DisconnectPlayersByRemoteIp(const std::string &ip) + { + MinecraftServer *server = MinecraftServer::getInstance(); + if (server == NULL || server->getPlayers() == NULL) + { + return 0; + } + + const std::string normalizedIp = NormalizeIpToken(ip); + std::vector > playerSnapshot = server->getPlayers()->players; + int disconnectedCount = 0; + for (size_t i = 0; i < playerSnapshot.size(); ++i) + { + std::shared_ptr player = playerSnapshot[i]; + std::string playerIp; + if (!TryGetPlayerRemoteIp(player, &playerIp)) + { + continue; + } + + if (NormalizeIpToken(playerIp) == normalizedIp) + { + if (player != NULL && player->connection != NULL) + { + player->connection->disconnect(DisconnectPacket::eDisconnect_Banned); + ++disconnectedCount; + } + } + } + + return disconnectedCount; + } + } + + const char *CliCommandBanIp::Name() const + { + return "ban-ip"; + } + + const char *CliCommandBanIp::Usage() const + { + return "ban-ip [reason ...]"; + } + + const char *CliCommandBanIp::Description() const + { + return "Ban an IP address or a player's current IP."; + } + + /** + * Resolves either a literal IP or an online player's current IP, persists the ban, and disconnects every matching connection + * IPまたは接続中プレイヤーの現在IPをBANし一致する接続を切断する + */ + bool CliCommandBanIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() < 2) + { + engine->LogWarn("Usage: ban-ip [reason ...]"); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + const std::string targetToken = line.tokens[1]; + std::string remoteIp; + // Match Java Edition behavior by accepting either a literal IP or an online player name. + std::shared_ptr targetPlayer = engine->FindPlayerByNameUtf8(targetToken); + if (targetPlayer != NULL) + { + if (!TryGetPlayerRemoteIp(targetPlayer, &remoteIp)) + { + engine->LogWarn("Cannot ban that player's IP because no current remote IP is available."); + return false; + } + } + else if (IsIpLiteral(targetToken)) + { + remoteIp = StringUtils::TrimAscii(targetToken); + } + else + { + engine->LogWarn("Unknown player or invalid IP address: " + targetToken); + return false; + } + + // Refuse duplicate bans so operators get immediate feedback instead of rewriting the same entry. + if (ServerRuntime::Access::IsIpBanned(remoteIp)) + { + engine->LogWarn("That IP address is already banned."); + return false; + } + + ServerRuntime::Access::BanMetadata metadata = ServerRuntime::Access::BanManager::BuildDefaultMetadata("Console"); + metadata.reason = JoinTokens(line.tokens, 2); + if (metadata.reason.empty()) + { + metadata.reason = "Banned by an operator."; + } + + // Publish the ban before disconnecting players so reconnect attempts are rejected immediately. + if (!ServerRuntime::Access::AddIpBan(remoteIp, metadata)) + { + engine->LogError("Failed to write IP ban."); + return false; + } + + const int disconnectedCount = DisconnectPlayersByRemoteIp(remoteIp); + // Report the resolved IP rather than the original token so player-name targets are explicit in the console. + engine->LogInfo("Banned IP address " + remoteIp + "."); + if (disconnectedCount > 0) + { + engine->LogInfo("Disconnected " + std::to_string(disconnectedCount) + " player(s) with that IP."); + } + return true; + } + + /** + * Suggests online player names for the player-target form of the Java Edition command + * プレイヤー名指定時の補完候補を返す + */ + void CliCommandBanIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + if (context.currentTokenIndex == 1) + { + engine->SuggestPlayers(context.prefix, context.linePrefix, out); + } + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandBanIp.h b/Minecraft.Server/Console/commands/CliCommandBanIp.h new file mode 100644 index 000000000..a717405f7 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBanIp.h @@ -0,0 +1,19 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + /** + * Applies a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence + */ + class CliCommandBanIp : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual const char *Usage() const; + virtual const char *Description() const; + virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); + virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + }; +} \ No newline at end of file diff --git a/Minecraft.Server/Console/commands/CliCommandBanList.cpp b/Minecraft.Server/Console/commands/CliCommandBanList.cpp new file mode 100644 index 000000000..bac4fc1d7 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBanList.cpp @@ -0,0 +1,135 @@ +#include "stdafx.h" + +#include "CliCommandBanList.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" + +#include + +namespace ServerRuntime +{ + namespace + { + static void AppendUniqueText(const std::string &text, std::vector *out) + { + if (out == NULL || text.empty()) + { + return; + } + + if (std::find(out->begin(), out->end(), text) == out->end()) + { + out->push_back(text); + } + } + + static bool CompareLowerAscii(const std::string &left, const std::string &right) + { + return StringUtils::ToLowerAscii(left) < StringUtils::ToLowerAscii(right); + } + + static bool LogBannedPlayers(ServerCliEngine *engine) + { + std::vector entries; + if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries)) + { + engine->LogError("Failed to read banned players."); + return false; + } + + std::vector names; + for (size_t i = 0; i < entries.size(); ++i) + { + AppendUniqueText(entries[i].name, &names); + } + std::sort(names.begin(), names.end(), CompareLowerAscii); + + engine->LogInfo("There are " + std::to_string(names.size()) + " banned player(s)."); + for (size_t i = 0; i < names.size(); ++i) + { + engine->LogInfo(" " + names[i]); + } + return true; + } + + static bool LogBannedIps(ServerCliEngine *engine) + { + std::vector entries; + if (!ServerRuntime::Access::SnapshotBannedIps(&entries)) + { + engine->LogError("Failed to read banned IPs."); + return false; + } + + std::vector ips; + for (size_t i = 0; i < entries.size(); ++i) + { + AppendUniqueText(entries[i].ip, &ips); + } + std::sort(ips.begin(), ips.end(), CompareLowerAscii); + + engine->LogInfo("There are " + std::to_string(ips.size()) + " banned IP(s)."); + for (size_t i = 0; i < ips.size(); ++i) + { + engine->LogInfo(" " + ips[i]); + } + return true; + } + + static bool LogAllBans(ServerCliEngine *engine) + { + if (!LogBannedPlayers(engine)) + { + return false; + } + + // Always print the IP snapshot as well so ban-ip entries are visible from the same command output. + return LogBannedIps(engine); + } + } + + const char *CliCommandBanList::Name() const + { + return "banlist"; + } + + const char *CliCommandBanList::Usage() const + { + return "banlist"; + } + + const char *CliCommandBanList::Description() const + { + return "List all banned players and IPs."; + } + + /** + * Reads the current Access snapshots and always prints both banned players and banned IPs + * Access の一覧を読みプレイヤーBANとIP BANをまとめて表示する + */ + bool CliCommandBanList::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() > 1) + { + engine->LogWarn("Usage: banlist"); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + return LogAllBans(engine); + } + + void CliCommandBanList::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + (void)context; + (void)engine; + (void)out; + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandBanList.h b/Minecraft.Server/Console/commands/CliCommandBanList.h new file mode 100644 index 000000000..f948fc9f4 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandBanList.h @@ -0,0 +1,22 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + /** + * **Ban List Command** + * + * Lists dedicated-server player bans and IP bans in a single command output + * 専用サーバーのプレイヤーBANとIP BANをまとめて表示する + */ + class CliCommandBanList : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual const char *Usage() const; + virtual const char *Description() const; + virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); + virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + }; +} diff --git a/Minecraft.Server/Console/commands/CliCommandPardon.cpp b/Minecraft.Server/Console/commands/CliCommandPardon.cpp new file mode 100644 index 000000000..c43479cec --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandPardon.cpp @@ -0,0 +1,173 @@ +#include "stdafx.h" + +#include "CliCommandPardon.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" +#include "..\..\..\Minecraft.Client\ServerPlayer.h" + +#include + +namespace ServerRuntime +{ + namespace + { + static void AppendUniqueText(const std::string &text, std::vector *out) + { + if (out == NULL || text.empty()) + { + return; + } + + if (std::find(out->begin(), out->end(), text) == out->end()) + { + out->push_back(text); + } + } + + static void AppendUniqueXuid(PlayerUID xuid, std::vector *out) + { + if (out == NULL || xuid == INVALID_XUID) + { + return; + } + + if (std::find(out->begin(), out->end(), xuid) == out->end()) + { + out->push_back(xuid); + } + } + } + + const char *CliCommandPardon::Name() const + { + return "pardon"; + } + + const char *CliCommandPardon::Usage() const + { + return "pardon "; + } + + const char *CliCommandPardon::Description() const + { + return "Remove a player ban."; + } + + /** + * Removes every Access ban entry that matches the requested player name so dual-XUID entries are cleared together + * 名前に一致するBANをまとめて解除する + */ + bool CliCommandPardon::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() != 2) + { + engine->LogWarn("Usage: pardon "); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + std::vector xuidsToRemove; + std::vector matchedNames; + std::shared_ptr onlineTarget = engine->FindPlayerByNameUtf8(line.tokens[1]); + if (onlineTarget != NULL) + { + if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getXuid())) + { + AppendUniqueXuid(onlineTarget->getXuid(), &xuidsToRemove); + } + if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getOnlineXuid())) + { + AppendUniqueXuid(onlineTarget->getOnlineXuid(), &xuidsToRemove); + } + } + + std::vector entries; + if (!ServerRuntime::Access::SnapshotBannedPlayers(&entries)) + { + engine->LogError("Failed to read banned players."); + return false; + } + + const std::string loweredTarget = StringUtils::ToLowerAscii(line.tokens[1]); + for (size_t i = 0; i < entries.size(); ++i) + { + if (StringUtils::ToLowerAscii(entries[i].name) == loweredTarget) + { + unsigned long long numericXuid = _strtoui64(entries[i].xuid.c_str(), NULL, 0); + if (numericXuid != 0ULL) + { + AppendUniqueXuid((PlayerUID)numericXuid, &xuidsToRemove); + } + AppendUniqueText(entries[i].name, &matchedNames); + } + } + + if (xuidsToRemove.empty()) + { + engine->LogWarn("That player is not banned."); + return false; + } + + for (size_t i = 0; i < xuidsToRemove.size(); ++i) + { + if (!ServerRuntime::Access::RemovePlayerBan(xuidsToRemove[i])) + { + engine->LogError("Failed to remove player ban."); + return false; + } + } + + std::string playerName = line.tokens[1]; + if (!matchedNames.empty()) + { + playerName = matchedNames[0]; + } + else if (onlineTarget != NULL) + { + playerName = StringUtils::WideToUtf8(onlineTarget->getName()); + } + + engine->LogInfo("Unbanned player " + playerName + "."); + return true; + } + + /** + * Suggests currently banned player names first and then online names for convenience + * BAN済み名とオンライン名を補完候補に出す + */ + void CliCommandPardon::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + if (context.currentTokenIndex != 1 || out == NULL) + { + return; + } + + std::vector entries; + if (ServerRuntime::Access::SnapshotBannedPlayers(&entries)) + { + const std::string loweredPrefix = StringUtils::ToLowerAscii(context.prefix); + std::vector names; + for (size_t i = 0; i < entries.size(); ++i) + { + AppendUniqueText(entries[i].name, &names); + } + + for (size_t i = 0; i < names.size(); ++i) + { + if (StringUtils::ToLowerAscii(names[i]).compare(0, loweredPrefix.size(), loweredPrefix) == 0) + { + out->push_back(context.linePrefix + names[i]); + } + } + } + + engine->SuggestPlayers(context.prefix, context.linePrefix, out); + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandPardon.h b/Minecraft.Server/Console/commands/CliCommandPardon.h new file mode 100644 index 000000000..d3e8bcb3c --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandPardon.h @@ -0,0 +1,19 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + /** + * Removes dedicated-server player bans using Java Edition style syntax and Access-backed persistence + */ + class CliCommandPardon : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual const char *Usage() const; + virtual const char *Description() const; + virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); + virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + }; +} \ No newline at end of file diff --git a/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp b/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp new file mode 100644 index 000000000..7945f0711 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandPardonIp.cpp @@ -0,0 +1,116 @@ +#include "stdafx.h" + +#include "CliCommandPardonIp.h" + +#include "..\ServerCliEngine.h" +#include "..\ServerCliParser.h" +#include "..\..\Access\Access.h" +#include "..\..\Common\StringUtils.h" + +#include + +namespace ServerRuntime +{ + namespace + { + // Keep validation shared with ban-ip so both commands agree on what counts as a literal address. + static bool IsIpLiteral(const std::string &text) + { + const std::string trimmed = StringUtils::TrimAscii(text); + if (trimmed.empty()) + { + return false; + } + + IN_ADDR ipv4 = {}; + IN6_ADDR ipv6 = {}; + return InetPtonA(AF_INET, trimmed.c_str(), &ipv4) == 1 || InetPtonA(AF_INET6, trimmed.c_str(), &ipv6) == 1; + } + } + + const char *CliCommandPardonIp::Name() const + { + return "pardon-ip"; + } + + const char *CliCommandPardonIp::Usage() const + { + return "pardon-ip
"; + } + + const char *CliCommandPardonIp::Description() const + { + return "Remove an IP ban."; + } + + /** + * Validates the literal IP argument and removes the matching Access IP ban entry + * リテラルIPを検証して一致するIP BANを解除する + */ + bool CliCommandPardonIp::Execute(const ServerCliParsedLine &line, ServerCliEngine *engine) + { + if (line.tokens.size() != 2) + { + engine->LogWarn("Usage: pardon-ip
"); + return false; + } + if (!ServerRuntime::Access::IsInitialized()) + { + engine->LogWarn("Access manager is not initialized."); + return false; + } + + // Java Edition pardon-ip only operates on a literal address, so do not resolve player names here. + const std::string ip = StringUtils::TrimAscii(line.tokens[1]); + if (!IsIpLiteral(ip)) + { + engine->LogWarn("Invalid IP address: " + line.tokens[1]); + return false; + } + // Distinguish invalid input from a valid but currently unbanned address for clearer operator feedback. + if (!ServerRuntime::Access::IsIpBanned(ip)) + { + engine->LogWarn("That IP address is not banned."); + return false; + } + if (!ServerRuntime::Access::RemoveIpBan(ip)) + { + engine->LogError("Failed to remove IP ban."); + return false; + } + + engine->LogInfo("Unbanned IP address " + ip + "."); + return true; + } + + /** + * Suggests currently banned IP addresses for the Java Edition literal-IP argument + * BAN済みIPの補完候補を返す + */ + void CliCommandPardonIp::Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const + { + (void)engine; + // Complete from the persisted IP-ban snapshot because this command only accepts already-banned literals. + if (context.currentTokenIndex != 1 || out == NULL) + { + return; + } + + std::vector entries; + if (!ServerRuntime::Access::SnapshotBannedIps(&entries)) + { + return; + } + + // Reuse the normalized prefix match used by other commands so completion stays case-insensitive. + const std::string loweredPrefix = StringUtils::ToLowerAscii(context.prefix); + for (size_t i = 0; i < entries.size(); ++i) + { + const std::string candidate = entries[i].ip; + if (StringUtils::ToLowerAscii(candidate).compare(0, loweredPrefix.size(), loweredPrefix) == 0) + { + out->push_back(context.linePrefix + candidate); + } + } + } +} diff --git a/Minecraft.Server/Console/commands/CliCommandPardonIp.h b/Minecraft.Server/Console/commands/CliCommandPardonIp.h new file mode 100644 index 000000000..631a20701 --- /dev/null +++ b/Minecraft.Server/Console/commands/CliCommandPardonIp.h @@ -0,0 +1,19 @@ +#pragma once + +#include "IServerCliCommand.h" + +namespace ServerRuntime +{ + /** + * Removes a dedicated-server IP ban using Java Edition style syntax and Access-backed persistence + */ + class CliCommandPardonIp : public IServerCliCommand + { + public: + virtual const char *Name() const; + virtual const char *Usage() const; + virtual const char *Description() const; + virtual bool Execute(const ServerCliParsedLine &line, ServerCliEngine *engine); + virtual void Complete(const ServerCliCompletionContext &context, const ServerCliEngine *engine, std::vector *out) const; + }; +} \ No newline at end of file diff --git a/Minecraft.Server/Minecraft.Server.vcxproj b/Minecraft.Server/Minecraft.Server.vcxproj index 82cb20801..23854c364 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj +++ b/Minecraft.Server/Minecraft.Server.vcxproj @@ -652,9 +652,14 @@ + + + + + @@ -681,9 +686,14 @@ + + + + + diff --git a/Minecraft.Server/Minecraft.Server.vcxproj.filters b/Minecraft.Server/Minecraft.Server.vcxproj.filters index 2261091d1..87a66e205 100644 --- a/Minecraft.Server/Minecraft.Server.vcxproj.filters +++ b/Minecraft.Server/Minecraft.Server.vcxproj.filters @@ -558,6 +558,15 @@ + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands @@ -567,6 +576,12 @@ Server\Console\Commands + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands @@ -615,6 +630,15 @@ Server + + Server\Console\Commands + + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands @@ -624,6 +648,12 @@ Server\Console\Commands + + Server\Console\Commands + + + Server\Console\Commands + Server\Console\Commands diff --git a/Minecraft.Server/ServerLogManager.cpp b/Minecraft.Server/ServerLogManager.cpp index 554cfedeb..d4204af80 100644 --- a/Minecraft.Server/ServerLogManager.cpp +++ b/Minecraft.Server/ServerLogManager.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "stdafx.h" #include "ServerLogManager.h" @@ -367,6 +367,30 @@ namespace ServerRuntime DisconnectReasonToString(reason)); } + /** + * For logging purposes, the responsibility is technically misplaced, but the IP is cached in `LogManager`. + * Those cached values are then used to retrieve the player's IP. + * + * Eventually, this should be implemented in a separate class or on the `Minecraft.Client` side instead. + */ + bool TryGetConnectionRemoteIp(unsigned char smallId, std::string *outIp) + { + if (!IsDedicatedServerLoggingEnabled() || outIp == NULL) + { + return false; + } + + std::lock_guard stateLock(g_serverLogState.stateLock); + const ConnectionLogEntry &entry = g_serverLogState.entries[smallId]; + if (entry.remoteIp.empty() || entry.remoteIp == "unknown") + { + return false; + } + + *outIp = entry.remoteIp; + return true; + } + // Provide explicit cache cleanup for paths that terminate without going through disconnect logging. void ClearConnection(unsigned char smallId) { diff --git a/Minecraft.Server/ServerLogManager.h b/Minecraft.Server/ServerLogManager.h index 9425fc5c6..989553caa 100644 --- a/Minecraft.Server/ServerLogManager.h +++ b/Minecraft.Server/ServerLogManager.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include @@ -109,10 +109,18 @@ namespace ServerRuntime DisconnectPacket::eDisconnectReason reason, bool initiatedByServer); + /** + * Reads the cached remote IP for a live smallId without consuming the entry + * Eventually, this should be implemented in a separate class or on the `Minecraft.Client` side instead. + * + * 指定smallIdの接続IPをキャッシュから参照する + */ + bool TryGetConnectionRemoteIp(unsigned char smallId, std::string *outIp); + /** * Removes any remembered IP or player name for the specified smallId * 指定smallIdに紐づく接続キャッシュを消去 */ void ClearConnection(unsigned char smallId); } -} \ No newline at end of file +}