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
Binary file removed Content/Showcase/BP_HttpServerShowcase.uasset
Binary file not shown.
Binary file removed Content/Showcase/Cube/BP_Cube.uasset
Binary file not shown.
Binary file removed Content/Showcase/Cube/M_Cube.uasset
Binary file not shown.
Binary file removed Content/Showcase/Showcase.umap
Binary file not shown.
190 changes: 166 additions & 24 deletions Source/SimpleHttpServer/Private/SimpleHttpServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,42 @@

DEFINE_LOG_CATEGORY(LogSimpleHttpServer);

namespace
{
FHttpPath MakeHttpPathForRoute(const FString& HttpPath)
{
return FHttpPath(HttpPath);
}

FString NormalizeHttpPath(FString InPath)
{
InPath.TrimStartAndEndInline();
if (InPath.IsEmpty())
{
return TEXT("/");
}

if (!InPath.StartsWith(TEXT("/")))
{
InPath = TEXT("/") + InPath;
}

while (InPath.Len() > 1 && InPath.EndsWith(TEXT("/")))
{
InPath.LeftChopInline(1, false);
}

return InPath;
}

bool VerbsMatch(ENativeHttpServerRequestVerbs AllowedVerbs, EHttpServerRequestVerbs RequestVerb)
{
const uint8 AllowedMask = static_cast<uint8>(AllowedVerbs);
const uint8 RequestMask = static_cast<uint8>((ENativeHttpServerRequestVerbs)RequestVerb);
return (AllowedMask & RequestMask) != 0;
}
}

