lyra_game_ue/Source/LyraGame/System/LyraGameInstance.cpp
Goran Lazarevski 3bcab085f8 Initial commit
2025-03-20 11:06:26 +01:00

338 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LyraGameInstance.h"
#include "CommonSessionSubsystem.h"
#include "CommonUserSubsystem.h"
#include "Components/GameFrameworkComponentManager.h"
#include "HAL/IConsoleManager.h"
#include "LyraGameplayTags.h"
#include "Player/LyraPlayerController.h"
#include "Player/LyraLocalPlayer.h"
#include "GameFramework/PlayerState.h"
#if UE_WITH_DTLS
#include "DTLSCertStore.h"
#include "DTLSHandlerComponent.h"
#include "Misc/FileHelper.h"
#endif // UE_WITH_DTLS
#include UE_INLINE_GENERATED_CPP_BY_NAME(LyraGameInstance)
namespace Lyra
{
static bool bTestEncryption = false;
static FAutoConsoleVariableRef CVarLyraTestEncryption(
TEXT("Lyra.TestEncryption"),
bTestEncryption,
TEXT("If true, clients will send an encryption token with their request to join the server and attempt to encrypt the connection using a debug key. This is NOT SECURE and for demonstration purposes only."),
ECVF_Default);
#if UE_WITH_DTLS
static bool bUseDTLSEncryption = false;
static FAutoConsoleVariableRef CVarLyraUseDTLSEncryption(
TEXT("Lyra.UseDTLSEncryption"),
bUseDTLSEncryption,
TEXT("Set to true if using Lyra.TestEncryption and the DTLS packet handler."),
ECVF_Default);
/* Intended for testing with multiple game instances on the same device (desktop builds) */
static bool bTestDTLSFingerprint = false;
static FAutoConsoleVariableRef CVarLyraTestDTLSFingerprint(
TEXT("Lyra.TestDTLSFingerprint"),
bTestDTLSFingerprint,
TEXT("If true and using DTLS encryption, generate unique cert per connection and fingerprint will be written to file to simulate passing through an online service."),
ECVF_Default);
#if !UE_BUILD_SHIPPING
static FAutoConsoleCommandWithWorldAndArgs CmdGenerateDTLSCertificate(
TEXT("GenerateDTLSCertificate"),
TEXT("Generate a DTLS self-signed certificate for testing and export to PEM."),
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& InArgs, UWorld* InWorld)
{
if (InArgs.Num() == 1)
{
const FString& CertName = InArgs[0];
FTimespan CertExpire = FTimespan::FromDays(365);
TSharedPtr<FDTLSCertificate> Cert = FDTLSCertStore::Get().CreateCert(CertExpire, CertName);
if (Cert.IsValid())
{
const FString CertPath = FPaths::ProjectContentDir() / TEXT("DTLS") / FPaths::MakeValidFileName(FString::Printf(TEXT("%s.pem"), *CertName));
if (!Cert->ExportCertificate(CertPath))
{
UE_LOG(LogTemp, Error, TEXT("GenerateDTLSCertificate: Failed to export certificate."));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("GenerateDTLSCertificate: Failed to generate certificate."));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("GenerateDTLSCertificate: Invalid argument(s)."));
}
}));
#endif // UE_BUILD_SHIPPING
#endif // UE_WITH_DTLS
};
ULyraGameInstance::ULyraGameInstance(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
void ULyraGameInstance::Init()
{
Super::Init();
// Register our custom init states
UGameFrameworkComponentManager* ComponentManager = GetSubsystem<UGameFrameworkComponentManager>(this);
if (ensure(ComponentManager))
{
ComponentManager->RegisterInitState(LyraGameplayTags::InitState_Spawned, false, FGameplayTag());
ComponentManager->RegisterInitState(LyraGameplayTags::InitState_DataAvailable, false, LyraGameplayTags::InitState_Spawned);
ComponentManager->RegisterInitState(LyraGameplayTags::InitState_DataInitialized, false, LyraGameplayTags::InitState_DataAvailable);
ComponentManager->RegisterInitState(LyraGameplayTags::InitState_GameplayReady, false, LyraGameplayTags::InitState_DataInitialized);
}
// Initialize the debug key with a set value for AES256. This is not secure and for example purposes only.
DebugTestEncryptionKey.SetNum(32);
for (int32 i = 0; i < DebugTestEncryptionKey.Num(); ++i)
{
DebugTestEncryptionKey[i] = uint8(i);
}
if (UCommonSessionSubsystem* SessionSubsystem = GetSubsystem<UCommonSessionSubsystem>())
{
SessionSubsystem->OnPreClientTravelEvent.AddUObject(this, &ULyraGameInstance::OnPreClientTravelToSession);
}
}
void ULyraGameInstance::Shutdown()
{
if (UCommonSessionSubsystem* SessionSubsystem = GetSubsystem<UCommonSessionSubsystem>())
{
SessionSubsystem->OnPreClientTravelEvent.RemoveAll(this);
}
Super::Shutdown();
}
ALyraPlayerController* ULyraGameInstance::GetPrimaryPlayerController() const
{
return Cast<ALyraPlayerController>(Super::GetPrimaryPlayerController(false));
}
bool ULyraGameInstance::CanJoinRequestedSession() const
{
// Temporary first pass: Always return true
// This will be fleshed out to check the player's state
if (!Super::CanJoinRequestedSession())
{
return false;
}
return true;
}
void ULyraGameInstance::HandlerUserInitialized(const UCommonUserInfo* UserInfo, bool bSuccess, FText Error, ECommonUserPrivilege RequestedPrivilege, ECommonUserOnlineContext OnlineContext)
{
Super::HandlerUserInitialized(UserInfo, bSuccess, Error, RequestedPrivilege, OnlineContext);
// If login succeeded, tell the local player to load their settings
if (bSuccess && ensure(UserInfo))
{
ULyraLocalPlayer* LocalPlayer = Cast<ULyraLocalPlayer>(GetLocalPlayerByIndex(UserInfo->LocalPlayerIndex));
// There will not be a local player attached to the dedicated server user
if (LocalPlayer)
{
LocalPlayer->LoadSharedSettingsFromDisk();
}
}
}
void ULyraGameInstance::ReceivedNetworkEncryptionToken(const FString& EncryptionToken, const FOnEncryptionKeyResponse& Delegate)
{
// This is a simple implementation to demonstrate using encryption for game traffic using a hardcoded key.
// For a complete implementation, you would likely want to retrieve the encryption key from a secure source,
// such as from a web service over HTTPS. This could be done in this function, even asynchronously - just
// call the response delegate passed in once the key is known. The contents of the EncryptionToken is up to the user,
// but it will generally contain information used to generate a unique encryption key, such as a user and/or session ID.
FEncryptionKeyResponse Response(EEncryptionResponse::Failure, TEXT("Unknown encryption failure"));
if (EncryptionToken.IsEmpty())
{
Response.Response = EEncryptionResponse::InvalidToken;
Response.ErrorMsg = TEXT("Encryption token is empty.");
}
else
{
#if UE_WITH_DTLS
if (Lyra::bUseDTLSEncryption)
{
TSharedPtr<FDTLSCertificate> Cert;
if (Lyra::bTestDTLSFingerprint)
{
// Generate server cert for this identifier, post the fingerprint
FTimespan CertExpire = FTimespan::FromHours(4);
Cert = FDTLSCertStore::Get().CreateCert(CertExpire, EncryptionToken);
}
else
{
// Load cert from disk for testing purposes (never in production)
const FString CertPath = FPaths::ProjectContentDir() / TEXT("DTLS") / TEXT("LyraTest.pem");
Cert = FDTLSCertStore::Get().GetCert(EncryptionToken);
if (!Cert.IsValid())
{
Cert = FDTLSCertStore::Get().ImportCert(CertPath, EncryptionToken);
}
}
if (Cert.IsValid())
{
if (Lyra::bTestDTLSFingerprint)
{
// Fingerprint should be posted to a secure web service for discovery
// Writing to disk for local testing
TArrayView<const uint8> Fingerprint = Cert->GetFingerprint();
FString DebugFile = FPaths::Combine(*FPaths::ProjectSavedDir(), TEXT("DTLS")) / FPaths::MakeValidFileName(EncryptionToken) + TEXT("_server.txt");
FString FingerprintStr = BytesToHex(Fingerprint.GetData(), Fingerprint.Num());
FFileHelper::SaveStringToFile(FingerprintStr, *DebugFile);
}
// Server currently only needs the identifier
Response.EncryptionData.Identifier = EncryptionToken;
Response.EncryptionData.Key = DebugTestEncryptionKey;
Response.Response = EEncryptionResponse::Success;
}
else
{
Response.Response = EEncryptionResponse::Failure;
Response.ErrorMsg = TEXT("Unable to obtain certificate.");
}
}
else
#endif // UE_WITH_DTLS
{
Response.Response = EEncryptionResponse::Success;
Response.EncryptionData.Key = DebugTestEncryptionKey;
}
}
Delegate.ExecuteIfBound(Response);
}
void ULyraGameInstance::ReceivedNetworkEncryptionAck(const FOnEncryptionKeyResponse& Delegate)
{
// This is a simple implementation to demonstrate using encryption for game traffic using a hardcoded key.
// For a complete implementation, you would likely want to retrieve the encryption key from a secure source,
// such as from a web service over HTTPS. This could be done in this function, even asynchronously - just
// call the response delegate passed in once the key is known.
FEncryptionKeyResponse Response;
#if UE_WITH_DTLS
if (Lyra::bUseDTLSEncryption)
{
Response.Response = EEncryptionResponse::Failure;
APlayerController* const PlayerController = GetFirstLocalPlayerController();
if (PlayerController && PlayerController->PlayerState && PlayerController->PlayerState->GetUniqueId().IsValid())
{
const FUniqueNetIdRepl& PlayerUniqueId = PlayerController->PlayerState->GetUniqueId();
// Ideally the encryption token is passed in directly rather than having to attempt to rebuild it
const FString EncryptionToken = PlayerUniqueId.ToString();
Response.EncryptionData.Identifier = EncryptionToken;
// Server's fingerprint should be pulled from a secure service
if (Lyra::bTestDTLSFingerprint)
{
// But for testing purposes...
FString DebugFile = FPaths::Combine(*FPaths::ProjectSavedDir(), TEXT("DTLS")) / FPaths::MakeValidFileName(EncryptionToken) + TEXT("_server.txt");
FString FingerprintStr;
FFileHelper::LoadFileToString(FingerprintStr, *DebugFile);
Response.EncryptionData.Fingerprint.AddUninitialized(FingerprintStr.Len() / 2);
HexToBytes(FingerprintStr, Response.EncryptionData.Fingerprint.GetData());
}
else
{
// Pulling expected fingerprint from disk for testing, this should come from a secure service
const FString CertPath = FPaths::ProjectContentDir() / TEXT("DTLS") / TEXT("LyraTest.pem");
TSharedPtr<FDTLSCertificate> Cert = FDTLSCertStore::Get().GetCert(EncryptionToken);
if (!Cert.IsValid())
{
Cert = FDTLSCertStore::Get().ImportCert(CertPath, EncryptionToken);
}
if (Cert.IsValid())
{
TArrayView<const uint8> Fingerprint = Cert->GetFingerprint();
Response.EncryptionData.Fingerprint = Fingerprint;
}
else
{
Response.Response = EEncryptionResponse::Failure;
Response.ErrorMsg = TEXT("Unable to obtain certificate.");
}
}
Response.EncryptionData.Key = DebugTestEncryptionKey;
Response.Response = EEncryptionResponse::Success;
}
}
else
#endif // UE_WITH_DTLS
{
Response.Response = EEncryptionResponse::Success;
Response.EncryptionData.Key = DebugTestEncryptionKey;
}
Delegate.ExecuteIfBound(Response);
}
void ULyraGameInstance::OnPreClientTravelToSession(FString& URL)
{
// Add debug encryption token if desired.
if (Lyra::bTestEncryption)
{
#if UE_WITH_DTLS
if (Lyra::bUseDTLSEncryption)
{
APlayerController* const PlayerController = GetFirstLocalPlayerController();
if (PlayerController && PlayerController->PlayerState && PlayerController->PlayerState->GetUniqueId().IsValid())
{
const FUniqueNetIdRepl& PlayerUniqueId = PlayerController->PlayerState->GetUniqueId();
const FString EncryptionToken = PlayerUniqueId.ToString();
URL += TEXT("?EncryptionToken=") + EncryptionToken;
}
}
else
#endif // UE_WITH_DTLS
{
// This is just a value for testing/debugging, the server will use the same key regardless of the token value.
// But the token could be a user ID and/or session ID that would be used to generate a unique key per user and/or session, if desired.
URL += TEXT("?EncryptionToken=1");
}
}
}