Files
UnrealEngine/Engine/Plugins/Online/OnlineSubsystemSteam/Source/Private/OnlineAuthInterfaceSteam.cpp
2025-05-18 13:04:45 +08:00

596 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "OnlineAuthInterfaceSteam.h"
#include "OnlineAuthInterfaceUtilsSteam.h"
#include "OnlineSubsystemSteamTypes.h"
// Determining if we need to be disabled or not
#include "Misc/ConfigCacheIni.h"
#include <steam/isteamgameserver.h>
#include <steam/isteamuser.h>
// Steam tells us this number in documentation, however there's no define within the SDK
#define STEAM_AUTH_MAX_TICKET_LENGTH_IN_BYTES 1024
#if WITH_ENGINE
#include "OnlineSubsystemUtils.h"
#include "PacketHandler.h"
// Headers needed to kick users.
#include "GameFramework/GameModeBase.h"
#include "GameFramework/GameSession.h"
#include "GameFramework/PlayerState.h"
#include "GameFramework/PlayerController.h"
#endif //WITH_ENGINE
FOnlineAuthSteam::FOnlineAuthSteam(FOnlineSubsystemSteam* InSubsystem, FOnlineAuthSteamUtilsPtr InAuthUtils) :
SteamUserPtr(SteamUser()),
SteamServerPtr(SteamGameServer()),
SteamSubsystem(InSubsystem),
AuthUtils(InAuthUtils),
bEnabled(false),
bBadKey(false),
bReuseKey(false),
bBadWrite(false),
bDropAll(false),
bRandomDrop(false),
bNeverSendKey(false),
bSendBadId(false)
{
#if WITH_ENGINE
const FString SteamModuleName(TEXT("SteamAuthComponentModuleInterface"));
if (!PacketHandler::DoesAnyProfileHaveComponent(SteamModuleName))
{
// Pull the components to see if there's anything we can use.
TArray<FString> ComponentList;
GConfig->GetArray(TEXT("PacketHandlerComponents"), TEXT("Components"), ComponentList, GEngineIni);
// Check if Steam Auth is enabled anywhere.
for (FString CompStr : ComponentList)
{
if (CompStr.Contains(SteamModuleName))
{
bEnabled = true;
break;
}
}
}
else
#endif //WITH_ENGINE
{
bEnabled = true;
}
if (bEnabled)
{
UE_LOG_ONLINE(Log, TEXT("AUTH: Steam Auth Enabled"));
}
}
FOnlineAuthSteam::FOnlineAuthSteam() :
SteamUserPtr(nullptr),
SteamServerPtr(nullptr),
SteamSubsystem(nullptr),
AuthUtils(nullptr),
bEnabled(false),
bBadKey(false),
bReuseKey(false),
bBadWrite(false),
bDropAll(false),
bRandomDrop(false),
bNeverSendKey(false),
bSendBadId(false)
{
}
FOnlineAuthSteam::~FOnlineAuthSteam()
{
RevokeAllTickets();
}
uint32 FOnlineAuthSteam::GetMaxTicketSizeInBytes()
{
return STEAM_AUTH_MAX_TICKET_LENGTH_IN_BYTES;
}
FString FOnlineAuthSteam::GetAuthTicket(uint32& AuthTokenHandle)
{
FString ResultToken;
AuthTokenHandle = k_HAuthTicketInvalid;
// Double check they are properly logged in
if (SteamUserPtr != nullptr && SteamUserPtr->BLoggedOn())
{
uint8 AuthToken[STEAM_AUTH_MAX_TICKET_LENGTH_IN_BYTES];
uint32 AuthTokenSize = 0;
AuthTokenHandle = SteamUserPtr->GetAuthSessionTicket(AuthToken, UE_ARRAY_COUNT(AuthToken), &AuthTokenSize, nullptr);
if (AuthTokenHandle != k_HAuthTicketInvalid && AuthTokenSize > 0)
{
ResultToken = BytesToHex(AuthToken, AuthTokenSize);
SteamTicketHandles.AddUnique(AuthTokenHandle);
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Generated steam authticket %s handle %d"), OSS_REDACT(*ResultToken), AuthTokenHandle);
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Failed to create Steam auth session ticket"));
}
}
return ResultToken;
}
void FOnlineAuthSteam::GetAuthTicketForWebApi(const FString& RemoteServiceIdentity, FOnGetAuthTicketForWebApiCompleteDelegate CompletionDelegate)
{
if (SteamUserPtr != NULL && SteamUserPtr->BLoggedOn())
{
HAuthTicket TicketHandle = SteamUserPtr->GetAuthTicketForWebApi((const char*)StringCast<UTF8CHAR>(*RemoteServiceIdentity).Get());
ActiveAuthTicketForWebApiRequests.Emplace(TicketHandle, CompletionDelegate);
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Failed to get Steam auth ticket for web api"));
CompletionDelegate.ExecuteIfBound(k_HAuthTicketInvalid, TEXT(""));
}
}
FOnlineAuthSteam::SharedAuthUserSteamPtr FOnlineAuthSteam::GetUser(const FUniqueNetId& InUserId)
{
if (SharedAuthUserSteamPtr* AuthUserPtr = AuthUsers.Find(InUserId.AsShared()))
{
return *AuthUserPtr;
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Trying to fetch user %s entry but the user does not exist"), *InUserId.ToString());
return nullptr;
}
}
FOnlineAuthSteam::SharedAuthUserSteamPtr FOnlineAuthSteam::GetOrCreateUser(const FUniqueNetId& InUserId)
{
if (SharedAuthUserSteamPtr* AuthUserPtr = AuthUsers.Find(InUserId.AsShared()))
{
return *AuthUserPtr;
}
SharedAuthUserSteamPtr AuthUserPtr = MakeShareable(new FSteamAuthUser);
AuthUsers.Add(InUserId.AsShared(), AuthUserPtr);
return AuthUserPtr;
}
bool FOnlineAuthSteam::AuthenticateUser(const FUniqueNetId& InUserId)
{
const FUniqueNetIdSteam& SteamUserId = FUniqueNetIdSteam::Cast(InUserId);
if (SteamUserId.IsValid() && bEnabled)
{
// Create the user in the list if we don't already have them.
SharedAuthUserSteamPtr TargetUser = GetOrCreateUser(SteamUserId);
// Do not attempt to reauth this user if we are currently doing this.
if (EnumHasAnyFlags(TargetUser->Status, ESteamAuthStatus::HasOrIsPendingAuth))
{
UE_LOG_ONLINE(Log, TEXT("AUTH: The user %s has authenticated or is currently authenticating. Skipping reauth"), *InUserId.ToString());
return true;
}
// If the user has already failed auth, do not attempt to re-auth them.
if (EnumHasAnyFlags(TargetUser->Status, ESteamAuthStatus::FailKick))
{
return false;
}
// Blank tickets are an immediate failure. A ticket should always have data.
if (TargetUser->RecvTicket.IsEmpty())
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Ticket from user %s is empty"), *InUserId.ToString());
TargetUser->Status |= ESteamAuthStatus::AuthFail;
return false;
}
// If the ticket is over the size we're expecting, mark them as failure
if (TargetUser->RecvTicket.Len() > STEAM_AUTH_MAX_TICKET_LENGTH_IN_BYTES)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Ticket from user is over max size of ticket length"));
TargetUser->Status |= ESteamAuthStatus::AuthFail;
return false;
}
// Check to see if the ticket is actually in hex
for (int32 i = 0; i < TargetUser->RecvTicket.Len(); ++i)
{
if (!CheckTCharIsHex(TargetUser->RecvTicket.GetCharArray()[i]))
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Ticket from user is not stored in hex!"));
TargetUser->Status |= ESteamAuthStatus::AuthFail;
return false;
}
}
uint8 AuthTokenRaw[STEAM_AUTH_MAX_TICKET_LENGTH_IN_BYTES];
int32 TicketSize = HexToBytes(TargetUser->RecvTicket, AuthTokenRaw);
CSteamID UserCSteamId = SteamUserId;
if (IsRunningDedicatedServer())
{
check(SteamServerPtr != nullptr);
// For a dedicated server, we need to check the ticket's validity and boot if that check doesn't start properly.
// Nothing else is needed on the ds.
EBeginAuthSessionResult Result = SteamServerPtr->BeginAuthSession(AuthTokenRaw, TicketSize, UserCSteamId);
if (Result == k_EBeginAuthSessionResultOK)
{
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Steam user authentication task started for %s successfully"), *InUserId.ToString());
TargetUser->Status |= ESteamAuthStatus::ValidationStarted;
return true;
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: User %s failed authentication %d"), *InUserId.ToString(), (int32)Result);
TargetUser->Status |= ESteamAuthStatus::AuthFail;
}
}
else
{
check(SteamUserPtr != nullptr);
EBeginAuthSessionResult Result = SteamUserPtr->BeginAuthSession(AuthTokenRaw, TicketSize, UserCSteamId);
if (Result == k_EBeginAuthSessionResultOK)
{
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Steam user authentication task started for %s successfully"), *InUserId.ToString());
TargetUser->Status |= ESteamAuthStatus::ValidationStarted;
return true;
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: User %s failed authentication %d"), *InUserId.ToString(), (int32)Result);
TargetUser->Status |= ESteamAuthStatus::AuthFail;
}
}
}
else if(bEnabled)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: UserId was invalid!"));
}
return false;
}
void FOnlineAuthSteam::EndAuthentication(const FUniqueNetId& InUserId)
{
const FUniqueNetIdSteam& SteamId = FUniqueNetIdSteam::Cast(InUserId);
if (SteamId.IsValid())
{
CSteamID UserCSteamId = CSteamID(*(uint64*)SteamId.GetBytes());
if (IsRunningDedicatedServer())
{
check(SteamServerPtr != nullptr);
SteamServerPtr->EndAuthSession(UserCSteamId);
}
else
{
check(SteamUserPtr != nullptr);
SteamUserPtr->EndAuthSession(UserCSteamId);
}
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Ended authentication with %s"), *SteamId.ToString());
}
}
void FOnlineAuthSteam::RevokeTicket(const uint32& Handle)
{
if (SteamUserPtr != nullptr)
{
if (SteamTicketHandles.Contains(Handle))
{
SteamUserPtr->CancelAuthTicket(Handle);
SteamTicketHandles.Remove(Handle);
UE_LOG_ONLINE(Log, TEXT("AUTH: Revoking auth ticket with handle %d"), (int32)Handle);
}
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Cannot revoke ticket with handle %d"), (int32)Handle);
}
}
void FOnlineAuthSteam::RevokeAllTickets()
{
UE_LOG_ONLINE(Log, TEXT("AUTH: Revoking all tickets."));
// Removes all prior authorizations. Happens on disconnection.
// Also cleans up any other previous auth data.
for (SteamAuthentications::TIterator Users(AuthUsers); Users; ++Users)
{
EndAuthentication(*Users->Key);
}
// Clean up all handles if they haven't been cleared already
if (SteamUserPtr != nullptr)
{
for (int HandleIdx = 0; HandleIdx < SteamTicketHandles.Num(); ++HandleIdx)
{
SteamUserPtr->CancelAuthTicket(SteamTicketHandles[HandleIdx]);
}
}
SteamTicketHandles.Empty();
AuthUsers.Empty();
}
void FOnlineAuthSteam::MarkPlayerForKick(const FUniqueNetId& InUserId)
{
const FUniqueNetIdSteam& SteamId = FUniqueNetIdSteam::Cast(InUserId);
SharedAuthUserSteamPtr TargetUser = GetUser(SteamId);
if (TargetUser.IsValid())
{
TargetUser->Status |= ESteamAuthStatus::AuthFail;
UE_LOG_ONLINE(Log, TEXT("AUTH: Marking %s for kick"), *InUserId.ToString());
}
}
bool FOnlineAuthSteam::KickPlayer(const FUniqueNetId& InUserId, bool bSuppressFailure)
{
#if WITH_ENGINE
bool bKickSuccess = false;
const FUniqueNetIdSteam& SteamId = FUniqueNetIdSteam::Cast(InUserId);
UWorld* World = (SteamSubsystem != nullptr) ? GetWorldForOnline(SteamSubsystem->GetInstanceName()) : nullptr;
if (SteamUserPtr != nullptr && SteamUserPtr->GetSteamID() == SteamId)
{
if (!bSuppressFailure)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Cannot kick ourselves!"));
}
return false;
}
// If we are overridden, respect that.
if (AuthUtils.IsValid() && AuthUtils->OverrideFailureDelegate.IsBound())
{
AuthUtils->OverrideFailureDelegate.Execute(InUserId);
RemoveUser(InUserId);
return true;
}
if (World)
{
AGameModeBase* GameMode = World->GetAuthGameMode();
if (GameMode == nullptr || GameMode->GameSession == nullptr)
{
if (!bSuppressFailure)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Cannot kick player %s as we do not have a gamemode or session"), *InUserId.ToString());
}
return false;
}
for (FConstPlayerControllerIterator Itr = World->GetPlayerControllerIterator(); Itr; ++Itr)
{
APlayerController* PC = Itr->Get();
if (PC && PC->PlayerState != nullptr && PC->PlayerState->GetUniqueId().IsValid() &&
*(PC->PlayerState->GetUniqueId().GetUniqueNetId()) == InUserId)
{
const FText AuthKickReason = NSLOCTEXT("NetworkErrors", "HostClosedConnection", "Host closed the connection.");
bKickSuccess = GameMode->GameSession->KickPlayer(PC, AuthKickReason);
break;
}
}
}
// If we were able to kick them properly, call to remove their data.
// Otherwise, they'll be attempted to be kicked again later.
if (bKickSuccess)
{
UE_LOG_ONLINE(Log, TEXT("AUTH: Successfully kicked player %s"), *InUserId.ToString());
RemoveUser(InUserId);
}
else if(!bSuppressFailure)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Was not able to kick player %s Valid world: %d."), *InUserId.ToString(), (World != nullptr));
}
return bKickSuccess;
#else
return false;
#endif //WITH_ENGINE
}
void FOnlineAuthSteam::RemoveUser(const FUniqueNetId& TargetUser)
{
if (!IsServer() || !bEnabled)
{
return;
}
if (AuthUsers.Contains(TargetUser.AsShared()))
{
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Removing user %s"), *TargetUser.ToString());
EndAuthentication(TargetUser);
AuthUsers.Remove(TargetUser.AsShared());
}
}
bool FOnlineAuthSteam::Tick(float DeltaTime)
{
if (!bEnabled || !IsServer())
{
return true;
}
// Loop through all users to detect if we need to do anything regarding resends
for (SteamAuthentications::TIterator It(AuthUsers); It; ++It)
{
if (It->Value.IsValid())
{
SharedAuthUserSteamPtr CurUser = It->Value;
const FUniqueNetId& CurUserId = *It->Key;
// Kick any players that have failed authentication.
if (EnumHasAnyFlags(CurUser->Status, ESteamAuthStatus::FailKick))
{
if (KickPlayer(CurUserId, EnumHasAnyFlags(CurUser->Status, ESteamAuthStatus::KickUser)))
{
// If we've modified the list, we can just end this frame.
return true;
}
CurUser->Status |= ESteamAuthStatus::KickUser;
}
}
}
return true;
}
bool FOnlineAuthSteam::Exec(const TCHAR* Cmd)
{
#if UE_BUILD_SHIPPING
return false;
#else
bool bWasHandled = false;
if (FParse::Command(&Cmd, TEXT("BADKEY")))
{
bWasHandled = true;
bBadKey = !bBadKey;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set send only bad auth keys flag to %d"), bBadKey);
}
else if (FParse::Command(&Cmd, TEXT("BADWRITES")))
{
bWasHandled = true;
bBadWrite = !bBadWrite;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set bad writes flag to %d"), bBadWrite);
}
else if (FParse::Command(&Cmd, TEXT("SENDBADID")))
{
bWasHandled = true;
bSendBadId = !bSendBadId;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set send bad id flag to %d"), bSendBadId);
}
else if (FParse::Command(&Cmd, TEXT("NEVERSENDKEY")))
{
bWasHandled = true;
bNeverSendKey = !bNeverSendKey;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set block key send flag to %d"), bNeverSendKey);
}
else if (FParse::Command(&Cmd, TEXT("REUSEKEY")))
{
bWasHandled = true;
bReuseKey = !bReuseKey;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set reuse auth key flag to %d"), bReuseKey);
}
else if (FParse::Command(&Cmd, TEXT("DROPALL")))
{
bWasHandled = true;
bDropAll = !bDropAll;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set drop all packets flag to %d"), bDropAll);
}
else if (FParse::Command(&Cmd, TEXT("DROPRANDOM")))
{
bWasHandled = true;
bRandomDrop = !bRandomDrop;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Set drop random packets flag to %d"), bRandomDrop);
}
else if (FParse::Command(&Cmd, TEXT("ENABLE")))
{
bWasHandled = true;
bEnabled = true;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Enabling the Auth Interface"));
}
else if (FParse::Command(&Cmd, TEXT("DISABLE")))
{
bWasHandled = true;
bEnabled = false;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Disabling the Auth Interface"));
}
else if (FParse::Command(&Cmd, TEXT("FREEALLKEYS")))
{
bWasHandled = true;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Freeing all keys."));
RevokeAllTickets();
}
else if (FParse::Command(&Cmd, TEXT("RESET")))
{
bEnabled = bWasHandled = true;
bSendBadId = bNeverSendKey = bRandomDrop = bBadKey = bBadWrite = bDropAll = bReuseKey = false;
UE_LOG_ONLINE(Warning, TEXT("AUTH: Reset all cheats."));
}
return bWasHandled;
#endif
}
void FOnlineAuthSteam::OnGetTicketForWebResponse(uint32 AuthTicketHandle, const FString& ResultToken)
{
FOnGetAuthTicketForWebApiCompleteDelegate Delegate;
if (ActiveAuthTicketForWebApiRequests.RemoveAndCopyValue(AuthTicketHandle, Delegate))
{
Delegate.ExecuteIfBound(AuthTicketHandle, ResultToken);
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Unexpected TicketForWebResponse."));
}
}
void FOnlineAuthSteam::OnAuthResult(const FUniqueNetId& TargetId, int32 Response)
{
if (!bEnabled)
{
return;
}
const FUniqueNetIdSteam& SteamId = FUniqueNetIdSteam::Cast(TargetId);
if (!SteamId.IsValid())
{
UE_LOG_ONLINE(Warning, TEXT("AUTH: Auth Callback cannot process invalid users!"));
return;
}
bool bDidAuthSucceed = (Response == k_EAuthSessionResponseOK);
SharedAuthUserSteamPtr TargetUser = GetUser(TargetId);
if (!TargetUser.IsValid())
{
// If we are missing an user here, this means that they were recently deleted or we never knew about them.
UE_LOG_ONLINE(Warning, TEXT("AUTH: Could not find user data on result callback for %s, were they were recently deleted?"),
*SteamId.ToString());
return;
}
// Remove the validation start flag
TargetUser->Status &= ~ESteamAuthStatus::ValidationStarted;
TargetUser->RecvTicket.Empty(); // Remove their ticket, we no longer need to store it.
UE_LOG_ONLINE(Verbose, TEXT("AUTH: Finished auth with %s. Result ok? %d Response code %d"), *SteamId.ToString(), bDidAuthSucceed, Response);
if (bDidAuthSucceed)
{
TargetUser->Status |= ESteamAuthStatus::AuthSuccess;
}
else
{
TargetUser->Status |= ESteamAuthStatus::AuthFail;
}
ExecuteResultDelegate(SteamId, bDidAuthSucceed, (ESteamAuthResponseCode)Response);
}
void FOnlineAuthSteam::ExecuteResultDelegate(const FUniqueNetId& TargetId, bool bWasSuccessful, ESteamAuthResponseCode ResponseCode)
{
if (AuthUtils.IsValid())
{
// Fire both of these delegates.
AuthUtils->OnAuthenticationResultDelegate.ExecuteIfBound(TargetId, bWasSuccessful);
AuthUtils->OnAuthenticationResultWithCodeDelegate.ExecuteIfBound(TargetId, bWasSuccessful, ResponseCode);
}
}
void FOnlineAuthSteam::FSteamAuthUser::SetKey(const FString& NewKey)
{
if (!EnumHasAnyFlags(Status, ESteamAuthStatus::HasOrIsPendingAuth))
{
RecvTicket = NewKey;
}
}
// Implementation of the public helper to determine if the Auth interface is enabled.
bool FOnlineAuthUtilsSteam::IsSteamAuthEnabled() const
{
const FOnlineSubsystemSteam* SteamSub = static_cast<const FOnlineSubsystemSteam*>(IOnlineSubsystem::Get(STEAM_SUBSYSTEM));
return (SteamSub && SteamSub->GetAuthInterface().IsValid() && SteamSub->GetAuthInterface()->IsSessionAuthEnabled());
}