diff --git a/Minecraft.Client/AuthScreen.cpp b/Minecraft.Client/AuthScreen.cpp index 0f0d3330..bdaa2b9b 100644 --- a/Minecraft.Client/AuthScreen.cpp +++ b/Minecraft.Client/AuthScreen.cpp @@ -3,6 +3,7 @@ #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" @@ -12,24 +13,33 @@ using json = nlohmann::json; static constexpr auto PROFILES_FILE = L"auth_profiles.dat"; -static constexpr auto MS_CLIENT_ID = "00000000441cc96b"; +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 elybyValidate(const string &accessToken, const string &clientToken); -static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken); +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; - file.read(reinterpret_cast(&count), sizeof(count)); + if (hasVariation) + file.read(reinterpret_cast(&count), sizeof(count)); + else + count = header; for (uint32_t i = 0; i < count && file.good(); i++) { @@ -52,6 +62,10 @@ void AuthProfileManager::load() 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)); } @@ -66,6 +80,9 @@ 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)); @@ -83,16 +100,17 @@ void AuthProfileManager::save() 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) +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}); + profiles.push_back({type, finalUid, username, token, clientToken, variation}); selectedProfile = static_cast(profiles.size()) - 1; save(); } @@ -137,12 +155,14 @@ bool AuthProfileManager::applySelectedProfile() } } } - else if (p.type == AuthProfile::ELYBY && !p.token.empty()) + else if (p.type == AuthProfile::YGGDRASIL && !p.token.empty()) { - if (!elybyValidate(narrowStr(p.token), narrowStr(p.clientToken))) + 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 (elybyRefresh(narrowStr(p.token), narrowStr(p.clientToken), 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); @@ -169,7 +189,6 @@ bool AuthProfileManager::applySelectedProfile() return true; } -// --- AuthFlow --- std::thread AuthFlow::workerThread; std::atomic AuthFlow::state{AuthFlowState::IDLE}; @@ -177,6 +196,7 @@ std::atomic AuthFlow::cancelRequested{false}; AuthResult AuthFlow::result; wstring AuthFlow::userCode; wstring AuthFlow::verificationUri; +wstring AuthFlow::activeVariation; void AuthFlow::reset() { @@ -187,6 +207,7 @@ void AuthFlow::reset() result = {}; userCode.clear(); verificationUri.clear(); + activeVariation.clear(); cancelRequested = false; } @@ -197,11 +218,15 @@ void AuthFlow::startMicrosoft() workerThread = std::thread(microsoftFlowThread); } -void AuthFlow::startElyBy(const wstring &username, const wstring &password) +void AuthFlow::startYggdrasil(const wstring &username, const wstring &password, const wstring &providerName) { reset(); state = AuthFlowState::EXCHANGING; - workerThread = std::thread(elybyFlowThread, narrowStr(username), narrowStr(password)); + 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) @@ -210,7 +235,6 @@ static void authFail(AuthResult &result, std::atomic &state, cons state = AuthFlowState::FAILED; } -// parse json response body, return discarded json on bad status static json parseResponse(const HttpResponse &resp, int expectedStatus) { if (resp.statusCode != expectedStatus) return json::value_t::discarded; @@ -273,19 +297,15 @@ static bool msRefreshOAuth(const string &refreshToken, string &newAccessToken, s newRefreshToken = j.value("refresh_token", ""); return !newAccessToken.empty(); } - -// validate ely.by token via yggdrasil /validate endpoint -static bool elybyValidate(const string &accessToken, const string &clientToken) +static bool yggdrasilValidate(const string &accessToken, const string &clientToken, const string &validateUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/validate", + auto resp = HttpClient::post(validateUrl, json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); return resp.statusCode == 200; } - -// refresh ely.by token via yggdrasil /refresh endpoint -static bool elybyRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken) +static bool yggdrasilRefresh(const string &accessToken, const string &clientToken, string &newAccessToken, string &newClientToken, const string &refreshUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/refresh", + auto resp = HttpClient::post(refreshUrl, json({{"accessToken", accessToken}, {"clientToken", clientToken}}).dump()); auto j = parseResponse(resp); @@ -403,9 +423,9 @@ void AuthFlow::microsoftFlowThread() state = AuthFlowState::COMPLETE; } -void AuthFlow::elybyFlowThread(const string &username, const string &password) +void AuthFlow::yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl) { - auto resp = HttpClient::post("https://authserver.ely.by/auth/authenticate", json({ + auto resp = HttpClient::post(authenticateUrl, json({ {"username", username}, {"password", password}, {"clientToken", "mcconsoles"}, @@ -416,7 +436,7 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) if (resp.statusCode != 200 || respJson.is_discarded()) { - string msg = "Ely.by auth failed"; + string msg = "Yggdrasil auth failed"; if (!respJson.is_discarded()) msg = respJson.value("errorMessage", msg); result = {false, {}, {}, {}, {}, convStringToWstring(msg)}; state = AuthFlowState::FAILED; @@ -424,16 +444,16 @@ void AuthFlow::elybyFlowThread(const string &username, const string &password) } string accessToken = respJson.value("accessToken", ""); - string elyClientToken = respJson.value("clientToken", ""); + 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"Ely.by response missing profile"); + authFail(result, state, L"Yggdrasil response missing profile"); return; } - result = {true, convStringToWstring(name), convStringToWstring(uuid), convStringToWstring(accessToken), convStringToWstring(elyClientToken), {}}; + 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 index c24afc26..38425f0c 100644 --- a/Minecraft.Client/AuthScreen.h +++ b/Minecraft.Client/AuthScreen.h @@ -5,13 +5,14 @@ using namespace std; struct AuthProfile { - enum Type : uint8_t { MICROSOFT, ELYBY, OFFLINE }; + enum Type : uint8_t { MICROSOFT, YGGDRASIL, OFFLINE }; Type type; wstring uid; wstring username; wstring token; wstring clientToken; + wstring variation; }; class AuthProfileManager @@ -23,7 +24,7 @@ private: 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""); + 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(); @@ -60,17 +61,17 @@ private: static AuthResult result; static wstring userCode; static wstring verificationUri; - + static wstring activeVariation; static void microsoftFlowThread(); - static void elybyFlowThread(const string &username, const string &password); + static void yggdrasilFlowThread(const string &username, const string &password, const string &authenticateUrl); public: static void startMicrosoft(); - static void startElyBy(const wstring &username, const wstring &password); - + 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 9f75efd2..9f52cd53 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 2765e921..23585e8e 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -56,7 +56,6 @@ #include "DLCTexturePack.h" #include "..\Minecraft.World\HandshakeManager.h" #include "..\Minecraft.World\SessionAuthModule.h" -#include "..\Minecraft.World\OfflineAuthModule.h" #include "AuthScreen.h" #ifdef _WINDOWS64 @@ -4141,21 +4140,33 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< void ClientConnection::beginAuth() { + app.DebugPrintf("AUTH: beginAuth() starting\n"); handshakeManager = new HandshakeManager(false); - handshakeManager->registerModule(std::make_unique()); - handshakeManager->registerModule(std::make_unique()); - handshakeManager->registerModule(std::make_unique()); 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::ELYBY) variation = L"elyby"; + 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); diff --git a/Minecraft.Client/Common/Network/GameNetworkManager.cpp b/Minecraft.Client/Common/Network/GameNetworkManager.cpp index 50aeae68..934a9105 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/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 3d1febc3..4070de8b 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -2,6 +2,7 @@ #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" @@ -459,7 +460,7 @@ void UIScene_MainMenu::RunAction(int iPad) static int s_authPad = 0; static void *s_authParam = nullptr; -static wstring s_elybyUsername; +static wstring s_yggdrasilUsername; static bool s_authFlowActive = false; static wstring ReadKeyboardText(int maxLen) @@ -509,11 +510,17 @@ static const wchar_t *BuildAuthProfileText() for (int i = 0; i < static_cast(profiles.size()); i++) { const auto &p = profiles[i]; - const wchar_t *type = (p.type == AuthProfile::MICROSOFT) ? L"[MS]" - : (p.type == AuthProfile::ELYBY) ? L"[Ely]" : L"[Off]"; + 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 += wstring(type) + L" " + p.username; + text += label + L" " + p.username; } } return text.c_str(); @@ -531,7 +538,9 @@ void UIScene_MainMenu::ShowAuthMenu(int iPad, void *pClass) void UIScene_MainMenu::ShowAuthAddMenu(int iPad, void *pClass) { - static const wchar_t *addOptions[] = { L"Microsoft Auth", L"Ely.by Auth", L"Add Offline User", L"Back" }; + 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); } @@ -602,15 +611,15 @@ int UIScene_MainMenu::AuthAddMenuReturned(LPVOID lpParam, int iPad, const C4JSto { #ifdef _WINDOWS64 UIKeyboardInitData kbData; - kbData.title = L"Ely.by Username"; + kbData.title = L"Username"; kbData.defaultText = L""; kbData.maxChars = 64; - kbData.callback = &UIScene_MainMenu::ElyByUsernameReturned; + kbData.callback = &UIScene_MainMenu::YggdrasilUsernameReturned; kbData.lpParam = lpParam; kbData.pcMode = g_KBMInput.IsKBMActive(); ui.NavigateToScene(iPad, eUIScene_Keyboard, &kbData); #else - InputManager.RequestKeyboard(L"Ely.by Username", L"", (DWORD)0, 64, &UIScene_MainMenu::ElyByUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); + InputManager.RequestKeyboard(L"Username", L"", (DWORD)0, 64, &UIScene_MainMenu::YggdrasilUsernameReturned, lpParam, C_4JInput::EKeyboardMode_Default); #endif break; } @@ -646,26 +655,26 @@ int UIScene_MainMenu::AuthMsFlowReturned(LPVOID lpParam, int iPad, const C4JStor return 0; } -int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes) +int UIScene_MainMenu::YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes) { if (bRes) { wstring name = ReadKeyboardText(128); if (!name.empty()) { - s_elybyUsername = std::move(name); + s_yggdrasilUsername = std::move(name); #ifdef _WINDOWS64 UIKeyboardInitData kbData; - kbData.title = L"Ely.by Password"; + kbData.title = L"Password"; kbData.defaultText = L""; kbData.maxChars = 128; - kbData.callback = &UIScene_MainMenu::ElyByPasswordReturned; + 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"Ely.by Password", L"", (DWORD)0, 128, &UIScene_MainMenu::ElyByPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); + InputManager.RequestKeyboard(L"Password", L"", (DWORD)0, 128, &UIScene_MainMenu::YggdrasilPasswordReturned, lpParam, C_4JInput::EKeyboardMode_Password); #endif return 0; } @@ -674,18 +683,20 @@ int UIScene_MainMenu::ElyByUsernameReturned(LPVOID lpParam, const bool bRes) return 0; } -int UIScene_MainMenu::ElyByPasswordReturned(LPVOID lpParam, const bool bRes) +int UIScene_MainMenu::YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes) { if (bRes) { wstring pass = ReadKeyboardText(256); if (!pass.empty()) { - AuthFlow::startElyBy(s_elybyUsername, pass); + 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, L"Ely.by Login", L"Authenticating...", + ShowAuthMessageBox(s_authPad, loginTitle.c_str(), L"Authenticating...", waitOptions, 1, &UIScene_MainMenu::AuthMsFlowReturned, lpParam, true); SecureZeroMemory(pass.data(), pass.size() * sizeof(wchar_t)); @@ -2144,8 +2155,9 @@ void UIScene_MainMenu::tick() { s_authFlowActive = false; const auto &r = AuthFlow::getResult(); - auto type = AuthFlow::getUserCode().empty() ? AuthProfile::ELYBY : AuthProfile::MICROSOFT; - AuthProfileManager::addProfile(type, r.username, r.uuid, r.accessToken, r.clientToken); + 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); diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 87de602c..07945668 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -151,8 +151,8 @@ private: 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 ElyByUsernameReturned(LPVOID lpParam, const bool bRes); - static int ElyByPasswordReturned(LPVOID lpParam, const bool bRes); + static int YggdrasilUsernameReturned(LPVOID lpParam, const bool bRes); + static int YggdrasilPasswordReturned(LPVOID lpParam, const bool bRes); static void LoadTrial(); diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index a85e3378..32d59bfa 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -16,7 +16,6 @@ #include "Settings.h" #include "..\Minecraft.World\HandshakeManager.h" #include "..\Minecraft.World\SessionAuthModule.h" -#include "..\Minecraft.World\OfflineAuthModule.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" @@ -105,8 +104,14 @@ void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason) void PendingConnection::handlePreLogin(shared_ptr packet) { - if (handshakeManager && !authComplete) + 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; @@ -413,10 +418,12 @@ bool PendingConnection::isDisconnected() void PendingConnection::initAuth() { handshakeManager = new HandshakeManager(true); - if (server->authMode == "session") - handshakeManager->registerModule(std::make_unique()); - else + 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) diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index fa5f4ccc..2abaf7c8 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.World/AuthModule.h b/Minecraft.World/AuthModule.h index ae5875b4..308ddcbe 100644 --- a/Minecraft.World/AuthModule.h +++ b/Minecraft.World/AuthModule.h @@ -25,3 +25,21 @@ public: 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/HandshakeManager.cpp b/Minecraft.World/HandshakeManager.cpp index 96d83b26..8ccc4b04 100644 --- a/Minecraft.World/HandshakeManager.cpp +++ b/Minecraft.World/HandshakeManager.cpp @@ -48,7 +48,16 @@ shared_ptr HandshakeManager::handlePacket(const shared_ptr HandshakeManager::createInitialPacket() { state = HandshakeState::VERSION_SENT; - return makePacket(AuthStage::ANNOUNCE_VERSION, {{L"version", PROTOCOL_VERSION}}); + 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) @@ -61,11 +70,32 @@ shared_ptr HandshakeManager::handleServer(const shared_ptrsecond.get(); + 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}, @@ -133,6 +163,8 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields, 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(); @@ -149,24 +181,39 @@ shared_ptr HandshakeManager::handleClient(const shared_ptrfields, L"serverId"); - wstring sessionEndpoint = getField(packet->fields, L"sessionEndpoint"); + wstring joinUrlW = getField(packet->fields, L"joinUrl"); wstring scheme(activeModule->schemeName()); - if (scheme == L"mcconsoles:session" && !accessToken.empty()) + + 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)} }; - auto resp = HttpClient::post(narrowStr(sessionEndpoint) + "/session/minecraft/join", body.dump()); - if (resp.statusCode != 204) + 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(); } diff --git a/Minecraft.World/MapItem.cpp b/Minecraft.World/MapItem.cpp index 61c203e3..3ff8206c 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/OfflineAuthModule.cpp b/Minecraft.World/OfflineAuthModule.cpp deleted file mode 100644 index 5369ceac..00000000 --- a/Minecraft.World/OfflineAuthModule.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "stdafx.h" -#include "OfflineAuthModule.h" - -const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; } - -vector KeypairOfflineAuthModule::supportedVariations() -{ - return {L"rsa2048", L"ed25519"}; -} - -vector> KeypairOfflineAuthModule::getSettings(const wstring &variation) -{ - return {{L"key_type", variation}}; -} - -bool KeypairOfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} - -const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; } - -vector OfflineAuthModule::supportedVariations() -{ - return {}; -} - -vector> OfflineAuthModule::getSettings(const wstring &variation) -{ - return {}; -} - -bool OfflineAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) -{ - return extractIdentity(fields, outUid, outUsername); -} diff --git a/Minecraft.World/OfflineAuthModule.h b/Minecraft.World/OfflineAuthModule.h deleted file mode 100644 index b2b4454d..00000000 --- a/Minecraft.World/OfflineAuthModule.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once -using namespace std; - -#include "AuthModule.h" - -class KeypairOfflineAuthModule : public AuthModule -{ -public: - const wchar_t *schemeName() override; - vector supportedVariations() override; - vector> getSettings(const wstring &variation) override; - bool onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) override; -}; - -class OfflineAuthModule : public AuthModule -{ -public: - 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/SessionAuthModule.cpp b/Minecraft.World/SessionAuthModule.cpp index 50844b39..5f100aba 100644 --- a/Minecraft.World/SessionAuthModule.cpp +++ b/Minecraft.World/SessionAuthModule.cpp @@ -4,6 +4,8 @@ #include "StringHelpers.h" #include "Common/vendor/nlohmann/json.hpp" #include +#include +#include static wstring generateServerId() { @@ -14,50 +16,96 @@ static wstring generateServerId() return id; } -SessionAuthModule::SessionAuthModule() +static vector s_registryProviders; +static bool s_registryLoaded = false; + +void YggdrasilRegistry::load() { - endpoints[L"mojang"] = {L"https://authserver.mojang.com", L"https://sessionserver.mojang.com"}; - endpoints[L"elyby"] = {L"https://authserver.ely.by", L"https://authserver.ely.by"}; + 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() { - return {L"mojang", L"elyby"}; + vector result; + for (const auto &p : YggdrasilRegistry::providers()) + result.push_back(p.name); + return result; } vector> SessionAuthModule::getSettings(const wstring &variation) { - auto it = endpoints.find(variation); - if (it == endpoints.end()) return {}; + auto *p = YggdrasilRegistry::find(variation); + if (!p) return {}; - activeSessionEndpoint = it->second.sessionEndpoint; + activeProvider = p; activeServerId = generateServerId(); - return { - {L"authEndpoint", it->second.authEndpoint}, - {L"sessionEndpoint", it->second.sessionEndpoint}, + {L"joinUrl", convStringToWstring(p->joinUrl())}, {L"serverId", activeServerId} }; } -bool SessionAuthModule::onAuthData(const vector> &fields, wstring &outUid, wstring &outUsername) +bool SessionAuthModule::serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername) { - wstring username; - for (const auto &[k, v] : fields) - { - if (k == L"username") username = v; - } - - if (username.empty() || activeServerId.empty() || activeSessionEndpoint.empty()) - return false; - - string url = narrowStr(activeSessionEndpoint) - + "/session/minecraft/hasJoined?username=" + narrowStr(username) - + "&serverId=" + narrowStr(activeServerId); + 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; @@ -67,12 +115,25 @@ bool SessionAuthModule::onAuthData(const vector> &fields, 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 index ecb60c06..2990edbd 100644 --- a/Minecraft.World/SessionAuthModule.h +++ b/Minecraft.World/SessionAuthModule.h @@ -1,24 +1,41 @@ #pragma once using namespace std; - #include "AuthModule.h" -#include +#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 { -public: - struct EndpointPair { - wstring authEndpoint; - wstring sessionEndpoint; - }; - private: - unordered_map endpoints; - wstring activeSessionEndpoint; + const YggdrasilProviderConfig *activeProvider = nullptr; wstring activeServerId; + bool serverVerify(const YggdrasilProviderConfig &provider, const wstring &username, const wstring &serverId, wstring &outUid, wstring &outUsername); + public: - SessionAuthModule(); + SessionAuthModule() = default; const wchar_t *schemeName() override; vector supportedVariations() override; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 8bf351e9..dc843f13 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -263,8 +263,6 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/SessionAuthModule.h" - "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/OfflineAuthModule.h" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AuthPackets.h" "${CMAKE_CURRENT_SOURCE_DIR}/UUID.cpp" diff --git a/yggdrasil.json b/yggdrasil.json new file mode 100644 index 00000000..4fbd9549 --- /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" + } +]