void USimpleHttpServer::BeginDestroy()
{
Super::BeginDestroy();
Expand Down Expand Up @@ -56,6 +92,12 @@ void USimpleHttpServer::StopServer()

if (HttpRouter.IsValid())
{
if (bRootPreprocessorRegistered)
{
HttpRouter->UnregisterRequestPreprocessor(RootRequestPreprocessorHandle);
bRootPreprocessorRegistered = false;
}

// Editor will crash after receive request if you start game from editor, close it and start again.
// It is because HttpRouter lived in FHttpServerModule and don't be destroyed on game ending.
// When server stopped or being destroyed we must unbind all handlers to prevent errors on the next game start.
Expand All @@ -68,22 +110,72 @@ void USimpleHttpServer::StopServer()

void USimpleHttpServer::BindRoute(FString HttpPath, ENativeHttpServerRequestVerbs Verbs, FHttpServerRequestDelegate OnHttpServerRequest)
{
RouteDelegates.Add(HttpPath, OnHttpServerRequest);
const FString NormalizedPath = NormalizeHttpPath(HttpPath);
RouteDelegates.Add(NormalizedPath, OnHttpServerRequest);
if (ENativeHttpServerRequestVerbs* ExistingVerbs = RouteVerbs.Find(NormalizedPath))
{
*ExistingVerbs = (ENativeHttpServerRequestVerbs)((uint8)(*ExistingVerbs) | (uint8)Verbs);
}
else
{
RouteVerbs.Add(NormalizedPath, Verbs);
}

if (HttpRouter.IsValid())
{
FHttpRouteHandle HttpRouteHandle = HttpRouter->BindRoute(FHttpPath(HttpPath), (EHttpServerRequestVerbs)Verbs,
#if UE_VERSION_OLDER_THAN(5, 4, 0)
[&, HttpPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return HandleRequest(HttpPath, Request, OnComplete);
});
#else
FHttpRequestHandler::CreateLambda([&, HttpPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
if (NormalizedPath == TEXT("/"))
{
if (!bRootPreprocessorRegistered)
{
return HandleRequest(HttpPath, Request, OnComplete);
}));
#endif
RootRequestPreprocessorHandle = HttpRouter->RegisterRequestPreprocessor(
FHttpRequestHandler::CreateLambda([this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
if (!Request.RelativePath.IsRoot())
{
return false;
}

if (const ENativeHttpServerRequestVerbs* AllowedVerbs = RouteVerbs.Find(TEXT("/")))
{
if (!VerbsMatch(*AllowedVerbs, Request.Verb))
{
return false;
}
}

if (RouteDelegates.Contains(TEXT("/")))
{
return HandleRequest(TEXT("/"), Request, OnComplete);
}

if (RouteHandlers.Contains(TEXT("/")))
{
return HandleRequestNative(TEXT("/"), Request, OnComplete);
}

return false;
}));

bRootPreprocessorRegistered = true;
}

return;
}

const FHttpPath RoutePath = MakeHttpPathForRoute(NormalizedPath);
if (!RoutePath.IsValidPath())
{
UE_LOG(LogSimpleHttpServer, Error, TEXT("Invalid route path: '%s'. This route will not be bound."), *NormalizedPath);
return;
}

FHttpRouteHandle HttpRouteHandle = HttpRouter->BindRoute(RoutePath, (EHttpServerRequestVerbs)Verbs,

FHttpRequestHandler::CreateLambda([&, NormalizedPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return HandleRequest(NormalizedPath, Request, OnComplete);
}));


CreatedRouteHandlers.Add(HttpRouteHandle);
}
Expand All @@ -95,22 +187,72 @@ void USimpleHttpServer::BindRoute(FString HttpPath, ENativeHttpServerRequestVerb

void USimpleHttpServer::BindRouteNative(FString HttpPath, ENativeHttpServerRequestVerbs Verbs, FHttpRouteHandler Handler)
{
RouteHandlers.Add(HttpPath, Handler);
const FString NormalizedPath = NormalizeHttpPath(HttpPath);
RouteHandlers.Add(NormalizedPath, Handler);
if (ENativeHttpServerRequestVerbs* ExistingVerbs = RouteVerbs.Find(NormalizedPath))
{
*ExistingVerbs = (ENativeHttpServerRequestVerbs)((uint8)(*ExistingVerbs) | (uint8)Verbs);
}
else
{
RouteVerbs.Add(NormalizedPath, Verbs);
}

if (HttpRouter.IsValid())
{
FHttpRouteHandle HttpRouteHandle = HttpRouter->BindRoute(FHttpPath(HttpPath), (EHttpServerRequestVerbs)Verbs,
#if UE_VERSION_OLDER_THAN(5, 4, 0)
[&, HttpPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return HandleRequestNative(HttpPath, Request, OnComplete);
});
#else
FHttpRequestHandler::CreateLambda([&, HttpPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
if (NormalizedPath == TEXT("/"))
{
if (!bRootPreprocessorRegistered)
{
return HandleRequestNative(HttpPath, Request, OnComplete);
}));
#endif
RootRequestPreprocessorHandle = HttpRouter->RegisterRequestPreprocessor(
FHttpRequestHandler::CreateLambda([this](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
if (!Request.RelativePath.IsRoot())
{
return false;
}

if (const ENativeHttpServerRequestVerbs* AllowedVerbs = RouteVerbs.Find(TEXT("/")))
{
if (!VerbsMatch(*AllowedVerbs, Request.Verb))
{
return false;
}
}

if (RouteDelegates.Contains(TEXT("/")))
{
return HandleRequest(TEXT("/"), Request, OnComplete);
}

if (RouteHandlers.Contains(TEXT("/")))
{
return HandleRequestNative(TEXT("/"), Request, OnComplete);
}

return false;
}));

bRootPreprocessorRegistered = true;
}

return;
}

const FHttpPath RoutePath = MakeHttpPathForRoute(NormalizedPath);
if (!RoutePath.IsValidPath())
{
UE_LOG(LogSimpleHttpServer, Error, TEXT("Invalid route path: '%s'. This route will not be bound."), *NormalizedPath);
return;
}

FHttpRouteHandle HttpRouteHandle = HttpRouter->BindRoute(RoutePath, (EHttpServerRequestVerbs)Verbs,

FHttpRequestHandler::CreateLambda([&, NormalizedPath](const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete)
{
return HandleRequestNative(NormalizedPath, Request, OnComplete);
}));


CreatedRouteHandlers.Add(HttpRouteHandle);
}
Expand Down
8 changes: 7 additions & 1 deletion Source/SimpleHttpServer/Public/SimpleHttpServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class SIMPLEHTTPSERVER_API USimpleHttpServer : public UObject
bool IsServerStarted() const { return bServerStarted; }

UFUNCTION(BlueprintCallable, Category = "Simple HTTP Server")
void StartServer(int32 ServerPort = 8080);
void StartServer(int32 ServerPort = 9080);

UFUNCTION(BlueprintCallable, Category = "Simple HTTP Server")
void StopServer();
Expand Down Expand Up @@ -128,10 +128,16 @@ class SIMPLEHTTPSERVER_API USimpleHttpServer : public UObject
// Usualy used for c++
TMap<FString, FHttpRouteHandler> RouteHandlers;

// Keep track of verbs for paths that are handled without route binding (e.g. "/").
TMap<FString, ENativeHttpServerRequestVerbs> RouteVerbs;

// Cached Route Handlers. We should unbing them from route on self destroy
TArray<FHttpRouteHandle> CreatedRouteHandlers;

TSharedPtr<class IHttpRouter> HttpRouter;

FDelegateHandle RootRequestPreprocessorHandle;
bool bRootPreprocessorRegistered = false;

bool bServerStarted = false;
};