A toolkit to create code-first HTTP Reverse Proxies hosted in ASP.NET Core as middleware. Thisallows focused code-first proxies that can be embedded in existing ASP.NET Coreapplications or deployed as a standalone server. Deployable anywhere ASP.NETCore is deployable such as Windows, Linux, Containers and Serverless (withcaveats).
Having built proxies many times before, I felt it is time to make a package. Forkedfrom ASP.NET labs, it has been heavily modified with a differentAPI, to facilitate a wider variety of proxying scenarios (i.e. routing based ona JWT claim) and interception of the proxy requests / responses forcustomization of headers and (optionally) request / response bodies. It alsouses HttpClientFactory
internally that will mitigate against DNS cachingissues making it suitable for microservice / container environments.
ProxyKit is a NetStandard2.0
package. Install into your ASP.NET Core project:
dotnet add package ProxyKit
In your Startup
, add the proxy service:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddProxy();
...
}
Forward HTTP requests to upstream-server:5001
:
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context => context
.ForwardTo("http://upstream-server:5001/")
.AddXForwardedHeaders()
.Send());
}
What is happening here?
context.ForwardTo(upstreamHost)
is an extension method onHttpContext
that creates and initializes an HttpRequestMessage
withthe original request headers copied over, yielding a ForwardContext
.AddXForwardedHeaders
adds X-Forwarded-For
, X-Forwarded-Host
,X-Forwarded-Proto
and X-Forwarded-PathBase
headers to the upstreamrequest.Send
Sends the forward request to the upstream server and returns anHttpResponseMessage
.HttpContext.Response
.Note: RunProxy
is terminal - anything added to the pipeline afterRunProxy
will never be executed.
Forward WebSocket requests to upstream-server:5002
:
public void Configure(IApplicationBuilder app)
{
app.UseWebSockets();
app.UseWebSocketProxy(
context => new Uri("ws://upstream-host:80/"),
options => options.AddXForwardedHeaders());
}
What is happening here?
app.UseWebSockets()
must first be added otherwise websocket requests willnever be handled by ProxyKit.ws://
.options
allows you to do some customisation of theinitial upstream requests such as adding some headers.One can modify the upstream request headers prior to sending them to suitcustomisation needs. ProxyKit doesn't add, remove, nor modify any headers bydefault; one must opt in any behaviours explicitly.
In this example we will add a X-Correlation-ID
header if the incoming request does not bear one:
public const string XCorrelationId = "X-Correlation-ID";
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context =>
{
var forwardContext = context.ForwardTo("http://upstream-server:5001/");
if (!forwardContext.UpstreamRequest.Headers.Contains(XCorrelationId))
{
forwardContext.UpstreamRequest.Headers.Add(XCorrelationId, Guid.NewGuid().ToString());
}
return forwardContext.Send();
});
}
This can be encapsulated as an extension method:
public static class CorrelationIdExtensions
{
public const string XCorrelationId = "X-Correlation-ID";
public static ForwardContext ApplyCorrelationId(this ForwardContext forwardContext)
{
if (!forwardContext.UpstreamRequest.Headers.Contains(XCorrelationId))
{
forwardContext.UpstreamRequest.Headers.Add(XCorrelationId, Guid.NewGuid().ToString());
}
return forwardContext;
}
}
... making the proxy code a little nicer to read:
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context => context
.ForwardTo("http://upstream-server:5001/")
.ApplyCorrelationId()
.Send());
}
The response from an upstream server can be modified before it is sent to theclient. In this example we are removing a header:
public void Configure(IApplicationBuilder app)
{
app.RunProxy(async context =>
{
var response = await context
.ForwardTo("http://localhost:5001/")
.Send();
response.Headers.Remove("MachineID");
return response;
});
}
X-Forward-*
headers from the incoming request to the upstreamrequest by default. Copying them requires opting in; see 2.3.3 CopyingX-Forwarded headers below.
X-Forwarded-*
HeadersMany applications will need to know what their "outside" host / URL is in orderto generate correct values. This is achieved using X-Forwarded-*
andForwarded
headers. ProxyKit supports applying X-Forward-*
headers out of thebox (applying Forwarded
headers support is on backlog). At the time of writing,Forwarded
is not supportedin ASP.NET Core.
To add X-Forwarded-*
headers to the request to the upstream server:
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context => context
.ForwardTo("http://upstream-server:5001/")
.AddXForwardedHeaders()
.Send());
}
This will add X-Forwarded-For
, X-Forwarded-Host
and X-Forwarded-Proto
headers to the upstream request using values from HttpContext
. If the proxymiddleware is hosted on a path and a PathBase
exists on the request, then anX-Forwarded-PathBase
is also added.
X-Forwarded
headersChaining proxies is a common pattern in more complex setups. In this case, ifthe proxy is an "internal" proxy, you will want to copy the "X-Forwarded-*"headers from previous proxy. To do so, use CopyXForwardedHeaders()
:
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context => context
.ForwardTo("http://upstream-server:5001/")
.CopyXForwardedHeaders()
.Send());
}
You may optionally also add the "internal" proxy details to the X-Forwarded-*
header values by combining CopyXForwardedHeaders()
andAddXForwardedHeaders()
(note the order is important):
public void Configure(IApplicationBuilder app)
{
app.RunProxy(context => context
.ForwardTo("http://upstream-server:5001/")
.CopyXForwardedHeaders()
.AddXForwardedHeaders()
.Send());
}
When adding the Proxy to your application's service collection, there is anopportunity to configure the internal HttpClient. AsHttpClientFactory
is used, its builder is exposed for you to configure:
services.AddProxy(httpClientBuilder => /* configure http client builder */);
Below are two examples of what you might want to do:
Configure the HTTP Client's timeout to 5 seconds:
services.AddProxy(httpClientBuilder =>
httpClientBuilder.ConfigureHttpClient =
client => client.Timeout = TimeSpan.FromSeconds(5));
Configure the primary HttpMessageHandler
. This is typically used in testingto inject a test handler (see Testing below).
services.AddProxy(httpClientBuilder =>
httpClientBuilder.ConfigurePrimaryHttpMessageHandler =
() => _testMessageHandler);
When HttpClient
throws, the following logic applies:
503 ServiceUnavailable
is returned.504 GatewayTimeout
isreturned.Not all exception scenarios and variations are caught, which may result in aInternalServerError
being returned to your clients. Please create an issue ifa scenario is missing.
As ProxyKit is a standard ASP.NET Core middleware, it can be tested using thestandard in-memory TestServer
mechanism.
Often you will want to test ProxyKit with your application and perhaps test thebehaviour of your application when load balanced with two or more instances asindicated below.
+----------+
|"Outside" |
|HttpClient|
+-----+----+
|
|
|
+-----------+---------+
+-------------------->RoutingMessageHandler|
| +-----------+---------+
| |
| |
| +--------------------+-------------------------+
| | | |
+---+-----------v----+ +--------v---------+ +---------v--------+
|Proxy TestServer | |Host1 TestServer | |Host2 TestServer |
|with Routing Handler| |HttpMessageHandler| |HttpMessageHandler|
+--------------------+ +------------------+ +------------------+
RoutingMessageHandler
is an HttpMessageHandler
that will route requeststo specific hosts based on the origin it is configured with. For ProxyKitto forward requests (in memory) to the upstream hosts, it needs to be configuredto use the RoutingMessageHandler
as its primary HttpMessageHandler
.
Full example can been viewed in Recipe 6.
Load balancing is a mechanism to decide which upstream server to forward therequest to. Out of the box, ProxyKit currently supports one type ofload balancing - Weighted Round Robin. Other types are planned.
Round Robin simply distributes requests as they arrive to the next host in adistribution list. With optional weighting, more requests are sent to the host withthe greater weight.
public void Configure(IApplicationBuilder app)
{
var roundRobin = new RoundRobin
{
new UpstreamHost("http://localhost:5001/", weight: 1),
new UpstreamHost("http://localhost:5002/", weight: 2)
};
app.RunProxy(
async context =>
{
var host = roundRobin.Next();
return await context
.ForwardTo(host)
.Send();
});
}
New in version 2.1.0
Instead of specifying a delegate, it is possible to use a typed handler. Thereason you may want to do this is when you want to better leverage dependencyinjection.
Typed handlers must implement IProxyHandler
that has a single method with samesignature as HandleProxyRequest
. In this example our typed handler has adependency on an imaginary service to lookup hosts:
public class MyTypedHandler : IProxyHandler
{
private IUpstreamHostLookup _upstreamHostLookup;
public MyTypeHandler(IUpstreamHostLookup upstreamHostLookup)
{
_upstreamHostLookup = upstreamHostLookup;
}
public Task<HttpResponseMessage> HandleProxyRequest(HttpContext context)
{
var upstreamHost = _upstreamHostLookup.Find(context);
return context
.ForwardTo(upstreamHost)
.AddXForwardedHeaders()
.Send();
}
}
We then need to register our typed handler service:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSingleton<MyTypedHandler>();
...
}
When adding the proxy to the pipeline, use the generic form:
public void ConfigureServices(IServiceCollection services)
{
...
appInner.RunProxy<MyTypedHandler>());
...
}
Recipes have moved to own repo.
Applications that are deployed behind a reverse proxy typically need to besomewhat aware of that so they can generate correct URLs and paths whenresponding to a browser. That is, they look at X-Forward-*
/ Forwarded
headers and use their values.
In ASP.NET Core, this means using the ForwardedHeaders
middleware in yourapplication. Please refer to the documentationfor correct usage (and note the security advisory!).
Note: the Forwarded Headers middleware does not supportX-Forwarded-PathBase
. This means if you proxy http://example.com/foo/
tohttp://upstream-host/
the /foo/
part is lost and absolute URLs cannot begenerated unless you configure your application's PathBase
directly.
Related issues and discussions:
To support PathBase dynamically in your application with X-Forwarded-PathBase
,examine the header early in your pipeline and set the PathBase
accordingly:
var options = new ForwardedHeadersOptions
{
...
};
app.UseForwardedHeaders(options);
app.Use((context, next) =>
{
if (context.Request.Headers.TryGetValue("X-Forwarded-PathBase", out var pathBases))
{
context.Request.PathBase = pathBases.First();
}
return next();
});
Alternatively you can use ProxyKit's UseXForwardedHeaders
extension thatperforms the same as the above (including calling UseForwardedHeaders
):
var options = new ForwardedHeadersOptions
{
...
};
app.UseXForwardedHeaders(options);
According to TechEmpower's Web Framework Benchmarks, ASP.NET Core is up therewith the fastest for plaintext.As ProxyKit simply captures headers and async copies request and response bodystreams, it will be fast enough for most scenarios.
If absolute raw throughput is a concern for you, thenconsider nginx or alternatives. For me being able to create flexible proxiesusing C# is a reasonable tradeoff for the (small) performance cost. Note thatwhat your specific proxy (and its specific configuration) does will impact performanceso you should measure for yourself in your context.
On Windows, ProxyKit is ~3x faster than nginx. However, nginx has clearlydocumented that it has knownperformance issues on Windows. Sinceone wouldn't be running production nginx on Windows, this comparison isacademic.
Memory wise, ProxyKit maintained a steady ~20MB of RAM after processing millionsof requests for simple forwarding. Again, it depends on what your proxy does soyou should analyse and measure yourself.
Whilst it is possible to run full ASP.NET Core web application in AWSLambda and Azure Functions it should be noted that Serverless systems aremessage based and not stream based. Incoming and outgoing HTTP request messageswill be buffered and potentially encoded as Base64 if binary (so larger). Thismeans ProxyKit should only be used for API (json) proxying in production onServerless. (Though proxying other payloads is fine for dev / exploration /quick'n'dirty purposes.)
Ocelot is an API Gateway that also runs on ASP.NET Core. A key differencebetween API Gateways and general Reverse Proxies is that the former tend to bemessage based whereas a reverse proxy is stream based. That is, an APIGateway will typically buffer every request and response message to be ableto perform transformations. This is fine for an API Gateway but not suitable fora general reverse proxy performance wise nor for responses that arechunked-encoded. See Not Supported Ocelot docs.
Combining ProxyKit with Ocelot would give some nice options for a variety ofscenarios.
Requirements: .NET Core SDK 2.2.100 or later.
On Windows:
.\build.cmd
On Linux:
./build.sh
Any ideas for features, bugs or questions, please create an issue. Pull requestsgratefully accepted but please create an issue for discussion first.
I can be reached on twitter at @randompunter
logo is distribute by ChangHoon Baek from the Noun Project.
方法一:这个方法很漂亮,但是,有问题,不知道什么原因,cookie偶尔会收不到,而造成验证错误,提交内容也会错误 扩展类: public class MyTypedHandler : IProxyHandler { private IConfiguration _upstreamHostLookup; public MyTypedHandler(I