Files
2025-09-18 17:55:52 -04:00

666 lines
20 KiB
C++

/**
* AuthenticationHelper.cpp
* Copyright (c) 2013 ROBLOX Corp. All Rights Reserved.
*/
#include "stdafx.h"
#include "AuthenticationHelper.h"
// Qt Headers
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkInterface>
#include <QTimer>
#include <QCoreApplication>
#include <QTime>
#include <QtConcurrentRun>
// Roblox Headers
#include "util/standardout.h"
#include "util/Http.h"
#include "util/Statistics.h"
#include "RobloxServicesTools.h"
// Roblox Studio Headers
#include "RobloxNetworkAccessManager.h"
#include "RobloxSettings.h"
#include "QtUtilities.h"
#include "UpdateUIManager.h"
#include "RobloxNetworkReply.h"
#include "RobloxMainWindow.h"
#include "RobloxCookieJar.h"
#include "Util/RobloxGoogleAnalytics.h"
#include "Util/MachineIdUploader.h"
#include "V8DataModel/ContentProvider.h"
#include "RobloxWebDoc.h"
#include "RobloxDocManager.h"
#include "RobloxServicesTools.h"
FASTFLAG(GoogleAnalyticsTrackingEnabled)
FASTFLAGVARIABLE(StudioInSyncWebKitAuthentication, false)
FASTFLAGVARIABLE(UseAssetGameSubdomainForGetCurrentUser, true)
FASTFLAG(UseBuildGenericGameUrl)
//////////////////////////////////////////////////////////////////////////////////////////////////////
// AuthenticationHelper
QString AuthenticationHelper::userAgentStr = "";
bool AuthenticationHelper::httpAuthenticationPending = false;
bool AuthenticationHelper::verifyUserAuthenticationPending = false;
bool AuthenticationHelper::previouslyAuthenticated = false;
QString AuthenticationHelper::getLoggedInUserUrl()
{
QString result;
if (FFlag::UseBuildGenericGameUrl) {
result = QString::fromStdString(BuildGenericGameUrl(RobloxSettings::getBaseURL().toStdString(), "game/GetCurrentUser.ashx"));
}
else
{
result = RobloxSettings::getBaseURL() + "/game/GetCurrentUser.ashx";
}
if (FFlag::UseAssetGameSubdomainForGetCurrentUser)
{
return QString::fromStdString(
ReplaceTopSubdomain(result.toStdString(), "assetgame"));
}
return result;
}
bool AuthenticationHelper::verifyUserAndAuthenticate(int timeOutTime)
{
if (verifyUserAuthenticationPending || httpAuthenticationPending)
return false;
verifyUserAuthenticationPending = true;
bool result = true;
try
{
QString requestUserUrl = getLoggedInUserUrl();
QUrl url(requestUserUrl);
QNetworkRequest networkRequest(url);
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
RobloxNetworkReply* networkReply = RobloxNetworkAccessManager::Instance().get(networkRequest);
if (!networkReply->waitForFinished(timeOutTime, 100))
{
verifyUserAuthenticationPending = false;
networkReply->deleteLater();
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Unable to get current user. Request timed out.");
networkReply->deleteLater();
RobloxMainWindow::sendCounterEvent("StudioAuthenticationFailure", false);
if (FFlag::GoogleAnalyticsTrackingEnabled)
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "StudioAuthenticationFailure");
return false;
}
// Get the logged in user Webkit
QString userIdWebkit(networkReply->readAll());
int statusCode = networkReply->error();
networkReply->deleteLater();
switch (statusCode)
{
case QNetworkReply::NoError:
{
if(userIdWebkit.toInt() > 0)
{
// Get the logged in user HTTP
QString currentUser = getLoggedInUserUrl();
QString userIdHttp = doHttpGetRequest(currentUser);
// If different authenticate the HTTP layer with the webkit user
if(userIdHttp != userIdWebkit)
result = authenticateHttpLayer();
}
else
{
// Sync with HTTP layer: Curl will be using the saved cookie file if any, to login!
result = authenticateWebKitFromHttp();
if (!result)
{
// Error (probably user not authenticated) time to logout the user from Http
deAuthenticateHttpLayer();
result = false;
}
}
break;
}
default:
{
// Error (probably user not authenticated) time to logout the user from Http
deAuthenticateHttpLayer();
result = false;
break;
}
}
}
catch (std::exception const& e)
{
result = false;
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what());
RobloxMainWindow::sendCounterEvent("StudioAuthenticationFailure", false);
if (FFlag::GoogleAnalyticsTrackingEnabled)
{
RBX::RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_ERROR, "StudioAuthenticationFailure");
}
}
verifyUserAuthenticationPending = false;
return result;
}
int AuthenticationHelper::getHttpUserId()
{
QString currentUser = getLoggedInUserUrl();
QString userIdHttp = doHttpGetRequest(currentUser);
bool ok;
int result = userIdHttp.toInt(&ok, 10);
if (!ok)
{
return 0;
}
return result;
}
/*
* Gets user-id of the user logged into webkit layer
* @Returns: user-id, 0 if no user is logged in
*/
int AuthenticationHelper::getWebKitUserId()
{
QString requestUserUrl = getLoggedInUserUrl();
QUrl url(requestUserUrl);
int userId = 0;
QNetworkRequest networkRequest(url);
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
RobloxNetworkReply* networkReply = RobloxNetworkAccessManager::Instance().get(networkRequest);
if (networkReply && networkReply->waitForFinished(5000, 100) && (networkReply->error() == QNetworkReply::NoError))
{
QString userIdWebkit(networkReply->readAll());
userId = userIdWebkit.toInt();
networkReply->deleteLater();
}
return userId;
}
bool AuthenticationHelper::authenticateHttpLayer()
{
Q_EMIT authenticationChanged(true);
Q_EMIT authenticationDone(true);
return true;
}
void AuthenticationHelper::deAuthenticateHttpLayer()
{
QString logoutUrl;
if (FFlag::UseBuildGenericGameUrl)
{
logoutUrl = QString::fromStdString(BuildGenericGameUrl(RobloxSettings::getBaseURL().toStdString(),"game/logout.aspx"));
}
else
{
logoutUrl = RobloxSettings::getBaseURL() + "/game/logout.aspx";
}
//
doHttpGetRequest(logoutUrl);
Q_EMIT authenticationChanged(false);
}
QString AuthenticationHelper::generateAuthenticationUrl(const QString& authUrl, const QString& ticket)
{
QString compoundStr(authUrl);
if (!ticket.isEmpty())
{
compoundStr.append("?suggest=");
compoundStr.append(ticket);
}
return compoundStr;
}
int AuthenticationHelper::authenticateWebKitFromUrlWithTicket(const QString& url, const QString& ticket)
{
if(!url.isEmpty() && !ticket.isEmpty())
{
return doWebKitAuthentication(generateAuthenticationUrl(url, ticket));
}
return 0;
}
/*
* Authenticate the webkit layer by making a request to this url. Follows redirects recursively.
* @Returns: status code of response
*/
int AuthenticationHelper::doWebKitAuthentication(const QString& url)
{
QNetworkRequest networkRequest(url);
networkRequest.setRawHeader("RBXAuthenticationNegotiation:", QByteArray(GetBaseURL().c_str(), GetBaseURL().length()));
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
RobloxNetworkReply *networkReply = RobloxNetworkAccessManager::Instance().get(networkRequest);
if (!networkReply->waitForFinished(5000, 100))
return 0; // timed out
networkReply->deleteLater();
return networkReply->statusCode();
}
/*
* De-authenticate the webkit layer.
*/
void AuthenticationHelper::deAuthenticateWebKitLayer()
{
QString logoutUrl;
if (FFlag::UseBuildGenericGameUrl)
{
logoutUrl = QString::fromStdString(BuildGenericGameUrl(RobloxSettings::getBaseURL().toStdString(), "game/logout.aspx"));
}
else
{
logoutUrl = RobloxSettings::getBaseURL() + "/game/logout.aspx";
}
QNetworkRequest networkRequest(logoutUrl);
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
RobloxNetworkReply* networkReply = RobloxNetworkAccessManager::Instance().get(networkRequest);
if (networkReply)
{
networkReply->waitForFinished(5000, 100);
networkReply->deleteLater();
}
}
/*
* Authenticate the WinINet layer by making a request to this url.
* @Returns: userId of authenticated user, empty if failed
*/
QString AuthenticationHelper::authenticateWinINetFromUrlWithTicket(const QString& url, const QString& ticket)
{
if(!url.isEmpty() && !ticket.isEmpty())
{
QString compoundStr = generateAuthenticationUrl(url, ticket);
return doHttpGetRequest(compoundStr);
}
return "";
}
QString AuthenticationHelper::doHttpGetRequest(const QString& str, const RBX::HttpAux::AdditionalHeaders& externalHeaders)
{
std::string result = "";
try
{
QByteArray ba = str.toAscii();
const char *c_str = ba.data();
RBX::Http http(c_str);
#ifdef Q_WS_MAC
http.setAuthDomain(::GetBaseURL());
#else
http.additionalHeaders["RBXAuthenticationNegotiation:"] = ::GetBaseURL();
#endif
http.additionalHeaders.insert(externalHeaders.begin(), externalHeaders.end());
http.get(result);
}
catch (std::exception& e)
{
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, e.what());
return "";
}
return QString::fromStdString(result);
}
AuthenticationHelper::AuthenticationHelper()
: m_CurrentHttpRequestState(-1)
, m_bHttpSessionAuthenticationState(false)
, m_bIgnoreFinishedSignal(false)
{
userAgentStr = RobloxNetworkAccessManager::Instance().getUserAgent();
if (FFlag::StudioInSyncWebKitAuthentication)
qApp->installEventFilter(this);
connect(&RobloxNetworkAccessManager::Instance(), SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> & )),
this, SLOT(onSSLErrors(QNetworkReply*, const QList<QSslError> & )));
connect(&m_HttpAuthenticationFutureWatcher, SIGNAL(finished()), this, SLOT(httpSessionAuthenticationDone()));
}
bool AuthenticationHelper::validateMachine()
{
return RBX::MachineIdUploader::uploadMachineId(qPrintable(RobloxSettings::getBaseURL()))
!= RBX::MachineIdUploader::RESULT_MachineBanned;
}
bool AuthenticationHelper::eventFilter(QObject * watched, QEvent * evt)
{
QEvent::Type eventType = evt->type();
if (eventType == QEvent::ApplicationDeactivate)
{
// save webkit cookies
savedCookieKey = RobloxNetworkAccessManager::Instance().cookieJar()->getCookieValue(RobloxSettings::getBaseURL(), ".ROBLOSECURITY");
}
else if (eventType == QEvent::ApplicationActivate)
{
QString currentCookieKey = RobloxNetworkAccessManager::Instance().cookieJar()->getCookieValue(RobloxSettings::getBaseURL(), ".ROBLOSECURITY");
if (QString::compare(savedCookieKey, currentCookieKey, Qt::CaseSensitive) != 0)
{
// if we've some cookies saved, then authenticate studio with the saved cookies
if (!currentCookieKey.isEmpty())
{
RobloxNetworkAccessManager::Instance().cookieJar()->reloadCookies(true);
}
// if no cookies, just deauthenticate http layer too
else
{
deAuthenticateWebKitLayer();
}
// to avoid multiple reload of start page, we must call refresh here
RobloxWebDoc* startPage = dynamic_cast<RobloxWebDoc*>(RobloxDocManager::Instance().getStartPageDoc());
if (startPage)
startPage->refreshPage();
// http user and webkit user must be in sync
RBXASSERT(getHttpUserId() == getWebKitUserId());
}
}
return QObject::eventFilter(watched, evt);
}
int AuthenticationHelper::authenticateWebKitFromHttp()
{
// first verify is we've an user id for http, if not then just return
if (getHttpUserId() <=0 )
return 0;
QString requestURL = RobloxSettings::getBaseURL() + "/login/RequestAuth.ashx";
requestURL.replace("http", "https");
// need to add user agent, to make sure we get authorization URL
RBX::HttpAux::AdditionalHeaders externalHeaders;
externalHeaders["User-Agent"] = userAgentStr.toStdString();
// got the URL, now we can authorize webkit
QString returnURL = doHttpGetRequest(requestURL, externalHeaders);
if (!returnURL.isEmpty())
return doWebKitAuthentication(returnURL);
return 0;
}
void AuthenticationHelper::onSSLErrors(QNetworkReply* networkReply, const QList<QSslError> & errlist)
{
QString urlString = networkReply->url().toString();
if (urlString.contains(".robloxlabs.com"))
networkReply->ignoreSslErrors();
}
void AuthenticationHelper::authenticateUserAsync(const QString& url, const QString& ticket)
{
if (!url.isEmpty() && !ticket.isEmpty())
{
// make sure network manager instance get's created in Main thread
RobloxNetworkAccessManager::Instance();
QString authenticationUrl = generateAuthenticationUrl(url, ticket);
m_CurrentHttpRequestState = true;
m_HttpAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::authenticateHttpSession, authenticationUrl);
m_HttpAuthenticationFutureWatcher.setFuture(m_HttpAuthenticationFuture);
m_QtWebkitAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::authenticateQtWebkitSession, authenticationUrl);
m_QtWebkitAuthenticationFutureWatcher.setFuture(m_QtWebkitAuthenticationFuture);
}
else
{
// if we do not have security cookie, then try to authentication using Http session
QString secCookie = RobloxNetworkAccessManager::Instance().cookieJar()->getCookieValue(RobloxSettings::getBaseURL(), ".ROBLOSECURITY");
if (secCookie.isEmpty())
{
m_QtWebkitAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::authenticateQtWebkitSession);
m_QtWebkitAuthenticationFutureWatcher.setFuture(m_QtWebkitAuthenticationFuture);
}
else
{
m_CurrentHttpRequestState = true;
m_HttpAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::authenticateHttpSession);
m_HttpAuthenticationFutureWatcher.setFuture(m_HttpAuthenticationFuture);
}
}
}
void AuthenticationHelper::authenticateHttpSessionAsync(bool state)
{
// if the authentication state is same as the requested then do nothing
if (state && m_bHttpSessionAuthenticationState)
return;
// if either of the concurrent jobs are running then we will have the authentication done
if (((int)state == m_CurrentHttpRequestState) && (m_HttpAuthenticationFutureWatcher.isRunning() || m_QtWebkitAuthenticationFuture.isRunning()))
return;
// make sure there's no other http authentication request is running
waitForHttpAuthentication();
m_CurrentHttpRequestState = state;
if (state)
{
m_HttpAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::authenticateHttpSession);
}
else
{
m_HttpAuthenticationFuture = QtConcurrent::run(this, &AuthenticationHelper::deAuthenticateHttpSession);
}
m_HttpAuthenticationFutureWatcher.setFuture(m_HttpAuthenticationFuture);
}
bool AuthenticationHelper::authenticateHttpSession(const QString& authenticationURL)
{
return getHttpRequest(authenticationURL).isEmpty();
}
bool AuthenticationHelper::authenticateHttpSession()
{
bool result = false;
QString baseURL = RobloxSettings::getBaseURL();
QString authRequest = baseURL + "/login/RequestAuth.ashx";
authRequest.replace("http", "https");
QNetworkRequest networkRequest(authRequest);
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
// we need to create new AccessManager as we are in a different thread, also set cookies loaded from disk to new cookiejar
QNetworkCookieJar* cookieJar = new QNetworkCookieJar;
cookieJar->setCookiesFromUrl(RobloxNetworkAccessManager::Instance().cookieJar()->cookiesForUrl(baseURL), baseURL);
QNetworkAccessManager* accessManager = new QNetworkAccessManager;
accessManager->setCookieJar(cookieJar);
QNetworkReply* networkReply = accessManager->get(networkRequest);
if (baseURL.contains(".robloxlabs.com"))
networkReply->ignoreSslErrors();
RobloxNetworkReply* rbxNetworkReply = new RobloxNetworkReply(networkReply, true);
if (rbxNetworkReply->waitForFinished(10000, 100))
{
if (rbxNetworkReply->error() == QNetworkReply::NoError)
{
result = getHttpRequest(rbxNetworkReply->readAll()).isEmpty();
}
else
{
m_CurrentHttpRequestState = false;
result = deAuthenticateHttpSession();
}
}
else
{
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Unable to authenticate user. Request timed out.");
}
rbxNetworkReply->deleteLater();
accessManager->deleteLater();
return result;
}
bool AuthenticationHelper::deAuthenticateHttpSession()
{
QString logoutUrl;
if (FFlag::UseBuildGenericGameUrl)
{
logoutUrl = QString::fromStdString(BuildGenericGameUrl(RobloxSettings::getBaseURL().toStdString(), "game/logout.aspx"));
}
else
{
logoutUrl = RobloxSettings::getBaseURL() + "/game/logout.aspx";
}
getHttpRequest(logoutUrl);
return true;
}
bool AuthenticationHelper::authenticateQtWebkitSession(const QString& authenticationUrl)
{
bool result = false;
QString baseURL = RobloxSettings::getBaseURL();
QNetworkRequest networkRequest(authenticationUrl);
networkRequest.setRawHeader("RBXAuthenticationNegotiation:", baseURL.toAscii());
networkRequest.setRawHeader("User-Agent", userAgentStr.toAscii());
// we need to create new AccessManager as we are in a different thread
QNetworkAccessManager* accessManager = new QNetworkAccessManager;
QNetworkReply* networkReply = accessManager->get(networkRequest);
if (baseURL.contains(".robloxlabs.com"))
networkReply->ignoreSslErrors();
RobloxNetworkReply* rbxNetworkReply = new RobloxNetworkReply(networkReply, true);
if (rbxNetworkReply->waitForFinished(10000, 100))
{
if (rbxNetworkReply->statusCode() == 200)
{
// pass on the cookies from authenticated session to the static network access manager's cookiejar
RobloxNetworkAccessManager::Instance().cookieJar()->setCookiesFromUrl(accessManager->cookieJar()->cookiesForUrl(baseURL), baseURL);
result = true;
}
}
else
{
RBX::StandardOut::singleton()->print(RBX::MESSAGE_ERROR, "Unable to authenticate user. Request timed out.");
}
rbxNetworkReply->deleteLater();
accessManager->deleteLater();
return result;
}
bool AuthenticationHelper::authenticateQtWebkitSession()
{
// check if already authenticated
QString result = getHttpRequest(getLoggedInUserUrl());
int httpUserId = result.toInt();
// if authenticated then authenticate webkit session too
if (httpUserId > 0)
{
QString requestURL = RobloxSettings::getBaseURL() + "/login/RequestAuth.ashx";
requestURL.replace("http", "https");
// need to add user agent, to make sure we get authorization URL
RBX::HttpAux::AdditionalHeaders externalHeaders;
externalHeaders["User-Agent"] = userAgentStr.toStdString();
// get negotiate URL and authenticate QtWebkit using this
QString negotiateURL = getHttpRequest(requestURL, externalHeaders);
if (!negotiateURL.isEmpty())
return authenticateQtWebkitSession(negotiateURL);
}
return false;
}
QString AuthenticationHelper::getHttpRequest(const QString& urlString, const RBX::HttpAux::AdditionalHeaders& externalHeaders)
{
static QMutex httpRequestMutex;
QMutexLocker lock(&httpRequestMutex);
RBX::HttpOptions options;
options.addHeader("RBXAuthenticationNegotiation:", ::GetBaseURL());
for (RBX::HttpAux::AdditionalHeaders::const_iterator iter = externalHeaders.begin(); iter != externalHeaders.end(); ++iter)
options.addHeader(iter->first, iter->second);
RBX::HttpFuture request = RBX::HttpAsync::get(urlString.toStdString(), options);
std::string result;
try
{
result = request.get();
}
catch (const RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Error while authenticating user: %s", e.what());
}
return QString::fromStdString(result);
}
void AuthenticationHelper::httpSessionAuthenticationDone()
{
if (m_bIgnoreFinishedSignal)
{
m_bIgnoreFinishedSignal = false;
return;
}
RBXASSERT(m_CurrentHttpRequestState != -1);
bool requestedState = (bool)m_CurrentHttpRequestState;
m_CurrentHttpRequestState = -1;
if (m_HttpAuthenticationFutureWatcher.result())
{
m_bHttpSessionAuthenticationState = requestedState;
Q_EMIT authenticationChanged(requestedState);
}
else
{
m_bHttpSessionAuthenticationState = false;
}
}
void AuthenticationHelper::waitForHttpAuthentication()
{
if (m_HttpAuthenticationFutureWatcher.isRunning())
{
m_HttpAuthenticationFutureWatcher.waitForFinished();
// if a function is waiting for authentication to complete then call 'httpSessionAuthenticationDone' here itself
httpSessionAuthenticationDone();
// make sure we do not calling 'httpSessionAuthenticationDone' again called on 'finished' signal
m_bIgnoreFinishedSignal = true;
}
}
void AuthenticationHelper::waitForQtWebkitAuthentication()
{
if (m_QtWebkitAuthenticationFutureWatcher.isRunning())
m_QtWebkitAuthenticationFutureWatcher.waitForFinished();
}