diff --git a/Content/Showcase/BP_HttpServerShowcase.uasset b/Content/Showcase/BP_HttpServerShowcase.uasset deleted file mode 100644 index 52eb701..0000000 Binary files a/Content/Showcase/BP_HttpServerShowcase.uasset and /dev/null differ diff --git a/Content/Showcase/Cube/BP_Cube.uasset b/Content/Showcase/Cube/BP_Cube.uasset deleted file mode 100644 index 6a20a4e..0000000 Binary files a/Content/Showcase/Cube/BP_Cube.uasset and /dev/null differ diff --git a/Content/Showcase/Cube/M_Cube.uasset b/Content/Showcase/Cube/M_Cube.uasset deleted file mode 100644 index fb5c04e..0000000 Binary files a/Content/Showcase/Cube/M_Cube.uasset and /dev/null differ diff --git a/Content/Showcase/Showcase.umap b/Content/Showcase/Showcase.umap deleted file mode 100644 index f15c639..0000000 Binary files a/Content/Showcase/Showcase.umap and /dev/null differ diff --git a/Source/SimpleHttpServer/Private/SimpleHttpServer.cpp b/Source/SimpleHttpServer/Private/SimpleHttpServer.cpp index e2277ae..ad5010e 100644 --- a/Source/SimpleHttpServer/Private/SimpleHttpServer.cpp +++ b/Source/SimpleHttpServer/Private/SimpleHttpServer.cpp @@ -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(AllowedVerbs); + const uint8 RequestMask = static_cast((ENativeHttpServerRequestVerbs)RequestVerb); + return (AllowedMask & RequestMask) != 0; + } +} + void USimpleHttpServer::BeginDestroy() { Super::BeginDestroy(); @@ -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. @@ -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); } @@ -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); } diff --git a/Source/SimpleHttpServer/Public/SimpleHttpServer.h b/Source/SimpleHttpServer/Public/SimpleHttpServer.h index 031576c..46f8e01 100644 --- a/Source/SimpleHttpServer/Public/SimpleHttpServer.h +++ b/Source/SimpleHttpServer/Public/SimpleHttpServer.h @@ -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(); @@ -128,10 +128,16 @@ class SIMPLEHTTPSERVER_API USimpleHttpServer : public UObject // Usualy used for c++ TMap RouteHandlers; + // Keep track of verbs for paths that are handled without route binding (e.g. "/"). + TMap RouteVerbs; + // Cached Route Handlers. We should unbing them from route on self destroy TArray CreatedRouteHandlers; TSharedPtr HttpRouter; + FDelegateHandle RootRequestPreprocessorHandle; + bool bRootPreprocessorRegistered = false; + bool bServerStarted = false; };