diff --git a/Minecraft.Client/Windows64/Windows64_LceLive.cpp b/Minecraft.Client/Windows64/Windows64_LceLive.cpp index 35f0000f..5da320fd 100644 --- a/Minecraft.Client/Windows64/Windows64_LceLive.cpp +++ b/Minecraft.Client/Windows64/Windows64_LceLive.cpp @@ -692,6 +692,81 @@ namespace return true; } + // Sends an authenticated request, transparently refreshing the session + // on HTTP 401 and retrying once. Caller fills in `req.path`, `req.body`, + // and `req.method` (if non-default); the bearer header is applied here. + // + // On return: + // - true → request reached the server. Inspect *outStatus for the + // final HTTP code; treat any non-2xx as a logical failure. + // - false → transport failed, or refresh on 401 failed. *outError is + // populated with a human-readable reason. The session may + // have been cleared (cookie-equivalent expired). + bool PerformAuthenticatedJsonRequest( + RequestContext &req, + DWORD *outStatus, + std::string *outResponseBody, + std::string *outError) + { + if (outStatus == nullptr || outResponseBody == nullptr) + return false; + + std::string accessToken; + std::string refreshToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + { + accessToken = g_state.session.accessToken; + refreshToken = g_state.session.refreshToken; + } + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + { + if (outError != nullptr) + *outError = "Not signed in to LCELive."; + return false; + } + + req.authorization = "Bearer " + accessToken; + outResponseBody->clear(); + if (!PerformJsonRequest(req, outStatus, outResponseBody)) + { + if (outError != nullptr) + *outError = "Failed to contact LCELive."; + return false; + } + + if (*outStatus != 401 || refreshToken.empty()) + return true; + + // Access token rejected. Try to refresh and retry exactly once. + std::string refreshError; + if (!RefreshSessionSync(&refreshError)) + { + if (outError != nullptr) + *outError = refreshError.empty() + ? std::string("LCELive sign-in expired.") + : refreshError; + return false; + } + + EnterCriticalSection(&g_state.lock); + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + req.authorization = "Bearer " + accessToken; + outResponseBody->clear(); + if (!PerformJsonRequest(req, outStatus, outResponseBody)) + { + if (outError != nullptr) + *outError = "Failed to contact LCELive after refreshing sign-in."; + return false; + } + + return true; + } + DWORD WINAPI RequestThreadProc(LPVOID) { RequestContext request = {}; @@ -1130,24 +1205,18 @@ namespace Win64LceLive { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, std::string(), "Not signed in to LCELive." }; - RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/sessions/ticket"; req.body = "{}"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, std::string(), authError }; + + if (status < 200 || status >= 300) return { false, std::string(), ParseErrorMessage(responseBody, "Failed to obtain join ticket.") }; const Json responseJson = Json::parse(responseBody, nullptr, false); @@ -1196,23 +1265,18 @@ namespace Win64LceLive FriendsListResult GetFriendsSync() { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, {}, "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/social/friends"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, {}, authError }; + + if (status < 200 || status >= 300) return { false, {}, ParseErrorMessage(responseBody, "Failed to get friends list.") }; const Json responseJson = Json::parse(responseBody, nullptr, false); @@ -1240,23 +1304,18 @@ namespace Win64LceLive PendingRequestsResult GetPendingRequestsSync() { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, {}, {}, "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/social/requests"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, {}, {}, authError }; + + if (status < 200 || status >= 300) return { false, {}, {}, ParseErrorMessage(responseBody, "Failed to get pending requests.") }; const Json responseJson = Json::parse(responseBody, nullptr, false); @@ -1291,18 +1350,6 @@ namespace Win64LceLive SocialActionResult SendFriendRequestSync(const std::string &username) { EnsureInitialized(); - std::string accessToken; - std::string refreshToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - { - accessToken = g_state.session.accessToken; - refreshToken = g_state.session.refreshToken; - } - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; LCELOG("LCE", "sending friend request for username='%s'", username.c_str()); @@ -1310,41 +1357,17 @@ namespace Win64LceLive bodyJson["username"] = username; RequestContext req = {}; - req.type = ERequestType::None; - req.path = "/api/social/request"; - req.body = bodyJson.dump(); - req.authorization = "Bearer " + accessToken; + req.type = ERequestType::None; + req.path = "/api/social/request"; + req.body = bodyJson.dump(); DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody)) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) { - LCELOG("LCE", "friend request transport failure"); - return { false, "Failed to contact LCELive while sending friend request." }; - } - - if (status == 401 && !refreshToken.empty()) - { - std::string refreshError; - if (RefreshSessionSync(&refreshError)) - { - EnterCriticalSection(&g_state.lock); - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - req.authorization = "Bearer " + accessToken; - responseBody.clear(); - if (!PerformJsonRequest(req, &status, &responseBody)) - { - LCELOG("LCE", "friend request transport failure after refresh"); - return { false, "Failed to contact LCELive while sending friend request." }; - } - } - else - { - LCELOG("LCE", "friend request refresh failed: %s", refreshError.c_str()); - return { false, refreshError }; - } + LCELOG("LCE", "friend request transport/auth failure: %s", authError.c_str()); + return { false, authError }; } if (status < 200 || status >= 300) @@ -1366,24 +1389,19 @@ namespace Win64LceLive SocialActionResult AcceptFriendRequestSync(const std::string &fromAccountId) { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; RequestContext req = {}; - req.type = ERequestType::None; - req.path = "/api/social/requests/" + fromAccountId + "/accept"; - req.body = "{}"; - req.authorization = "Bearer " + accessToken; + req.type = ERequestType::None; + req.path = "/api/social/requests/" + fromAccountId + "/accept"; + req.body = "{}"; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; + + if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to accept friend request.") }; return { true, std::string() }; @@ -1392,24 +1410,19 @@ namespace Win64LceLive SocialActionResult DeclineFriendRequestSync(const std::string &fromAccountId) { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; RequestContext req = {}; - req.type = ERequestType::None; - req.path = "/api/social/requests/" + fromAccountId + "/decline"; - req.body = "{}"; - req.authorization = "Bearer " + accessToken; + req.type = ERequestType::None; + req.path = "/api/social/requests/" + fromAccountId + "/decline"; + req.body = "{}"; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; + + if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to decline friend request.") }; return { true, std::string() }; @@ -1418,24 +1431,19 @@ namespace Win64LceLive SocialActionResult RemoveFriendSync(const std::string &accountId) { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; RequestContext req = {}; - req.type = ERequestType::None; - req.path = "/api/social/friends/" + accountId; - req.method = "DELETE"; - req.authorization = "Bearer " + accessToken; + req.type = ERequestType::None; + req.path = "/api/social/friends/" + accountId; + req.method = "DELETE"; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; + + if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to remove friend.") }; return { true, std::string() }; @@ -1444,23 +1452,18 @@ namespace Win64LceLive GameInvitesResult GetGameInvitesSync() { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, {}, {}, "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/sessions/invites"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, {}, {}, authError }; + + if (status < 200 || status >= 300) return { false, {}, {}, ParseErrorMessage(responseBody, "Failed to get game invites.") }; const Json responseJson = Json::parse(responseBody, nullptr, false); @@ -1505,18 +1508,6 @@ namespace Win64LceLive SocialActionResult SendGameInviteSync(const std::string &recipientAccountId, const std::string &hostIp, int hostPort, const std::string &hostName, const std::string &signalingSessionId) { EnsureInitialized(); - std::string accessToken; - std::string refreshToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - { - accessToken = g_state.session.accessToken; - refreshToken = g_state.session.refreshToken; - } - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; Json bodyJson; bodyJson["recipientAccountId"] = recipientAccountId; @@ -1532,32 +1523,12 @@ namespace Win64LceLive req.type = ERequestType::None; req.path = "/api/sessions/invites"; req.body = bodyJson.dump(); - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody)) - return { false, "Failed to contact LCELive while sending game invite." }; - - if (status == 401 && !refreshToken.empty()) - { - std::string refreshError; - if (RefreshSessionSync(&refreshError)) - { - EnterCriticalSection(&g_state.lock); - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - req.authorization = "Bearer " + accessToken; - responseBody.clear(); - if (!PerformJsonRequest(req, &status, &responseBody)) - return { false, "Failed to contact LCELive while sending game invite." }; - } - else - { - return { false, refreshError }; - } - } + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to send game invite.") }; @@ -1568,24 +1539,19 @@ namespace Win64LceLive GameInviteActionResult AcceptGameInviteSync(const std::string &inviteId) { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, std::string(), std::string(), 0, std::string(), "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/sessions/invites/" + inviteId + "/accept"; req.body = "{}"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, std::string(), std::string(), 0, std::string(), authError }; + + if (status < 200 || status >= 300) return { false, std::string(), std::string(), 0, std::string(), ParseErrorMessage(responseBody, "Failed to accept game invite.") }; const Json responseJson = Json::parse(responseBody, nullptr, false); @@ -1605,24 +1571,19 @@ namespace Win64LceLive SocialActionResult DeclineGameInviteSync(const std::string &inviteId) { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/sessions/invites/" + inviteId + "/decline"; req.body = "{}"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; + + if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to decline game invite.") }; return { true, std::string() }; @@ -1631,24 +1592,19 @@ namespace Win64LceLive SocialActionResult DeactivateGameInvitesSync() { EnsureInitialized(); - std::string accessToken; - EnterCriticalSection(&g_state.lock); - if (g_state.session.valid) - accessToken = g_state.session.accessToken; - LeaveCriticalSection(&g_state.lock); - - if (accessToken.empty()) - return { false, "Not signed in to LCELive." }; RequestContext req = {}; req.type = ERequestType::None; req.path = "/api/sessions/invites/deactivate"; req.body = "{}"; - req.authorization = "Bearer " + accessToken; DWORD status = 0; std::string responseBody; - if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + std::string authError; + if (!PerformAuthenticatedJsonRequest(req, &status, &responseBody, &authError)) + return { false, authError }; + + if (status < 200 || status >= 300) return { false, ParseErrorMessage(responseBody, "Failed to deactivate game invites.") }; return { true, std::string() };