This commit is contained in:
Matthew Toro 2026-04-07 20:07:17 +00:00 committed by GitHub
commit 49ff61e29a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2245 additions and 35 deletions

View file

@ -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$<$<CONFIG:Debug>: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$<$<CONFIG:Debug>:Debug>")
function(configure_compiler_target target)
# MSVC and compatible compilers (like Clang-cl)

View file

@ -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 <chrono>
#include <fstream>
#include <shellapi.h>
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<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;
if (hasVariation)
file.read(reinterpret_cast<char *>(&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<char *>(&type), sizeof(type));
p.type = static_cast<AuthProfile::Type>(type);
auto readWstr = [&file]() -> wstring {
uint16_t len = 0;
file.read(reinterpret_cast<char *>(&len), sizeof(len));
if (!file || len > 4096) return {};
wstring s(len, L'\0');
file.read(reinterpret_cast<char *>(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<char *>(&savedIdx), sizeof(savedIdx));
if (!profiles.empty())
selectedProfile = (savedIdx >= 0 && savedIdx < static_cast<int>(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<const char *>(&magic), sizeof(magic));
uint32_t count = static_cast<uint32_t>(profiles.size());
file.write(reinterpret_cast<const char *>(&count), sizeof(count));
auto writeWstr = [&file](const wstring &s) {
uint16_t len = static_cast<uint16_t>(s.length());
file.write(reinterpret_cast<const char *>(&len), sizeof(len));
file.write(reinterpret_cast<const char *>(s.data()), len * sizeof(wchar_t));
};
for (const auto &p : profiles)
{
uint8_t type = static_cast<uint8_t>(p.type);
file.write(reinterpret_cast<const char *>(&type), sizeof(type));
writeWstr(p.uid);
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, const wstring &variation)
{
wstring finalUid = uid.empty() ? L"offline_" + username : uid;
profiles.push_back({type, finalUid, username, token, clientToken, variation});
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
void AuthProfileManager::removeSelectedProfile()
{
if (selectedProfile < 0 || selectedProfile >= static_cast<int>(profiles.size()))
return;
profiles.erase(profiles.begin() + selectedProfile);
if (selectedProfile >= static_cast<int>(profiles.size()))
selectedProfile = static_cast<int>(profiles.size()) - 1;
save();
}
bool AuthProfileManager::applySelectedProfile()
{
if (selectedProfile < 0 || selectedProfile >= static_cast<int>(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<AuthFlowState> AuthFlow::state{AuthFlowState::IDLE};
std::atomic<bool> 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<AuthFlowState> &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;
}

View file

@ -0,0 +1,77 @@
#pragma once
using namespace std;
#include <atomic>
#include <thread>
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<AuthProfile> 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<AuthProfile> &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<AuthFlowState> state;
static std::atomic<bool> 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();
};

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

@ -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<DeferredEntityLinkPacket>();
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<AddPlayerPacket> 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<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::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);
}
void ClientConnection::handleAuth(const shared_ptr<AuthPacket> &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";
}
}

View file

@ -6,6 +6,7 @@ class MultiPlayerLevel;
class SavedDataStorage;
class Socket;
class MultiplayerLocalPlayer;
class HandshakeManager;
class ClientConnection : public PacketListener
{
@ -179,4 +180,10 @@ private:
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<AuthPacket> &packet);
};

View file

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

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

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

View file

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

View file

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

View file

@ -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<UIKeyboardInitData *>(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"<password>" : finalText);
// Store the typed text so callbacks can retrieve it via Win64_GetKeyboardText()
wcsncpy_s(g_Win64KeyboardResult, 256, finalText, _TRUNCATE);

View file

@ -13,6 +13,7 @@ private:
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

View file

@ -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<int>(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<int>(eControl_UnlockOrDLC)].init(IDS_DOWNLOADABLECONTENT,eControl_UnlockOrDLC);
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(L"Switch User",eControl_UnlockOrDLC);
}
else
{
m_bTrialVersion=true;
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC);
m_buttons[static_cast<int>(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<int>(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT);
// once again replacing the shop with auth. not a bad thing.
m_buttons[static_cast<int>(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<uint16_t> 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<wchar_t>(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, &param, eUILayer_Alert, eUIGroup_Fullscreen);
if (keepOpen)
if (auto *scene = ui.FindScene(eUIScene_MessageBox))
static_cast<UIScene_MessageBox *>(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<int>(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<int>(profiles.size());
AuthProfileManager::setSelectedIndex(next);
}
if (auto *scene = ui.FindScene(eUIScene_MessageBox))
static_cast<UIScene_MessageBox *>(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<UIScene_MessageBox *>(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<UIScene_MessageBox *>(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

View file

@ -144,7 +144,16 @@ private:
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

View file

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

View file

@ -18,6 +18,7 @@ private:
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 @@ public:
protected:
void handlePress(F64 controlId, F64 childId);
public:
void updateContent(const wchar_t *text);
void setKeepOpen(bool keep) { m_keepOpen = keep; }
};

View file

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

View file

@ -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<PreLoginPacket>(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<PreLoginPacket>(minecraft->user->name));
}
connection->tick();
}
}

View file

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

View file

@ -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('<27>', '$');
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<int>(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);

View file

@ -113,6 +113,7 @@ private:
CRITICAL_SECTION m_consoleInputCS;
public:
bool onlineMode;
std::string authMode;
bool animals;
bool npcs;
bool pvp;

View file

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

View file

@ -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 @@ public:
wstring getName();
virtual bool isServerPacketListener();
virtual bool isDisconnected();
HandshakeManager *handshakeManager;
bool authComplete;
void initAuth();
virtual void handleAuth(const shared_ptr<AuthPacket> &packet);
private:
void sendPreLoginResponse();

View file

@ -1298,6 +1298,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,
@ -1306,6 +1334,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

@ -5867,6 +5867,10 @@ Press{*CONTROLLER_VK_B*} if you already know about Fireworks.</value>
<value>You cannot join this game as the player you are trying to join is running a newer version of the game.</value>
</data>
<data name="IDS_DISCONNECTED_AUTH_FAILED">
<value>Authentication required. This server requires you to be signed in.</value>
</data>
<data name="IDS_DEFAULT_SAVENAME">
<value>New World</value>
</data>

View file

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

View file

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

View file

@ -371,7 +371,10 @@ namespace ServerRuntime
}
auto whitelistManager = std::make_shared<WhitelistManager>(*current);
const WhitelistedPlayerEntry entry = { formatted, name, metadata };
WhitelistedPlayerEntry entry;
entry.xuid = formatted;
entry.name = name;
entry.metadata = metadata;
if (!whitelistManager->AddPlayer(entry))
{
return false;

View file

@ -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 <algorithm>
#include <stdio.h>
@ -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;

View file

@ -22,6 +22,7 @@ namespace ServerRuntime
struct BannedPlayerEntry
{
std::string xuid;
std::string uuid;
std::string name;
BanMetadata metadata;
};

View file

@ -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 <algorithm>
@ -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;

View file

@ -16,6 +16,7 @@ namespace ServerRuntime
struct WhitelistedPlayerEntry
{
std::string xuid;
std::string uuid;
std::string name;
WhitelistMetadata metadata;
};

View file

@ -3,7 +3,7 @@
#include "FileUtils.h"
#include "StringUtils.h"
#include "..\vendor\nlohmann\json.hpp"
#include "Common/vendor/nlohmann/json.hpp"
#include <stdio.h>

View file

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

View file

@ -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)
{

View file

@ -73,7 +73,7 @@ namespace ServerRuntime
bool doTileDrops;
bool naturalRegeneration;
bool doDaylightCycle;
std::string authMode;
/** other MinecraftServer runtime settings */
int maxBuildHeight;
std::string levelType;

View file

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

View file

@ -55,6 +55,7 @@ AddPlayerPacket::AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, Play
this->xuid = xuid;
this->OnlineXuid = OnlineXuid;
this->gameUuid = player->getGameUUID();
m_playerIndex = static_cast<BYTE>(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 )
{

View file

@ -3,6 +3,7 @@ using namespace std;
#include "Packet.h"
#include "SynchedEntityData.h"
#include "UUID.h"
class Player;
@ -21,6 +22,7 @@ public:
int carriedItem;
PlayerUID xuid; // 4J Added
PlayerUID OnlineXuid; // 4J Added
GameUUID gameUuid;
BYTE m_playerIndex; // 4J Added
DWORD m_skinId; // 4J Added
DWORD m_capeId; // 4J Added

View file

@ -0,0 +1,17 @@
#include "stdafx.h"
#include "AuthModule.h"
bool AuthModule::validate(const wstring &uid, const wstring &username)
{
return !uid.empty() && !username.empty() && username.length() <= 16;
}
bool AuthModule::extractIdentity(const vector<pair<wstring, wstring>> &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);
}

View file

@ -0,0 +1,45 @@
#pragma once
using namespace std;
#include <string>
#include <vector>
#include <utility>
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<wstring> supportedVariations() = 0;
virtual vector<pair<wstring, wstring>> getSettings(const wstring &variation) = 0;
virtual bool onAuthData(const vector<pair<wstring, wstring>> &fields, wstring &outUid, wstring &outUsername) = 0;
bool validate(const wstring &uid, const wstring &username);
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

@ -0,0 +1,47 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthPackets.h"
AuthPacket::AuthPacket(AuthStage stage, vector<pair<wstring, wstring>> fields)
: stage(stage), fields(std::move(fields))
{
}
void AuthPacket::read(DataInputStream *dis)
{
stage = static_cast<AuthStage>(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<byte>(stage));
dos->writeShort(static_cast<short>(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<int>((key.length() + value.length()) * sizeof(wchar_t));
return size;
}

View file

@ -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<AuthPacket>
{
public:
AuthStage stage;
vector<pair<wstring, wstring>> fields;
AuthPacket(AuthStage stage = AuthStage::ANNOUNCE_VERSION, vector<pair<wstring, wstring>> fields = {});
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return std::make_shared<AuthPacket>(); }
virtual int getId() { return 72; }
};

View file

@ -27,4 +27,6 @@ target_compile_definitions(Minecraft.World PRIVATE
)
target_precompile_headers(Minecraft.World PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:stdafx.h>")
target_link_libraries(Minecraft.World PUBLIC CURL::libcurl)
configure_compiler_target(Minecraft.World)

View file

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

View file

@ -139,6 +139,7 @@ public:
static wstring getPlayerDir() { return sc_szPlayerDir; }
private:
void migratePlayerXuidsToUuids();
void dontSaveMapMappingForPlayer(PlayerUID xuid);
void deleteMapFilesForPlayer(PlayerUID xuid);
};

View file

@ -44,6 +44,7 @@ public:
eDisconnect_Banned,
eDisconnect_NotFriendsWithHost,
eDisconnect_NATMismatch,
eDisconnect_AuthFailed,
#ifdef __ORBIS__
eDisconnect_NetworkError,
#endif

View file

@ -0,0 +1,271 @@
#include "stdafx.h"
#include "HandshakeManager.h"
#include "AuthModule.h"
#include "HttpClient.h"
#include "StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
static constexpr auto PROTOCOL_VERSION = L"1.0";
static wstring getField(const vector<pair<wstring, wstring>> &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<AuthModule> 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<shared_ptr<AuthPacket>> HandshakeManager::drainPendingPackets()
{
auto out = std::move(pendingPackets);
pendingPackets.clear();
return out;
}
shared_ptr<AuthPacket> HandshakeManager::handlePacket(const shared_ptr<AuthPacket> &packet)
{
return isServer ? handleServer(packet) : handleClient(packet);
}
shared_ptr<AuthPacket> 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<AuthPacket> HandshakeManager::handleServer(const shared_ptr<AuthPacket> &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<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},
{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<AuthPacket> HandshakeManager::handleClient(const shared_ptr<AuthPacket> &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<AuthPacket> HandshakeManager::makePacket(AuthStage stage, vector<pair<wstring, wstring>> fields)
{
return std::make_shared<AuthPacket>(stage, std::move(fields));
}
shared_ptr<AuthPacket> HandshakeManager::fail()
{
state = HandshakeState::FAILED;
return makePacket(AuthStage::AUTH_FAILURE);
}

View file

@ -0,0 +1,66 @@
#pragma once
using namespace std;
#include <string>
#include <unordered_map>
#include <memory>
#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<wstring, unique_ptr<AuthModule>> modules;
AuthModule *activeModule;
wstring activeVariation;
wstring protocolVersion;
wstring accessToken;
wstring clientUid;
wstring clientUsername;
wstring preferredVariation;
vector<shared_ptr<AuthPacket>> pendingPackets;
public:
wstring finalUid;
wstring finalUsername;
HandshakeManager(bool isServer);
~HandshakeManager() = default;
void registerModule(unique_ptr<AuthModule> module);
void setCredentials(const wstring &token, const wstring &uid, const wstring &username, const wstring &variation = L"");
shared_ptr<AuthPacket> handlePacket(const shared_ptr<AuthPacket> &packet);
shared_ptr<AuthPacket> createInitialPacket();
vector<shared_ptr<AuthPacket>> drainPendingPackets();
bool isComplete() const { return state == HandshakeState::COMPLETE; }
bool isFailed() const { return state == HandshakeState::FAILED; }
HandshakeState getState() const { return state; }
private:
shared_ptr<AuthPacket> handleServer(const shared_ptr<AuthPacket> &packet);
shared_ptr<AuthPacket> handleClient(const shared_ptr<AuthPacket> &packet);
shared_ptr<AuthPacket> makePacket(AuthStage stage, vector<pair<wstring, wstring>> fields = {});
shared_ptr<AuthPacket> fail();
};

View file

@ -0,0 +1,66 @@
#include "stdafx.h"
#include "HttpClient.h"
#include <curl/curl.h>
static size_t writeCallback(char *data, size_t size, size_t nmemb, void *userdata)
{
auto *buf = static_cast<std::string *>(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<std::string> &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<std::string> &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<std::string> &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<long>(body.size()));
auto *headerList = buildHeaders(headers);
headerList = curl_slist_append(headerList, ("Content-Type: " + contentType).c_str());
return performRequest(curl, headerList);
}

View file

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <vector>
struct HttpResponse
{
int statusCode;
std::string body;
};
class HttpClient
{
public:
static HttpResponse get(const std::string &url, const std::vector<std::string> &headers = {});
static HttpResponse post(const std::string &url, const std::string &body, const std::string &contentType = "application/json", const std::vector<std::string> &headers = {});
};

View file

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

View file

@ -2,6 +2,7 @@
using namespace std;
#include "Packet.h"
#include "UUID.h"
class LevelType;
class LoginPacket : public Packet, public enable_shared_from_this<LoginPacket>
@ -12,6 +13,7 @@ public:
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

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

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

View file

@ -491,3 +491,8 @@ void PacketListener::handleGameCommand(shared_ptr<GameCommandPacket> packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}
void PacketListener::handleAuth(const shared_ptr<AuthPacket> &packet)
{
onUnhandledPacket( (shared_ptr<Packet> ) packet);
}

View file

@ -111,6 +111,7 @@ class KickPlayerPacket;
class AdditionalModelPartsPacket;
class XZPacket;
class GameCommandPacket;
class AuthPacket;
class PacketListener
{
@ -227,4 +228,6 @@ public:
virtual void handleKickPlayer(shared_ptr<KickPlayerPacket> packet);
virtual void handleXZ(shared_ptr<XZPacket> packet);
virtual void handleGameCommand(shared_ptr<GameCommandPacket> packet);
virtual void handleAuth(const shared_ptr<AuthPacket> &packet);
};

View file

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

View file

@ -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 @@ public:
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 @@ public:
private:
PlayerUID m_xuid;
PlayerUID m_OnlineXuid;
GameUUID m_gameUuid;
protected:
bool m_bShownOnMaps;

View file

@ -0,0 +1,139 @@
#include "stdafx.h"
#include "SessionAuthModule.h"
#include "HttpClient.h"
#include "StringHelpers.h"
#include "Common/vendor/nlohmann/json.hpp"
#include <random>
#include <fstream>
#include <algorithm>
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<YggdrasilProviderConfig> 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<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()
{
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 *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<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

@ -0,0 +1,44 @@
#pragma once
using namespace std;
#include "AuthModule.h"
#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
{
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<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;
};

144
Minecraft.World/UUID.cpp Normal file
View file

@ -0,0 +1,144 @@
#include "stdafx.h"
#include "UUID.h"
#include "Random.h"
#include <cstring>
#include <vector>
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<uint8_t> 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());
}

27
Minecraft.World/UUID.h Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <string>
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 };

View file

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

View file

@ -108,4 +108,5 @@
#include "KickPlayerPacket.h"
#include "XZPacket.h"
#include "GameCommandPacket.h"
#include "AuthPackets.h"

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"
}
]