major refactor: auth v3

This commit is contained in:
Matthew Toro 2026-04-06 02:33:39 -04:00
parent 8a8b4b2573
commit 179d21d7b6
18 changed files with 376 additions and 166 deletions

View file

@ -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<AuthProfile> 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<char *>(&header), sizeof(header));
bool hasVariation = (header == PROFILE_MAGIC);
uint32_t count = 0;
file.read(reinterpret_cast<char *>(&count), sizeof(count));
if (hasVariation)
file.read(reinterpret_cast<char *>(&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<const char *>(&magic), sizeof(magic));
uint32_t count = static_cast<uint32_t>(profiles.size());
file.write(reinterpret_cast<const char *>(&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<int32_t>(selectedProfile);
file.write(reinterpret_cast<const char *>(&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<int>(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<AuthFlowState> AuthFlow::state{AuthFlowState::IDLE};
@ -177,6 +196,7 @@ std::atomic<bool> 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<AuthFlowState> &state, const wchar_t *msg)
@ -210,7 +235,6 @@ static void authFail(AuthResult &result, std::atomic<AuthFlowState> &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;
}

View file

@ -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();
};

View file

@ -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 "$<TARGET_FILE_DIR:Minecraft.Client>"

View file

@ -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<SessionAuthModule>());
handshakeManager->registerModule(std::make_unique<KeypairOfflineAuthModule>());
handshakeManager->registerModule(std::make_unique<OfflineAuthModule>());
const auto &profiles = AuthProfileManager::getProfiles();
int idx = AuthProfileManager::getSelectedIndex();
bool isOffline = true;
if (idx >= 0 && idx < static_cast<int>(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<SessionAuthModule>());
handshakeManager->registerModule(std::make_unique<KeypairOfflineAuthModule>());
handshakeManager->registerModule(std::make_unique<OfflineAuthModule>());
auto initial = handshakeManager->createInitialPacket();
if (initial) send(initial);

View file

@ -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<PreLoginPacket>(minecraft->user->name));
// Tick connection until we're ready to go. The stages involved in this are:

View file

@ -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<int>(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);

View file

@ -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();

View file

@ -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<PreLoginPacket> 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<SessionAuthModule>());
else
handshakeManager->registerModule(std::make_unique<SessionAuthModule>());
if (server->authMode != "session")
{
handshakeManager->registerModule(std::make_unique<KeypairOfflineAuthModule>());
handshakeManager->registerModule(std::make_unique<OfflineAuthModule>());
}
}
void PendingConnection::handleAuth(const shared_ptr<AuthPacket> &packet)

View file

@ -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] = {};

View file

@ -25,3 +25,21 @@ public:
protected:
bool extractIdentity(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername);
};
class KeypairOfflineAuthModule : public AuthModule
{
public:
const wchar_t *schemeName() override { return L"mcconsoles:keypair_offline"; }
vector<wstring> supportedVariations() override { return {L"rsa2048", L"ed25519"}; }
vector<pair<wstring, wstring>> getSettings(const wstring &variation) override { return {{L"key_type", variation}}; }
bool onAuthData(const vector<pair<wstring, wstring>> &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<wstring> supportedVariations() override { return {}; }
vector<pair<wstring, wstring>> getSettings(const wstring &) override { return {}; }
bool onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername) override { return extractIdentity(fields, outUid, outUsername); }
};

View file

@ -48,7 +48,16 @@ shared_ptr<AuthPacket> HandshakeManager::handlePacket(const shared_ptr<AuthPacke
shared_ptr<AuthPacket> 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<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacket> &packet)
@ -61,11 +70,32 @@ shared_ptr<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacke
if (protocolVersion != PROTOCOL_VERSION)
return fail();
// Pick first registered module as the scheme
if (modules.empty())
return fail();
activeModule = modules.begin()->second.get();
auto splitCsv = [](const wstring &s) {
vector<wstring> 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<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacke
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();
@ -149,24 +181,39 @@ shared_ptr<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacke
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 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();
}

View file

