diff --git a/Caddyfile b/Caddyfile deleted file mode 100644 index 535c7b8..0000000 --- a/Caddyfile +++ /dev/null @@ -1,22 +0,0 @@ -:5002 { - @cors_preflight method OPTIONS - - header { - Access-Control-Allow-Origin "{header.origin}" - Vary Origin - Access-Control-Expose-Headers "*" - +Access-Control-Allow-Headers "*" - +Access-Control-Allow-Headers "Authorization,Content-Type,Accept,User-Agent" - Access-Control-Allow-Credentials "true" - } - - handle @cors_preflight { - header { - Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE" - Access-Control-Max-Age "3600" - } - respond "" 204 - } - - reverse_proxy host.docker.internal:5001 -} diff --git a/DysonNetwork.Control/AppHost.cs b/DysonNetwork.Control/AppHost.cs index e4385a7..f37a07d 100644 --- a/DysonNetwork.Control/AppHost.cs +++ b/DysonNetwork.Control/AppHost.cs @@ -1,4 +1,3 @@ -using Aspire.Hosting.Yarp.Transforms; using Microsoft.Extensions.Hosting; var builder = DistributedApplication.CreateBuilder(args); @@ -60,37 +59,15 @@ for (var idx = 0; idx < services.Count; idx++) // Extra double-ended references ringService.WithReference(passService); -var gateway = builder.AddYarp("gateway") - .WithConfiguration(yarp => - { - var ringCluster = yarp.AddCluster(ringService.GetEndpoint("http")); - yarp.AddRoute("/ws", ringCluster); - yarp.AddRoute("/ring/{**catch-all}", ringCluster) - .WithTransformPathRemovePrefix("/ring") - .WithTransformPathPrefix("/api"); - var passCluster = yarp.AddCluster(passService.GetEndpoint("http")); - yarp.AddRoute("/.well-known/openid-configuration", passCluster); - yarp.AddRoute("/.well-known/jwks", passCluster); - yarp.AddRoute("/id/{**catch-all}", passCluster) - .WithTransformPathRemovePrefix("/id") - .WithTransformPathPrefix("/api"); - var driveCluster = yarp.AddCluster(driveService.GetEndpoint("http")); - yarp.AddRoute("/api/tus", driveCluster); - yarp.AddRoute("/drive/{**catch-all}", driveCluster) - .WithTransformPathRemovePrefix("/drive") - .WithTransformPathPrefix("/api"); - var sphereCluster = yarp.AddCluster(sphereService.GetEndpoint("http")); - yarp.AddRoute("/sphere/{**catch-all}", sphereCluster) - .WithTransformPathRemovePrefix("/sphere") - .WithTransformPathPrefix("/api"); - var developCluster = yarp.AddCluster(developService.GetEndpoint("http")); - yarp.AddRoute("/develop/{**catch-all}", developCluster) - .WithTransformPathRemovePrefix("/develop") - .WithTransformPathPrefix("/api"); - }); - -if (isDev) gateway.WithHostPort(5001); +var gateway = builder.AddProject("gateway") + .WithReference(ringService) + .WithReference(passService) + .WithReference(driveService) + .WithReference(sphereService) + .WithReference(developService) + .WithEnvironment("HTTP_PORTS", "5001") + .WithHttpEndpoint(port: 5001, targetPort: null, isProxied: false, name: "http"); builder.AddDockerComposeEnvironment("docker-compose"); -builder.Build().Run(); \ No newline at end of file +builder.Build().Run(); diff --git a/DysonNetwork.Control/DysonNetwork.Control.csproj b/DysonNetwork.Control/DysonNetwork.Control.csproj index 970a972..f8a51c4 100644 --- a/DysonNetwork.Control/DysonNetwork.Control.csproj +++ b/DysonNetwork.Control/DysonNetwork.Control.csproj @@ -16,7 +16,6 @@ - @@ -25,6 +24,7 @@ + diff --git a/DysonNetwork.Gateway/Dockerfile b/DysonNetwork.Gateway/Dockerfile new file mode 100644 index 0000000..58f2fb9 --- /dev/null +++ b/DysonNetwork.Gateway/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["DysonNetwork.Gateway/DysonNetwork.Gateway.csproj", "DysonNetwork.Gateway/"] +RUN dotnet restore "DysonNetwork.Gateway/DysonNetwork.Gateway.csproj" +COPY . . +WORKDIR "/src/DysonNetwork.Gateway" +RUN dotnet build "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./DysonNetwork.Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DysonNetwork.Gateway.dll"] diff --git a/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj b/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj new file mode 100644 index 0000000..41e5276 --- /dev/null +++ b/DysonNetwork.Gateway/DysonNetwork.Gateway.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/DysonNetwork.Gateway/Program.cs b/DysonNetwork.Gateway/Program.cs new file mode 100644 index 0000000..524047c --- /dev/null +++ b/DysonNetwork.Gateway/Program.cs @@ -0,0 +1,175 @@ +using System.Threading.RateLimiting; +using DysonNetwork.Shared.Http; +using Microsoft.AspNetCore.RateLimiting; +using Yarp.ReverseProxy.Configuration; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.ConfigureAppKestrel(builder.Configuration, maxRequestBodySize: long.MaxValue, enableGrpc: false); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy( + policy => + { + policy.SetIsOriginAllowed(origin => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +builder.Services.AddRateLimiter(options => +{ + options.AddFixedWindowLimiter("fixed", limiterOptions => + { + limiterOptions.PermitLimit = 120; + limiterOptions.Window = TimeSpan.FromMinutes(1); + limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; + limiterOptions.QueueLimit = 0; + }); +}); + +var routes = new[] +{ + new RouteConfig() + { + RouteId = "ring-ws", + ClusterId = "ring", + Match = new RouteMatch { Path = "/ws" } + }, + new RouteConfig() + { + RouteId = "ring-api", + ClusterId = "ring", + Match = new RouteMatch { Path = "/ring/{**catch-all}" }, + Transforms = + [ + new Dictionary { { "PathRemovePrefix", "/ring" } }, + new Dictionary { { "PathPrefix", "/api" } } + ] + }, + new RouteConfig() + { + RouteId = "pass-openid", + ClusterId = "pass", + Match = new RouteMatch { Path = "/.well-known/openid-configuration" } + }, + new RouteConfig() + { + RouteId = "pass-jwks", + ClusterId = "pass", + Match = new RouteMatch { Path = "/.well-known/jwks" } + }, + new RouteConfig() + { + RouteId = "pass-api", + ClusterId = "pass", + Match = new RouteMatch { Path = "/id/{**catch-all}" }, + Transforms = + [ + new Dictionary { { "PathRemovePrefix", "/id" } }, + new Dictionary { { "PathPrefix", "/api" } } + ] + }, + new RouteConfig() + { + RouteId = "drive-tus", + ClusterId = "drive", + Match = new RouteMatch { Path = "/api/tus" } + }, + new RouteConfig() + { + RouteId = "drive-api", + ClusterId = "drive", + Match = new RouteMatch { Path = "/drive/{**catch-all}" }, + Transforms = + [ + new Dictionary { { "PathRemovePrefix", "/drive" } }, + new Dictionary { { "PathPrefix", "/api" } } + ] + }, + new RouteConfig() + { + RouteId = "sphere-api", + ClusterId = "sphere", + Match = new RouteMatch { Path = "/sphere/{**catch-all}" }, + Transforms = + [ + new Dictionary { { "PathRemovePrefix", "/sphere" } }, + new Dictionary { { "PathPrefix", "/api" } } + ] + }, + new RouteConfig() + { + RouteId = "develop-api", + ClusterId = "develop", + Match = new RouteMatch { Path = "/develop/{**catch-all}" }, + Transforms = + [ + new Dictionary { { "PathRemovePrefix", "/develop" } }, + new Dictionary { { "PathPrefix", "/api" } } + ] + } +}; + +var clusters = new[] +{ + new ClusterConfig() + { + ClusterId = "ring", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig() { Address = "http://ring" } } + } + }, + new ClusterConfig() + { + ClusterId = "pass", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig() { Address = "http://pass" } } + } + }, + new ClusterConfig() + { + ClusterId = "drive", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig() { Address = "http://drive" } } + } + }, + new ClusterConfig() + { + ClusterId = "sphere", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig() { Address = "http://sphere" } } + } + }, + new ClusterConfig() + { + ClusterId = "develop", + Destinations = new Dictionary + { + { "destination1", new DestinationConfig() { Address = "http://develop" } } + } + } +}; + +builder.Services +.AddReverseProxy() +.LoadFromMemory(routes, clusters) +.AddServiceDiscoveryDestinationResolver(); + +var app = builder.Build(); + +app.UseCors(); + +app.UseRateLimiter(); + +app.MapReverseProxy().RequireRateLimiting("fixed"); + +app.Run(); diff --git a/DysonNetwork.Gateway/Properties/launchSettings.json b/DysonNetwork.Gateway/Properties/launchSettings.json new file mode 100644 index 0000000..55b32fa --- /dev/null +++ b/DysonNetwork.Gateway/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DysonNetwork.Gateway/appsettings.json b/DysonNetwork.Gateway/appsettings.json new file mode 100644 index 0000000..ec04bc1 --- /dev/null +++ b/DysonNetwork.Gateway/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/DysonNetwork.Shared/Http/KestrelConfiguration.cs b/DysonNetwork.Shared/Http/KestrelConfiguration.cs index 0eea594..9868d9b 100644 --- a/DysonNetwork.Shared/Http/KestrelConfiguration.cs +++ b/DysonNetwork.Shared/Http/KestrelConfiguration.cs @@ -12,22 +12,27 @@ public static class KestrelConfiguration public static WebApplicationBuilder ConfigureAppKestrel( this WebApplicationBuilder builder, IConfiguration configuration, - long maxRequestBodySize = 50 * 1024 * 1024 + long maxRequestBodySize = 50 * 1024 * 1024, + bool enableGrpc = true ) { builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestBodySize = maxRequestBodySize; - // gRPC - var grpcPort = int.Parse(configuration.GetValue("GRPC_PORT", "5001")); - options.ListenAnyIP(grpcPort, listenOptions => + if (enableGrpc) { - listenOptions.Protocols = HttpProtocols.Http2; + // gRPC + var grpcPort = int.Parse(configuration.GetValue("GRPC_PORT", "5001")); + options.ListenAnyIP(grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + + var selfSignedCert = _CreateSelfSignedCertificate(); + listenOptions.UseHttps(selfSignedCert); + }); + } - var selfSignedCert = _CreateSelfSignedCertificate(); - listenOptions.UseHttps(selfSignedCert); - }); var httpPorts = configuration.GetValue("HTTP_PORTS", "6000") .Split(',', StringSplitOptions.RemoveEmptyEntries) @@ -68,4 +73,4 @@ public static class KestrelConfiguration var pfxBytes = certificate.Export(X509ContentType.Pfx); return X509CertificateLoader.LoadPkcs12(pfxBytes, password: null); } -} \ No newline at end of file +} diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 0c2aacf..b7ddf7c 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -4,7 +4,6 @@ using DysonNetwork.Shared.Registry; using DysonNetwork.Sphere; using DysonNetwork.Sphere.Startup; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); @@ -47,4 +46,4 @@ using (var scope = app.Services.CreateScope()) // Configure application middleware pipeline app.ConfigureAppMiddleware(builder.Configuration); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/DysonNetwork.sln b/DysonNetwork.sln index 30ddd02..21c60e9 100644 --- a/DysonNetwork.sln +++ b/DysonNetwork.sln @@ -22,6 +22,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Control", "Dys EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.ServiceDefaults", "DysonNetwork.ServiceDefaults\DysonNetwork.ServiceDefaults.csproj", "{877AAD96-C257-4305-9F1C-C9D9C9BEE615}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DysonNetwork.Gateway", "DysonNetwork.Gateway\DysonNetwork.Gateway.csproj", "{AA4D244C-6C3A-4CD0-9DA4-5CAFFBB55085}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,5 +62,9 @@ Global {877AAD96-C257-4305-9F1C-C9D9C9BEE615}.Debug|Any CPU.Build.0 = Debug|Any CPU {877AAD96-C257-4305-9F1C-C9D9C9BEE615}.Release|Any CPU.ActiveCfg = Release|Any CPU {877AAD96-C257-4305-9F1C-C9D9C9BEE615}.Release|Any CPU.Build.0 = Release|Any CPU + {AA4D244C-6C3A-4CD0-9DA4-5CAFFBB55085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA4D244C-6C3A-4CD0-9DA4-5CAFFBB55085}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA4D244C-6C3A-4CD0-9DA4-5CAFFBB55085}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA4D244C-6C3A-4CD0-9DA4-5CAFFBB55085}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal