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

458 lines
11 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "OnlineAuthHandlerSteam.h"
#include "OnlineAuthInterfaceSteam.h"
#include "OnlineAuthInterfaceUtilsSteam.h"
#include <steam/isteamuser.h>
enum class ESteamAuthMsgType : uint8
{
None = 0,
Auth,
Result,
ResendKey,
ResendResult,
Max
};
struct FSteamAuthInfoData
{
FSteamAuthInfoData() : Type(ESteamAuthMsgType::None) {}
FSteamAuthInfoData(ESteamAuthMsgType InType) : Type(InType) {}
virtual ~FSteamAuthInfoData() {}
ESteamAuthMsgType Type;
virtual void SerializeData(FArchive& Ar)
{
Ar << Type;
}
friend FArchive& operator<<(FArchive& Ar, FSteamAuthInfoData& AuthData)
{
AuthData.SerializeData(Ar);
return Ar;
}
};
struct FSteamAuthResult : public FSteamAuthInfoData
{
FSteamAuthResult() : FSteamAuthInfoData(ESteamAuthMsgType::Result), bWasSuccess(false) {}
virtual ~FSteamAuthResult() {}
bool bWasSuccess;
virtual void SerializeData(FArchive& Ar) override
{
FSteamAuthInfoData::SerializeData(Ar);
Ar << bWasSuccess;
}
friend FArchive& operator<<(FArchive& Ar, FSteamAuthResult& AuthData)
{
AuthData.SerializeData(Ar);
return Ar;
}
};
struct FSteamAuthUserData : public FSteamAuthInfoData
{
FSteamAuthUserData()
: FSteamAuthInfoData(ESteamAuthMsgType::Auth)
, SteamId(FUniqueNetIdSteam::EmptyId())
{}
virtual ~FSteamAuthUserData() {}
FString AuthKey;
FUniqueNetIdSteamRef SteamId;
virtual void SerializeData(FArchive& Ar) override
{
FSteamAuthInfoData::SerializeData(Ar);
if (Ar.IsLoading())
{
// Create a new one to avoid changing the hash of a pre-existing NetId, which would break any FUniqueNetIdSet/TUniqueNetIdMap containing the pre-existing NetId.
SteamId = FUniqueNetIdSteam::Create();
}
Ar << AuthKey << ConstCastSharedRef<FUniqueNetIdSteam>(SteamId)->UniqueNetId;
}
friend FArchive& operator<<(FArchive& Ar, FSteamAuthUserData& AuthData)
{
AuthData.SerializeData(Ar);
return Ar;
}
};
// Easy resaving key for easy usage in testing
#if !UE_BUILD_SHIPPING
FString ReusableKey;
#endif
/* Steam Auth Packet Handler */
FSteamAuthHandlerComponent::FSteamAuthHandlerComponent() :
AuthInterface(nullptr),
SteamUserPtr(SteamUser()),
State(ESteamAuthHandlerState::Uninitialized),
bIsEnabled(true),
LastTimestamp(0.0f),
TicketHandle(k_HAuthTicketInvalid),
SteamId(SteamUserPtr ? FUniqueNetIdSteam::Create(SteamUserPtr->GetSteamID()) : FUniqueNetIdSteam::EmptyId())
{
SetActive(true);
bRequiresHandshake = true;
FOnlineSubsystemSteam* OSS = (FOnlineSubsystemSteam*)(IOnlineSubsystem::Get(STEAM_SUBSYSTEM));
if (OSS != nullptr)
{
AuthInterface = OSS->GetAuthInterface();
if (!AuthInterface.IsValid() || !AuthInterface->IsSessionAuthEnabled())
{
bIsEnabled = false;
}
}
else
{
bIsEnabled = false;
}
}
FSteamAuthHandlerComponent::~FSteamAuthHandlerComponent()
{
if (!bIsEnabled || !AuthInterface.IsValid())
{
return;
}
if (Handler->Mode == UE::Handler::Mode::Client)
{
AuthInterface->RevokeTicket(TicketHandle);
}
else
{
AuthInterface->RemoveUser(*SteamId);
}
}
void FSteamAuthHandlerComponent::CountBytes(FArchive& Ar) const
{
HandlerComponent::CountBytes(Ar);
const SIZE_T SizeOfThis = sizeof(*this) - sizeof(HandlerComponent);
Ar.CountBytes(SizeOfThis, SizeOfThis);
UserTicket.CountBytes(Ar);
}
void FSteamAuthHandlerComponent::Initialize()
{
if (!AuthInterface.IsValid() || !AuthInterface->IsSessionAuthEnabled())
{
UE_LOG_ONLINE(Warning, TEXT("AUTH HANDLER: Deactivating due to missing requirements"));
bIsEnabled = false;
if (Handler != nullptr)
{
SetComponentReady();
}
else
{
SetActive(false);
}
}
}
void FSteamAuthHandlerComponent::NotifyHandshakeBegin()
{
if (!bIsEnabled)
{
return;
}
if (Handler->Mode == UE::Handler::Mode::Client)
{
SendAuthKey(true);
}
else
{
SetState(ESteamAuthHandlerState::WaitingForKey);
LastTimestamp = FPlatformTime::Seconds();
}
}
void FSteamAuthHandlerComponent::SendAuthKey(bool bGenerateNewKey)
{
FBitWriter AuthDataPacket((sizeof(FSteamAuthUserData) + FOnlineAuthSteam::GetMaxTicketSizeInBytes()) * 8 + 1);
FSteamAuthUserData UserData;
UserData.SteamId = SteamId;
if (bGenerateNewKey || TicketHandle == k_HAuthTicketInvalid)
{
UserTicket = AuthInterface->GetAuthTicket(TicketHandle);
}
#if !UE_BUILD_SHIPPING
if (ReusableKey.IsEmpty())
{
ReusableKey = UserTicket;
}
else if (AuthInterface->bReuseKey)
{
UserTicket = ReusableKey;
}
if (AuthInterface->bNeverSendKey)
{
SetState(ESteamAuthHandlerState::SentAuthKey);
return;
}
if (AuthInterface->bBadKey)
{
UserTicket = TEXT("THIS IS A BAD STEAM KEY");
}
if (AuthInterface->bSendBadId)
{
UserData.SteamId = FUniqueNetIdSteam::EmptyId();
}
#endif
UserData.AuthKey = UserTicket;
AuthDataPacket.WriteBit(1);
AuthDataPacket << UserData;
SendPacket(AuthDataPacket);
SetState(ESteamAuthHandlerState::SentAuthKey);
UE_LOG_ONLINE(Log, TEXT("AUTH HANDLER: Sending auth key"));
}
bool FSteamAuthHandlerComponent::SendAuthResult()
{
// This function is safe to call multiple times. If we're already in progress, we let the user go through.
bool AuthStatusResult = AuthInterface->AuthenticateUser(*SteamId);
FSteamAuthResult AllowedPacket;
FBitWriter ResultPacketWriter(sizeof(FSteamAuthResult) * 8 + 1, true);
ResultPacketWriter.WriteBit(1);
AllowedPacket.bWasSuccess = AuthStatusResult;
ResultPacketWriter << AllowedPacket;
SendPacket(ResultPacketWriter);
UE_LOG_ONLINE(Log, TEXT("AUTH HANDLER: Sending auth result to user %s with flag success? %d"), *SteamId->ToString(), AuthStatusResult);
return AuthStatusResult;
}
void FSteamAuthHandlerComponent::SendPacket(FBitWriter& OutboundPacket)
{
#if !UE_BUILD_SHIPPING
if (AuthInterface->bBadWrite)
{
OutboundPacket.SetError();
}
if (AuthInterface->bDropAll)
{
return;
}
if (AuthInterface->bRandomDrop && FMath::RandBool() == false)
{
UE_LOG_ONLINE(Warning, TEXT("AUTH HANDLER: Random packet was dropped!"));
return;
}
#endif
FOutPacketTraits Traits;
Handler->SendHandlerPacket(this, OutboundPacket, Traits);
LastTimestamp = FPlatformTime::Seconds();
}
void FSteamAuthHandlerComponent::RequestResend()
{
FBitWriter ResendWriter(sizeof(FSteamAuthInfoData) * 8 + 1);
FSteamAuthInfoData ResendingPacket;
ResendWriter.WriteBit(1);
// Steam Auth is so simplistic that we really only have two messages we need to handle.
ResendingPacket.Type = (Handler->Mode == UE::Handler::Mode::Server) ?
ESteamAuthMsgType::ResendKey : ESteamAuthMsgType::ResendResult;
ResendWriter << ResendingPacket;
SendPacket(ResendWriter);
}
bool FSteamAuthHandlerComponent::IsValid() const
{
return bIsEnabled;
}
void FSteamAuthHandlerComponent::Incoming(FBitReader& Packet)
{
bool bForSteamAuth = !!Packet.ReadBit() && !Packet.IsError();
if (!bIsEnabled || !AuthInterface.IsValid() || !bForSteamAuth)
{
return;
}
#if !UE_BUILD_SHIPPING
if (AuthInterface->bDropAll)
{
Packet.SetError();
return;
}
#endif
// Save our position so we can parse the header.
FBitReaderMark PacketMarker(Packet);
FSteamAuthInfoData Header;
// Try to grab information from the packet.
Packet << Header;
if (Packet.IsError())
{
UE_LOG_ONLINE(Error, TEXT("AUTH HANDLER: Incoming steam auth packet could not be properly serialized."));
return;
}
// Reset to actually read the data.
PacketMarker.Pop(Packet);
if (State == ESteamAuthHandlerState::WaitingForKey && Header.Type == ESteamAuthMsgType::Auth)
{
FSteamAuthUserData AuthData;
Packet << AuthData;
if (Packet.IsError())
{
// Really this is if we somehow overflow and cannot fit the packet.
UE_LOG_ONLINE(Warning, TEXT("AUTH HANDLER: Packet was marked as error after serializing"));
return;
}
SteamId = AuthData.SteamId;
if (!SteamId->IsValid())
{
UE_LOG_ONLINE(Error, TEXT("AUTH HANDLER: Got an invalid steamid"));
AuthInterface->ExecuteResultDelegate(*SteamId, false, ESteamAuthResponseCode::NotConnectedToSteam);
Packet.SetError();
return;
}
FOnlineAuthSteam::SharedAuthUserSteamPtr TargetUser = AuthInterface->GetOrCreateUser(*SteamId);
if (!TargetUser.IsValid())
{
UE_LOG_ONLINE(Error, TEXT("AUTH HANDLER: Could not create user listing for %s"), *SteamId->ToString());
AuthInterface->ExecuteResultDelegate(*SteamId, false, ESteamAuthResponseCode::FailedToCreateUser);
Packet.SetError();
return;
}
TargetUser->SetKey(AuthData.AuthKey);
if (!SendAuthResult())
{
AuthInterface->MarkPlayerForKick(*SteamId);
}
SetComponentReady();
}
else if (State == ESteamAuthHandlerState::SentAuthKey)
{
if (Header.Type == ESteamAuthMsgType::Result)
{
FSteamAuthResult AuthResult;
Packet << AuthResult;
UE_LOG_ONLINE(Verbose, TEXT("AUTH HANDLER: Got result from server, was success? %d"), AuthResult.bWasSuccess);
// Regardless of success, we need to ready up, this allows kicks to work.
SetComponentReady();
}
else if (Header.Type == ESteamAuthMsgType::ResendKey)
{
UE_LOG_ONLINE(Log, TEXT("AUTH HANDLER: Server requested us to resend our key."));
SendAuthKey(false);
}
}
else if (Handler && Handler->Mode == UE::Handler::Mode::Server && Header.Type == ESteamAuthMsgType::ResendResult)
{
if (State == ESteamAuthHandlerState::Initialized)
{
UE_LOG_ONLINE(Log, TEXT("AUTH HANDLER: Got request from %s to resend result"), *SteamId->ToString());
SendAuthResult();
}
else
{
UE_LOG_ONLINE(Warning, TEXT("AUTH HANDLER: User has not sent ticket and requesting results."));
RequestResend();
}
}
}
void FSteamAuthHandlerComponent::Outgoing(FBitWriter& Packet, FOutPacketTraits& Traits)
{
#if !UE_BUILD_SHIPPING
if (AuthInterface.IsValid() && AuthInterface->bDropAll)
{
Packet.SetError();
return;
}
#endif
FBitWriter NewPacket(Packet.GetNumBits() + 1, true);
// We want to specify this is not a Steam auth packet.
NewPacket.WriteBit(0);
NewPacket.SerializeBits(Packet.GetData(), Packet.GetNumBits());
Packet = MoveTemp(NewPacket);
}
void FSteamAuthHandlerComponent::Tick(float DeltaTime)
{
// Don't do anything if we're not enabled or not ready.
// Alternatively, if we're already finished then just don't do anything here either
if (!bIsEnabled || State == ESteamAuthHandlerState::Initialized || !Handler)
{
return;
}
float CurTime = FPlatformTime::Seconds();
if (LastTimestamp != 0.0 && CurTime - LastTimestamp > 2.0f)
{
RequestResend();
}
}
int32 FSteamAuthHandlerComponent::GetReservedPacketBits() const
{
// Add a singular bit to figure out if the message is for Steam Auth
return 1;
}
void FSteamAuthHandlerComponent::SetComponentReady()
{
if (State != ESteamAuthHandlerState::Initialized)
{
SetState(ESteamAuthHandlerState::Initialized);
Initialized();
}
}
/* Module handler */
USteamAuthComponentModuleInterface::USteamAuthComponentModuleInterface(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
TSharedPtr<HandlerComponent> USteamAuthComponentModuleInterface::CreateComponentInstance(FString& Options)
{
return MakeShareable(new FSteamAuthHandlerComponent);
}