Files
UnrealEngine/Engine/Source/Runtime/ExternalRPCRegistry/Private/ExternalRpcRegistry.cpp
2025-05-18 13:04:45 +08:00

518 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ExternalRpcRegistry.h"
#include "HttpServerModule.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include "Misc/App.h"
#include "Misc/CoreDelegates.h"
#include "HttpServerResponse.h"
#include "Serialization/JsonWriter.h"
#include "Logging/LogMacros.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(ExternalRpcRegistry)
#define USE_RPC_REGISTRY_IN_SHIPPING 0
#ifndef WITH_RPC_REGISTRY
#define WITH_RPC_REGISTRY (USE_RPC_REGISTRY_IN_SHIPPING || !UE_BUILD_SHIPPING )
#endif
DEFINE_LOG_CATEGORY(LogExternalRpcRegistry)
UExternalRpcRegistry* UExternalRpcRegistry::ObjectInstance = nullptr;
FString GetHttpRouteVerbString(EHttpServerRequestVerbs InVerbs)
{
#if WITH_RPC_REGISTRY
switch (InVerbs)
{
case EHttpServerRequestVerbs::VERB_POST:
{
return TEXT("POST");
}
case EHttpServerRequestVerbs::VERB_PUT:
{
return TEXT("PUT");
}
case EHttpServerRequestVerbs::VERB_GET:
{
return TEXT("GET");
}
case EHttpServerRequestVerbs::VERB_PATCH:
{
return TEXT("PATCH");
}
case EHttpServerRequestVerbs::VERB_DELETE:
{
return TEXT("DELETE");
}
case EHttpServerRequestVerbs::VERB_NONE:
{
return TEXT("NONE");
}
}
#endif
return TEXT("UNKNOWN");
}
bool UExternalRpcRegistry::IsActiveRpcCategory(FString InCategory)
{
#if WITH_RPC_REGISTRY
if (!ActiveRpcCategories.Num() || ActiveRpcCategories.Contains(InCategory))
{
return true;
}
#endif
return false;
}
UExternalRpcRegistry::~UExternalRpcRegistry()
{
CleanUpAllRoutes();
}
bool UExternalRpcRegistry::IsEnabled()
{
#if WITH_RPC_REGISTRY
int32 RpcPort = 0;
// Not just returning this if because it'll cause static analysis issues for unreachable code in non-shipping
if (FParse::Value(FCommandLine::Get(), TEXT("rpcport="), RpcPort))
{
return true;
}
#endif
return false;
}
UExternalRpcRegistry* UExternalRpcRegistry::GetInstance()
{
#if WITH_RPC_REGISTRY
if (ObjectInstance == nullptr)
{
ObjectInstance = NewObject<UExternalRpcRegistry>();
FString InCommandLineValue;
if (FParse::Value(FCommandLine::Get(), TEXT("enabledrpccategories="), InCommandLineValue))
{
TArray<FString> Substrings;
InCommandLineValue.ParseIntoArray(Substrings, TEXT(","));
for (int i = 0; i < Substrings.Num(); i++)
{
if (!ObjectInstance->ActiveRpcCategories.Contains(Substrings[i]))
{
ObjectInstance->ActiveRpcCategories.Add(Substrings[i]);
}
}
}
FParse::Value(FCommandLine::Get(), TEXT("rpcport="), ObjectInstance->PortToUse);
FParse::Value(FCommandLine::Get(), TEXT("rpcledgersize="), ObjectInstance->RequestLedgerCapacity);
// We always want the ListRegisteredRpcs route bound, no matter what.
FHttpRequestHandler ListRoutesRequestHandler = FHttpRequestHandler::CreateUObject(ObjectInstance, &ThisClass::HttpListOpenRoutes);
ObjectInstance->RegisterNewRoute(TEXT("ListRegisteredRpcs"), FHttpPath("/listrpcs"), EHttpServerRequestVerbs::VERB_GET,
ListRoutesRequestHandler, true, true);
FHttpRequestHandler PrintLedgerRequestHandler = FHttpRequestHandler::CreateUObject(ObjectInstance, &ThisClass::HttpPrintRequestLedger);
// We always want the ListRegisteredRpcs route bound, no matter what.
ObjectInstance->RegisterNewRoute(TEXT("GetRequestHistory"), FHttpPath("/requesthistory"), EHttpServerRequestVerbs::VERB_GET,
PrintLedgerRequestHandler, true, true);
FHttpRequestHandler ListOASv3RequestHandler = FHttpRequestHandler::CreateUObject(ObjectInstance, &ThisClass::HttpListOASv3JSONRoutes);
// /swagger.json escaped as %2e
ObjectInstance->RegisterNewRoute(TEXT("ListSwaggerJson"), FHttpPath("/swagger.json"), EHttpServerRequestVerbs::VERB_GET,
ListOASv3RequestHandler, true, true);
FHttpRequestHandler SwaggerUIHandler = FHttpRequestHandler::CreateUObject(ObjectInstance, &ThisClass::HttpSwaggerUI);
ObjectInstance->RegisterNewRoute(TEXT("SwaggerUIHTML"), FHttpPath("/swagger/index.html"), EHttpServerRequestVerbs::VERB_GET,
SwaggerUIHandler, true, true);
ObjectInstance->AddToRoot();
}
#endif
return ObjectInstance;
}
bool UExternalRpcRegistry::GetRegisteredRoute(FName RouteName, FExternalRouteInfo& OutRouteInfo)
{
#if WITH_RPC_REGISTRY
if (RegisteredRoutes.Find(RouteName))
{
OutRouteInfo.RouteName = RouteName;
OutRouteInfo.RoutePath = RegisteredRoutes[RouteName].Handle->Path;
OutRouteInfo.RequestVerbs = RegisteredRoutes[RouteName].Handle->Verbs;
OutRouteInfo.InputContentType = RegisteredRoutes[RouteName].InputContentType;
OutRouteInfo.ExpectedArguments = RegisteredRoutes[RouteName].ExpectedArguments;
return true;
}
#endif
return false;
}
void UExternalRpcRegistry::RegisterNewRouteWithArguments(FName RouteName, const FHttpPath& HttpPath, const EHttpServerRequestVerbs& RequestVerbs, const FHttpRequestHandler& Handler, TArray<FExternalRpcArgumentDesc> InArguments, bool bOverrideIfBound /* = false */, bool bIsAlwaysOn /* = false */, FString OptionalCategory /* = FString("Unknown") */, FString OptionalContentType /* = TEXT("")*/)
{
#if WITH_RPC_REGISTRY
FExternalRouteInfo InRouteInfo;
InRouteInfo.RouteName = RouteName;
InRouteInfo.RoutePath = HttpPath;
InRouteInfo.RequestVerbs = RequestVerbs;
InRouteInfo.InputContentType = OptionalContentType;
InRouteInfo.ExpectedArguments = MoveTemp(InArguments);
InRouteInfo.RpcCategory = OptionalCategory;
InRouteInfo.bAlwaysOn = bIsAlwaysOn;
RegisterNewRoute(MoveTemp(InRouteInfo), Handler, bOverrideIfBound);
#endif
}
void UExternalRpcRegistry::RegisterNewRoute(FName RouteName, const FHttpPath& HttpPath, const EHttpServerRequestVerbs& RequestVerbs, const FHttpRequestHandler& Handler, bool bOverrideIfBound /* = false */, bool bIsAlwaysOn /* = false */, FString OptionalCategory /* = FString("Unknown") */, FString OptionalContentType /* = TEXT("")*/, FString OptionalExpectedFormat /*= TEXT("")*/)
{
#if WITH_RPC_REGISTRY
FExternalRouteInfo InRouteInfo;
InRouteInfo.RouteName = RouteName;
InRouteInfo.RoutePath = HttpPath;
InRouteInfo.RequestVerbs = RequestVerbs;
InRouteInfo.InputContentType = OptionalContentType;
InRouteInfo.RpcCategory = OptionalCategory;
InRouteInfo.bAlwaysOn = bIsAlwaysOn;
RegisterNewRoute(MoveTemp(InRouteInfo), Handler, bOverrideIfBound);
#endif
}
void UExternalRpcRegistry::RegisterNewRoute(FExternalRouteInfo InRouteInfo, const FHttpRequestHandler& Handler, bool bOverrideIfBound /* = false */)
{
#if WITH_RPC_REGISTRY
if (!InRouteInfo.bAlwaysOn && !IsActiveRpcCategory(InRouteInfo.RpcCategory))
{
return;
}
TSharedPtr<IHttpRouter> HttpRouter = FHttpServerModule::Get().GetHttpRouter(PortToUse);
if (RegisteredRoutes.Find(InRouteInfo.RouteName))
{
if (!bOverrideIfBound)
{
UE_LOG(LogExternalRpcRegistry, Error, TEXT("Failed to bind route with friendly key %s - a route at location %s already exists."), *InRouteInfo.RouteName.ToString(), *InRouteInfo.RoutePath.GetPath());
return;
}
UE_LOG(LogExternalRpcRegistry, Log, TEXT("Overwriting route at friendly key %s - from %s to %s "), *InRouteInfo.RouteName.ToString(), *RegisteredRoutes[InRouteInfo.RouteName].Handle->Path, *InRouteInfo.RoutePath.GetPath());
HttpRouter->UnbindRoute(RegisteredRoutes[InRouteInfo.RouteName].Handle);
}
FExternalRouteDesc RouteDesc;
RouteDesc.Handle = HttpRouter->BindRoute(InRouteInfo.RoutePath, InRouteInfo.RequestVerbs, Handler);
RouteDesc.InputContentType = InRouteInfo.InputContentType;
RouteDesc.ExpectedArguments = InRouteInfo.ExpectedArguments;
RouteDesc.RpcCategory = InRouteInfo.RpcCategory;
RegisteredRoutes.Add(InRouteInfo.RouteName, RouteDesc);
#endif
}
void UExternalRpcRegistry::CleanUpAllRoutes()
{
#if WITH_RPC_REGISTRY
TArray<FName> OutRouteKeys;
RegisteredRoutes.GetKeys(OutRouteKeys);
for (const FName& RouteKey : OutRouteKeys)
{
CleanUpRoute(RouteKey);
}
#endif
}
void UExternalRpcRegistry::CleanUpRoute(FName RouteName, bool bFailIfUnbound /* = false */)
{
#if WITH_RPC_REGISTRY
if (RegisteredRoutes.Find(RouteName))
{
TSharedPtr<IHttpRouter> HttpRouter = FHttpServerModule::Get().GetHttpRouter(PortToUse);
HttpRouter->UnbindRoute(RegisteredRoutes[RouteName].Handle);
RegisteredRoutes.Remove(RouteName);
UE_LOG(LogExternalRpcRegistry, Log, TEXT("Route name %s was unbound!"), *RouteName.ToString());
}
else
{
UE_LOG(LogExternalRpcRegistry, Warning, TEXT("Route name %s does not exist, could not unbind."), *RouteName.ToString());
check(!bFailIfUnbound);
}
#endif
}
bool UExternalRpcRegistry::HttpListOpenRoutes(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
#if WITH_RPC_REGISTRY
FString ResponseStr;
TArray<FName> OutRouteKeys;
RegisteredRoutes.GetKeys(OutRouteKeys);
TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&ResponseStr);
JsonWriter->WriteArrayStart();
for (const FName& RouteKey : OutRouteKeys)
{
JsonWriter->WriteObjectStart();
JsonWriter->WriteValue(TEXT("name"), RouteKey.ToString());
JsonWriter->WriteValue(TEXT("route"), RegisteredRoutes[RouteKey].Handle->Path);
JsonWriter->WriteValue(TEXT("verb"), GetHttpRouteVerbString(RegisteredRoutes[RouteKey].Handle->Verbs));
if (!RegisteredRoutes[RouteKey].InputContentType.IsEmpty())
{
JsonWriter->WriteValue(TEXT("inputContentType"), RegisteredRoutes[RouteKey].InputContentType);
}
if (!RegisteredRoutes[RouteKey].ExpectedArguments.IsEmpty())
{
JsonWriter->WriteArrayStart(TEXT("args"));
for (const FExternalRpcArgumentDesc& ArgDesc : RegisteredRoutes[RouteKey].ExpectedArguments)
{
JsonWriter->WriteObjectStart();
JsonWriter->WriteValue(TEXT("name"), ArgDesc.Name);
JsonWriter->WriteValue(TEXT("type"), ArgDesc.Type);
JsonWriter->WriteValue(TEXT("desc"), ArgDesc.Desc);
JsonWriter->WriteValue(TEXT("optional"), ArgDesc.bIsOptional);
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteArrayEnd();
}
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteArrayEnd();
JsonWriter->Close();
auto Response = FHttpServerResponse::Create(ResponseStr, TEXT("application/json"));
OnComplete(MoveTemp(Response));
#endif
return true;
}
void UExternalRpcRegistry::AddRequestToLedger(const FHttpServerRequest& Request)
{
#if WITH_RPC_REGISTRY
if (Request.Headers.Find(TEXT("rpcname")))
{
FRpcLedgerEntry NewEntry;
NewEntry.RpcName = Request.Headers[TEXT("rpcname")][0];
FUTF8ToTCHAR WByteBuffer(reinterpret_cast<const ANSICHAR*>(Request.Body.GetData()), Request.Body.Num());
NewEntry.RequestBody = FString::ConstructFromPtrSize(WByteBuffer.Get(), WByteBuffer.Length());
NewEntry.RequestTime = FDateTime::UtcNow();
RequestLedger.Add(NewEntry);
}
// Reduce ledger to proper max size.
while (RequestLedger.Num() > RequestLedgerCapacity)
{
RequestLedger.RemoveAt(0);
}
#endif
}
bool UExternalRpcRegistry::HttpPrintRequestLedger(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
#if WITH_RPC_REGISTRY
FString ResponseStr;
TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&ResponseStr);
JsonWriter->WriteArrayStart();
for (const FRpcLedgerEntry& LoggedRequest : RequestLedger)
{
JsonWriter->WriteObjectStart();
JsonWriter->WriteValue(TEXT("rpcname"), LoggedRequest.RpcName);
JsonWriter->WriteValue(TEXT("requesttimestamp"), LoggedRequest.RequestTime.ToString());
JsonWriter->WriteValue(TEXT("requestbody"), LoggedRequest.RequestBody);
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteArrayEnd();
JsonWriter->Close();
auto Response = FHttpServerResponse::Create(ResponseStr, TEXT("application/json"));
OnComplete(MoveTemp(Response));
#endif
return true;
}
bool UExternalRpcRegistry::HttpSwaggerUI(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
#if WITH_RPC_REGISTRY
/*TODO: Maybe read this from contents, but for now just bake it in code.*/
FString ResponseStr = R"(<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="SwaggerIU"
/>
<title>SwaggerUI</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.4.1/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.4.1/swagger-ui-bundle.js" crossorigin></script>
<script src="https://unpkg.com/swagger-ui-dist@4.4.1/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: 'http://{0}:{1}/swagger.json',
dom_id: '#swagger-ui',
});
};
</script>
</body>
</html>)";
// Default to multihome address if provided, else use localhost.
FString Address;
if (FParse::Value(FCommandLine::Get(), TEXT("MULTIHOMEHTTP="), Address))
{
} else if (FParse::Value(FCommandLine::Get(), TEXT("MULTIHOME="), Address))
{
} else
{
Address = TEXT("127.0.0.1");
}
FStringFormatOrderedArguments Args{ Address,PortToUse };
FString ListFormat = FString::Format(*ResponseStr, Args);
auto Response = FHttpServerResponse::Create(ListFormat, TEXT("text/html"));
OnComplete(MoveTemp(Response));
#endif
return true;
}
bool UExternalRpcRegistry::HttpListOASv3JSONRoutes(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
#if WITH_RPC_REGISTRY
FString ResponseStr;
TArray<FName> OutRouteKeys;
RegisteredRoutes.GetKeys(OutRouteKeys);
TSharedRef<TJsonWriter<>> JsonWriter = TJsonWriterFactory<>::Create(&ResponseStr);
JsonWriter->WriteObjectStart();
// Based on OpenApi Spec v3.0.0, update this string
// as necessary.
JsonWriter->WriteValue(TEXT("openapi"), TEXT("3.0.0"));
JsonWriter->WriteObjectStart(TEXT("info"));
JsonWriter->WriteValue(TEXT("title"), FString::Printf(TEXT("UE-%s - RPC API"), FApp::GetProjectName()));
JsonWriter->WriteValue(TEXT("description"), TEXT("Auto-generated Swagger API"));
JsonWriter->WriteValue(TEXT("version"), FApp::GetBuildVersion());
JsonWriter->WriteObjectEnd();
JsonWriter->WriteArrayStart(TEXT("servers"));
TArray<FString> Addresses;
FString MultiHomeFromCommandLine;
if (FParse::Value(FCommandLine::Get(), TEXT("MULTIHOMEHTTP="), MultiHomeFromCommandLine))
{
Addresses.Push(MultiHomeFromCommandLine);
} else if (FParse::Value(FCommandLine::Get(), TEXT("MULTIHOME="), MultiHomeFromCommandLine))
{
Addresses.Push(MultiHomeFromCommandLine);
}
Addresses.Push(TEXT("127.0.0.1"));
Addresses.Push(TEXT("localhost"));
for (FString InternetAddr : Addresses)
{
JsonWriter->WriteObjectStart();
JsonWriter->WriteValue(TEXT("url"), FString::Printf(TEXT("http://%s:%d"), *InternetAddr, PortToUse));
JsonWriter->WriteValue(TEXT("description"), TEXT("Default server access ip"));
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteArrayEnd();
JsonWriter->WriteObjectStart(TEXT("paths"));
for (const FName& RouteKey : OutRouteKeys)
{
JsonWriter->WriteObjectStart(RegisteredRoutes[RouteKey].Handle->Path);
JsonWriter->WriteObjectStart(GetHttpRouteVerbString(RegisteredRoutes[RouteKey].Handle->Verbs).ToLower());
JsonWriter->WriteValue(TEXT("summary"), RouteKey.ToString());
JsonWriter->WriteValue(TEXT("operationId"), RouteKey.ToString());
if (!RegisteredRoutes[RouteKey].RpcCategory.IsEmpty())
{
JsonWriter->WriteArrayStart(TEXT("tags"));
JsonWriter->WriteValue(RegisteredRoutes[RouteKey].RpcCategory);
JsonWriter->WriteArrayEnd();
}
// TODO: Ask C++ implementers to provide a description of their RPC call should do.
// We'll dump the InputContentType for now.
if (!RegisteredRoutes[RouteKey].InputContentType.IsEmpty())
{
JsonWriter->WriteValue(TEXT("description"), RegisteredRoutes[RouteKey].InputContentType);
}
else
{
JsonWriter->WriteValue(TEXT("description"), TEXT("No content type required to call this."));
}
if (!RegisteredRoutes[RouteKey].ExpectedArguments.IsEmpty())
{
JsonWriter->WriteObjectStart(TEXT("requestBody"));
JsonWriter->WriteObjectStart(TEXT("content"));
JsonWriter->WriteObjectStart(TEXT("application/json"));
JsonWriter->WriteObjectStart(TEXT("schema"));
JsonWriter->WriteValue(TEXT("type"), TEXT("object"));
JsonWriter->WriteObjectStart(TEXT("properties"));
TArray<FString> RequiredObjects;
for (const FExternalRpcArgumentDesc& ArgDesc : RegisteredRoutes[RouteKey].ExpectedArguments)
{
JsonWriter->WriteObjectStart(ArgDesc.Name);
//TODO: Provide better typing in RPC framework so we can auto-gen some values here.
JsonWriter->WriteValue(TEXT("description"), ArgDesc.Desc);
//JsonWriter->WriteValue(TEXT("type"), ArgDesc.Type);
JsonWriter->WriteValue(TEXT("type"), TEXT("string"));
if (!ArgDesc.bIsOptional)
{
RequiredObjects.Push(ArgDesc.Name);
}
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteObjectEnd();
if (RequiredObjects.Num() > 0)
{
JsonWriter->WriteArrayStart("required");
for (FString RequiredName : RequiredObjects)
{
JsonWriter->WriteValue(RequiredName);
}
JsonWriter->WriteArrayEnd();
}
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteObjectStart(TEXT("responses"));
JsonWriter->WriteObjectStart(TEXT("200"));
JsonWriter->WriteValue(TEXT("description"), TEXT("Successful return."));
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
}
JsonWriter->WriteObjectEnd();
JsonWriter->WriteObjectEnd();
JsonWriter->Close();
auto Response = FHttpServerResponse::Create(ResponseStr, TEXT("application/json"));
OnComplete(MoveTemp(Response));
#endif
return true;
}
IMPLEMENT_MODULE(FDefaultModuleImpl, ExternalRpcRegistry);