@ -21,6 +21,7 @@ MapItem::MapItem(int id) : ComplexItem(id)
shared_ptr<MapItemSavedData> MapItem::getSavedData(short idNum, Level *level)
{
if (!level) return nullptr;
std::wstring id = wstring( L"map_" ) + std::to_wstring(idNum);
shared_ptr<MapItemSavedData> mapItemSavedData = dynamic_pointer_cast<MapItemSavedData>(level->getSavedData(typeid(MapItemSavedData), id));
@ -42,6 +43,7 @@ shared_ptr<MapItemSavedData> MapItem::getSavedData(short idNum, Level *level)
shared_ptr<MapItemSavedData> MapItem::getSavedData(shared_ptr<ItemInstance> 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<Entity> player, shared_ptr<MapItem
void MapItem::inventoryTick(shared_ptr<ItemInstance> itemInstance, Level *level, shared_ptr<Entity> owner, int slot, bool selected)
{
if (!level) return;
if (level->isClientSide) return;
shared_ptr<MapItemSavedData> data = getSavedData(itemInstance, level);
@ -293,7 +296,10 @@ void MapItem::inventoryTick(shared_ptr<ItemInstance> itemInstance, Level *level,
shared_ptr<Packet> MapItem::getUpdatePacket(shared_ptr<ItemInstance> itemInstance, Level *level, shared_ptr<Player> 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<Packet> MapItem::getUpdatePacket(shared_ptr<ItemInstance> itemInstanc
void MapItem::onCraftedBy(shared_ptr<ItemInstance> itemInstance, Level *level, shared_ptr<Player> player)
{
if (!level) return;
wchar_t buf[64];
int mapScale = 3;

View file

@ -1,36 +0,0 @@
#include "stdafx.h"
#include "OfflineAuthModule.h"
const wchar_t *KeypairOfflineAuthModule::schemeName() { return L"mcconsoles:keypair_offline"; }
vector<wstring> KeypairOfflineAuthModule::supportedVariations()
{
return {L"rsa2048", L"ed25519"};
}
vector<pair<wstring, wstring>> KeypairOfflineAuthModule::getSettings(const wstring &variation)
{
return {{L"key_type", variation}};
}
bool KeypairOfflineAuthModule::onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername)
{
return extractIdentity(fields, outUid, outUsername);
}
const wchar_t *OfflineAuthModule::schemeName() { return L"mcconsoles:offline"; }
vector<wstring> OfflineAuthModule::supportedVariations()
{
return {};
}
vector<pair<wstring, wstring>> OfflineAuthModule::getSettings(const wstring &variation)
{
return {};
}
bool OfflineAuthModule::onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername)
{
return extractIdentity(fields, outUid, outUsername);
}

View file

@ -1,22 +0,0 @@
#pragma once
using namespace std;
#include "AuthModule.h"
class KeypairOfflineAuthModule : public AuthModule
{
public:
const wchar_t *schemeName() override;
vector<wstring> supportedVariations() override;
vector<pair<wstring, wstring>> getSettings(const wstring &variation) override;
bool onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername) override;
};
class OfflineAuthModule : public AuthModule
{
public:
const wchar_t *schemeName() override;
vector<wstring> supportedVariations() override;
vector<pair<wstring, wstring>> getSettings(const wstring &variation) override;
bool onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername) override;
};

View file

@ -4,6 +4,8 @@
#include "StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
#include <random>
#include <fstream>
#include <algorithm>
static wstring generateServerId()
{
@ -14,50 +16,96 @@ static wstring generateServerId()
return id;
}
SessionAuthModule::SessionAuthModule()
static vector<YggdrasilProviderConfig> 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<YggdrasilProviderConfig> &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<wstring> SessionAuthModule::supportedVariations()
{
return {L"mojang", L"elyby"};
vector<wstring> result;
for (const auto &p : YggdrasilRegistry::providers())
result.push_back(p.name);
return result;
}
vector<pair<wstring, wstring>> 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<pair<wstring, wstring>> &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<pair<wstring, wstring>> &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<pair<wstring, wstring>> &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);
}

View file

@ -1,24 +1,41 @@
#pragma once
using namespace std;
#include "AuthModule.h"
#include <unordered_map>
#include <vector>
#include <memory>
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<YggdrasilProviderConfig> &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<wstring, EndpointPair> 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<wstring> supportedVariations() override;

View file

@ -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"

8
yggdrasil.json Normal file
View file

@ -0,0 +1,8 @@
[
{
"name": "elyby",
"displayName": "Ely.by",
"authUrl": "https://authserver.ely.by/auth",
"sessionUrl": "https://authserver.ely.by/session"
}
]