feat: Implement authenticated JSON request handling with session refresh

This commit is contained in:
veroxsity
2026-05-10 20:23:43 +01:00
parent 5378d722bb
commit bbbed8aca4

View File

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