Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions Source/FicsitRemoteMonitoring/Private/FicsitRemoteMonitoring.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "FicsitRemoteMonitoring.h"

#include <atomic>
#include <sstream>

#include "Runtime/Core/Public/Logging/LogCategory.h"
Expand Down Expand Up @@ -37,6 +38,36 @@
us_listen_socket_t* SocketListener;
bool SocketRunning = false;

// Set as soon as teardown begins, cleared when a server starts. Deliberately a
// file-scope atomic rather than a member: once teardown starts, the subsystem
// actor and the world are being destroyed, so the uWS worker thread cannot
// safely touch `this`, the World, or any UObject — not even to call IsValid(),
// which must dereference the object to read its flags. A dangling pointer is
// not null, so the IsValid() guards inside CallEndpoint() are reached and then
// fault. This flag is the only state a request handler can consult without
// dereferencing anything that may already be freed.
std::atomic<bool> GFRMShuttingDown{false};

// Shutdown barrier for uWS request handlers. Answers 503 and reports true when
// teardown has begun, so the caller returns before touching the subsystem, the
// world, or any other UObject. Touches nothing but the response socket, which
// uWS owns and keeps alive for the duration of the callback.
static bool FRMRejectIfShuttingDown(uWS::HttpResponse<false>* Res)
{
if (!GFRMShuttingDown.load(std::memory_order_acquire))
{
return false;
}

if (Res)
{
Res->writeStatus("503 Service Unavailable");
Res->end("{\"error\":\"Server is shutting down\"}");
}

return true;
}

AFicsitRemoteMonitoring* AFicsitRemoteMonitoring::Get(UWorld* WorldContext)
{
for (TActorIterator<AFicsitRemoteMonitoring> It(WorldContext, StaticClass(), EActorIteratorFlags::AllActors); It; ++It) {
Expand Down Expand Up @@ -65,23 +96,33 @@ void AFicsitRemoteMonitoring::BeginPlay()
// Load FRM's API Endpoints
InitAPIRegistry();

// Read the auth token ONCE here, on the game thread, and cache it in the
// AuthenticationToken member. The uWS request handlers must not read config
// per-request: that runs on the uWS worker thread and reaches GEngine, which
// is unsafe while the engine is tearing down (observed SIGSEGV).
const FString AuthToken = UFRMConfigManager::GetConfigOrDefault<FString>(TEXT("uWS.AuthenticationToken"), "");

// Debug log to verify token retrieval -Porisius
// UE_LOGFMT(LogHttpServer, Log, "DEBUG: AuthToken - {AuthToken}", *AuthToken);

if (AuthToken.IsEmpty())
{
if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GenerateAuthToken(32), false))
const FString GeneratedToken = GenerateAuthToken(32);
if (!UFRMConfigManager::SetConfigFromInput(TEXT("uWS.AuthenticationToken"), GeneratedToken, false))
{
UE_LOG(LogTemp, Warning, TEXT("Failed to apply setting"));
return;
}

UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *AuthToken);
AuthenticationToken = GeneratedToken;

// Log the token that was actually generated. This previously logged
// AuthToken, which is empty on this branch by definition.
UE_LOG(LogTemp, Log, TEXT("Generated and saved new token: %s"), *GeneratedToken);
}
else
{
AuthenticationToken = AuthToken;
UE_LOG(LogTemp, Log, TEXT("Token already exists."));
}

Expand Down Expand Up @@ -128,6 +169,11 @@ void AFicsitRemoteMonitoring::EndPlay(const EEndPlayReason::Type EndPlayReason)

void AFicsitRemoteMonitoring::StopWebSocketServer()
{
// Raise the shutdown barrier FIRST, before anything is torn down, so any
// request already inside the uWS worker thread bails out with 503 instead
// of walking into a half-destroyed world.
GFRMShuttingDown.store(true, std::memory_order_release);

bShouldStop = true;

// Signal the WebSocket server to stop
Expand Down Expand Up @@ -168,10 +214,15 @@ FArduinoConfig AFicsitRemoteMonitoring::GetSerialConfig()
return Config;
}

void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)
void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)
{
UE_LOGFMT(LogHttpServer, Log, "Initializing WebSocket Service");

// Lower the shutdown barrier: a previous StopWebSocketServer() raised it, and
// without this a server restarted in the same session (e.g. `/frm http start`
// after a stop) would answer 503 to every request forever.
GFRMShuttingDown.store(false, std::memory_order_release);

if (SocketRunning)
{
if (bSkipIfRunning)
Expand Down Expand Up @@ -290,13 +341,14 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)
});

app.get("/api/:APIEndpoint", [this, World](auto* res, auto* req) {
if (FRMRejectIfShuttingDown(res)) return;
std::string url(req->getParameter("APIEndpoint"));
FString Endpoint = FString(url.c_str());

// Log the request URL
//UE_LOGFMT(LogHttpServer, Log, "Request URL: {0}", Endpoint);

const FString AuthToken = UFRMConfigManager::GetConfigOrDefault<FString>(TEXT("uWS.AuthenticationToken"), "");
const FString AuthToken = AuthenticationToken;

FRequestData RequestData;
RequestData.bIsAuthorized = IsAuthorizedRequest(req, AuthToken);
Expand All @@ -313,11 +365,13 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)

app.post("/*", [this, World](auto* res, uWS::HttpRequest* req)
{
if (FRMRejectIfShuttingDown(res)) return;
const std::string URL(req->getUrl().begin(), req->getUrl().end());
FString RelativePath = FString(URL.c_str()).Mid(1);

res->onData([this, res, req, World, RelativePath](const std::string_view data, bool)
{
if (FRMRejectIfShuttingDown(res)) return;
try
{
const std::string PostData(data);
Expand All @@ -330,7 +384,7 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)
return UFRM_RequestLibrary::SendErrorMessage(res, "400 Bad Request", FString("Invalid Request Body"));
}

const FString AuthToken = UFRMConfigManager::GetConfigOrDefault<FString>(TEXT("uWS.AuthenticationToken"), "");
const FString AuthToken = AuthenticationToken;

FRequestData RequestData;
RequestData.Method = "POST";
Expand Down Expand Up @@ -363,11 +417,12 @@ void AFicsitRemoteMonitoring::StartWebSocketServer(bool bSkipIfRunning)
});

app.get("/*", [this, UIPath, World](auto* res, uWS::HttpRequest* req) {
if (FRMRejectIfShuttingDown(res)) return;
if (!res) return;

std::string url(req->getUrl().begin(), req->getUrl().end());

const FString AuthToken = UFRMConfigManager::GetConfigOrDefault<FString>(TEXT("uWS.AuthenticationToken"), "");
const FString AuthToken = AuthenticationToken;

bool bFileExists = false;
// Remove initial '/'
Expand Down
11 changes: 11 additions & 0 deletions Source/FicsitRemoteMonitoring/Public/Libraries/FRMConfigManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ class FICSITREMOTEMONITORING_API UFRMConfigManager : public UBlueprintFunctionLi
template<typename T>
static bool GetConfig(const FString& StrID, T& OutValue)
{
// GetFGGameUserSettings() dereferences GEngine internally, so GEngine must
// be checked BEFORE the call — the UserSettings null-check below happens
// too late to help. During engine teardown GEngine is destroyed while the
// uWS worker thread may still be serving a request, and the dereference
// faults (SIGSEGV reading a small member offset). Degrade to the caller's
// default instead of crashing the server.
if (!GEngine)
{
return false;
}

UFGGameUserSettings* UserSettings = UFGGameUserSettings::GetFGGameUserSettings();
if (!UserSettings)
{
Expand Down