mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-08-20 09:57:09 +00:00
The old Windows64 port had no real player identity — it used hardcoded fake XUIDs, so anyone could impersonate anyone. This replaces that with proper auth supporting Mojang, Ely.by, and offline accounts. MCAuth library (new, MCAuth/): Mojang auth via MSA device code flow (XBL, SISU, MC services), Ely.by via Yggdrasil with 2FA, offline UUID generation matching Java Edition (MD5 v3 from "OfflinePlayer:<name>"). Multi-account manager with background token refresh, per-slot sessions, and on-disk token persistence. Server-side session verification via Mojang/Ely.by hasJoined API. Skin fetching and PNG validation from texture servers. Network protocol (version bumped to 80): Three new packets (AuthScheme, AuthResponse, AuthResult) implement a server-driven auth handshake before login completes. Player identity migrated from 64-bit XUID to 128-bit GameUUID backed by two uint64 fields (hi/lo). readPlayerUID/writePlayerUID now serialize 16 bytes on the wire. Old and new clients cannot connect to each other — version mismatch is rejected at PreLogin. Save migration: Map data mappings auto-migrate from old format: the old 64-bit XUID is placed in hi, lo is set to 0 as a sentinel. On first access by the real player, the sentinel entry is upgraded in-place to the full 128-bit UUID. Format detection is by file size (2080, 2112, or 4160 bytes). Player .dat filenames inside saveData.ms change from decimal XUID to dashed UUID — old saves need manual entry renaming in the archive. UI: NativeUIRenderer: immediate-mode drawing system (quads, text, 9-slice panels, scrollbars, focus lists) for rendering auth screens without Flash/Scaleform. UIScene_MSAuth handles device code display, Ely.by credential input with 2FA, per-account skin head preview, and multi-account add/remove/switch. Server: online-mode and auth-provider (mojang/elyby) in server.properties. Whitelist and ban checks validate against the server-verified UUID. Incompatible auth scheme logs which provider the server expects vs what the client is using. Also fixes a pre-existing exploit where any client could send a DebugOptionsPacket to grant themselves CraftAnything and other debug privileges on any server — now requires OP status server-side.
765 lines
25 KiB
C++
765 lines
25 KiB
C++
#include "stdafx.h"
|
||
#include "PendingConnection.h"
|
||
#include "PlayerConnection.h"
|
||
#include "ServerConnection.h"
|
||
#include "ServerPlayer.h"
|
||
#include "ServerPlayerGameMode.h"
|
||
#include "ServerLevel.h"
|
||
#include "PlayerList.h"
|
||
#include "MinecraftServer.h"
|
||
#include "..\Minecraft.World\net.minecraft.network.h"
|
||
#include "..\Minecraft.World\net.minecraft.network.packet.h"
|
||
#include "..\Minecraft.World\pos.h"
|
||
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
|
||
#include "..\Minecraft.World\net.minecraft.world.level.storage.h"
|
||
#include "..\Minecraft.World\net.minecraft.world.item.h"
|
||
#include "..\Minecraft.World\SharedConstants.h"
|
||
#include "..\Minecraft.World\GameUUID.h"
|
||
#include "Settings.h"
|
||
#ifdef _WINDOWS64
|
||
#include "..\..\MCAuth\include\MCAuth.h"
|
||
#include <thread>
|
||
#endif
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
#include "..\Minecraft.Server\ServerLogManager.h"
|
||
#include "..\Minecraft.Server\ServerLogger.h"
|
||
#include "..\Minecraft.Server\Access\Access.h"
|
||
#include "..\Minecraft.World\Socket.h"
|
||
#endif
|
||
// #ifdef __PS3__
|
||
// #include "PS3\Network\NetworkPlayerSony.h"
|
||
// #endif
|
||
|
||
// Auth logging: use INFO-level server logger on dedicated server, DebugPrintf on client
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
static void AuthLog(const char* fmt, ...)
|
||
{
|
||
char buf[2048];
|
||
va_list args;
|
||
va_start(args, fmt);
|
||
vsnprintf_s(buf, sizeof(buf), _TRUNCATE, fmt, args);
|
||
va_end(args);
|
||
// Strip trailing newline for server logger (it adds its own)
|
||
size_t len = strlen(buf);
|
||
while (len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = '\0';
|
||
ServerRuntime::LogInfof("auth", "%s", buf);
|
||
}
|
||
#define AUTH_LOG(fmt, ...) AuthLog(fmt, ##__VA_ARGS__)
|
||
#else
|
||
#define AUTH_LOG(fmt, ...) app.DebugPrintf(fmt, ##__VA_ARGS__)
|
||
#endif
|
||
|
||
Random *PendingConnection::random = new Random();
|
||
|
||
#ifdef _WINDOWS64
|
||
bool g_bRejectDuplicateNames = true;
|
||
#endif
|
||
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
namespace
|
||
{
|
||
static unsigned char GetPendingConnectionSmallId(Connection *connection)
|
||
{
|
||
if (connection != nullptr)
|
||
{
|
||
Socket *socket = connection->getSocket();
|
||
if (socket != nullptr)
|
||
{
|
||
return socket->getSmallId();
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
struct UgcGatherResult
|
||
{
|
||
PlayerUID *xuids;
|
||
DWORD cappedCount;
|
||
BYTE friendsOnlyBits;
|
||
BYTE cappedHostIndex;
|
||
char uniqueMapName[14];
|
||
};
|
||
|
||
static UgcGatherResult GatherUgcData()
|
||
{
|
||
UgcGatherResult r = {};
|
||
StorageManager.GetSaveUniqueFilename(r.uniqueMapName);
|
||
|
||
r.xuids = new PlayerUID[MINECRAFT_NET_MAX_PLAYERS];
|
||
DWORD count = 0;
|
||
DWORD hostIndex = 0;
|
||
|
||
PlayerList *playerList = MinecraftServer::getInstance()->getPlayers();
|
||
for (auto& player : playerList->players)
|
||
{
|
||
if (player != nullptr && player->connection->m_xuid != INVALID_XUID)
|
||
{
|
||
if (player->connection->m_friendsOnlyUGC)
|
||
r.friendsOnlyBits |= (1 << count);
|
||
r.xuids[count] = player->connection->m_xuid;
|
||
if (player->connection->getNetworkPlayer() != nullptr && player->connection->getNetworkPlayer()->IsHost())
|
||
hostIndex = count;
|
||
++count;
|
||
}
|
||
}
|
||
|
||
r.cappedCount = (count > 255u) ? 255u : count;
|
||
r.cappedHostIndex = (hostIndex >= 255u) ? 254 : static_cast<BYTE>(hostIndex);
|
||
return r;
|
||
}
|
||
|
||
PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id)
|
||
{
|
||
// 4J - added initialisers
|
||
done = false;
|
||
_tick = 0;
|
||
name = L"";
|
||
acceptedLogin = nullptr;
|
||
loginKey = L"";
|
||
|
||
this->server = server;
|
||
connection = new Connection(socket, id, this);
|
||
connection->fakeLag = FAKE_LAG;
|
||
}
|
||
|
||
PendingConnection::~PendingConnection()
|
||
{
|
||
if (m_authVerifyResult)
|
||
m_authVerifyResult->cancelled.store(true, std::memory_order_release);
|
||
delete connection;
|
||
}
|
||
|
||
void PendingConnection::tick()
|
||
{
|
||
if (acceptedLogin != nullptr)
|
||
{
|
||
this->handleAcceptedLogin(acceptedLogin);
|
||
acceptedLogin = nullptr;
|
||
}
|
||
|
||
#ifdef _WINDOWS64
|
||
// Poll async Mojang verification result
|
||
if (m_authState == eAuth_Verifying && m_authVerifyResult && m_authVerifyResult->ready.load(std::memory_order_acquire))
|
||
{
|
||
std::lock_guard<std::mutex> lock(m_authVerifyResult->mutex);
|
||
|
||
if (m_authVerifyResult->success)
|
||
{
|
||
m_authUsername = m_authVerifyResult->username;
|
||
m_authUuid = MCAuth::DashUuid(m_authVerifyResult->uuid);
|
||
AUTH_LOG("[Auth] %s verification SUCCESS for '%s' (uuid=%s)\n",
|
||
m_authScheme.c_str(), m_authUsername.c_str(), m_authUuid.c_str());
|
||
|
||
// Store skin data for later use in placeNewPlayer
|
||
m_authSkinData = std::move(m_authVerifyResult->skinData);
|
||
if (!m_authSkinData.empty())
|
||
{
|
||
if (m_authScheme == "elyby")
|
||
m_authSkinUrl = MCAuth::MakeElybySkinKey(m_authVerifyResult->uuid);
|
||
else
|
||
m_authSkinUrl = MCAuth::MakeSkinKey(m_authVerifyResult->uuid);
|
||
AUTH_LOG("[Auth] Downloaded %s skin for %s (%zu bytes)\n",
|
||
m_authScheme.c_str(), m_authUsername.c_str(), m_authSkinData.size());
|
||
}
|
||
|
||
wstring wUuid(m_authUuid.begin(), m_authUuid.end());
|
||
wstring wName(m_authUsername.begin(), m_authUsername.end());
|
||
// Send skin key + bytes inline so the client doesn't need a separate download
|
||
wstring wSkinKey(m_authSkinUrl.begin(), m_authSkinUrl.end());
|
||
connection->send(make_shared<AuthResultPacket>(true, wUuid, wName, L"", wSkinKey, m_authSkinData));
|
||
m_authState = eAuth_WaitingAck;
|
||
}
|
||
else
|
||
{
|
||
AUTH_LOG("[Auth] %s verification FAILED for pending connection (error=%s), disconnecting\n",
|
||
m_authScheme.c_str(), m_authVerifyResult->errorDetail.c_str());
|
||
connection->send(make_shared<AuthResultPacket>(false, L"", L"", L"Authentication failed"));
|
||
connection->sendAndQuit();
|
||
m_authState = eAuth_Done;
|
||
done = true;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
if (_tick++ == MAX_TICKS_BEFORE_LOGIN)
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_LoginTooLong);
|
||
}
|
||
else
|
||
{
|
||
connection->tick();
|
||
}
|
||
}
|
||
|
||
void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
|
||
{
|
||
if (m_authVerifyResult)
|
||
m_authVerifyResult->cancelled.store(true, std::memory_order_release);
|
||
|
||
const char* reasonStr = "unknown";
|
||
switch (reason)
|
||
{
|
||
case DisconnectPacket::eDisconnect_Closed: reasonStr = "Closed"; break;
|
||
case DisconnectPacket::eDisconnect_Kicked: reasonStr = "Kicked"; break;
|
||
case DisconnectPacket::eDisconnect_LoginTooLong: reasonStr = "LoginTooLong"; break;
|
||
case DisconnectPacket::eDisconnect_OutdatedServer: reasonStr = "OutdatedServer"; break;
|
||
case DisconnectPacket::eDisconnect_OutdatedClient: reasonStr = "OutdatedClient"; break;
|
||
case DisconnectPacket::eDisconnect_ServerFull: reasonStr = "ServerFull"; break;
|
||
case DisconnectPacket::eDisconnect_AuthFailed: reasonStr = "AuthFailed"; break;
|
||
case DisconnectPacket::eDisconnect_UnexpectedPacket: reasonStr = "UnexpectedPacket"; break;
|
||
default: break;
|
||
}
|
||
AUTH_LOG("[Auth] Pending connection disconnect: reason=%d (%s)\n", reason, reasonStr);
|
||
connection->send(std::make_shared<DisconnectPacket>(reason));
|
||
connection->sendAndQuit();
|
||
done = true;
|
||
// } catch (Exception e) {
|
||
// e.printStackTrace();
|
||
// }
|
||
}
|
||
|
||
void PendingConnection::handlePreLogin(shared_ptr<PreLoginPacket> packet)
|
||
{
|
||
if (packet->m_netcodeVersion != MINECRAFT_NET_VERSION)
|
||
{
|
||
app.DebugPrintf("Netcode version is %d not equal to %d\n", packet->m_netcodeVersion, MINECRAFT_NET_VERSION);
|
||
if (packet->m_netcodeVersion > MINECRAFT_NET_VERSION)
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_OutdatedServer);
|
||
}
|
||
else
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_OutdatedClient);
|
||
}
|
||
return;
|
||
}
|
||
// printf("Server: handlePreLogin\n");
|
||
name = packet->loginKey; // 4J Stu - Change from the login packet as we know better on client end during the pre-login packet
|
||
sendPreLoginResponse();
|
||
}
|
||
|
||
void PendingConnection::sendPreLoginResponse()
|
||
{
|
||
// 4J Stu - Calculate the players with UGC privileges set
|
||
UgcGatherResult ugc = GatherUgcData();
|
||
connection->send(std::make_shared<PreLoginPacket>(L"-", ugc.xuids, ugc.cappedCount, ugc.friendsOnlyBits, server->m_ugcPlayersVersion, ugc.uniqueMapName, app.GetGameHostOption(eGameHostOption_All), ugc.cappedHostIndex, server->m_texturePackId));
|
||
|
||
// --- Auth handshake ---
|
||
// Generate random serverId (20 hex chars)
|
||
{
|
||
static const char hex[] = "0123456789abcdef";
|
||
m_serverId.resize(20);
|
||
for (int i = 0; i < 20; i++)
|
||
m_serverId[i] = hex[random->nextInt(16)];
|
||
}
|
||
|
||
vector<wstring> schemes;
|
||
if (server->onlineMode)
|
||
{
|
||
if (server->authProvider == "elyby")
|
||
{
|
||
schemes.push_back(L"elyby");
|
||
AUTH_LOG("[Auth] Server onlineMode=true, authProvider=elyby, offering scheme: [elyby]\n");
|
||
}
|
||
else
|
||
{
|
||
schemes.push_back(L"mojang");
|
||
AUTH_LOG("[Auth] Server onlineMode=true, authProvider=mojang, offering scheme: [mojang]\n");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
schemes.push_back(L"mojang");
|
||
schemes.push_back(L"offline");
|
||
AUTH_LOG("[Auth] Server onlineMode=false, offering schemes: [mojang, offline]\n");
|
||
}
|
||
|
||
wstring wServerId(m_serverId.begin(), m_serverId.end());
|
||
connection->send(make_shared<AuthSchemePacket>(schemes, wServerId));
|
||
m_authState = eAuth_WaitingResponse;
|
||
AUTH_LOG("[Auth] Sent AuthSchemePacket to client, serverId=%s\n", m_serverId.c_str());
|
||
}
|
||
|
||
void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
|
||
{
|
||
// printf("Server: handleLogin\n");
|
||
//name = packet->userName;
|
||
|
||
// Reject login if auth handshake has not completed.
|
||
// eAuth_None means no auth handshake occurred — reject to prevent auth bypass.
|
||
if (m_authState == eAuth_WaitingAck)
|
||
{
|
||
// LoginPacket completes the auth handshake
|
||
wstring wName(m_authUsername.begin(), m_authUsername.end());
|
||
name = wName;
|
||
m_authState = eAuth_Done;
|
||
AUTH_LOG("[Auth] Auth handshake complete for '%s' (implicit ack via LoginPacket)\n", m_authUsername.c_str());
|
||
}
|
||
else if (m_authState == eAuth_Done)
|
||
{
|
||
// Already completed — allow through
|
||
}
|
||
else
|
||
{
|
||
// eAuth_None, eAuth_WaitingResponse, eAuth_Verifying — all invalid
|
||
AUTH_LOG("[Auth] Received LoginPacket before auth completed (state=%d), disconnecting\n", (int)m_authState);
|
||
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
|
||
return;
|
||
}
|
||
|
||
if (packet->clientVersion != SharedConstants::NETWORK_PROTOCOL_VERSION)
|
||
{
|
||
app.DebugPrintf("Client version is %d not equal to %d\n", packet->clientVersion, SharedConstants::NETWORK_PROTOCOL_VERSION);
|
||
if (packet->clientVersion > SharedConstants::NETWORK_PROTOCOL_VERSION)
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_OutdatedServer);
|
||
}
|
||
else
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_OutdatedClient);
|
||
}
|
||
return;
|
||
}
|
||
|
||
//if (true)// 4J removed !server->onlineMode)
|
||
PlayerUID loginXuid = packet->m_xuid;
|
||
// INVALID_XUID may indicate a truncated packet (readPlayerUID returns {0,0} on EOF)
|
||
if (loginXuid == INVALID_XUID)
|
||
{
|
||
AUTH_LOG("[Auth] Warning: received INVALID_XUID in LoginPacket — possible truncated packet or offline client\n");
|
||
}
|
||
|
||
bool duplicateXuid = (loginXuid != INVALID_XUID && server->getPlayers()->getPlayer(loginXuid) != nullptr);
|
||
|
||
bool bannedXuid = (loginXuid != INVALID_XUID && server->getPlayers()->isXuidBanned(loginXuid));
|
||
// Also check ban against server-verified auth UUID to prevent bypass via INVALID_XUID
|
||
if (!bannedXuid && !m_authUuid.empty())
|
||
{
|
||
GameUUID authUid = GameUUID::fromDashed(m_authUuid);
|
||
if (authUid.isValid())
|
||
{
|
||
bannedXuid = server->getPlayers()->isXuidBanned(authUid);
|
||
}
|
||
}
|
||
|
||
bool whitelistSatisfied = true;
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
if (ServerRuntime::Access::IsWhitelistEnabled())
|
||
{
|
||
whitelistSatisfied = (loginXuid != INVALID_XUID && ServerRuntime::Access::IsPlayerWhitelisted(loginXuid));
|
||
if (!whitelistSatisfied && !m_authUuid.empty())
|
||
{
|
||
GameUUID authUid = GameUUID::fromDashed(m_authUuid);
|
||
if (authUid.isValid())
|
||
{
|
||
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(authUid);
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
if (bannedXuid)
|
||
{
|
||
#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);
|
||
}
|
||
else if (duplicateXuid)
|
||
{
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateXuid);
|
||
#endif
|
||
// Reject the incoming connection — a player with this UID is already
|
||
// on the server. Allowing duplicates causes invisible players and
|
||
// other undefined behaviour.
|
||
app.DebugPrintf("LOGIN: Rejecting duplicate xuid for name: %ls\n", name.c_str());
|
||
disconnect(DisconnectPacket::eDisconnect_Banned);
|
||
}
|
||
#ifdef _WINDOWS64
|
||
else if (g_bRejectDuplicateNames)
|
||
{
|
||
bool nameTaken = false;
|
||
vector<shared_ptr<ServerPlayer> >& pl = server->getPlayers()->players;
|
||
for (const auto& i : pl)
|
||
{
|
||
if (i != nullptr && i->name == name)
|
||
{
|
||
nameTaken = true;
|
||
break;
|
||
}
|
||
}
|
||
if (nameTaken)
|
||
{
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_DuplicateName);
|
||
#endif
|
||
app.DebugPrintf("Rejecting duplicate name: %ls\n", name.c_str());
|
||
disconnect(DisconnectPacket::eDisconnect_Banned);
|
||
}
|
||
else
|
||
{
|
||
handleAcceptedLogin(packet);
|
||
}
|
||
}
|
||
#endif
|
||
else
|
||
{
|
||
handleAcceptedLogin(packet);
|
||
}
|
||
|
||
}
|
||
|
||
void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
|
||
{
|
||
if(packet->m_ugcPlayersVersion != server->m_ugcPlayersVersion)
|
||
{
|
||
// UGC version mismatch — resend pre-login info but do NOT restart auth handshake.
|
||
// Only resend the PreLoginPacket with updated UGC player list; the auth state
|
||
// (m_authState, m_authUsername, m_authUuid) remains valid from the completed handshake.
|
||
UgcGatherResult ugc = GatherUgcData();
|
||
connection->send(std::make_shared<PreLoginPacket>(L"-", ugc.xuids, ugc.cappedCount, ugc.friendsOnlyBits, server->m_ugcPlayersVersion, ugc.uniqueMapName, app.GetGameHostOption(eGameHostOption_All), ugc.cappedHostIndex, server->m_texturePackId));
|
||
return;
|
||
}
|
||
|
||
PlayerUID playerXuid = packet->m_xuid;
|
||
// Fallback: generate offline UUID from player name if still invalid
|
||
if (playerXuid == INVALID_XUID)
|
||
{
|
||
if (name.empty())
|
||
{
|
||
AUTH_LOG("[Auth] Rejecting login with empty name and no UUID\n");
|
||
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
|
||
return;
|
||
}
|
||
playerXuid = GameUUID::generateOffline(std::string(name.begin(), name.end()));
|
||
}
|
||
|
||
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid);
|
||
if (playerEntity != nullptr)
|
||
{
|
||
// Use server-verified auth UUID instead of client-supplied packet->m_mojangUuid
|
||
if (!m_authUuid.empty())
|
||
{
|
||
GameUUID verifiedUuid = GameUUID::fromDashed(m_authUuid);
|
||
if (verifiedUuid.isValid())
|
||
{
|
||
playerEntity->setXuid(verifiedUuid);
|
||
}
|
||
}
|
||
|
||
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
|
||
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
|
||
#endif
|
||
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
|
||
|
||
// Override with Mojang skin after placeNewPlayer has set the DLC skin
|
||
if (!m_authSkinData.empty() && !m_authSkinUrl.empty())
|
||
{
|
||
wstring wSkinKey(m_authSkinUrl.begin(), m_authSkinUrl.end());
|
||
// AddMemoryTextureFile takes ownership of the pointer — must be heap-allocated
|
||
DWORD skinSize = (DWORD)m_authSkinData.size();
|
||
PBYTE skinBuf = new BYTE[skinSize];
|
||
memcpy(skinBuf, m_authSkinData.data(), skinSize);
|
||
app.AddMemoryTextureFile(wSkinKey, skinBuf, skinSize);
|
||
playerEntity->customTextureUrl = wSkinKey;
|
||
app.DebugPrintf("Set Mojang skin for player %s: %s (%zu bytes)\n",
|
||
m_authUsername.c_str(), m_authSkinUrl.c_str(), m_authSkinData.size());
|
||
|
||
// Send Mojang skin to all other clients
|
||
for (auto& otherPlayer : server->getPlayers()->players)
|
||
{
|
||
if (otherPlayer != nullptr && otherPlayer != playerEntity && otherPlayer->connection != nullptr)
|
||
{
|
||
// Send the skin PNG data so the other client has it in memory
|
||
PBYTE otherBuf = new BYTE[skinSize];
|
||
memcpy(otherBuf, m_authSkinData.data(), skinSize);
|
||
otherPlayer->connection->send(
|
||
std::make_shared<TextureAndGeometryPacket>(wSkinKey, otherBuf, skinSize));
|
||
|
||
// Notify the other client that this player now uses this skin
|
||
otherPlayer->connection->send(
|
||
std::make_shared<TextureAndGeometryChangePacket>(playerEntity, wSkinKey));
|
||
}
|
||
}
|
||
|
||
// Send existing Mojang skins to the new joiner
|
||
for (auto& otherPlayer : server->getPlayers()->players)
|
||
{
|
||
if (otherPlayer != nullptr && otherPlayer != playerEntity &&
|
||
!otherPlayer->customTextureUrl.empty() &&
|
||
(otherPlayer->customTextureUrl.substr(0, 6) == L"mojang" ||
|
||
otherPlayer->customTextureUrl.substr(0, 5) == L"elyby"))
|
||
{
|
||
PBYTE existingData = nullptr;
|
||
DWORD existingSize = 0;
|
||
app.GetMemFileDetails(otherPlayer->customTextureUrl, &existingData, &existingSize);
|
||
if (existingData != nullptr && existingSize > 0)
|
||
{
|
||
PBYTE copyBuf = new BYTE[existingSize];
|
||
memcpy(copyBuf, existingData, existingSize);
|
||
playerEntity->connection->send(
|
||
std::make_shared<TextureAndGeometryPacket>(otherPlayer->customTextureUrl, copyBuf, existingSize));
|
||
playerEntity->connection->send(
|
||
std::make_shared<TextureAndGeometryChangePacket>(otherPlayer, otherPlayer->customTextureUrl));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor
|
||
}
|
||
else
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_ServerFull);
|
||
return;
|
||
}
|
||
done = true;
|
||
|
||
}
|
||
|
||
void PendingConnection::handleAuthResponse(shared_ptr<AuthResponsePacket> packet)
|
||
{
|
||
if (m_authState != eAuth_WaitingResponse)
|
||
{
|
||
AUTH_LOG("[Auth] Received AuthResponse in unexpected state %d, disconnecting\n", (int)m_authState);
|
||
disconnect(DisconnectPacket::eDisconnect_UnexpectedPacket);
|
||
return;
|
||
}
|
||
|
||
// Convert wstring fields to std::string for MCAuth
|
||
string scheme(packet->chosenScheme.begin(), packet->chosenScheme.end());
|
||
string username(packet->username.begin(), packet->username.end());
|
||
string uuid(packet->mojangUuid.begin(), packet->mojangUuid.end());
|
||
|
||
AUTH_LOG("[Auth] Received AuthResponse: scheme='%s', username='%s', uuid='%s'\n",
|
||
scheme.c_str(), username.c_str(), uuid.c_str());
|
||
|
||
if (username.empty())
|
||
{
|
||
AUTH_LOG("[Auth] REJECTED: empty username\n");
|
||
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
|
||
return;
|
||
}
|
||
|
||
m_authScheme = scheme;
|
||
|
||
#ifdef _WINDOWS64
|
||
if (scheme == "mojang")
|
||
{
|
||
AUTH_LOG("[Auth] Starting Mojang session verification for '%s'...\n", username.c_str());
|
||
// Verify with Mojang sessionserver in a background thread
|
||
// shared_ptr so the result outlives PendingConnection if destroyed
|
||
m_authState = eAuth_Verifying;
|
||
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
|
||
|
||
string serverId = m_serverId;
|
||
auto sharedResult = m_authVerifyResult; // capture shared_ptr by value
|
||
|
||
std::thread([username, serverId, sharedResult]() {
|
||
try {
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
string error;
|
||
auto result = MCAuth::HasJoined(username, serverId, error);
|
||
|
||
// Client may have disconnected
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
std::vector<uint8_t> skinBytes;
|
||
if (result.success && !result.skinUrl.empty())
|
||
{
|
||
string skinError;
|
||
skinBytes = MCAuth::FetchSkinPng(result.skinUrl, skinError);
|
||
}
|
||
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
// Prevents data race with tick()
|
||
{
|
||
std::lock_guard<std::mutex> lock(sharedResult->mutex);
|
||
sharedResult->success = result.success;
|
||
if (result.success)
|
||
{
|
||
sharedResult->username = result.username;
|
||
sharedResult->uuid = result.uuid;
|
||
sharedResult->skinUrl = result.skinUrl;
|
||
sharedResult->skinData = std::move(skinBytes);
|
||
}
|
||
else
|
||
{
|
||
sharedResult->errorDetail = error;
|
||
}
|
||
}
|
||
// Signal main thread
|
||
sharedResult->ready.store(true, std::memory_order_release);
|
||
}
|
||
catch (...) {
|
||
std::lock_guard<std::mutex> lock(sharedResult->mutex);
|
||
sharedResult->success = false;
|
||
sharedResult->errorDetail = "unknown exception";
|
||
sharedResult->ready.store(true, std::memory_order_release);
|
||
}
|
||
}).detach();
|
||
}
|
||
else if (scheme == "elyby")
|
||
{
|
||
AUTH_LOG("[Auth] Starting ely.by session verification for '%s'...\n", username.c_str());
|
||
m_authState = eAuth_Verifying;
|
||
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
|
||
|
||
string capturedServerId = m_serverId;
|
||
auto sharedResult = m_authVerifyResult;
|
||
|
||
std::thread([username, capturedServerId, sharedResult]() {
|
||
try {
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
string error;
|
||
auto result = MCAuth::ElybyHasJoined(username, capturedServerId, error);
|
||
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
std::vector<uint8_t> skinBytes;
|
||
if (result.success && !result.skinUrl.empty())
|
||
{
|
||
string skinError;
|
||
skinBytes = MCAuth::FetchSkinPng(result.skinUrl, skinError);
|
||
}
|
||
|
||
if (sharedResult->cancelled.load(std::memory_order_acquire))
|
||
return;
|
||
|
||
{
|
||
std::lock_guard<std::mutex> lock(sharedResult->mutex);
|
||
sharedResult->success = result.success;
|
||
if (result.success)
|
||
{
|
||
sharedResult->username = result.username;
|
||
sharedResult->uuid = result.uuid;
|
||
sharedResult->skinUrl = result.skinUrl;
|
||
sharedResult->skinData = std::move(skinBytes);
|
||
}
|
||
else
|
||
{
|
||
sharedResult->errorDetail = error;
|
||
}
|
||
}
|
||
sharedResult->ready.store(true, std::memory_order_release);
|
||
}
|
||
catch (const std::exception& ex) {
|
||
std::lock_guard<std::mutex> lock(sharedResult->mutex);
|
||
sharedResult->success = false;
|
||
sharedResult->errorDetail = std::string("exception: ") + ex.what();
|
||
sharedResult->ready.store(true, std::memory_order_release);
|
||
}
|
||
catch (...) {
|
||
std::lock_guard<std::mutex> lock(sharedResult->mutex);
|
||
sharedResult->success = false;
|
||
sharedResult->errorDetail = "unknown exception";
|
||
sharedResult->ready.store(true, std::memory_order_release);
|
||
}
|
||
}).detach();
|
||
}
|
||
else
|
||
#endif
|
||
if (scheme == "offline")
|
||
{
|
||
if (server->onlineMode)
|
||
{
|
||
AUTH_LOG("[Auth] REJECTED: Client chose 'offline' but server requires online-mode (onlineMode=true)\n");
|
||
connection->send(make_shared<AuthResultPacket>(
|
||
false, L"", L"", L"Server requires Microsoft authentication"));
|
||
connection->sendAndQuit();
|
||
m_authState = eAuth_Done;
|
||
done = true;
|
||
return;
|
||
}
|
||
|
||
// Server-generated UUID prevents UUID forgery
|
||
GameUUID offlineGuid = GameUUID::generateOffline(username);
|
||
std::string offlineUuid = offlineGuid.toDashed();
|
||
AUTH_LOG("[Auth] Accepted offline auth for '%s' (server-assigned uuid=%s, client-sent uuid=%s)\n",
|
||
username.c_str(), offlineUuid.c_str(), uuid.c_str());
|
||
m_authUsername = username;
|
||
m_authUuid = offlineUuid;
|
||
|
||
wstring wUuid(offlineUuid.begin(), offlineUuid.end());
|
||
wstring wName(username.begin(), username.end());
|
||
connection->send(make_shared<AuthResultPacket>(true, wUuid, wName, L""));
|
||
m_authState = eAuth_WaitingAck;
|
||
}
|
||
else
|
||
{
|
||
AUTH_LOG("[Auth] REJECTED: Unknown auth scheme '%s', disconnecting\n", scheme.c_str());
|
||
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
|
||
}
|
||
}
|
||
|
||
|
||
void PendingConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects)
|
||
{
|
||
// logger.info(getName() + " lost connection");
|
||
done = true;
|
||
}
|
||
|
||
void PendingConnection::handleGetInfo(shared_ptr<GetInfoPacket> packet)
|
||
{
|
||
//try {
|
||
//String message = server->motd + "<22>" + server->players->getPlayerCount() + "<22>" + server->players->getMaxPlayers();
|
||
//connection->send(new DisconnectPacket(message));
|
||
connection->send(std::make_shared<DisconnectPacket>(DisconnectPacket::eDisconnect_ServerFull));
|
||
connection->sendAndQuit();
|
||
server->connection->removeSpamProtection(connection->getSocket());
|
||
done = true;
|
||
//} catch (Exception e) {
|
||
// e.printStackTrace();
|
||
//}
|
||
}
|
||
|
||
void PendingConnection::handleKeepAlive(shared_ptr<KeepAlivePacket> packet)
|
||
{
|
||
// Ignore
|
||
}
|
||
|
||
void PendingConnection::onUnhandledPacket(shared_ptr<Packet> packet)
|
||
{
|
||
disconnect(DisconnectPacket::eDisconnect_UnexpectedPacket);
|
||
}
|
||
|
||
void PendingConnection::send(shared_ptr<Packet> packet)
|
||
{
|
||
connection->send(packet);
|
||
}
|
||
|
||
wstring PendingConnection::getName()
|
||
{
|
||
return L"Unimplemented";
|
||
// if (name != null) return name + " [" + connection.getRemoteAddress().toString() + "]";
|
||
// return connection.getRemoteAddress().toString();
|
||
}
|
||
|
||
bool PendingConnection::isServerPacketListener()
|
||
{
|
||
return true;
|
||
}
|
||
|
||
bool PendingConnection::isDisconnected()
|
||
{
|
||
return done;
|
||
}
|