Skip to content

Commit

Permalink
Add support for adding metadata to proxy routes
Browse files Browse the repository at this point in the history
  • Loading branch information
Kahbazi committed Sep 22, 2020
1 parent c06d818 commit 19a7313
Show file tree
Hide file tree
Showing 17 changed files with 731 additions and 472 deletions.
5 changes: 4 additions & 1 deletion samples/ReverseProxy.Config.Sample/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.ReverseProxy.Middleware;
Expand Down Expand Up @@ -63,7 +64,9 @@ public void Configure(IApplicationBuilder app)
proxyPipeline.UseAffinitizedDestinationLookup();
proxyPipeline.UseProxyLoadBalancing();
proxyPipeline.UseRequestAffinitizer();
});
})
.ConfigureRoutes((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}"))
.ConfigureRoute("route1", builder => builder.WithDisplayName("ReverseProxy (My special route name)"));
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,5 @@ internal interface IRuntimeRouteBuilder
/// <paramref name="source"/> and <paramref name="cluster"/>.
/// </param>
RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute);

/// <summary>
/// Sets the middleware pipeline to use when building routes.
/// </summary>
void SetProxyPipeline(RequestDelegate pipeline);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.ReverseProxy.Abstractions;
using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract;
using Microsoft.ReverseProxy.Configuration.Contract;
using Microsoft.ReverseProxy.Service;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.ReverseProxy;
using Microsoft.ReverseProxy.Configuration;
using Microsoft.ReverseProxy.Configuration.Contract;
using Microsoft.ReverseProxy.Configuration.DependencyInjection;
Expand Down Expand Up @@ -32,6 +34,8 @@ public static IReverseProxyBuilder AddReverseProxy(this IServiceCollection servi
.AddProxy()
.AddBackgroundWorkers();

services.TryAddSingleton<ProxyEndpointFactory, ProxyEndpointFactory>();

services.AddDataProtection();
services.AddAuthorization();
services.AddCors();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Licensed under the MIT License.

using System;
using System.Linq;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.ReverseProxy;
using Microsoft.ReverseProxy.Abstractions;
using Microsoft.ReverseProxy.Middleware;
using Microsoft.ReverseProxy.Service;
using Microsoft.ReverseProxy.Service.Management;

namespace Microsoft.AspNetCore.Builder
{
Expand All @@ -33,7 +36,7 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints)
/// Adds Reverse Proxy routes to the route table with the customized processing pipeline. The pipeline includes
/// by default the initialization step and the final proxy step, but not LoadBalancingMiddleware or other intermediate components.
/// </summary>
public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action<IApplicationBuilder> configureApp)
public static ReverseProxyConventionBuilder MapReverseProxy(this IEndpointRouteBuilder endpoints, Action<IApplicationBuilder> configureApp)
{
if (endpoints is null)
{
Expand All @@ -50,16 +53,27 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints, Action<
appBuilder.UseMiddleware<ProxyInvokerMiddleware>();
var app = appBuilder.Build();

var routeBuilder = endpoints.ServiceProvider.GetRequiredService<IRuntimeRouteBuilder>();
routeBuilder.SetProxyPipeline(app);
var proxyEndpointFactory = endpoints.ServiceProvider.GetRequiredService<ProxyEndpointFactory>();
proxyEndpointFactory.SetProxyPipeline(app);

var configManager = endpoints.ServiceProvider.GetRequiredService<IProxyConfigManager>();
return GetOrCreateDataSource(endpoints).DefaultBuilder;
}

private static ProxyConfigManager GetOrCreateDataSource(IEndpointRouteBuilder endpoints)
{
var dataSource = (ProxyConfigManager)endpoints.DataSources.OfType<IProxyConfigManager>().FirstOrDefault();
if (dataSource == null)
{
dataSource = (ProxyConfigManager)endpoints.ServiceProvider.GetRequiredService<IProxyConfigManager>();
endpoints.DataSources.Add(dataSource);

// Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent
// the app from starting, and B) so that all the config is ready before the server starts accepting requests.
// Reloads will be async.
dataSource.InitialLoadAsync().GetAwaiter().GetResult();
}

// Config validation is async but startup is sync. We want this to block so that A) any validation errors can prevent
// the app from starting, and B) so that all the config is ready before the server starts accepting requests.
// Reloads will be async.
var dataSource = configManager.InitialLoadAsync().GetAwaiter().GetResult();
endpoints.DataSources.Add(dataSource);
return dataSource;
}
}
}
72 changes: 0 additions & 72 deletions src/ReverseProxy/Service/Config/RuntimeRouteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,13 @@ namespace Microsoft.ReverseProxy.Service
/// </summary>
internal class RuntimeRouteBuilder : IRuntimeRouteBuilder
{
private static readonly IAuthorizeData DefaultAuthorization = new AuthorizeAttribute();
private static readonly IEnableCorsAttribute DefaultCors = new EnableCorsAttribute();
private static readonly IDisableCorsAttribute DisableCors = new DisableCorsAttribute();

private readonly ITransformBuilder _transformBuilder;
private RequestDelegate _pipeline;

public RuntimeRouteBuilder(ITransformBuilder transformBuilder)
{
_transformBuilder = transformBuilder ?? throw new ArgumentNullException(nameof(transformBuilder));
}

public void SetProxyPipeline(RequestDelegate pipeline)
{
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
}

/// <inheritdoc/>
public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute)
{
Expand All @@ -46,74 +36,12 @@ public RouteConfig Build(ProxyRoute source, ClusterInfo cluster, RouteInfo runti

var transforms = _transformBuilder.Build(source.Transforms);

// NOTE: `new RouteConfig(...)` needs a reference to the list of ASP .NET Core endpoints,
// but the ASP .NET Core endpoints cannot be created without a `RouteConfig` metadata item.
// We solve this chicken-egg problem by creating an (empty) list first
// and passing a read-only wrapper of it to `RouteConfig.ctor`.
// Recall that `List<T>.AsReadOnly()` creates a wrapper over the original list,
// and changes to the underlying list *are* reflected on the read-only view.
var aspNetCoreEndpoints = new List<Endpoint>(1);
var newRouteConfig = new RouteConfig(
runtimeRoute,
source,
cluster,
aspNetCoreEndpoints.AsReadOnly(),
transforms);

// Catch-all pattern when no path was specified
var pathPattern = string.IsNullOrEmpty(source.Match.Path) ? "/{**catchall}" : source.Match.Path;

var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder(
requestDelegate: _pipeline,
routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern),
order: source.Order.GetValueOrDefault());

endpointBuilder.DisplayName = source.RouteId;
endpointBuilder.Metadata.Add(newRouteConfig);

if (source.Match.Hosts != null && source.Match.Hosts.Count != 0)
{
endpointBuilder.Metadata.Add(new AspNetCore.Routing.HostAttribute(source.Match.Hosts.ToArray()));
}

bool acceptCorsPreflight;
if (string.Equals(CorsConstants.Default, source.CorsPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(DefaultCors);
acceptCorsPreflight = true;
}
else if (string.Equals(CorsConstants.Disable, source.CorsPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(DisableCors);
acceptCorsPreflight = true;
}
else if (!string.IsNullOrEmpty(source.CorsPolicy))
{
endpointBuilder.Metadata.Add(new EnableCorsAttribute(source.CorsPolicy));
acceptCorsPreflight = true;
}
else
{
acceptCorsPreflight = false;
}

if (source.Match.Methods != null && source.Match.Methods.Count > 0)
{
endpointBuilder.Metadata.Add(new AspNetCore.Routing.HttpMethodMetadata(source.Match.Methods, acceptCorsPreflight));
}

if (string.Equals(AuthorizationConstants.Default, source.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(DefaultAuthorization);
}
else if (!string.IsNullOrEmpty(source.AuthorizationPolicy))
{
endpointBuilder.Metadata.Add(new AuthorizeAttribute(source.AuthorizationPolicy));
}

var endpoint = endpointBuilder.Build();
aspNetCoreEndpoints.Add(endpoint);

return newRouteConfig;
}
}
Expand Down
109 changes: 109 additions & 0 deletions src/ReverseProxy/Service/DynamicEndpoint/ProxyEndpointFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract;
using Microsoft.ReverseProxy.RuntimeModel;
using CorsConstants = Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract.CorsConstants;

namespace Microsoft.ReverseProxy
{
internal class ProxyEndpointFactory
{
private static readonly IAuthorizeData _defaultAuthorization = new AuthorizeAttribute();
private static readonly IEnableCorsAttribute _defaultCors = new EnableCorsAttribute();
private static readonly IDisableCorsAttribute _disableCors = new DisableCorsAttribute();

private RequestDelegate _pipeline;

public void AddEndpoint(List<Endpoint> endpoints,
RouteConfig route,
IReadOnlyList<Action<EndpointBuilder>> conventions)
{
var proxyRoute = route.ProxyRoute;
var proxyMatch = proxyRoute.Match;

// Catch-all pattern when no path was specified
var pathPattern = string.IsNullOrEmpty(proxyMatch.Path) ? "/{**catchall}" : proxyMatch.Path;

var endpointBuilder = new AspNetCore.Routing.RouteEndpointBuilder(
requestDelegate: _pipeline ?? Invoke,
routePattern: AspNetCore.Routing.Patterns.RoutePatternFactory.Parse(pathPattern),
order: proxyRoute.Order.GetValueOrDefault())
{
DisplayName = proxyRoute.RouteId
};

endpointBuilder.Metadata.Add(route);

if (proxyMatch.Hosts != null && proxyMatch.Hosts.Count != 0)
{
endpointBuilder.Metadata.Add(new HostAttribute(proxyMatch.Hosts.ToArray()));
}

bool acceptCorsPreflight;
if (string.Equals(CorsConstants.Default, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(_defaultCors);
acceptCorsPreflight = true;
}
else if (string.Equals(CorsConstants.Disable, proxyRoute.CorsPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(_disableCors);
acceptCorsPreflight = true;
}
else if (!string.IsNullOrEmpty(proxyRoute.CorsPolicy))
{
endpointBuilder.Metadata.Add(new EnableCorsAttribute(proxyRoute.CorsPolicy));
acceptCorsPreflight = true;
}
else
{
acceptCorsPreflight = false;
}

if (proxyMatch.Methods != null && proxyMatch.Methods.Count > 0)
{
endpointBuilder.Metadata.Add(new HttpMethodMetadata(proxyMatch.Methods, acceptCorsPreflight));
}

if (string.Equals(AuthorizationConstants.Default, proxyRoute.AuthorizationPolicy, StringComparison.OrdinalIgnoreCase))
{
endpointBuilder.Metadata.Add(_defaultAuthorization);
}
else if (!string.IsNullOrEmpty(proxyRoute.AuthorizationPolicy))
{
endpointBuilder.Metadata.Add(new AuthorizeAttribute(proxyRoute.AuthorizationPolicy));
}

for (var i = 0; i < conventions.Count; i++)
{
conventions[i](endpointBuilder);
}

endpoints.Add(endpointBuilder.Build());
}

// This indirection is needed because on startup the routes are loaded from config and built before the
// proxy pipeline gets built.
private Task Invoke(HttpContext context)
{
var pipeline = _pipeline ?? throw new InvalidOperationException("The pipeline hasn't been provided yet.");
return pipeline(context);
}

public void SetProxyPipeline(RequestDelegate pipeline)
{
_pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
}
}
}
Loading

0 comments on commit 19a7313

Please sign in to comment.