diff --git a/CMakeLists.txt b/CMakeLists.txt index 63071a0ed7..979dd2a558 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,21 @@ project(MinecraftConsoles LANGUAGES C CXX RC ASM_MASM) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +include(FetchContent) +set(BUILD_CURL_EXE OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(CURL_USE_SCHANNEL ON CACHE BOOL "" FORCE) +set(BUILD_TESTING OFF CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAP ON CACHE BOOL "" FORCE) +set(CURL_DISABLE_LDAPS ON CACHE BOOL "" FORCE) +FetchContent_Declare( + curl + URL https://github.com/curl/curl/releases/download/curl-8_11_1/curl-8.11.1.tar.xz + URL_HASH SHA256=c7ca7db48b0909743eaef34250da02c19bc61d4f1dcedd6603f109409536ab56 + FIND_PACKAGE_ARGS +) +FetchContent_MakeAvailable(curl) if(NOT WIN32) message(FATAL_ERROR "This CMake build currently supports Windows only.") @@ -18,7 +33,6 @@ set(CMAKE_CONFIGURATION_TYPES "Release" CACHE STRING "" FORCE ) -set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") function(configure_compiler_target target) # MSVC and compatible compilers (like Clang-cl) diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp new file mode 100644 index 0000000000..bdaa2b9b7b --- /dev/null +++ b/Minecraft.Client/AuthScreen.cpp @@ -0,0 +1,459 @@ +#include "stdafx.h" +#include "AuthScreen.h" +#include "Minecraft.h" +#include "User.h" +#include "..\Minecraft.World\AuthModule.h" +#include "..\Minecraft.World\SessionAuthModule.h" +#include "..\Minecraft.World\HttpClient.h" +#include "..\Minecraft.World\StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" +#include +#include +#include + +using json = nlohmann::json; +static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; +static constexpr auto MS_CLIENT_ID = "00000000402b5328"; +static json parseResponse(const HttpResponse &resp, int expectedStatus = 200); +static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName); +static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken); +static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl); +static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl); + +vector AuthProfileManager::profiles; +int AuthProfileManager::selectedProfile = -1; + +static constexpr uint32_t PROFILE_MAGIC = 0x4D435032; + +void AuthProfileManager::load() +{ + profiles.clear(); + std::ifstream file(PROFILES_FILE, std::ios::binary); + if (!file) return; + + uint32_t header = 0; + file.read(reinterpret_cast(&header), sizeof(header)); + bool hasVariation = (header == PROFILE_MAGIC); + + uint32_t count = 0; + if (hasVariation) + file.read(reinterpret_cast(&count), sizeof(count)); + else + count = header; + + for (uint32_t i = 0; i < count && file.good(); i++) + { + AuthProfile p; + uint8_t type; + file.read(reinterpret_cast(&type), sizeof(type)); + p.type = static_cast(type); + + auto readWstr = [&file]() -> wstring { + uint16_t len = 0; + file.read(reinterpret_cast(&len), sizeof(len)); + if (!file || len > 4096) return {}; + wstring s(len, L'\0'); + file.read(reinterpret_cast(s.data()), len * sizeof(wchar_t)); + if (!file) return {}; + return s; + }; + + p.uid = readWstr(); + p.username = readWstr(); + p.token = readWstr(); + p.clientToken = readWstr(); + if (hasVariation) + p.variation = readWstr(); + if (p.variation.empty() && p.type == AuthProfile::YGGDRASIL) + p.variation = L"elyby"; + profiles.push_back(std::move(p)); + } + + int32_t savedIdx = 0; + file.read(reinterpret_cast(&savedIdx), sizeof(savedIdx)); + if (!profiles.empty()) + selectedProfile = (savedIdx >= 0 && savedIdx < static_cast(profiles.size())) ? savedIdx : 0; +} + +void AuthProfileManager::save() +{ + std::ofstream file(PROFILES_FILE, std::ios::binary | std::ios::trunc); + if (!file) return; + + uint32_t magic = PROFILE_MAGIC; + file.write(reinterpret_cast(&magic), sizeof(magic)); + + uint32_t count = static_cast(profiles.size()); + file.write(reinterpret_cast(&count), sizeof(count)); + + auto writeWstr = [&file](const wstring &s) { + uint16_t len = static_cast(s.length()); + file.write(reinterpret_cast(&len), sizeof(len)); + file.write(reinterpret_cast(s.data()), len * sizeof(wchar_t)); + }; + + for (const auto &p : profiles) + { + uint8_t type = static_cast(p.type); + file.write(reinterpret_cast(&type), sizeof(type)); + writeWstr(p.uid); + writeWstr(p.username); + writeWstr(p.token); + writeWstr(p.clientToken); + writeWstr(p.variation); + } + + int32_t idx = static_cast(selectedProfile); + file.write(reinterpret_cast(&idx), sizeof(idx)); +} + +void AuthProfileManager::addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid, const wstring &token, const wstring &clientToken, const wstring &variation) +{ + wstring finalUid = uid.empty() ? L"offline_" + username : uid; + profiles.push_back({type, finalUid, username, token, clientToken, variation}); + selectedProfile = static_cast(profiles.size()) - 1; + save(); +} + +void AuthProfileManager::removeSelectedProfile() +{ + if (selectedProfile < 0 || selectedProfile >= static_cast(profiles.size())) + return; + + profiles.erase(profiles.begin() + selectedProfile); + if (selectedProfile >= static_cast(profiles.size())) + selectedProfile = static_cast(profiles.size()) - 1; + save(); +} + +bool AuthProfileManager::applySelectedProfile() +{ + if (selectedProfile < 0 || selectedProfile >= static_cast(profiles.size())) + return false; + + auto &p = profiles[selectedProfile]; + + if (p.type == AuthProfile::MICROSOFT && !p.clientToken.empty()) + { + auto checkResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", + {"Authorization: Bearer " + narrowStr(p.token)}); + + if (checkResp.statusCode != 200) + { + string newMsAccess, newMsRefresh; + if (msRefreshOAuth(narrowStr(p.clientToken), newMsAccess, newMsRefresh)) + { + string mcToken, profId, profName; + if (msTokenExchange(newMsAccess, mcToken, profId, profName)) + { + p.token = convStringToWstring(mcToken); + p.clientToken = convStringToWstring(newMsRefresh); + p.username = convStringToWstring(profName); + p.uid = convStringToWstring(profId); + save(); + } + } + } + } + else if (p.type == AuthProfile::YGGDRASIL && !p.token.empty()) + { + auto *provider = YggdrasilRegistry::find(p.variation); + if (!provider) provider = &YggdrasilRegistry::defaultProvider(); + if (!yggdrasilValidate(narrowStr(p.token), narrowStr(p.clientToken), provider->validateUrl())) + { + string newAccess, newClient; + if (yggdrasilRefresh(narrowStr(p.token), narrowStr(p.clientToken), newAccess, newClient, provider->refreshUrl())) + { + p.token = convStringToWstring(newAccess); + if (!newClient.empty()) p.clientToken = convStringToWstring(newClient); + save(); + } + } + } + + auto *mc = Minecraft::GetInstance(); + + if (mc->user) + delete mc->user; + + mc->user = new User(p.username, p.token); + + // push auth name into the platform globals so ProfileManager.GetGamertag() picks it up + // instead of returning the default "Player" + extern char g_Win64Username[17]; + extern wchar_t g_Win64UsernameW[17]; + string narrow = narrowStr(p.username); + strncpy_s(g_Win64Username, sizeof(g_Win64Username), narrow.c_str(), _TRUNCATE); + wcsncpy_s(g_Win64UsernameW, 17, p.username.c_str(), _TRUNCATE); + + return true; +} + + +std::thread AuthFlow::workerThread; +std::atomic AuthFlow::state{AuthFlowState::IDLE}; +std::atomic AuthFlow::cancelRequested{false}; +AuthResult AuthFlow::result; +wstring AuthFlow::userCode; +wstring AuthFlow::verificationUri; +wstring AuthFlow::activeVariation; + +void AuthFlow::reset() +{ + cancelRequested = true; + if (workerThread.joinable()) + workerThread.detach(); + state = AuthFlowState::IDLE; + result = {}; + userCode.clear(); + verificationUri.clear(); + activeVariation.clear(); + cancelRequested = false; +} + +void AuthFlow::startMicrosoft() +{ + reset(); + state = AuthFlowState::WAITING_FOR_USER; + workerThread = std::thread(microsoftFlowThread); +} + +void AuthFlow::startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName) +{ + reset(); + state = AuthFlowState::EXCHANGING; + wstring resolvedName = providerName.empty() ? YggdrasilRegistry::defaultProvider().name : providerName; + activeVariation = resolvedName; + auto *provider = YggdrasilRegistry::find(resolvedName); + string authUrl = provider ? provider->authenticateUrl() : YggdrasilRegistry::defaultProvider().authenticateUrl(); + workerThread = std::thread(yggdrasilFlowThread, narrowStr(username), narrowStr(password), authUrl); +} + +static void authFail(AuthResult &result, std::atomic &state, const wchar_t *msg) +{ + result = {false, {}, {}, {}, {}, msg}; + state = AuthFlowState::FAILED; +} + +static json parseResponse(const HttpResponse &resp, int expectedStatus) +{ + if (resp.statusCode != expectedStatus) return json::value_t::discarded; + return json::parse(resp.body, nullptr, false); +} +static bool msTokenExchange(const string &msAccessToken, string &mcToken, string &profId, string &profName) +{ + auto xblResp = HttpClient::post("https://user.auth.xboxlive.com/user/authenticate", json({ + {"Properties", {{"AuthMethod", "RPS"}, {"SiteName", "user.auth.xboxlive.com"}, {"RpsTicket", msAccessToken}}}, + {"RelyingParty", "http://auth.xboxlive.com"}, + {"TokenType", "JWT"} + }).dump()); + + auto xblJson = parseResponse(xblResp); + if (xblJson.is_discarded()) return false; + + string xblToken = xblJson.value("Token", ""); + string userHash; + try { userHash = xblJson["DisplayClaims"]["xui"][0].value("uhs", ""); } catch (...) {} + if (xblToken.empty() || userHash.empty()) return false; + + auto xstsResp = HttpClient::post("https://xsts.auth.xboxlive.com/xsts/authorize", json({ + {"Properties", {{"SandboxId", "RETAIL"}, {"UserTokens", {xblToken}}}}, + {"RelyingParty", "rp://api.minecraftservices.com/"}, + {"TokenType", "JWT"} + }).dump()); + + auto xstsJson = parseResponse(xstsResp); + string xstsToken = xstsJson.is_discarded() ? "" : xstsJson.value("Token", ""); + if (xstsToken.empty()) return false; + + auto mcResp = HttpClient::post("https://api.minecraftservices.com/authentication/login_with_xbox", + json({{"identityToken", "XBL3.0 x=" + userHash + ";" + xstsToken}}).dump()); + + auto mcJson = parseResponse(mcResp); + mcToken = mcJson.is_discarded() ? "" : mcJson.value("access_token", ""); + if (mcToken.empty()) return false; + + auto profResp = HttpClient::get("https://api.minecraftservices.com/minecraft/profile", + {"Authorization: Bearer " + mcToken}); + + auto profJson = parseResponse(profResp); + if (profJson.is_discarded()) return false; + + profId = profJson.value("id", ""); + profName = profJson.value("name", ""); + return !profId.empty() && !profName.empty(); +} + +static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, string &newRefreshToken) +{ + auto resp = HttpClient::post("https://login.live.com/oauth20_token.srf", + "client_id=" + string(MS_CLIENT_ID) + "&refresh_token=" + refreshToken + "&grant_type=refresh_token&scope=service::user.auth.xboxlive.com::MBI_SSL", + "application/x-www-form-urlencoded"); + + auto j = parseResponse(resp); + if (j.is_discarded()) return false; + + newAccessToken = j.value("access_token", ""); + newRefreshToken = j.value("refresh_token", ""); + return !newAccessToken.empty(); +} +static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl) +{ + auto resp = HttpClient::post(validateUrl, + json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); + return resp.statusCode == 200; +} +static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl) +{ + auto resp = HttpClient::post(refreshUrl, + json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); + + auto j = parseResponse(resp); + if (j.is_discarded()) return false; + + newAccessToken = j.value("accessToken", ""); + newClientToken = j.value("clientToken", ""); + return !newAccessToken.empty(); +} + +void AuthFlow::microsoftFlowThread() +{ + auto dcResp = HttpClient::post( + "https://login.live.com/oauth20_connect.srf", + "client_id=" + string(MS_CLIENT_ID) + "&scope=service::user.auth.xboxlive.com::MBI_SSL&response_type=device_code", + "application/x-www-form-urlencoded" + ); + + auto dcJson = parseResponse(dcResp); + if (dcJson.is_discarded()) + { + authFail(result, state, L"Failed to get device code"); + return; + } + + string deviceCode = dcJson.value("device_code", ""); + string uCode = dcJson.value("user_code", ""); + string vUri = dcJson.value("verification_uri", ""); + int interval = dcJson.value("interval", 5); + + if (deviceCode.empty() || uCode.empty()) + { + authFail(result, state, L"Missing device code fields"); + return; + } + + userCode = convStringToWstring(uCode); + verificationUri = convStringToWstring(vUri); + + // copy code to clipboard so the user can just paste it + if (OpenClipboard(nullptr)) + { + EmptyClipboard(); + size_t bytes = (uCode.size() + 1) * sizeof(char); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes); + if (hMem) + { + memcpy(GlobalLock(hMem), uCode.c_str(), bytes); + GlobalUnlock(hMem); + SetClipboardData(CF_TEXT, hMem); + } + CloseClipboard(); + } + + if (!vUri.empty()) + ShellExecuteW(nullptr, L"open", verificationUri.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + + state = AuthFlowState::POLLING; + string msAccessToken; + string msRefreshToken; + const string pollBody = "client_id=" + string(MS_CLIENT_ID) + "&device_code=" + deviceCode + "&grant_type=urn:ietf:params:oauth:grant-type:device_code"; + + for (int attempt = 0; attempt < 180; attempt++) + { + for (int ms = 0; ms < interval * 1000; ms += 250) + { + if (cancelRequested) return; + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + + auto pollResp = HttpClient::post( + "https://login.live.com/oauth20_token.srf", + pollBody, + "application/x-www-form-urlencoded" + ); + + auto pollJson = json::parse(pollResp.body, nullptr, false); + if (pollJson.is_discarded()) continue; + + if (pollResp.statusCode == 200) + { + msAccessToken = pollJson.value("access_token", ""); + msRefreshToken = pollJson.value("refresh_token", ""); + if (!msAccessToken.empty()) break; + } + + string err = pollJson.value("error", ""); + if (err == "authorization_pending") continue; + if (err == "slow_down") { interval += 5; continue; } + if (!err.empty()) + { + result = {false, {}, {}, {}, {}, convStringToWstring("Auth error: " + err)}; + state = AuthFlowState::FAILED; + return; + } + } + + if (msAccessToken.empty()) + { + authFail(result, state, L"Timed out waiting for login"); + return; + } + + state = AuthFlowState::EXCHANGING; + if (cancelRequested) return; + + string mcAccessToken, profId, profName; + if (!msTokenExchange(msAccessToken, mcAccessToken, profId, profName)) + { + authFail(result, state, L"Token exchange failed"); + return; + } + + result = {true, convStringToWstring(profName), convStringToWstring(profId), convStringToWstring(mcAccessToken), convStringToWstring(msRefreshToken), {}}; + state = AuthFlowState::COMPLETE; +} + +void AuthFlow::yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl) +{ + auto resp = HttpClient::post(authenticateUrl, json({ + {"username", username}, + {"password", password}, + {"clientToken", "mcconsoles"}, + {"agent", {{"name", "Minecraft"}, {"version", 1}}} + }).dump()); + + auto respJson = json::parse(resp.body, nullptr, false); + + if (resp.statusCode != 200 || respJson.is_discarded()) + { + string msg = "Yggdrasil auth failed"; + if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg); + result = {false, {}, {}, {}, {}, convStringToWstring(msg)}; + state = AuthFlowState::FAILED; + return; + } + + string accessToken = respJson.value("accessToken", ""); + string yggClientToken = respJson.value("clientToken", ""); + string uuid, name; + try { uuid = respJson["selectedProfile"].value("id", ""); name = respJson["selectedProfile"].value("name", ""); } catch (...) {} + + if (accessToken.empty() || uuid.empty() || name.empty()) + { + authFail(result, state, L"Yggdrasil response missing profile"); + return; + } + + result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(yggClientToken), {}}; + state = AuthFlowState::COMPLETE; +} diff --git a/Minecraft.Client/AuthScreen.h b/Minecraft.Client/AuthScreen.h new file mode 100644 index 0000000000..38425f0c10 --- /dev/null +++ b/Minecraft.Client/AuthScreen.h @@ -0,0 +1,77 @@ +#pragma once +using namespace std; +#include +#include + +struct AuthProfile +{ + enum Type : uint8_t { MICROSOFT, YGGDRASIL, OFFLINE }; + + Type type; + wstring uid; + wstring username; + wstring token; + wstring clientToken; + wstring variation; +}; + +class AuthProfileManager +{ +private: + static vector profiles; + static int selectedProfile; + +public: + static void load(); + static void save(); + static void addProfile(AuthProfile::Type type, const wstring &username, const wstring &uid = L"", const wstring &token = L"", const wstring &clientToken = L"", const wstring &variation = L""); + static void removeSelectedProfile(); + static bool applySelectedProfile(); + + static const vector &getProfiles() { return profiles; } + static int getSelectedIndex() { return selectedProfile; } + static void setSelectedIndex(int idx) { selectedProfile = idx; } +}; +struct AuthResult +{ + bool success; + wstring username; + wstring uuid; + wstring accessToken; + wstring clientToken; + wstring error; +}; + +enum class AuthFlowState : uint8_t +{ + IDLE, + WAITING_FOR_USER, + POLLING, + EXCHANGING, + COMPLETE, + FAILED +}; + +class AuthFlow +{ +private: + static std::thread workerThread; + static std::atomic state; + static std::atomic cancelRequested; + static AuthResult result; + static wstring userCode; + static wstring verificationUri; + static wstring activeVariation; + static void microsoftFlowThread(); + static void yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl); + +public: + static void startMicrosoft(); + static void startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName = L""); + static AuthFlowState getState() { return state.load(); } + static const AuthResult &getResult() { return result; } + static const wstring &getUserCode() { return userCode; } + static const wstring &getVerificationUri() { return verificationUri; } + static const wstring &getActiveVariation() { return activeVariation; } + static void reset(); +}; diff --git a/Minecraft.Client/CMakeLists.txt b/Minecraft.Client/CMakeLists.txt index 9f75efd219..9f52cd53bd 100644 --- a/Minecraft.Client/CMakeLists.txt +++ b/Minecraft.Client/CMakeLists.txt @@ -44,6 +44,8 @@ set_source_files_properties(compat_shims.cpp PROPERTIES SKIP_PRECOMPILE_HEADERS configure_compiler_target(Minecraft.Client) +target_link_options(Minecraft.Client PRIVATE "/MAP") + set_target_properties(Minecraft.Client PROPERTIES OUTPUT_NAME "Minecraft.Client" VS_DEBUGGER_WORKING_DIRECTORY "$" diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index a80af5d2c9..23585e8ead 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -54,6 +54,9 @@ #include "PS3/Network/SonyVoiceChat.h" #endif #include "DLCTexturePack.h" +#include "..\Minecraft.World\HandshakeManager.h" +#include "..\Minecraft.World\SessionAuthModule.h" +#include "AuthScreen.h" #ifdef _WINDOWS64 #include "Xbox\Network\NetworkPlayerXbox.h" @@ -140,6 +143,9 @@ ClientConnection::ClientConnection(Minecraft *minecraft, Socket *socket, int iUs } deferredEntityLinkPackets = vector(); + + handshakeManager = nullptr; + authComplete = false; } bool ClientConnection::isPrimaryConnection() const @@ -202,6 +208,7 @@ ClientConnection::~ClientConnection() delete connection; delete random; delete savedDataStorage; + delete handshakeManager; } void ClientConnection::tick() @@ -870,6 +877,7 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) player->yRotp = packet->yRot; player->yHeadRot = packet->yHeadRot * 360 / 256.0f; player->setXuid(packet->xuid); + player->setGameUUID(packet->gameUuid); #ifdef _DURANGO // On Durango request player display name from network manager @@ -4129,3 +4137,64 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< m_recievedTick = GetTickCount(); m_packet = packet; } + +void ClientConnection::beginAuth() +{ + app.DebugPrintf("AUTH: beginAuth() starting\n"); + handshakeManager = new HandshakeManager(false); + + const auto &profiles = AuthProfileManager::getProfiles(); + int idx = AuthProfileManager::getSelectedIndex(); + bool isOffline = true; + if (idx >= 0 && idx < static_cast(profiles.size())) + { + const auto &p = profiles[idx]; + isOffline = (p.type == AuthProfile::OFFLINE); + wstring variation; + if (p.type == AuthProfile::MICROSOFT) variation = L"mojang"; + else if (p.type == AuthProfile::YGGDRASIL) variation = p.variation; + app.DebugPrintf("AUTH: profile type=%d variation=%ls username=%ls\n", + (int)p.type, variation.c_str(), p.username.c_str()); + handshakeManager->setCredentials(p.token, p.uid, p.username, variation); + } + else + { + app.DebugPrintf("AUTH: no valid profile selected (idx=%d, count=%d)\n", + idx, (int)profiles.size()); + } + + if (!isOffline) + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); + + auto initial = handshakeManager->createInitialPacket(); + if (initial) send(initial); +} + +void ClientConnection::handleAuth(const shared_ptr &packet) +{ + if (done || authComplete) return; + if (!handshakeManager) return; + + auto response = handshakeManager->handlePacket(packet); + if (response) send(response); + + for (auto &p : handshakeManager->drainPendingPackets()) + send(p); + if (handshakeManager->isComplete()) + { + authComplete = true; + const wstring &authName = handshakeManager->finalUsername; + minecraft->user->name = authName; + extern char g_Win64Username[17]; + extern wchar_t g_Win64UsernameW[17]; + wcsncpy_s(g_Win64UsernameW, authName.c_str(), 16); + wcstombs_s(nullptr, g_Win64Username, g_Win64UsernameW, 16); + } + else if (handshakeManager->isFailed()) + { + done = true; + message = L"Auth handshake failed"; + } +} diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index 3448496d07..69ed280531 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -6,6 +6,7 @@ class MultiPlayerLevel; class SavedDataStorage; class Socket; class MultiplayerLocalPlayer; +class HandshakeManager; class ClientConnection : public PacketListener { @@ -179,4 +180,10 @@ class ClientConnection : public PacketListener static const int MAX_ENTITY_LINK_DEFERRAL_INTERVAL = 1000; void checkDeferredEntityLinkPackets(int newEntityId); + +public: + HandshakeManager *handshakeManager; + bool authComplete; + void beginAuth(); + virtual void handleAuth(const shared_ptr &packet); }; \ No newline at end of file diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 0a2fd159a4..39c2807e8a 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -4589,6 +4589,9 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter ) case DisconnectPacket::eDisconnect_OutdatedClient: exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD; break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } @@ -4649,6 +4652,10 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter ) break; case DisconnectPacket::eDisconnect_OutdatedClient: exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD; + break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index 50aeae689e..934a910580 100644 --- a/Minecraft.Client/Common/Network/GameNetworkManager.cpp +++ b/Minecraft.Client/Common/Network/GameNetworkManager.cpp @@ -413,6 +413,35 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame return false; } + if (!g_NetworkManager.IsHost()) + { + try { + connection->beginAuth(); + while (!connection->authComplete && !connection->isClosed() && IsInSession() && !g_NetworkManager.IsLeavingGame()) + { + connection->tick(); + Sleep(50); + } + } catch (...) { + app.DebugPrintf("AUTH: exception during auth handshake\n"); + connection->close(); + delete connection; + connection = nullptr; + MinecraftServer::HaltServer(); + return false; + } + + if (!connection->authComplete) + { + app.DebugPrintf("Auth handshake did not complete, aborting connection\n"); + connection->close(); + delete connection; + connection = nullptr; + MinecraftServer::HaltServer(); + return false; + } + } + connection->send(std::make_shared(minecraft->user->name)); // Tick connection until we're ready to go. The stages involved in this are: diff --git a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp index e88ed08cdb..036e5bec2a 100644 --- a/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp +++ b/Minecraft.Client/Common/UI/IUIScene_PauseMenu.cpp @@ -508,6 +508,10 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) exitReasonTitleId = IDS_CONNECTION_FAILED; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + exitReasonTitleId = IDS_CONNECTION_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; } @@ -609,6 +613,10 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter) exitReasonTitleId = IDS_CONNECTION_FAILED; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + exitReasonTitleId = IDS_CONNECTION_FAILED; + break; default: exitReasonStringId = IDS_DISCONNECTED; } diff --git a/Minecraft.Client/Common/UI/UIController.cpp b/Minecraft.Client/Common/UI/UIController.cpp index b12ea5e739..bf81472cf8 100644 --- a/Minecraft.Client/Common/UI/UIController.cpp +++ b/Minecraft.Client/Common/UI/UIController.cpp @@ -2948,7 +2948,7 @@ C4JStorage::EMessageResult UIController::RequestMessageBox(UINT uiTitle, UINT ui int( *Func)(LPVOID,int,const C4JStorage::EMessageResult),LPVOID lpParam, WCHAR *pwchFormatString,DWORD dwFocusButton, bool bIsError) { - MessageBoxInfo param; + MessageBoxInfo param = {}; param.uiTitle = uiTitle; param.uiText = uiText; param.uiOptionA = uiOptionA; diff --git a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp index 40557cd5c2..ae75ec03b2 100644 --- a/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp +++ b/Minecraft.Client/Common/UI/UIScene_ConnectingProgress.cpp @@ -133,6 +133,9 @@ void UIScene_ConnectingProgress::tick() case DisconnectPacket::eDisconnect_Banned: exitReasonStringId = IDS_DISCONNECTED_KICKED; break; + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; break; @@ -277,6 +280,9 @@ void UIScene_ConnectingProgress::handleTimerComplete(int id) exitReasonStringId = IDS_DISCONNECTED_NAT_TYPE_MISMATCH; break; #endif + case DisconnectPacket::eDisconnect_AuthFailed: + exitReasonStringId = IDS_DISCONNECTED_AUTH_FAILED; + break; default: exitReasonStringId = IDS_CONNECTION_LOST_SERVER; break; diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp index 35edf17fda..f57b21e13b 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.cpp @@ -27,6 +27,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye const wchar_t* defaultText = L""; m_bPCMode = false; + m_eKeyboardMode = C_4JInput::EKeyboardMode_Default; if (initData) { UIKeyboardInitData* kbData = static_cast(initData); @@ -36,6 +37,7 @@ UIScene_Keyboard::UIScene_Keyboard(int iPad, void *initData, UILayer *parentLaye if (kbData->defaultText) defaultText = kbData->defaultText; m_win64MaxChars = kbData->maxChars; m_bPCMode = kbData->pcMode; + m_eKeyboardMode = kbData->keyboardMode; } m_win64TextBuffer = defaultText; @@ -171,7 +173,7 @@ void UIScene_Keyboard::tick() // Sync our buffer from Flash so we pick up changes made via controller/on-screen buttons. // Without this, switching between controller and keyboard would use stale text. // In PC mode we own the buffer — skip sync to preserve cursor position. - if (!m_bPCMode) + if (!m_bPCMode && m_eKeyboardMode != C_4JInput::EKeyboardMode_Password) { const wchar_t* flashText = m_KeyboardTextInput.getLabel(); if (flashText) @@ -276,7 +278,12 @@ void UIScene_Keyboard::tick() } if (changed) - m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + { + if (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) + m_KeyboardTextInput.setLabel(wstring(m_win64TextBuffer.length(), L'*').c_str(), true); + else + m_KeyboardTextInput.setLabel(m_win64TextBuffer.c_str(), true /*instant*/); + } if (m_bPCMode) { @@ -410,8 +417,12 @@ void UIScene_Keyboard::KeyboardDonePressed() // Use getLabel() here — this is a timer callback (not an Iggy callback) so it's safe. // getLabel() reflects both physical keyboard input (pushed via setLabel) and // on-screen button input (set directly by Flash ActionScript). - const wchar_t* finalText = m_KeyboardTextInput.getLabel(); - app.DebugPrintf("UI Keyboard - DONE - [%ls]\n", finalText); + // in password mode the label is masked with asterisks, so use the real buffer instead + const wchar_t* finalText = (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) + ? m_win64TextBuffer.c_str() + : m_KeyboardTextInput.getLabel(); + app.DebugPrintf("UI Keyboard - DONE - [%ls]\n", + (m_eKeyboardMode == C_4JInput::EKeyboardMode_Password) ? L"" : finalText); // Store the typed text so callbacks can retrieve it via Win64_GetKeyboardText() wcsncpy_s(g_Win64KeyboardResult, 256, finalText, _TRUNCATE); diff --git a/Minecraft.Client/Common/UI/UIScene_Keyboard.h b/Minecraft.Client/Common/UI/UIScene_Keyboard.h index 146934c1f6..72ada08fdc 100644 --- a/Minecraft.Client/Common/UI/UIScene_Keyboard.h +++ b/Minecraft.Client/Common/UI/UIScene_Keyboard.h @@ -13,6 +13,7 @@ class UIScene_Keyboard : public UIScene wstring m_win64TextBuffer; int m_win64MaxChars; bool m_bPCMode; // Hides on-screen keyboard buttons; physical keyboard only + C_4JInput::EKeyboardMode m_eKeyboardMode; int m_iCursorPos; #endif diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 93f1edf119..8b7169515c 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -2,8 +2,10 @@ #include "..\..\..\Minecraft.World\Mth.h" #include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\..\Minecraft.World\Random.h" +#include "..\..\..\Minecraft.World\SessionAuthModule.h" #include "..\..\User.h" #include "..\..\MinecraftServer.h" +#include "..\..\AuthScreen.h" #include "UI.h" #include "UIScene_MainMenu.h" #ifdef __ORBIS__ @@ -32,6 +34,9 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_eAction=eAction_None; m_bIgnorePress=false; + // auto-apply saved auth profile on startup + AuthProfileManager::load(); + AuthProfileManager::applySelectedProfile(); m_buttons[static_cast(eControl_PlayGame)].init(IDS_PLAY_GAME,eControl_PlayGame); @@ -46,12 +51,12 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(IDS_DOWNLOADABLECONTENT,eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Switch User",eControl_UnlockOrDLC); } else { m_bTrialVersion=true; - m_buttons[static_cast(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC); + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Switch User",eControl_UnlockOrDLC); } #ifndef _DURANGO @@ -181,8 +186,8 @@ void UIScene_MainMenu::handleGainFocus(bool navBack) if(navBack && ProfileManager.IsFullVersion()) { - // Replace the Unlock Full Game with Downloadable Content - m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT); + // once again replacing the shop with auth. not a bad thing. + m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(L"Switch User"); } #if TO_BE_IMPLEMENTED @@ -357,7 +362,7 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId) ui.PlayUISFX(eSFX_Press); m_eAction=eAction_RunUnlockOrDLC; - signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned; + RunAction(primaryPad); break; case eControl_Exit: //CD - Added for audio @@ -438,8 +443,11 @@ void UIScene_MainMenu::RunAction(int iPad) RunHelpAndOptions(iPad); break; case eAction_RunUnlockOrDLC: - RunUnlockOrDLC(iPad); + { + AuthProfileManager::load(); + ShowAuthMenu(iPad, this); break; + } #ifdef _DURANGO case eAction_RunXboxHelp: // 4J: Launch the dummy xbox help application. @@ -450,6 +458,267 @@ void UIScene_MainMenu::RunAction(int iPad) } } +static int s_authPad = 0; +static void *s_authParam = nullptr; +static wstring s_yggdrasilUsername; +static bool s_authFlowActive = false; + +static wstring ReadKeyboardText(int maxLen) +{ + vector buf(maxLen, 0); +#ifdef _WINDOWS64 + Win64_GetKeyboardText(buf.data(), maxLen); +#else + InputManager.GetText(buf.data()); +#endif + wstring result; + for (int i = 0; i < maxLen && buf[i]; i++) + result += static_cast(buf[i]); + return result; +} + +static void ShowAuthMessageBox(int iPad, const wchar_t *title, const wchar_t *text, + const wchar_t **options, int optionCount, + int(*func)(LPVOID, int, C4JStorage::EMessageResult), void *lpParam, bool keepOpen = false) +{ + MessageBoxInfo param = {}; + param.uiOptionC = optionCount; + param.dwPad = iPad; + param.Func = func; + param.lpParam = lpParam; + param.rawTitle = title; + param.rawText = text; + param.rawOptions = options; + ui.NavigateToScene(iPad, eUIScene_MessageBox, ¶m, eUILayer_Alert, eUIGroup_Fullscreen); + if (keepOpen) + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->setKeepOpen(true); +} + +static const wchar_t *BuildAuthProfileText() +{ + static wstring text; + const auto &profiles = AuthProfileManager::getProfiles(); + int sel = AuthProfileManager::getSelectedIndex(); + if (profiles.empty()) + { + text = L"No profiles - press Add to create one"; + } + else + { + text.clear(); + for (int i = 0; i < static_cast(profiles.size()); i++) + { + const auto &p = profiles[i]; + wstring label; + if (p.type == AuthProfile::MICROSOFT) label = L"[MS]"; + else if (p.type == AuthProfile::YGGDRASIL) + { + auto *provider = YggdrasilRegistry::find(p.variation); + label = provider ? L"[" + provider->displayName + L"]" : L"[Ygg]"; + } + else label = L"[Off]"; + if (i > 0) text += L"\n"; + text += (i == sel) ? L"> " : L" "; + text += label + L" " + p.username; + } + } + return text.c_str(); +} + +void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) +{ + s_authPad = iPad; + s_authParam = pClass; + + static const wchar_t *authOptions[] = { L"Next", L"Use", L"Add", L"Remove" }; + ShowAuthMessageBox(iPad, L"Authentication", BuildAuthProfileText(), + authOptions, 4, &UIScene_MainMenu::AuthMenuReturned, pClass, true); +} + +void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass) +{ + static wstring yggLabel; + yggLabel = YggdrasilRegistry::defaultProvider().displayName + L" Auth"; + const wchar_t *addOptions[] = { L"Microsoft Auth", yggLabel.c_str(), L"Add Offline User", L"Back" }; + ShowAuthMessageBox(iPad, L"Add Profile", L"Select an auth type", + addOptions, 4, &UIScene_MainMenu::AuthAddMenuReturned, pClass); +} + +int UIScene_MainMenu::AuthMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + const auto &profiles = AuthProfileManager::getProfiles(); + switch (result) + { + case C4JStorage::EMessage_ResultAccept: + { + if (!profiles.empty()) + { + int next = (AuthProfileManager::getSelectedIndex() + 1) % static_cast(profiles.size()); + AuthProfileManager::setSelectedIndex(next); + } + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->updateContent(BuildAuthProfileText()); + return 0; + } + case C4JStorage::EMessage_ResultDecline: + { + ui.NavigateBack(iPad); + AuthProfileManager::applySelectedProfile(); + AuthProfileManager::save(); + break; + } + case C4JStorage::EMessage_ResultThirdOption: + { + ui.NavigateBack(iPad); + ShowAuthAddMenu(iPad, lpParam); + break; + } + case C4JStorage::EMessage_ResultFourthOption: + { + if (!profiles.empty()) + { + AuthProfileManager::removeSelectedProfile(); + if (auto *scene = ui.FindScene(eUIScene_MessageBox)) + static_cast(scene)->updateContent(BuildAuthProfileText()); + } + return 0; + } + default: + { + ui.NavigateBack(iPad); + break; + } + } + return 0; +} + +int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + switch (result) + { + case C4JStorage::EMessage_ResultAccept: + { + AuthFlow::startMicrosoft(); + s_authFlowActive = true; + + static const wchar_t *cancelOptions[] = { L"Cancel" }; + ShowAuthMessageBox(iPad, L"Microsoft Login", L"Starting...", + cancelOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); + break; + } + case C4JStorage::EMessage_ResultDecline: + { +#ifdef _WINDOWS64 + UIKeyboardInitData kbData; + kbData.title = L"Username"; + kbData.defaultText = L""; + kbData.maxChars = 64; + kbData.callback = &UIScene_MainMenu::YggdrasilUsernameReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); +#else + InputManager.RequestKeyboard(L"Username", L"", (DWORD)0, 64, &UIScene_MainMenu::YggdrasilUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); +#endif + break; + } + case C4JStorage::EMessage_ResultThirdOption: + { +#ifdef _WINDOWS64 + UIKeyboardInitData kbData; + kbData.title = L"Enter Username"; + kbData.defaultText = L"Player"; + kbData.maxChars = 16; + kbData.callback = &UIScene_MainMenu::AuthKeyboardReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); +#else + InputManager.RequestKeyboard(L"Enter Username", L"Player", (DWORD)0, 16, &UIScene_MainMenu::AuthKeyboardReturned, lpParam, C_4JInput::EKeyboardMode_Default); +#endif + break; + } + default: + ShowAuthMenu(iPad, lpParam); + break; + } + return 0; +} + +int UIScene_MainMenu::AuthMsFlowReturned(LPVOID lpParam, int iPad, const C4JStorage::EMessageResult result) +{ + s_authFlowActive = false; + AuthFlow::reset(); + ui.NavigateBack(iPad); + ShowAuthMenu(iPad, lpParam); + return 0; +} + +int UIScene_MainMenu::YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + wstring name = ReadKeyboardText(128); + if (!name.empty()) + { + s_yggdrasilUsername = std::move(name); +#ifdef _WINDOWS64 + UIKeyboardInitData kbData; + kbData.title = L"Password"; + kbData.defaultText = L""; + kbData.maxChars = 128; + kbData.callback = &UIScene_MainMenu::YggdrasilPasswordReturned; + kbData.lpParam = lpParam; + kbData.pcMode = g_KBMInput.IsKBMActive(); + kbData.keyboardMode = C_4JInput::EKeyboardMode_Password; + ui.NavigateToScene(s_authPad, eUIScene_Keyboard, &kbData); +#else + InputManager.RequestKeyboard(L"Password", L"", (DWORD)0, 128, &UIScene_MainMenu::YggdrasilPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); +#endif + return 0; + } + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + +int UIScene_MainMenu::YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + wstring pass = ReadKeyboardText(256); + if (!pass.empty()) + { + AuthFlow::startYggdrasil(s_yggdrasilUsername, pass); + s_authFlowActive = true; + + static wstring loginTitle; + loginTitle = YggdrasilRegistry::defaultProvider().displayName + L" Login"; + static const wchar_t *waitOptions[] = { L"Cancel" }; + ShowAuthMessageBox(s_authPad, loginTitle.c_str(), L"Authenticating...", + waitOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); + + SecureZeroMemory(pass.data(), pass.size() * sizeof(wchar_t)); + return 0; + } + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + +int UIScene_MainMenu::AuthKeyboardReturned(LPVOID lpParam, const bool bRes) +{ + if (bRes) + { + wstring name = ReadKeyboardText(128); + if (!name.empty()) + AuthProfileManager::addProfile(AuthProfile::OFFLINE, name); + } + ShowAuthMenu(s_authPad, lpParam); + return 0; +} + void UIScene_MainMenu::customDraw(IggyCustomDrawCallbackRegion *region) { if(wcscmp((wchar_t *)region->name,L"Splash")==0) @@ -1857,6 +2126,55 @@ void UIScene_MainMenu::RunUnlockOrDLC(int iPad) void UIScene_MainMenu::tick() { UIScene::tick(); + if (s_authFlowActive) + { + auto flowState = AuthFlow::getState(); + auto *scene = ui.FindScene(eUIScene_MessageBox); + auto *msgBox = scene ? static_cast(scene) : nullptr; + + switch (flowState) + { + case AuthFlowState::WAITING_FOR_USER: + case AuthFlowState::POLLING: + { + if (msgBox && !AuthFlow::getUserCode().empty()) + { + static wstring statusText; + statusText = L"Go to: " + AuthFlow::getVerificationUri() + L"\nEnter code: " + AuthFlow::getUserCode(); + if (flowState == AuthFlowState::POLLING) statusText += L"\n\nWaiting for login..."; + msgBox->updateContent(statusText.c_str()); + } + break; + } + case AuthFlowState::EXCHANGING: + { + if (msgBox) msgBox->updateContent(L"Exchanging tokens..."); + break; + } + case AuthFlowState::COMPLETE: + { + s_authFlowActive = false; + const auto &r = AuthFlow::getResult(); + auto type = AuthFlow::getUserCode().empty() ? AuthProfile::YGGDRASIL : AuthProfile::MICROSOFT; + wstring variation = (type == AuthProfile::YGGDRASIL) ? AuthFlow::getActiveVariation() : L""; + AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken, r.clientToken, variation); + AuthFlow::reset(); + if (scene) ui.NavigateBack(s_authPad); + ShowAuthMenu(s_authPad, s_authParam); + break; + } + case AuthFlowState::FAILED: + { + s_authFlowActive = false; + const auto &r = AuthFlow::getResult(); + if (msgBox) msgBox->updateContent(r.error.empty() ? L"Authentication failed" : r.error.c_str()); + AuthFlow::reset(); + break; + } + default: + break; + } + } if ( (eNavigateWhenReady >= 0) ) { @@ -1946,7 +2264,7 @@ void UIScene_MainMenu::tick() if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; - m_buttons[(int)eControl_UnlockOrDLC].init(app.GetString(IDS_DOWNLOADABLECONTENT),eControl_UnlockOrDLC); + m_buttons[(int)eControl_UnlockOrDLC].init(L"Switch User",eControl_UnlockOrDLC); } } #endif diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 2b49a44b3d..0794566804 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -144,7 +144,16 @@ class UIScene_MainMenu : public UIScene void RunHelpAndOptions(int iPad); void RunAction(int iPad); - + + static void ShowAuthMenu(int iPad, void *pClass); + static void ShowAuthAddMenu(int iPad, void *pClass); + static int AuthMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int AuthAddMenuReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int AuthKeyboardReturned(LPVOID lpParam, const bool bRes); + static int AuthMsFlowReturned(void *pParam, int iPad, C4JStorage::EMessageResult result); + static int YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes); + static int YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes); + static void LoadTrial(); #ifdef _XBOX_ONE diff --git a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp index 7a0749d4a2..08556054d5 100644 --- a/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MessageBox.cpp @@ -23,28 +23,29 @@ UIScene_MessageBox::UIScene_MessageBox(int iPad, void *initData, UILayer *parent int buttonIndex = 0; if(param->uiOptionC > 3) { - m_buttonButtons[eControl_Button0].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button0].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } if(param->uiOptionC > 2) { - m_buttonButtons[eControl_Button1].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button1].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } if(param->uiOptionC > 1) { - m_buttonButtons[eControl_Button2].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button2].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); ++buttonIndex; } - m_buttonButtons[eControl_Button3].init(app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); + m_buttonButtons[eControl_Button3].init(param->rawOptions ? param->rawOptions[buttonIndex] : app.GetString(param->uiOptionA[buttonIndex]),buttonIndex); - m_labelTitle.init(app.GetString(param->uiTitle)); - m_labelContent.init(app.GetString(param->uiText)); + m_labelTitle.init(param->rawTitle ? param->rawTitle : app.GetString(param->uiTitle)); + m_labelContent.init(param->rawText ? param->rawText : app.GetString(param->uiText)); out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcAutoResize , 0 , nullptr ); m_Func = param->Func; m_lpParam = param->lpParam; + m_keepOpen = false; parentLayer->addComponent(iPad,eUIComponent_MenuBackground); @@ -99,7 +100,7 @@ void UIScene_MessageBox::handleInput(int iPad, int key, bool repeat, bool presse case ACTION_MENU_CANCEL: if(pressed) { - navigateBack(); + if(!m_keepOpen) navigateBack(); if(m_Func) m_Func(m_lpParam, iPad, C4JStorage::EMessage_Cancelled); } break; @@ -136,10 +137,17 @@ void UIScene_MessageBox::handlePress(F64 controlId, F64 childId) break; } - navigateBack(); + if(!m_keepOpen) navigateBack(); if(m_Func) m_Func(m_lpParam, m_iPad, result); } +void UIScene_MessageBox::updateContent(const wchar_t *text) +{ + m_labelContent.init(text); + IggyDataValue result; + IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr); +} + bool UIScene_MessageBox::hasFocus(int iPad) { // 4J-JEV: Fix for PS4 #5204 - [TRC][R4033] The application can be locked up by second user logging out of the system. diff --git a/Minecraft.Client/Common/UI/UIScene_MessageBox.h b/Minecraft.Client/Common/UI/UIScene_MessageBox.h index c10f6ab83d..213a825da9 100644 --- a/Minecraft.Client/Common/UI/UIScene_MessageBox.h +++ b/Minecraft.Client/Common/UI/UIScene_MessageBox.h @@ -18,6 +18,7 @@ class UIScene_MessageBox : public UIScene int( *m_Func)(LPVOID,int,const C4JStorage::EMessageResult); LPVOID m_lpParam; int m_buttonCount; + bool m_keepOpen; UIControl_Button m_buttonButtons[eControl_COUNT]; UIControl_Label m_labelTitle, m_labelContent; @@ -57,4 +58,8 @@ class UIScene_MessageBox : public UIScene protected: void handlePress(F64 controlId, F64 childId); + +public: + void updateContent(const wchar_t *text); + void setKeepOpen(bool keep) { m_keepOpen = keep; } }; \ No newline at end of file diff --git a/Minecraft.Client/Common/UI/UIStructs.h b/Minecraft.Client/Common/UI/UIStructs.h index 99c3d7bda4..2c08daffa7 100644 --- a/Minecraft.Client/Common/UI/UIStructs.h +++ b/Minecraft.Client/Common/UI/UIStructs.h @@ -295,8 +295,9 @@ typedef struct _UIKeyboardInitData int(*callback)(LPVOID, const bool); LPVOID lpParam; bool pcMode; // When true, disables on-screen keyboard buttons (PC keyboard users only need the text field) + C_4JInput::EKeyboardMode keyboardMode; - _UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false) {} + _UIKeyboardInitData() : title(nullptr), defaultText(nullptr), maxChars(25), callback(nullptr), lpParam(nullptr), pcMode(false), keyboardMode(C_4JInput::EKeyboardMode_Default) {} } UIKeyboardInitData; // Stores the text typed in UIScene_Keyboard so callbacks can retrieve it @@ -485,6 +486,9 @@ typedef struct _MessageBoxInfo //C4JStringTable *pStringTable; // 4J Stu - We don't need this for our internal message boxes WCHAR *pwchFormatString; DWORD dwFocusButton; + const wchar_t *rawTitle; + const wchar_t *rawText; + const wchar_t **rawOptions; } MessageBoxInfo; typedef struct _DLCOffersParam diff --git a/Minecraft.Client/ConnectScreen.cpp b/Minecraft.Client/ConnectScreen.cpp index 18d50a5275..0a19291d46 100644 --- a/Minecraft.Client/ConnectScreen.cpp +++ b/Minecraft.Client/ConnectScreen.cpp @@ -11,13 +11,14 @@ ConnectScreen::ConnectScreen(Minecraft *minecraft, const wstring& ip, int port) { aborted = false; + preLoginSent = false; // System.out.println("Connecting to " + ip + ", " + port); minecraft->setLevel(nullptr); #if 1 // 4J - removed from separate thread, but need to investigate what we actually need here connection = new ClientConnection(minecraft, ip, port); if (aborted) return; - connection->send(std::make_shared(minecraft->user->name)); + connection->beginAuth(); #else new Thread() { @@ -47,6 +48,11 @@ void ConnectScreen::tick() { if (connection != nullptr) { + if (!preLoginSent && connection->authComplete) + { + preLoginSent = true; + connection->send(std::make_shared(minecraft->user->name)); + } connection->tick(); } } diff --git a/Minecraft.Client/ConnectScreen.h b/Minecraft.Client/ConnectScreen.h index 07e65cbbfd..76424a070f 100644 --- a/Minecraft.Client/ConnectScreen.h +++ b/Minecraft.Client/ConnectScreen.h @@ -10,6 +10,7 @@ class ConnectScreen : public Screen private: ClientConnection *connection; bool aborted; + bool preLoginSent; public: ConnectScreen(Minecraft *minecraft, const wstring& ip, int port); virtual void tick(); diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 27ee68b65f..8304007f3b 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -59,6 +59,7 @@ #include "PlayerChunkMap.h" #include "Common\Telemetry\TelemetryManager.h" #include "PlayerConnection.h" +#include "AuthScreen.h" #ifdef _XBOX_ONE #include "Durango\Network\NetworkPlayerDurango.h" #endif @@ -648,6 +649,19 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW //motd = settings->getString(L"motd", L"A Minecraft Server"); //motd.replace('�', '$'); + if (ShouldUseDedicatedServerProperties()) + { + wstring am = GetDedicatedServerString(settings, L"auth-mode", L"session"); + authMode = (am == L"offline") ? "offline" : "session"; + } + else + { + int idx = AuthProfileManager::getSelectedIndex(); + const auto &profiles = AuthProfileManager::getProfiles(); + authMode = (idx >= 0 && idx < static_cast(profiles.size()) && + profiles[idx].type != AuthProfile::OFFLINE) ? "session" : "offline"; + } + setAnimals(GetDedicatedServerBool(settings, L"spawn-animals", true)); setNpcsEnabled(GetDedicatedServerBool(settings, L"spawn-npcs", true)); setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index 1ed5db9d7c..4db4e10162 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -113,6 +113,7 @@ class MinecraftServer : public ConsoleInputSource CRITICAL_SECTION m_consoleInputCS; public: bool onlineMode; + std::string authMode; bool animals; bool npcs; bool pvp; diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index f24086c1b6..32d59bfa16 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -14,6 +14,8 @@ #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\SharedConstants.h" #include "Settings.h" +#include "..\Minecraft.World\HandshakeManager.h" +#include "..\Minecraft.World\SessionAuthModule.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" @@ -59,10 +61,14 @@ PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, co this->server = server; connection = new Connection(socket, id, this); connection->fakeLag = FAKE_LAG; + + handshakeManager = nullptr; + authComplete = false; } PendingConnection::~PendingConnection() { + delete handshakeManager; delete connection; } @@ -98,6 +104,19 @@ void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason) void PendingConnection::handlePreLogin(shared_ptr packet) { + if (!authComplete) + { + if (!handshakeManager) + { + app.DebugPrintf("PendingConnection: PreLogin received without auth handshake, disconnecting\n"); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + app.DebugPrintf("PendingConnection: PreLogin received before auth complete, disconnecting\n"); + disconnect(DisconnectPacket::eDisconnect_Closed); + return; + } + if (packet->m_netcodeVersion != MINECRAFT_NET_VERSION) { app.DebugPrintf("Netcode version is %d not equal to %d\n", packet->m_netcodeVersion, MINECRAFT_NET_VERSION); @@ -112,7 +131,8 @@ void PendingConnection::handlePreLogin(shared_ptr packet) 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 + if (!authComplete) + name = packet->loginKey; sendPreLoginResponse(); } @@ -394,3 +414,35 @@ bool PendingConnection::isDisconnected() { return done; } + +void PendingConnection::initAuth() +{ + handshakeManager = new HandshakeManager(true); + handshakeManager->registerModule(std::make_unique()); + if (server->authMode != "session") + { + handshakeManager->registerModule(std::make_unique()); + handshakeManager->registerModule(std::make_unique()); + } +} + +void PendingConnection::handleAuth(const shared_ptr &packet) +{ + if (done || authComplete) return; + + if (!handshakeManager) + initAuth(); + + auto response = handshakeManager->handlePacket(packet); + if (response) send(response); + + if (handshakeManager->isComplete()) + { + authComplete = true; + name = std::move(handshakeManager->finalUsername); + } + else if (handshakeManager->isFailed()) + { + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + } +} diff --git a/Minecraft.Client/PendingConnection.h b/Minecraft.Client/PendingConnection.h index e8a493b097..53724896c7 100644 --- a/Minecraft.Client/PendingConnection.h +++ b/Minecraft.Client/PendingConnection.h @@ -5,6 +5,7 @@ class Socket; class LoginPacket; class Connection; class Random; +class HandshakeManager; using namespace std; class PendingConnection : public PacketListener @@ -43,6 +44,10 @@ class PendingConnection : public PacketListener wstring getName(); virtual bool isServerPacketListener(); virtual bool isDisconnected(); + HandshakeManager *handshakeManager; + bool authComplete; + void initAuth(); + virtual void handleAuth(const shared_ptr &packet); private: void sendPreLoginResponse(); diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index fa5f4cccbd..2abaf7c8cb 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1296,6 +1296,34 @@ static Minecraft* InitialiseMinecraftRuntime() return pMinecraft; } +static LONG WINAPI CrashHandler(EXCEPTION_POINTERS *ep) +{ + auto *record = ep->ExceptionRecord; + auto *ctx = ep->ContextRecord; + HMODULE hMod = GetModuleHandleA(nullptr); + + wchar_t buf[1024]; + swprintf(buf, 1024, + L"Unhandled exception!\n\nCode: 0x%08X\nAddress: 0x%p\nBase: 0x%p\nRVA: 0x%llX\n\nPlease report this crash info.", + record->ExceptionCode, record->ExceptionAddress, hMod, + (unsigned long long)((char *)record->ExceptionAddress - (char *)hMod)); + + FILE *clog = nullptr; + if (fopen_s(&clog, "crash.log", "w") == 0 && clog) + { + fprintf(clog, "[CRASH] Code=0x%08X Addr=0x%p Base=0x%p RVA=0x%llX\n", + record->ExceptionCode, record->ExceptionAddress, hMod, + (unsigned long long)((char *)record->ExceptionAddress - (char *)hMod)); + fprintf(clog, " RIP=0x%llX RSP=0x%llX RBP=0x%llX\n", ctx->Rip, ctx->Rsp, ctx->Rbp); + fprintf(clog, " RAX=0x%llX RBX=0x%llX RCX=0x%llX\n", ctx->Rax, ctx->Rbx, ctx->Rcx); + fprintf(clog, " RDX=0x%llX RSI=0x%llX RDI=0x%llX\n", ctx->Rdx, ctx->Rsi, ctx->Rdi); + fclose(clog); + } + + MessageBoxW(nullptr, buf, L"MinecraftConsoles Crash", MB_OK | MB_ICONERROR); + return EXCEPTION_EXECUTE_HANDLER; +} + int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, @@ -1304,6 +1332,8 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); + SetUnhandledExceptionFilter(CrashHandler); + // 4J-Win64: set CWD to exe dir so asset paths resolve correctly { char szExeDir[MAX_PATH] = {}; diff --git a/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml b/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml index b5e58a9b6c..2b41f1daf7 100644 --- a/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml +++ b/Minecraft.Client/Windows64Media/loc/stringsGeneric.xml @@ -5867,6 +5867,10 @@ Press{*CONTROLLER_VK_B*} if you already know about Fireworks. You cannot join this game as the player you are trying to join is running a newer version of the game. + + Authentication required. This server requires you to be signed in. + + New World diff --git a/Minecraft.Client/Windows64Media/strings.h b/Minecraft.Client/Windows64Media/strings.h index 19a50eeebd..b7812b0fe3 100644 --- a/Minecraft.Client/Windows64Media/strings.h +++ b/Minecraft.Client/Windows64Media/strings.h @@ -2290,3 +2290,4 @@ #define IDS_RICHPRESENCESTATE_BREWING 2284 #define IDS_RICHPRESENCESTATE_ANVIL 2285 #define IDS_RICHPRESENCESTATE_TRADING 2286 +#define IDS_DISCONNECTED_AUTH_FAILED 2287 diff --git a/Minecraft.Client/cmake/sources/Common.cmake b/Minecraft.Client/cmake/sources/Common.cmake index 3936a9c34b..de727931f5 100644 --- a/Minecraft.Client/cmake/sources/Common.cmake +++ b/Minecraft.Client/cmake/sources/Common.cmake @@ -969,6 +969,8 @@ set(_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_SKINS source_group("net/minecraft/client/skins" FILES ${_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_SKINS}) set(_MINECRAFT_CLIENT_COMMON_NET_MINECRAFT_CLIENT_TITLE + "${CMAKE_CURRENT_SOURCE_DIR}/AuthScreen.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthScreen.h" "${CMAKE_CURRENT_SOURCE_DIR}/TitleScreen.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/TitleScreen.h" ) diff --git a/Minecraft.Server/Access/Access.cpp b/Minecraft.Server/Access/Access.cpp index 5767a9555c..85c7642b92 100644 --- a/Minecraft.Server/Access/Access.cpp +++ b/Minecraft.Server/Access/Access.cpp @@ -371,7 +371,10 @@ namespace ServerRuntime } auto whitelistManager = std::make_shared(*current); - const WhitelistedPlayerEntry entry = { formatted, name, metadata }; + WhitelistedPlayerEntry entry; + entry.xuid = formatted; + entry.name = name; + entry.metadata = metadata; if (!whitelistManager->AddPlayer(entry)) { return false; diff --git a/Minecraft.Server/Access/BanManager.cpp b/Minecraft.Server/Access/BanManager.cpp index 59f5bccc03..937a811322 100644 --- a/Minecraft.Server/Access/BanManager.cpp +++ b/Minecraft.Server/Access/BanManager.cpp @@ -7,7 +7,8 @@ #include "..\Common\NetworkUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" +#include "..\..\Minecraft.World\UUID.h" #include #include @@ -121,9 +122,19 @@ namespace ServerRuntime { return false; } + bool dirty = false; + for (auto &entry : players) + { + if (!entry.uuid.empty() || entry.xuid.empty()) continue; + try { + uint64_t xuid = std::stoull(entry.xuid, nullptr, 0); + if (xuid) { entry.uuid = GameUUID::fromXuid(xuid).toString(); dirty = true; } + } catch (...) {} + } m_bannedPlayers.swap(players); m_bannedIps.swap(ips); + if (dirty) Save(); return true; } @@ -199,6 +210,7 @@ namespace ServerRuntime } AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "uuid", &entry.uuid); AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); AccessStorageUtils::TryGetStringField(object, "expires", &entry.metadata.expires); @@ -302,6 +314,8 @@ namespace ServerRuntime { OrderedJson object = OrderedJson::object(); object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); + if (!entry.uuid.empty()) + object["uuid"] = entry.uuid; object["name"] = entry.name; object["created"] = entry.metadata.created; object["source"] = entry.metadata.source; diff --git a/Minecraft.Server/Access/BanManager.h b/Minecraft.Server/Access/BanManager.h index 59103bec7a..f02c2383e9 100644 --- a/Minecraft.Server/Access/BanManager.h +++ b/Minecraft.Server/Access/BanManager.h @@ -22,6 +22,7 @@ namespace ServerRuntime struct BannedPlayerEntry { std::string xuid; + std::string uuid; std::string name; BanMetadata metadata; }; diff --git a/Minecraft.Server/Access/WhitelistManager.cpp b/Minecraft.Server/Access/WhitelistManager.cpp index 33ea7e46a7..9b671445f5 100644 --- a/Minecraft.Server/Access/WhitelistManager.cpp +++ b/Minecraft.Server/Access/WhitelistManager.cpp @@ -6,7 +6,8 @@ #include "..\Common\FileUtils.h" #include "..\Common\StringUtils.h" #include "..\ServerLogger.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" +#include "..\..\Minecraft.World\UUID.h" #include @@ -44,8 +45,18 @@ namespace ServerRuntime { return false; } + bool dirty = false; + for (auto &entry : players) + { + if (!entry.uuid.empty() || entry.xuid.empty()) continue; + try { + uint64_t xuid = std::stoull(entry.xuid, nullptr, 0); + if (xuid) { entry.uuid = GameUUID::fromXuid(xuid).toString(); dirty = true; } + } catch (...) {} + } m_whitelistedPlayers.swap(players); + if (dirty) Save(); return true; } @@ -117,6 +128,7 @@ namespace ServerRuntime } AccessStorageUtils::TryGetStringField(object, "name", &entry.name); + AccessStorageUtils::TryGetStringField(object, "uuid", &entry.uuid); AccessStorageUtils::TryGetStringField(object, "created", &entry.metadata.created); AccessStorageUtils::TryGetStringField(object, "source", &entry.metadata.source); NormalizeMetadata(&entry.metadata); @@ -134,6 +146,8 @@ namespace ServerRuntime { OrderedJson object = OrderedJson::object(); object["xuid"] = AccessStorageUtils::NormalizeXuid(entry.xuid); + if (!entry.uuid.empty()) + object["uuid"] = entry.uuid; object["name"] = entry.name; object["created"] = entry.metadata.created; object["source"] = entry.metadata.source; diff --git a/Minecraft.Server/Access/WhitelistManager.h b/Minecraft.Server/Access/WhitelistManager.h index 1c2c5a0b57..40139141d9 100644 --- a/Minecraft.Server/Access/WhitelistManager.h +++ b/Minecraft.Server/Access/WhitelistManager.h @@ -16,6 +16,7 @@ namespace ServerRuntime struct WhitelistedPlayerEntry { std::string xuid; + std::string uuid; std::string name; WhitelistMetadata metadata; }; diff --git a/Minecraft.Server/Common/AccessStorageUtils.h b/Minecraft.Server/Common/AccessStorageUtils.h index c5d3477c70..26d0c80e52 100644 --- a/Minecraft.Server/Common/AccessStorageUtils.h +++ b/Minecraft.Server/Common/AccessStorageUtils.h @@ -3,7 +3,7 @@ #include "FileUtils.h" #include "StringUtils.h" -#include "..\vendor\nlohmann\json.hpp" +#include "Common/vendor/nlohmann/json.hpp" #include diff --git a/Minecraft.Server/ServerLogManager.cpp b/Minecraft.Server/ServerLogManager.cpp index 84805f7e4e..53f83f7404 100644 --- a/Minecraft.Server/ServerLogManager.cpp +++ b/Minecraft.Server/ServerLogManager.cpp @@ -196,6 +196,7 @@ namespace ServerRuntime case DisconnectPacket::eDisconnect_Banned: return "banned"; case DisconnectPacket::eDisconnect_NotFriendsWithHost: return "not-friends-with-host"; case DisconnectPacket::eDisconnect_NATMismatch: return "nat-mismatch"; + case DisconnectPacket::eDisconnect_AuthFailed: return "auth-failed"; default: return "unknown"; } } diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index d6ba64e7e2..da50955705 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -40,6 +40,7 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { { "allow-flight", "true" }, { "allow-nether", "true" }, + { "auth-mode", "session" }, { "autosave-interval", "60" }, { "bedrock-fog", "true" }, { "bonus-chest", "false" }, @@ -864,6 +865,9 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite); config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite); + config.authMode = ReadNormalizedStringProperty(&merged, "auth-mode", "session", 16, &shouldWrite); + if (config.authMode != "session" && config.authMode != "offline") + config.authMode = "session"; if (shouldWrite) { diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 3bb5aca802..03073ab922 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -73,7 +73,7 @@ namespace ServerRuntime bool doTileDrops; bool naturalRegeneration; bool doDaylightCycle; - + std::string authMode; /** other MinecraftServer runtime settings */ int maxBuildHeight; std::string levelType; diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 06aa0bfe19..0d6f382b20 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -5,6 +5,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AllowAllCuller.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArchiveFile.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ArrowRenderer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/AuthScreen.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatModel.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BatRenderer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/BeaconRenderer.cpp" diff --git a/Minecraft.World/AddPlayerPacket.cpp b/Minecraft.World/AddPlayerPacket.cpp index fbee1fc524..259fe758aa 100644 --- a/Minecraft.World/AddPlayerPacket.cpp +++ b/Minecraft.World/AddPlayerPacket.cpp @@ -55,6 +55,7 @@ AddPlayerPacket::AddPlayerPacket(shared_ptr player, PlayerUID xuid, Play this->xuid = xuid; this->OnlineXuid = OnlineXuid; + this->gameUuid = player->getGameUUID(); m_playerIndex = static_cast(player->getPlayerIndex()); m_skinId = player->getCustomSkin(); m_capeId = player->getCustomCape(); @@ -77,6 +78,8 @@ void AddPlayerPacket::read(DataInputStream *dis) //throws IOException carriedItem = dis->readShort(); xuid = dis->readPlayerUID(); OnlineXuid = dis->readPlayerUID(); + gameUuid.msb = dis->readLong(); + gameUuid.lsb = dis->readLong(); m_playerIndex = dis->readByte(); INT skinId = dis->readInt(); m_skinId = *(DWORD *)&skinId; @@ -102,6 +105,8 @@ void AddPlayerPacket::write(DataOutputStream *dos) //throws IOException dos->writeShort(carriedItem); dos->writePlayerUID(xuid); dos->writePlayerUID(OnlineXuid); + dos->writeLong(gameUuid.msb); + dos->writeLong(gameUuid.lsb); dos->writeByte(m_playerIndex); dos->writeInt(m_skinId); dos->writeInt(m_capeId); @@ -117,7 +122,7 @@ void AddPlayerPacket::handle(PacketListener *listener) int AddPlayerPacket::getEstimatedSize() { - int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(PlayerUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte); + int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(PlayerUID) + sizeof(GameUUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte); if( entityData != nullptr ) { diff --git a/Minecraft.World/AddPlayerPacket.h b/Minecraft.World/AddPlayerPacket.h index af90c97dfe..4bf44dee1b 100644 --- a/Minecraft.World/AddPlayerPacket.h +++ b/Minecraft.World/AddPlayerPacket.h @@ -3,6 +3,7 @@ using namespace std; #include "Packet.h" #include "SynchedEntityData.h" +#include "UUID.h" class Player; @@ -21,6 +22,7 @@ class AddPlayerPacket : public Packet, public enable_shared_from_this> &fields, wstring &outUid, wstring &outUsername) +{ + for (const auto &[key, value] : fields) + { + if (key == L"uid") outUid = value; + else if (key == L"username") outUsername = value; + } + return validate(outUid, outUsername); +} diff --git a/Minecraft.World/AuthModule.h b/Minecraft.World/AuthModule.h new file mode 100644 index 0000000000..308ddcbe1b --- /dev/null +++ b/Minecraft.World/AuthModule.h @@ -0,0 +1,45 @@ +#pragma once +using namespace std; + +#include +#include +#include + +inline string narrowStr(const wstring &w) +{ + return string(w.begin(), w.end()); +} + +class AuthModule +{ +public: + virtual ~AuthModule() = default; + + virtual const wchar_t *schemeName() = 0; + virtual vector supportedVariations() = 0; + virtual vector> getSettings(const wstring &variation) = 0; + virtual bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) = 0; + + bool validate(const wstring &uid, const wstring &username); + +protected: + bool extractIdentity(const vector> &fields, wstring &outUid, wstring &outUsername); +}; + +class KeypairOfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override { return L"mcconsoles:keypair_offline"; } + vector supportedVariations() override { return {L"rsa2048", L"ed25519"}; } + vector> getSettings(const wstring &variation) override { return {{L"key_type", variation}}; } + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override { return extractIdentity(fields, outUid, outUsername); } +}; + +class OfflineAuthModule : public AuthModule +{ +public: + const wchar_t *schemeName() override { return L"mcconsoles:offline"; } + vector supportedVariations() override { return {}; } + vector> getSettings(const wstring &) override { return {}; } + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override { return extractIdentity(fields, outUid, outUsername); } +}; diff --git a/Minecraft.World/AuthPackets.cpp b/Minecraft.World/AuthPackets.cpp new file mode 100644 index 0000000000..4490b264f7 --- /dev/null +++ b/Minecraft.World/AuthPackets.cpp @@ -0,0 +1,47 @@ +#include "stdafx.h" +#include "InputOutputStream.h" +#include "PacketListener.h" +#include "AuthPackets.h" + +AuthPacket::AuthPacket(AuthStage stage, vector> fields) + : stage(stage), fields(std::move(fields)) +{ +} + +void AuthPacket::read(DataInputStream *dis) +{ + stage = static_cast(dis->readByte()); + short count = dis->readShort(); + fields.clear(); + fields.reserve(count); + for (short i = 0; i < count; i++) + { + wstring key = readUtf(dis, 256); + wstring value = readUtf(dis, 4096); + fields.emplace_back(std::move(key), std::move(value)); + } +} + +void AuthPacket::write(DataOutputStream *dos) +{ + dos->writeByte(static_cast(stage)); + dos->writeShort(static_cast(fields.size())); + for (const auto &[key, value] : fields) + { + writeUtf(key, dos); + writeUtf(value, dos); + } +} + +void AuthPacket::handle(PacketListener *listener) +{ + listener->handleAuth(shared_from_this()); +} + +int AuthPacket::getEstimatedSize() +{ + int size = 1 + 2; + for (const auto &[key, value] : fields) + size += 4 + static_cast((key.length() + value.length()) * sizeof(wchar_t)); + return size; +} diff --git a/Minecraft.World/AuthPackets.h b/Minecraft.World/AuthPackets.h new file mode 100644 index 0000000000..6c73067f1f --- /dev/null +++ b/Minecraft.World/AuthPackets.h @@ -0,0 +1,36 @@ +#pragma once +using namespace std; + +#include "Packet.h" + +enum class AuthStage : uint8_t +{ + ANNOUNCE_VERSION, + DECLARE_SCHEME, + ACCEPT_SCHEME, + SCHEME_SETTINGS, + BEGIN_AUTH, + AUTH_DATA, + AUTH_DONE, + ASSIGN_IDENTITY, + CONFIRM_IDENTITY, + AUTH_SUCCESS, + AUTH_FAILURE +}; + +class AuthPacket : public Packet, public enable_shared_from_this +{ +public: + AuthStage stage; + vector> fields; + + AuthPacket(AuthStage stage = AuthStage::ANNOUNCE_VERSION, vector> fields = {}); + + virtual void read(DataInputStream *dis); + virtual void write(DataOutputStream *dos); + virtual void handle(PacketListener *listener); + virtual int getEstimatedSize(); + + static shared_ptr create() { return std::make_shared(); } + virtual int getId() { return 72; } +}; diff --git a/Minecraft.World/CMakeLists.txt b/Minecraft.World/CMakeLists.txt index e397bf2925..d649031419 100644 --- a/Minecraft.World/CMakeLists.txt +++ b/Minecraft.World/CMakeLists.txt @@ -27,4 +27,6 @@ target_compile_definitions(Minecraft.World PRIVATE ) target_precompile_headers(Minecraft.World PRIVATE "$<$:stdafx.h>") +target_link_libraries(Minecraft.World PUBLIC CURL::libcurl) + configure_compiler_target(Minecraft.World) diff --git a/Minecraft.World/DirectoryLevelStorage.cpp b/Minecraft.World/DirectoryLevelStorage.cpp index c557e37af1..892ccae5b5 100644 --- a/Minecraft.World/DirectoryLevelStorage.cpp +++ b/Minecraft.World/DirectoryLevelStorage.cpp @@ -11,6 +11,8 @@ #include "LevelData.h" #include "DirectoryLevelStorage.h" #include "ConsoleSaveFileIO.h" +#include "UUID.h" +#include "StringHelpers.h" const wstring DirectoryLevelStorage::sc_szPlayerDir(L"players/"); @@ -168,6 +170,8 @@ DirectoryLevelStorage::DirectoryLevelStorage(ConsoleSaveFile *saveFile, const Fi #ifdef _LARGE_WORLDS m_usedMappings = byteArray(MAXIMUM_MAP_SAVE_DATA/8); #endif + + migratePlayerXuidsToUuids(); } DirectoryLevelStorage::~DirectoryLevelStorage() @@ -398,7 +402,7 @@ void DirectoryLevelStorage::save(shared_ptr player) #elif defined(_DURANGO) ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getXuid().toString() + L".dat" ); #else - const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( player->getXuid() ) + L".dat" ); + const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getGameUUID().toWString() + L".dat" ); #endif // If saves are disabled (e.g. because we are writing the save buffer to disk) then cache this player data if(StorageManager.GetSaveDisabled()) @@ -447,7 +451,7 @@ CompoundTag *DirectoryLevelStorage::loadPlayerDataTag(PlayerUID xuid) #elif defined(_DURANGO) ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + xuid.toString() + L".dat" ); #else - const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( xuid ) + L".dat" ); + const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + GameUUID::fromXuid(xuid).toWString() + L".dat" ); #endif const auto it = m_cachedSaveData.find(realFile.getName()); if(it != m_cachedSaveData.end() ) @@ -818,3 +822,42 @@ void DirectoryLevelStorage::saveAllCachedData() } m_mapFilesToDelete.clear(); } +void DirectoryLevelStorage::migratePlayerXuidsToUuids() +{ + if (!m_saveFile) return; + + const ConsoleSavePath marker(L"migration_v1.dat"); + if (m_saveFile->doesFileExist(marker)) return; + + if (vector *playerFiles = m_saveFile->getFilesWithPrefix(playerDir.getName())) + { + for (FileEntry *file : *playerFiles) + { + wstring stem = replaceAll(replaceAll(file->data.filename, playerDir.getName(), L""), L".dat", L""); + + if (stem.empty() || !std::all_of(stem.begin(), stem.end(), [](wchar_t c) { return c >= L'0' && c <= L'9'; })) continue; + + const PlayerUID xuid = _fromString(stem); + if (xuid == INVALID_XUID) continue; + + ConsoleSaveFileInputStream fis(m_saveFile, file); + CompoundTag *tag = NbtIo::readCompressed(&fis); + if (!tag) continue; + + const wstring uuidStr = GameUUID::fromXuid(xuid).toWString(); + tag->putString(L"UUID", uuidStr); + + ConsoleSaveFileOutputStream fos(m_saveFile, ConsoleSavePath(playerDir.getName() + uuidStr + L".dat")); + NbtIo::writeCompressed(tag, &fos); + delete tag; + + m_saveFile->deleteFile(file); + app.DebugPrintf("migrated player %ls -> %ls\n", stem.c_str(), uuidStr.c_str()); + } + delete playerFiles; + } + + DWORD written = 0; + const uint8_t ver = 1; + m_saveFile->writeFile(m_saveFile->createFile(marker), &ver, 1, &written); +} diff --git a/Minecraft.World/DirectoryLevelStorage.h b/Minecraft.World/DirectoryLevelStorage.h index 6d305ba3f4..b0edff1e8f 100644 --- a/Minecraft.World/DirectoryLevelStorage.h +++ b/Minecraft.World/DirectoryLevelStorage.h @@ -139,6 +139,7 @@ class DirectoryLevelStorage : public LevelStorage, public PlayerIO static wstring getPlayerDir() { return sc_szPlayerDir; } private: + void migratePlayerXuidsToUuids(); void dontSaveMapMappingForPlayer(PlayerUID xuid); void deleteMapFilesForPlayer(PlayerUID xuid); }; diff --git a/Minecraft.World/DisconnectPacket.h b/Minecraft.World/DisconnectPacket.h index 3c96a429b1..22036a59a8 100644 --- a/Minecraft.World/DisconnectPacket.h +++ b/Minecraft.World/DisconnectPacket.h @@ -44,6 +44,7 @@ class DisconnectPacket : public Packet, public enable_shared_from_this> &fields, const wchar_t *key) +{ + for (const auto &[k, v] : fields) + if (k == key) return v; + return {}; +} + +HandshakeManager::HandshakeManager(bool isServer) + : isServer(isServer), state(HandshakeState::IDLE), activeModule(nullptr) +{ +} + +void HandshakeManager::registerModule(unique_ptr module) +{ + wstring name = module->schemeName(); + modules[std::move(name)] = std::move(module); +} + +void HandshakeManager::setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation) +{ + accessToken = token; + clientUid = uid; + clientUsername = username; + preferredVariation = variation; +} + +vector> HandshakeManager::drainPendingPackets() +{ + auto out = std::move(pendingPackets); + pendingPackets.clear(); + return out; +} + +shared_ptr HandshakeManager::handlePacket(const shared_ptr &packet) +{ + return isServer ? handleServer(packet) : handleClient(packet); +} + +shared_ptr HandshakeManager::createInitialPacket() +{ + state = HandshakeState::VERSION_SENT; + wstring schemes; + for (const auto &[name, mod] : modules) + { + if (!schemes.empty()) schemes += L","; + schemes += name; + } + return makePacket(AuthStage::ANNOUNCE_VERSION, { + {L"version", PROTOCOL_VERSION}, + {L"schemes", schemes} + }); +} + +shared_ptr HandshakeManager::handleServer(const shared_ptr &packet) +{ + switch (packet->stage) + { + case AuthStage::ANNOUNCE_VERSION: + { + protocolVersion = getField(packet->fields, L"version"); + if (protocolVersion != PROTOCOL_VERSION) + return fail(); + + if (modules.empty()) + return fail(); + + auto splitCsv = [](const wstring &s) { + vector out; + for (size_t p = 0; p < s.size(); ) { + size_t c = s.find(L',', p); + if (c == wstring::npos) c = s.size(); + out.push_back(s.substr(p, c - p)); + p = c + 1; + } + return out; + }; + auto supported = splitCsv(getField(packet->fields, L"schemes")); + + activeModule = nullptr; + for (auto &[name, mod] : modules) + { + if (supported.empty() || std::find(supported.begin(), supported.end(), name) != supported.end()) + { + activeModule = mod.get(); + break; + } + } + if (!activeModule) + return fail(); + state = HandshakeState::SCHEME_DECLARED; + return makePacket(AuthStage::DECLARE_SCHEME, { + {L"version", PROTOCOL_VERSION}, + {L"scheme", activeModule->schemeName()} + }); + } + + case AuthStage::ACCEPT_SCHEME: + { + activeVariation = getField(packet->fields, L"variation"); + state = HandshakeState::SETTINGS_SENT; + auto settings = activeModule->getSettings(activeVariation); + return makePacket(AuthStage::SCHEME_SETTINGS, std::move(settings)); + } + + case AuthStage::BEGIN_AUTH: + { + state = HandshakeState::AUTH_IN_PROGRESS; + return nullptr; + } + + case AuthStage::AUTH_DATA: + { + wstring uid, username; + if (!activeModule->onAuthData(packet->fields, uid, username)) + return fail(); + finalUid = uid; + finalUsername = username; + state = HandshakeState::AUTH_DATA_EXCHANGED; + return nullptr; + } + + case AuthStage::AUTH_DONE: + { + if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername) + return fail(); + + state = HandshakeState::IDENTITY_ASSIGNED; + return makePacket(AuthStage::ASSIGN_IDENTITY, { + {L"uid", finalUid}, + {L"username", finalUsername} + }); + } + + case AuthStage::CONFIRM_IDENTITY: + { + if (getField(packet->fields, L"uid") != finalUid || getField(packet->fields, L"username") != finalUsername) + return fail(); + + state = HandshakeState::COMPLETE; + return makePacket(AuthStage::AUTH_SUCCESS); + } + + default: + return fail(); + } +} + +shared_ptr HandshakeManager::handleClient(const shared_ptr &packet) +{ + switch (packet->stage) + { + case AuthStage::DECLARE_SCHEME: + { + protocolVersion = getField(packet->fields, L"version"); + wstring scheme = getField(packet->fields, L"scheme"); + + app.DebugPrintf("AUTH CLIENT: DECLARE_SCHEME scheme=%ls\n", scheme.c_str()); + + if (protocolVersion != PROTOCOL_VERSION) + return fail(); + + auto it = modules.find(scheme); + if (it == modules.end()) + return fail(); + + activeModule = it->second.get(); + + auto variations = activeModule->supportedVariations(); + if (!preferredVariation.empty() && + std::find(variations.begin(), variations.end(), preferredVariation) != variations.end()) + activeVariation = preferredVariation; + else + activeVariation = variations.empty() ? L"" : variations[0]; + + app.DebugPrintf("AUTH CLIENT: accepting variation=%ls\n", activeVariation.c_str()); + state = HandshakeState::SCHEME_ACCEPTED; + return makePacket(AuthStage::ACCEPT_SCHEME, {{L"variation", activeVariation}}); + } + + case AuthStage::SCHEME_SETTINGS: + { + if (!activeModule) + return fail(); + wstring serverId = getField(packet->fields, L"serverId"); + wstring joinUrlW = getField(packet->fields, L"joinUrl"); + wstring scheme(activeModule->schemeName()); + + app.DebugPrintf("AUTH CLIENT: SCHEME_SETTINGS joinUrl=%ls serverId=%ls\n", joinUrlW.c_str(), serverId.c_str()); + + if (scheme == L"mcconsoles:session" && !accessToken.empty() && !joinUrlW.empty()) + { + nlohmann::json body = { + {"accessToken", narrowStr(accessToken)}, + {"selectedProfile", narrowStr(clientUid)}, + {"serverId", narrowStr(serverId)} + }; + string joinUrl = narrowStr(joinUrlW); + app.DebugPrintf("AUTH CLIENT: POSTing join to %s\n", joinUrl.c_str()); + HttpResponse resp; + try { + resp = HttpClient::post(joinUrl, body.dump()); + } catch (...) { + app.DebugPrintf("AUTH CLIENT: join POST threw exception\n"); + return fail(); + } + app.DebugPrintf("AUTH CLIENT: join POST status=%d\n", resp.statusCode); + if (resp.statusCode < 200 || resp.statusCode >= 300) + return fail(); + } + + state = HandshakeState::AUTH_IN_PROGRESS; + pendingPackets.push_back(makePacket(AuthStage::BEGIN_AUTH)); + pendingPackets.push_back(makePacket(AuthStage::AUTH_DATA, { + {L"uid", clientUid}, + {L"username", clientUsername} + })); + pendingPackets.push_back(makePacket(AuthStage::AUTH_DONE, { + {L"uid", clientUid}, + {L"username", clientUsername} + })); + return nullptr; + } + + case AuthStage::ASSIGN_IDENTITY: + { + finalUid = getField(packet->fields, L"uid"); + finalUsername = getField(packet->fields, L"username"); + + state = HandshakeState::IDENTITY_CONFIRMED; + return makePacket(AuthStage::CONFIRM_IDENTITY, { + {L"uid", finalUid}, + {L"username", finalUsername} + }); + } + + case AuthStage::AUTH_SUCCESS: + { + state = HandshakeState::COMPLETE; + return nullptr; + } + + case AuthStage::AUTH_FAILURE: + { + state = HandshakeState::FAILED; + return nullptr; + } + + default: + return fail(); + } +} + +shared_ptr HandshakeManager::makePacket(AuthStage stage, vector> fields) +{ + return std::make_shared(stage, std::move(fields)); +} + +shared_ptr HandshakeManager::fail() +{ + state = HandshakeState::FAILED; + return makePacket(AuthStage::AUTH_FAILURE); +} diff --git a/Minecraft.World/HandshakeManager.h b/Minecraft.World/HandshakeManager.h new file mode 100644 index 0000000000..716f40a3ad --- /dev/null +++ b/Minecraft.World/HandshakeManager.h @@ -0,0 +1,66 @@ +#pragma once +using namespace std; + +#include +#include +#include +#include "AuthPackets.h" + +class AuthModule; + +enum class HandshakeState : uint8_t +{ + IDLE, + VERSION_SENT, + SCHEME_DECLARED, + SCHEME_ACCEPTED, + SETTINGS_SENT, + AUTH_IN_PROGRESS, + AUTH_DATA_EXCHANGED, + IDENTITY_ASSIGNED, + IDENTITY_CONFIRMED, + COMPLETE, + FAILED +}; + +class HandshakeManager +{ +private: + bool isServer; + HandshakeState state; + unordered_map> modules; + AuthModule *activeModule; + wstring activeVariation; + wstring protocolVersion; + + wstring accessToken; + wstring clientUid; + wstring clientUsername; + wstring preferredVariation; + + vector> pendingPackets; + +public: + wstring finalUid; + wstring finalUsername; + + HandshakeManager(bool isServer); + ~HandshakeManager() = default; + + void registerModule(unique_ptr module); + void setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation = L""); + shared_ptr handlePacket(const shared_ptr &packet); + shared_ptr createInitialPacket(); + vector> drainPendingPackets(); + + bool isComplete() const { return state == HandshakeState::COMPLETE; } + bool isFailed() const { return state == HandshakeState::FAILED; } + HandshakeState getState() const { return state; } + +private: + shared_ptr handleServer(const shared_ptr &packet); + shared_ptr handleClient(const shared_ptr &packet); + + shared_ptr makePacket(AuthStage stage, vector> fields = {}); + shared_ptr fail(); +}; diff --git a/Minecraft.World/HttpClient.cpp b/Minecraft.World/HttpClient.cpp new file mode 100644 index 0000000000..6e92963576 --- /dev/null +++ b/Minecraft.World/HttpClient.cpp @@ -0,0 +1,66 @@ +#include "stdafx.h" +#include "HttpClient.h" +#include + +static size_t writeCallback(char *data, size_t size, size_t nmemb, void *userdata) +{ + auto *buf = static_cast(userdata); + buf->append(data, size * nmemb); + return size * nmemb; +} + +static HttpResponse performRequest(CURL *curl, struct curl_slist *extraHeaders = nullptr) +{ + std::string responseBody; + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + if (extraHeaders) + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, extraHeaders); + + CURLcode res = curl_easy_perform(curl); + + int statusCode = 0; + if (res == CURLE_OK) + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode); + + curl_easy_cleanup(curl); + if (extraHeaders) + curl_slist_free_all(extraHeaders); + return {statusCode, std::move(responseBody)}; +} + +static struct curl_slist *buildHeaders(const std::vector &headers) +{ + struct curl_slist *list = nullptr; + for (const auto &h : headers) + list = curl_slist_append(list, h.c_str()); + return list; +} + +HttpResponse HttpClient::get(const std::string &url, const std::vector &headers) +{ + CURL *curl = curl_easy_init(); + if (!curl) + return {0, ""}; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + return performRequest(curl, headers.empty() ? nullptr : buildHeaders(headers)); +} + +HttpResponse HttpClient::post(const std::string &url, const std::string &body, const std::string &contentType, const std::vector &headers) +{ + CURL *curl = curl_easy_init(); + if (!curl) + return {0, ""}; + + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(body.size())); + + auto *headerList = buildHeaders(headers); + headerList = curl_slist_append(headerList, ("Content-Type: " + contentType).c_str()); + + return performRequest(curl, headerList); +} diff --git a/Minecraft.World/HttpClient.h b/Minecraft.World/HttpClient.h new file mode 100644 index 0000000000..f08c1e797f --- /dev/null +++ b/Minecraft.World/HttpClient.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +struct HttpResponse +{ + int statusCode; + std::string body; +}; + +class HttpClient +{ +public: + static HttpResponse get(const std::string &url, const std::vector &headers = {}); + static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json", const std::vector &headers = {}); +}; diff --git a/Minecraft.World/LoginPacket.cpp b/Minecraft.World/LoginPacket.cpp index 79cdfbae1d..b80318109c 100644 --- a/Minecraft.World/LoginPacket.cpp +++ b/Minecraft.World/LoginPacket.cpp @@ -111,6 +111,8 @@ void LoginPacket::read(DataInputStream *dis) //throws IOException maxPlayers = dis->readByte(); m_offlineXuid = dis->readPlayerUID(); m_onlineXuid = dis->readPlayerUID(); + m_gameUuid.msb = dis->readLong(); + m_gameUuid.lsb = dis->readLong(); m_friendsOnlyUGC = dis->readBoolean(); m_ugcPlayersVersion = dis->readInt(); difficulty = dis->readByte(); @@ -150,6 +152,8 @@ void LoginPacket::write(DataOutputStream *dos) //throws IOException dos->writeByte(maxPlayers); dos->writePlayerUID(m_offlineXuid); dos->writePlayerUID(m_onlineXuid); + dos->writeLong(m_gameUuid.msb); + dos->writeLong(m_gameUuid.lsb); dos->writeBoolean(m_friendsOnlyUGC); dos->writeInt(m_ugcPlayersVersion); dos->writeByte(difficulty); diff --git a/Minecraft.World/LoginPacket.h b/Minecraft.World/LoginPacket.h index 02a62b60b9..18d4afa301 100644 --- a/Minecraft.World/LoginPacket.h +++ b/Minecraft.World/LoginPacket.h @@ -2,6 +2,7 @@ using namespace std; #include "Packet.h" +#include "UUID.h" class LevelType; class LoginPacket : public Packet, public enable_shared_from_this @@ -12,6 +13,7 @@ class LoginPacket : public Packet, public enable_shared_from_this int64_t seed; char dimension; PlayerUID m_offlineXuid, m_onlineXuid; // 4J Added + GameUUID m_gameUuid; char difficulty; // 4J Added bool m_friendsOnlyUGC; // 4J Added DWORD m_ugcPlayersVersion; // 4J Added diff --git a/Minecraft.World/MapItem.cpp b/Minecraft.World/MapItem.cpp index 61c203e3b4..3ff8206c65 100644 --- a/Minecraft.World/MapItem.cpp +++ b/Minecraft.World/MapItem.cpp @@ -21,6 +21,7 @@ MapItem::MapItem(int id) : ComplexItem(id) shared_ptr MapItem::getSavedData(short idNum, Level *level) { + if (!level) return nullptr; std::wstring id = wstring( L"map_" ) + std::to_wstring(idNum); shared_ptr mapItemSavedData = dynamic_pointer_cast(level->getSavedData(typeid(MapItemSavedData), id)); @@ -42,6 +43,7 @@ shared_ptr MapItem::getSavedData(short idNum, Level *level) shared_ptr MapItem::getSavedData(shared_ptr itemInstance, Level *level) { + if (!level) return nullptr; MemSect(31); std::wstring id = wstring( L"map_" ) + std::to_wstring(itemInstance->getAuxValue() ); MemSect(0); @@ -254,6 +256,7 @@ void MapItem::update(Level *level, shared_ptr player, shared_ptr itemInstance, Level *level, shared_ptr owner, int slot, bool selected) { + if (!level) return; if (level->isClientSide) return; shared_ptr data = getSavedData(itemInstance, level); @@ -293,7 +296,10 @@ void MapItem::inventoryTick(shared_ptr itemInstance, Level *level, shared_ptr MapItem::getUpdatePacket(shared_ptr itemInstance, Level *level, shared_ptr player) { - charArray data = MapItem::getSavedData(itemInstance, level)->getUpdatePacket(itemInstance, level, player); + if (!level) return nullptr; + auto savedData = MapItem::getSavedData(itemInstance, level); + if (!savedData) return nullptr; + charArray data = savedData->getUpdatePacket(itemInstance, level, player); if (data.data == nullptr || data.length == 0) return nullptr; @@ -304,6 +310,7 @@ shared_ptr MapItem::getUpdatePacket(shared_ptr itemInstanc void MapItem::onCraftedBy(shared_ptr itemInstance, Level *level, shared_ptr player) { + if (!level) return; wchar_t buf[64]; int mapScale = 3; diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 05bf932dcb..325976c633 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -150,6 +150,7 @@ void Packet::staticCtor() //map(253, true, false, ServerAuthDataPacket.class); map(254, false, true, false, false, typeid(GetInfoPacket), GetInfoPacket::create); // TODO New for 1.8.2 - Needs sendToAny? map(255, true, true, true, false, typeid(DisconnectPacket), DisconnectPacket::create); + map(72, true, true, true, false, typeid(AuthPacket), AuthPacket::create); } IllegalArgumentException::IllegalArgumentException(const wstring& information) diff --git a/Minecraft.World/PacketListener.cpp b/Minecraft.World/PacketListener.cpp index 09fbb05251..bd340add3e 100644 --- a/Minecraft.World/PacketListener.cpp +++ b/Minecraft.World/PacketListener.cpp @@ -491,3 +491,8 @@ void PacketListener::handleGameCommand(shared_ptr packet) { onUnhandledPacket( (shared_ptr ) packet); } + +void PacketListener::handleAuth(const shared_ptr &packet) +{ + onUnhandledPacket( (shared_ptr ) packet); +} diff --git a/Minecraft.World/PacketListener.h b/Minecraft.World/PacketListener.h index f63b6bc7fd..bf2ebdadbd 100644 --- a/Minecraft.World/PacketListener.h +++ b/Minecraft.World/PacketListener.h @@ -111,6 +111,7 @@ class KickPlayerPacket; class AdditionalModelPartsPacket; class XZPacket; class GameCommandPacket; +class AuthPacket; class PacketListener { @@ -227,4 +228,6 @@ class PacketListener virtual void handleKickPlayer(shared_ptr packet); virtual void handleXZ(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); + + virtual void handleAuth(const shared_ptr &packet); }; diff --git a/Minecraft.World/Player.cpp b/Minecraft.World/Player.cpp index 00c7148e41..e0e72ff2fc 100644 --- a/Minecraft.World/Player.cpp +++ b/Minecraft.World/Player.cpp @@ -762,6 +762,8 @@ unsigned int Player::getSkinAnimOverrideBitmask(DWORD skinId) void Player::setXuid(PlayerUID xuid) { m_xuid = xuid; + if (m_gameUuid.isNil() && xuid != INVALID_XUID) + m_gameUuid = GameUUID::fromXuid(xuid); #ifdef _XBOX_ONE // 4J Stu - For XboxOne (and probably in the future all other platforms) we store a UUID for the player to use as the owner key for tamed animals // This should just be a string version of the xuid diff --git a/Minecraft.World/Player.h b/Minecraft.World/Player.h index 2e223a1e55..08e190bbc3 100644 --- a/Minecraft.World/Player.h +++ b/Minecraft.World/Player.h @@ -8,6 +8,7 @@ using namespace std; #include "PlayerEnderChestContainer.h" #include "CommandSender.h" #include "ScoreHolder.h" +#include "UUID.h" class AbstractContainerMenu; class Stats; @@ -417,7 +418,8 @@ class Player : public LivingEntity, public CommandSender, public ScoreHolder PlayerUID getXuid() { return m_xuid; } void setOnlineXuid(PlayerUID xuid) { m_OnlineXuid = xuid; } PlayerUID getOnlineXuid() { return m_OnlineXuid; } - + void setGameUUID(const GameUUID& uuid) { m_gameUuid = uuid; } + GameUUID getGameUUID() const { return m_gameUuid; } void setPlayerIndex(DWORD dwIndex) { m_playerIndex = dwIndex; } DWORD getPlayerIndex() { return m_playerIndex; } @@ -431,6 +433,7 @@ class Player : public LivingEntity, public CommandSender, public ScoreHolder private: PlayerUID m_xuid; PlayerUID m_OnlineXuid; + GameUUID m_gameUuid; protected: bool m_bShownOnMaps; diff --git a/Minecraft.World/SessionAuthModule.cpp b/Minecraft.World/SessionAuthModule.cpp new file mode 100644 index 0000000000..5f100aba2b --- /dev/null +++ b/Minecraft.World/SessionAuthModule.cpp @@ -0,0 +1,139 @@ +#include "stdafx.h" +#include "SessionAuthModule.h" +#include "HttpClient.h" +#include "StringHelpers.h" +#include "Common/vendor/nlohmann/json.hpp" +#include +#include +#include + +static wstring generateServerId() +{ + static constexpr wchar_t hex[] = L"0123456789abcdef"; + static std::mt19937 rng(std::random_device{}()); + wstring id(16, L'0'); + for (auto &c : id) c = hex[rng() & 0xF]; + return id; +} + +static vector s_registryProviders; +static bool s_registryLoaded = false; + +void YggdrasilRegistry::load() +{ + s_registryProviders.clear(); + + std::ifstream file("yggdrasil.json"); + if (file.is_open()) + { + auto j = nlohmann::json::parse(file, nullptr, false); + if (j.is_array()) + { + for (const auto &entry : j) + { + if (!entry.is_object()) continue; + YggdrasilProviderConfig cfg; + cfg.name = convStringToWstring(entry.value("name", "")); + cfg.displayName = convStringToWstring(entry.value("displayName", "")); + cfg.authUrl = entry.value("authUrl", ""); + cfg.sessionUrl = entry.value("sessionUrl", ""); + if (!cfg.name.empty() && !cfg.sessionUrl.empty()) + s_registryProviders.push_back(std::move(cfg)); + } + } + } + + if (std::none_of(s_registryProviders.begin(), s_registryProviders.end(), + [](const auto &p) { return p.name == L"elyby"; })) + s_registryProviders.insert(s_registryProviders.begin(), + {L"elyby", L"Ely.by", "https://authserver.ely.by/auth", "https://authserver.ely.by/session"}); + + s_registryProviders.push_back( + {L"mojang", L"Mojang", "", "https://sessionserver.mojang.com/session/minecraft"}); + + s_registryLoaded = true; +} + +const vector &YggdrasilRegistry::providers() +{ + if (!s_registryLoaded) load(); + return s_registryProviders; +} + +const YggdrasilProviderConfig *YggdrasilRegistry::find(const wstring &name) +{ + for (const auto &p : providers()) + if (p.name == name) return &p; + return nullptr; +} + +const YggdrasilProviderConfig &YggdrasilRegistry::defaultProvider() +{ + const auto &list = providers(); + for (const auto &p : list) + if (p.name != L"mojang") return p; + return list[0]; +} + +const wchar_t *SessionAuthModule::schemeName() { return L"mcconsoles:session"; } + +vector SessionAuthModule::supportedVariations() +{ + vector result; + for (const auto &p : YggdrasilRegistry::providers()) + result.push_back(p.name); + return result; +} + +vector> SessionAuthModule::getSettings(const wstring &variation) +{ + auto *p = YggdrasilRegistry::find(variation); + if (!p) return {}; + + activeProvider = p; + activeServerId = generateServerId(); + return { + {L"joinUrl", convStringToWstring(p->joinUrl())}, + {L"serverId", activeServerId} + }; +} + +bool SessionAuthModule::serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername) +{ + string url = provider.hasJoinedUrl() + "?username=" + narrowStr(username) + "&serverId=" + narrowStr(serverId); + + app.DebugPrintf("AUTH SERVER [%ls]: hasJoined GET %s\n", provider.name.c_str(), url.c_str()); + auto response = HttpClient::get(url); + app.DebugPrintf("AUTH SERVER [%ls]: hasJoined status=%d\n", provider.name.c_str(), response.statusCode); + + if (response.statusCode != 200) + return false; + + auto json = nlohmann::json::parse(response.body, nullptr, false); + if (json.is_discarded()) + return false; + + string id = json.value("id", ""); + string name = json.value("name", ""); + if (id.empty() || name.empty()) + return false; + + outUid = convStringToWstring(id); + outUsername = convStringToWstring(name); + return true; +} + +bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +{ + wstring username; + for (const auto &[k, v] : fields) + if (k == L"username") username = v; + + if (username.empty() || activeServerId.empty() || !activeProvider) + return false; + + if (!serverVerify(*activeProvider, username, activeServerId, outUid, outUsername)) + return false; + + return validate(outUid, outUsername); +} diff --git a/Minecraft.World/SessionAuthModule.h b/Minecraft.World/SessionAuthModule.h new file mode 100644 index 0000000000..2990edbd9b --- /dev/null +++ b/Minecraft.World/SessionAuthModule.h @@ -0,0 +1,44 @@ +#pragma once +using namespace std; +#include "AuthModule.h" +#include +#include + +struct YggdrasilProviderConfig +{ + wstring name; + wstring displayName; + string authUrl; + string sessionUrl; + string authenticateUrl() const { return authUrl + "/authenticate"; } + string validateUrl() const { return authUrl + "/validate"; } + string refreshUrl() const { return authUrl + "/refresh"; } + string joinUrl() const { return sessionUrl + "/join"; } + string hasJoinedUrl() const { return sessionUrl + "/hasJoined"; } +}; + +class YggdrasilRegistry +{ +public: + static void load(); + static const vector &providers(); + static const YggdrasilProviderConfig *find(const wstring &name); + static const YggdrasilProviderConfig &defaultProvider(); +}; + +class SessionAuthModule : public AuthModule +{ +private: + const YggdrasilProviderConfig *activeProvider = nullptr; + wstring activeServerId; + + bool serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername); + +public: + SessionAuthModule() = default; + + const wchar_t *schemeName() override; + vector supportedVariations() override; + vector> getSettings(const wstring &variation) override; + bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; +}; diff --git a/Minecraft.World/UUID.cpp b/Minecraft.World/UUID.cpp new file mode 100644 index 0000000000..4ca8173a06 --- /dev/null +++ b/Minecraft.World/UUID.cpp @@ -0,0 +1,144 @@ +#include "stdafx.h" +#include "UUID.h" +#include "Random.h" +#include +#include +static void sha1_block(uint32_t h[5], const uint8_t block[64]) +{ + uint32_t w[80]; + for (int i = 0; i < 16; i++) + w[i] = (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | (block[i * 4 + 2] << 8) | block[i * 4 + 3]; + for (int i = 16; i < 80; i++) { + uint32_t x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; + w[i] = (x << 1) | (x >> 31); + } + uint32_t a = h[0], b = h[1], c = h[2], d = h[3], e = h[4]; + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } + else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } + else { f = b ^ c ^ d; k = 0xCA62C1D6; } + uint32_t tmp = ((a << 5) | (a >> 27)) + f + e + k + w[i]; + e = d; d = c; c = (b << 30) | (b >> 2); b = a; a = tmp; + } + h[0] += a; h[1] += b; h[2] += c; h[3] += d; h[4] += e; +} + +static void sha1(const uint8_t* data, size_t len, uint8_t out[20]) +{ + uint32_t h[5] = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 }; + uint64_t bitLen = len * 8; + + size_t fullBlocks = len / 64; + for (size_t blk = 0; blk < fullBlocks; blk++) + sha1_block(h, data + blk * 64); + + uint8_t tail[128] = {}; + size_t rem = len - fullBlocks * 64; + if (rem) memcpy(tail, data + fullBlocks * 64, rem); + tail[rem] = 0x80; + size_t tailLen = (rem < 56) ? 64 : 128; + for (int i = 0; i < 8; i++) + tail[tailLen - 1 - i] = (uint8_t)(bitLen >> (i * 8)); + + for (size_t off = 0; off < tailLen; off += 64) + sha1_block(h, tail + off); + + for (int i = 0; i < 5; i++) { + out[i * 4] = (uint8_t)(h[i] >> 24); + out[i * 4 + 1] = (uint8_t)(h[i] >> 16); + out[i * 4 + 2] = (uint8_t)(h[i] >> 8); + out[i * 4 + 3] = (uint8_t)(h[i]); + } +} +static constexpr char HEX[] = "0123456789abcdef"; +static constexpr uint8_t hexVal(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return 10 + c - 'a'; + if (c >= 'A' && c <= 'F') return 10 + c - 'A'; + return 0; +} + +void GameUUID::toBytes(uint8_t out[16]) const +{ + for (int i = 7; i >= 0; i--) out[7 - i] = (uint8_t)(msb >> (i * 8)); + for (int i = 7; i >= 0; i--) out[15 - i] = (uint8_t)(lsb >> (i * 8)); +} + +GameUUID GameUUID::fromBytes(const uint8_t b[16]) +{ + GameUUID u; + for (int i = 0; i < 8; i++) u.msb = (u.msb << 8) | b[i]; + for (int i = 0; i < 8; i++) u.lsb = (u.lsb << 8) | b[8 + i]; + return u; +} +std::string GameUUID::toString() const +{ + uint8_t b[16]; + toBytes(b); + char buf[37]; + int p = 0; + for (int i = 0; i < 16; i++) { + if (i == 4 || i == 6 || i == 8 || i == 10) buf[p++] = '-'; + buf[p++] = HEX[b[i] >> 4]; + buf[p++] = HEX[b[i] & 0xf]; + } + buf[p] = '\0'; + return buf; +} + +std::wstring GameUUID::toWString() const +{ + std::string s = toString(); + return { s.begin(), s.end() }; +} + +GameUUID GameUUID::fromString(const std::string& s) +{ + uint8_t b[16] = {}; + int bi = 0; + for (size_t i = 0; i < s.size() && bi < 16; i++) { + if (s[i] == '-') continue; + if (i + 1 >= s.size()) break; + b[bi++] = (hexVal(s[i]) << 4) | hexVal(s[i + 1]); + i++; + } + return fromBytes(b); +} + +GameUUID GameUUID::fromWString(const std::wstring& s) +{ + return fromString({ s.begin(), s.end() }); +} + +GameUUID GameUUID::v4(uint64_t high, uint64_t low) +{ + return { (high & ~0xF000ULL) | 0x4000ULL, (low & ~0xC000000000000000ULL) | 0x8000000000000000ULL }; +} + +GameUUID GameUUID::v5(const GameUUID& ns, const std::string& name) +{ + std::vector input(16 + name.size()); + ns.toBytes(input.data()); + memcpy(input.data() + 16, name.data(), name.size()); + + uint8_t hash[20]; + sha1(input.data(), input.size(), hash); + GameUUID u = fromBytes(hash); + u.msb = (u.msb & ~0xF000ULL) | 0x5000ULL; + u.lsb = (u.lsb & ~0xC000000000000000ULL) | 0x8000000000000000ULL; + return u; +} + +GameUUID GameUUID::fromXuid(uint64_t xuid) +{ + return v5(MCCONSOLES_NAMESPACE_UUID, std::to_string(xuid)); +} + +GameUUID GameUUID::random() +{ + Random r; + return v4(r.nextLong(), r.nextLong()); +} diff --git a/Minecraft.World/UUID.h b/Minecraft.World/UUID.h new file mode 100644 index 0000000000..3bce7773b6 --- /dev/null +++ b/Minecraft.World/UUID.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +struct GameUUID { + uint64_t msb = 0; + uint64_t lsb = 0; + + constexpr bool isNil() const { return msb == 0 && lsb == 0; } + constexpr bool operator==(const GameUUID& o) const { return msb == o.msb && lsb == o.lsb; } + constexpr bool operator!=(const GameUUID& o) const { return !(*this == o); } + constexpr bool operator<(const GameUUID& o) const { return msb < o.msb || (msb == o.msb && lsb < o.lsb); } + + void toBytes(uint8_t out[16]) const; + std::string toString() const; + std::wstring toWString() const; + + static GameUUID fromBytes(const uint8_t b[16]); + static GameUUID fromString(const std::string& s); + static GameUUID fromWString(const std::wstring& s); + static GameUUID v4(uint64_t high, uint64_t low); + static GameUUID v5(const GameUUID& ns, const std::string& name); + static GameUUID fromXuid(uint64_t xuid); + static GameUUID random(); +}; + +inline constexpr GameUUID MCCONSOLES_NAMESPACE_UUID = { 0x4d696e6563726166ULL, 0x74436f6e736f6c65ULL }; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 8a4d683303..dc843f1385 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -259,6 +259,14 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AddPlayerPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" + "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" + "${CMAKE_CURRENT_SOURCE_DIR}/UUID.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/UUID.h" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/BlockRegionUpdatePacket.cpp" @@ -314,6 +322,10 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/GameEventPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/GetInfoPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/GetInfoPacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/HandshakeManager.h" + "${CMAKE_CURRENT_SOURCE_DIR}/HttpClient.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/HttpClient.h" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/InteractPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/KeepAlivePacket.cpp" diff --git a/Minecraft.World/net.minecraft.network.packet.h b/Minecraft.World/net.minecraft.network.packet.h index c1d52d1473..2981508e36 100644 --- a/Minecraft.World/net.minecraft.network.packet.h +++ b/Minecraft.World/net.minecraft.network.packet.h @@ -108,4 +108,5 @@ #include "KickPlayerPacket.h" #include "XZPacket.h" #include "GameCommandPacket.h" +#include "AuthPackets.h" diff --git a/Minecraft.Server/vendor/nlohmann/LICENSE.MIT b/include/Common/vendor/nlohmann/LICENSE.MIT similarity index 100% rename from Minecraft.Server/vendor/nlohmann/LICENSE.MIT rename to include/Common/vendor/nlohmann/LICENSE.MIT diff --git a/Minecraft.Server/vendor/nlohmann/json.hpp b/include/Common/vendor/nlohmann/json.hpp similarity index 100% rename from Minecraft.Server/vendor/nlohmann/json.hpp rename to include/Common/vendor/nlohmann/json.hpp diff --git a/yggdrasil.json b/yggdrasil.json new file mode 100644 index 0000000000..4fbd954975 --- /dev/null +++ b/yggdrasil.json @@ -0,0 +1,8 @@ +[ + { + "name": "elyby", + "displayName": "Ely.by", + "authUrl": "https://authserver.ely.by/auth", + "sessionUrl": "https://authserver.ely.by/session" + } +]