Coder Social home page Coder Social logo

proxykit / proxykit Goto Github PK

View Code? Open in Web Editor NEW
1.1K 44.0 132.0 699 KB

A toolkit to create code-first HTTP reverse proxies on ASP.NET Core

License: Apache License 2.0

C# 99.33% Batchfile 0.36% Shell 0.31%
dotnet proxy reverse-proxy serverless aspnetcore middleware obsolete-soon

proxykit's Introduction

ProxyKit

Image

Build Status NuGet Feedz

⚠️ ProxyKit is obsolescent. See announcement. Migrate to YARP.

A toolkit to create code-first HTTP Reverse Proxies hosted in ASP.NET Core as middleware. This allows focused code-first proxies that can be embedded in existing ASP.NET Core applications or deployed as a standalone server. Deployable anywhere ASP.NET Core is deployable such as Windows, Linux, Containers and Serverless (with caveats).

Having built proxies many times before, I felt it is time to make a package. Forked from ASP.NET labs, it has been heavily modified with a different API, to facilitate a wider variety of proxying scenarios (i.e. routing based on a JWT claim) and interception of the proxy requests / responses for customization of headers and (optionally) request / response bodies. It also uses HttpClientFactory internally that will mitigate against DNS caching issues making it suitable for microservice / container environments.

1. Quick Start

1.1. Install

ProxyKit is a NetStandard2.0 package. Install into your ASP.NET Core project:

dotnet add package ProxyKit

1.2. Forward HTTP Requests

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?

  1. context.ForwardTo(upstreamHost) is an extension method on HttpContext that creates and initializes an HttpRequestMessage with the original request headers copied over, yielding a ForwardContext.
  2. AddXForwardedHeaders adds X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto and X-Forwarded-PathBase headers to the upstream request.
  3. Send Sends the forward request to the upstream server and returns an HttpResponseMessage.
  4. The proxy middleware then takes the response and applies it to HttpContext.Response.

Note: RunProxy is terminal - anything added to the pipeline after RunProxy will never be executed.

1.3. Forward WebSocket Requests

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?

  1. app.UseWebSockets() must first be added otherwise websocket requests will never be handled by ProxyKit.
  2. The first parameter must return the URI of the upstream host with a scheme of ws://.
  3. The second parameter options allows you to do some customisation of the initial upstream requests such as adding some headers.

2. Core Features

2.1. Customising the upstream HTTP request

One can modify the upstream request headers prior to sending them to suit customisation needs. ProxyKit doesn't add, remove, nor modify any headers by default; 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());
}

2.2. Customising the upstream response

The response from an upstream server can be modified before it is sent to the client. 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;
    });
}

2.3. X-Forwarded Headers

2.3.1. Client Sent X-Forwarded-Headers

⚠️ To mitigate against spoofing attacks and misconfiguration ProxyKit does not copy X-Forward-* headers from the incoming request to the upstream request by default. Copying them requires opting in; see 2.3.3 Copying X-Forwarded headers below.

2.3.2. Adding X-Forwarded-* Headers

Many applications will need to know what their "outside" host / URL is in order to generate correct values. This is achieved using X-Forwarded-* and Forwarded headers. ProxyKit supports applying X-Forward-* headers out of the box (applying Forwarded headers support is on backlog). At the time of writing, Forwarded is not supported in 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 proxy middleware is hosted on a path and a PathBase exists on the request, then an X-Forwarded-PathBase is also added.

2.3.3. Copying X-Forwarded headers

Chaining proxies is a common pattern in more complex setups. In this case, if the 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() and AddXForwardedHeaders() (note the order is important):

public void Configure(IApplicationBuilder app)
{
    app.RunProxy(context => context
        .ForwardTo("http://upstream-server:5001/")
        .CopyXForwardedHeaders()
        .AddXForwardedHeaders()
        .Send());
}

2.4. Configuring ProxyKit's HttpClient

When adding the Proxy to your application's service collection, there is an opportunity to configure the internal HttpClient. As HttpClientFactory 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:

  1. Configure the HTTP Client's timeout to 5 seconds:

    services.AddProxy(httpClientBuilder =>
        httpClientBuilder.ConfigureHttpClient =
            client => client.Timeout = TimeSpan.FromSeconds(5));
  2. Configure the primary HttpMessageHandler. This is typically used in testing to inject a test handler (see Testing below).

    services.AddProxy(httpClientBuilder =>
        httpClientBuilder.ConfigurePrimaryHttpMessageHandler = 
            () => _testMessageHandler);

2.5. Error handling

When HttpClient throws, the following logic applies:

  1. When upstream server is not reachable, then 503 ServiceUnavailable is returned.
  2. When upstream server is slow and client timeouts, then 504 GatewayTimeout is returned.

Not all exception scenarios and variations are caught, which may result in a InternalServerError being returned to your clients. Please create an issue if a scenario is missing.

2.6. Testing

As ProxyKit is a standard ASP.NET Core middleware, it can be tested using the standard in-memory TestServer mechanism.

Often you will want to test ProxyKit with your application and perhaps test the behaviour of your application when load balanced with two or more instances as indicated 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 requests to specific hosts based on the origin it is configured with. For ProxyKit to forward requests (in memory) to the upstream hosts, it needs to be configured to use the RoutingMessageHandler as its primary HttpMessageHandler.

Full example can been viewed in Recipe 6.

2.7. Load Balancing

Load balancing is a mechanism to decide which upstream server to forward the request to. Out of the box, ProxyKit currently supports one type of load balancing - Weighted Round Robin. Other types are planned.

2.7.1. Weighted Round Robin

Round Robin simply distributes requests as they arrive to the next host in a distribution list. With optional weighting, more requests are sent to the host with the 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();
        });
}

2.8. Typed Handlers

New in version 2.1.0

Instead of specifying a delegate, it is possible to use a typed handler. The reason you may want to do this is when you want to better leverage dependency injection.

Typed handlers must implement IProxyHandler that has a single method with same signature as HandleProxyRequest. In this example our typed handler has a dependency 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>());
    ...
}

3. Recipes

Recipes have moved to own repo.

4. Making upstream servers reverse proxy friendly

Applications that are deployed behind a reverse proxy typically need to be somewhat aware of that so they can generate correct URLs and paths when responding 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 your application. Please refer to the documentation for correct usage (and note the security advisory!).

Note: the Forwarded Headers middleware does not support X-Forwarded-PathBase. This means if you proxy http://example.com/foo/ to http://upstream-host/ the /foo/ part is lost and absolute URLs cannot be generated 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 that performs the same as the above (including calling UseForwardedHeaders):

var options = new ForwardedHeadersOptions
{
   ...
};
app.UseXForwardedHeaders(options);

5. Performance considerations

According to TechEmpower's Web Framework Benchmarks, ASP.NET Core is up there with the fastest for plain text. As ProxyKit simply captures headers and async copies request and response body streams, it will be fast enough for most scenarios.

If absolute raw throughput is a concern for you, then consider nginx or alternatives. For me being able to create flexible proxies using C# is a reasonable tradeoff for the (small) performance cost. Note that what your specific proxy (and its specific configuration) does will impact performance so you should measure for yourself in your context.

On Windows, ProxyKit is ~3x faster than nginx. However, nginx has clearly documented that it has known performance issues on Windows. Since one wouldn't be running production nginx on Windows, this comparison is academic.

Memory wise, ProxyKit maintained a steady ~20MB of RAM after processing millions of requests for simple forwarding. Again, it depends on what your proxy does so you should analyse and measure yourself.

6. Note about serverless

Whilst it is possible to run full ASP.NET Core web application in AWS Lambda and Azure Functions it should be noted that Serverless systems are message based and not stream based. Incoming and outgoing HTTP request messages will be buffered and potentially encoded as Base64 if binary (so larger). This means ProxyKit should only be used for API (json) proxying in production on Serverless. (Though proxying other payloads is fine for dev / exploration / quick'n'dirty purposes.)

7. Comparison with Ocelot

Ocelot is an API Gateway that also runs on ASP.NET Core. A key difference between API Gateways and general Reverse Proxies is that the former tend to be message based whereas a reverse proxy is stream based. That is, an API Gateway will typically buffer every request and response message to be able to perform transformations. This is fine for an API Gateway but not suitable for a general reverse proxy performance wise nor for responses that are chunked-encoded. See Not Supported Ocelot docs.

Combining ProxyKit with Ocelot would give some nice options for a variety of scenarios.

8. How to build

Requirements: .NET Core SDK 2.2.100 or later.

On Windows:

.\build.cmd

On Linux:

./build.sh

9. Contributing / Feedback / Questions

Any ideas for features, bugs or questions, please create an issue. Pull requests gratefully accepted but please create an issue for discussion first.

I can be reached on twitter at @randompunter

10. Articles, blogs and other external links


logo is distribute by ChangHoon Baek from the Noun Project.

proxykit's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

proxykit's Issues

X-Forwarded-Host missing Port

Currently trying to build a proxy to an application that needs to build a redirect url to the Proxy location rather than the forwarded location. This application will be forwarded to from a number of locations so it must be dynamic. By default .Net core will build this redirect with either the Host header that can be overriden with the the X-Forwarded-Host header, using the app.UseXForwardedHeaders automatically.

My issue is that currently the X-Forwarded-Host header is missing the port (e.g. localhost rather than localhost:5000), this would probably not affect production scenarios as they will use the 80 port.

Is this intentionally to remove the port in XForwardedExtensions.cs Line 59 or can I submit a PR that will change that from host.Host to host.Value (which resolves the issue)

Here are the resources I have found suggesting that X-Forwarded-Host should contain the host and port:
https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Common_non-standard_request_fields
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host

Google Drive video playback

I am wanting to use this project to 'proxy' video playback to Google Drive. An example of the code I am currently using:

[HttpGet("{id}")]
[Authorize]
public async Task<IActionResult> GetAsync([FromRoute] string id, [FromQuery] int? quality)
{
    HttpResponseMessage result = null;
    using (var httpClient = new HttpClient())
    {
        httpClient.DefaultRequestHeaders.Authorization = await this._TokenManager.GetGoogleDriveAuthHeaderAsync();
        result = await httpClient.GetAsync($"https://drive.google.com/get_video_info?docid={id}");
        var actualResult = await result.Content.ReadAsStringAsync();
        var videoInfo = this._VideoInfoParser.Parse(actualResult);
        var q = videoInfo.maps.Where(m => m.quality == "medium").FirstOrDefault();
        return new FileStreamResult(await httpClient.GetStreamAsync(q.url), "video/mp4");
    }
}

Is there a way to do this with ProxyKit or do I need to look for another solution?

Inconsistent handling of url slashes compared to nginx

Give these similar configurations:

app.Map("/a", d => d.RunProxy(context => context
    .ForwardTo("http://10.0.75.1:5002/a")
    .Send()));
app.Map("/b", d => d.RunProxy(context => context
    .ForwardTo("http://10.0.75.1:5002/b/")
    .Send()));

location /a {
            proxy_pass   http://10.0.75.1:5002/a;
        }

location /b {
    proxy_pass   http://10.0.75.1:5002/b/;
}

All the requests are consistent except one.

Request At upstream via Nginx At upstream via ProxyKit
http://localhost/a /a /a
http://localhost/a/ /a/ /a/
http://localhost/b /b/ /b/
http://localhost/b/ /b// /b/

Fix OptionsPattern for ProxyOptions

(I am pushing a pull request soon for this issue)

Currently Action<ProxyOptions> configureOptions is never called because of wrong DI declaration:

            services.Configure(configureOptions);
            services.AddTransient<ProxyOptions>();

Basically, ProxyOptions is always created and never configured if we put configureOptions in.

Suggestion: change the code to:

            services.AddOptions<ProxyOptions>()
                .Configure(configureOptions);

And currently, only WebSocketProxyMiddleware is using it, the correct DI declaration should be:

        public WebSocketProxyMiddleware(
            RequestDelegate next,
            IOptionsMonitor<ProxyOptions> options,
            Uri destinationUri,
            ILogger<WebSocketProxyMiddleware> logger)
        {
            _next = next;
            _options = options.CurrentValue;
            _destinationUri = destinationUri;
            _logger = logger;
        }

Avoid GetRequiredService in ForwardTo?

Treat this issue as an invitation to a discussion rather than a bug.

I'm analyzing the memory usage with dotMemory when running 125 concurrent connections "bombarding" the proxy over 7 min. Total request count around 138k.

The memory pattern is quite bumpy:
image

I'm seeing this:
image

Which expands to this:
image

I'm thinking the call to GetRequiredService is triggering reflection?

Support for response caching

I would like to cache responses received from upstream servers based on standard http caching headers.

My scenario involves some slow-changing reference data that could be cached by ProxyKit and save a network trip to the upstream server.

It would be nice to be able to enable caching for individual routes, but in my scenario that isn't necessary.

Happy to contribute when given some guidelines.

Allow rewriting Response's Location header

Scenario:

  • User enters https://www.example.com/foo (Server1)

  • Server1 proxies the request to https://localhost:123/foo (Server2)

  • Server2 responds with 202 or 301 with header Location: https://localhost:123/bar.

  • Server1 forwards that header back, which when redirected, user is no longer accessing Server1.

Suggestion: add an option to allow rewriting Location header if it is present and is absolute Uri and the origin (scheme, host and port) is same as Server2's.

(I am working on it, going to create a pull request)

RoundRobbin: Integer overflow with Interlocked.Increment

Here:

https://github.com/damianh/ProxyKit/blob/ebc9ac5363122ded184d5f15277f96cbcb0af987/src/ProxyKit/RoundRobin.cs#L52

_position is incremented for each incoming request. After int.MaxValue (2 billion) requests, the _position will wrap around to -2,147,483,648 and break indexing into the distribution.

2 billion requests is a lot, but not improbable for months of uptime from a proxy. The easiest fix is to keep _position as a long. 2^63 requests is a lot and probably more than the lifetime of the process.

At 1,000,000 req/s, the signed 32-bit integer will overflow in about 35 minutes.
At 1,000,000 req/s, the signed 64-bit integer will overflow in about 292,471 years.

A "permanent" fix would be to reset the counter instead of using mod when reaching the end, but that can be tricky to do lockless and safely.

Performance and configuration advice

I'm running in to some performance troubles while running ProxyKit in Azure (ServicePlan: S1 and P2v2).
I'm using Azure Load test with 1000 concurrent users.

What's the expected capacity for this setup?

  • Are there any important configurations in hosting to make?
  • Out Of Process / In Process / Kestrel?
  • AspnetCoreModuleV2 vs AspnetCoreModule?

I get these exceptions:

System.Net.Http.HttpRequestException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full ---> System.Net.Sockets.SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.WaitForCreatedConnectionAsync(ValueTask`1 creationTask)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
System.Net.Http.HttpRequestException: Only one usage of each socket address (protocol/network address/port) is normally permitted ---> System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.CreateConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.WaitForCreatedConnectionAsync(ValueTask`1 creationTask)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at ReverseProxy.Proxy.<ProxyWeb>b__5_1(HttpContext context) in C:\redacted\ReverseProxy.cs:line 115
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Server.IISIntegration.IISMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

Changing the project title

After a straw poll at #flashcon, we’ve decided that we believe that the project name doesn’t reflect the ability of the project

Therefore, we need to track a motion to rename

HTTP to HTTPS Redirect

I have a simple redirect configured where the upstream host URL is https. The proxy's endpoint is http. During inspection (using webhook.site, the x-forwarded-proto and x-original-proto are both http. I am making an assumption the x-forwarded-proto should be https if my target/destination (right side of the proxy) was configured with an https protocol. Can someone elaborate or provide clarity on how I can configure to ultimately do an http-https forward?

Ignore Invalid SSL certificate?

I have a simple Proxy:

            app.RunProxy(context =>
            {
                var message = context
                    .ForwardTo("https://localhost:19101")
                    .Send();

                return message;
            });

Problem the SSL served at localhost:19101 is not valid (it is from Cloudflare). I have tried installing it to my Trusted Root CA but it still does not work.

The exception is:

AuthenticationException: The remote certificate is invalid according to the validation procedure.
System.Net.Security.SslState.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, ExceptionDispatchInfo exception)

HttpRequestException: The SSL connection could not be established, see inner exception.
System.Net.Http.ConnectHelper.EstablishSslConnectionAsyncCore(Stream stream, SslClientAuthenticationOptions sslOptions, CancellationToken cancellationToken)

I understand why I receive it but is there any way to bypass it?

Cannot use UsePathBase() in WebSockets/SignalR site

I've tried 2.1.0-beta.1, found it doesn't support UsePathBase() in the WebSockets/SignalR site.

I use code from 17_SignalROnPath.cs, it shows signalrApp.UseWebSocketProxy(context => new Uri("ws://upstream-host:80"));. First, I suggest to use ws://localhost:80 as sample target, because the next RunProxy uses http://localhost:80.

My target SignalR site uses UsePathBase("/basepath"). The problem happens when using UseWebSocketProxy(context => new Uri("ws://localhost:80/basepath/")). The log shows:

ProxyKit.WebSocketProxyMiddleware	Forwarding websocket connection to ws://localhost:80/path?id=...
ProxyKit.WebSocketProxyMiddleware	Error connecting to server	
Void MoveNext() System.Net.WebSockets.WebSocketException Unable to connect to the remote server
System.Net.WebSockets.WebSocketException (0x80004005): Unable to connect to the remote server
   at System.Net.WebSockets.WebSocketHandle.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
   at System.Net.WebSockets.ClientWebSocket.ConnectAsyncCore(Uri uri, CancellationToken cancellationToken)
   at ProxyKit.WebSocketProxyMiddleware.AcceptProxyWebSocketRequest(HttpContext context, Uri upstreamUri) in /home/travis/build/damianh/ProxyKit/src/ProxyKit/WebSocketProxyMiddleware.cs:line 122

The wrong ws://localhost:80/path?id=... should be ws://localhost:80/subpath/path?id=.... According to WebSocketProxyMiddleware.cs line 80, the relativePath value is /path. Line 82-84 var uriWithPath = new Uri(upstreamUri.Uri, relativePath.Length >= 0 ? relativePath : ""); will generate ws://localhost:80/path, that is wrong. The correct one is ws://localhost:80/subpath/path.

I use Try .NET with the following code:

using System;
public class Program
{
  public static void Main()
  {
    Console.WriteLine(GetPath("ws://localhost:80/subpath/", "/path"));
    Console.WriteLine(GetPath("ws://localhost:80/subpath/", "path"));
    Console.WriteLine(GetPath("ws://localhost:80/subpath", "/path"));
    Console.WriteLine(GetPath("ws://localhost:80/subpath", "path"));
   }
  private static Uri GetPath(string upstreamUrl, string relativePath)
  {
    return new Uri(new Uri(upstreamUrl), relativePath.Length >= 0 ? relativePath : "");
  }
}

The output is:

ws://localhost/path
ws://localhost/subpath/path
ws://localhost/path
ws://localhost/path

Only the second one is correct. According to Uri Constructor: Uri(Uri, String), its Remarks says: Additionally, if the relativeUri begins with a slash, then it will replace any relative part of the baseUri.

That means in the constructor of Uri, the baseUri should include the last back slash, and the relativeUri shouldn't includes back slash at the beginning. But for current code of WebSocketProxyMiddleware.cs line 80, the relativePath is always starts with back slash. That means I will never get ws://localhost/subpath/path, and have no way to use /subpath in WebSockets/SignalR site.

Clean up tests

Tests were originally imported as is from aspnet core proxy repo. As more tests added, style has gone in a different direction. They need a general cleanup and refactoring.

CopyProxyHttpResponse related Exception

I was catching 12 exceptions in a week that all related to this one.

System.NullReferenceException: Object reference not set to an instance of an object.
at ProxyKit.ProxyMiddleware.<CopyProxyHttpResponse>d__4.MoveNext() at offset 37
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at ProxyKit.ProxyMiddleware.<Invoke>d__3.MoveNext() at offset 236
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
...

Is this Exception related to CopyProxyHttpResponse? I doubted this could be related to client disconnects. What do you think?

GZip/Deflate Example

Could you please provide a demo of modifying a response which is GZip encoded?

OperationCanceledException: The operation was canceled

I just catched an exception today. The stack trace information below:

System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowOperationCanceledException() at offset 16
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.TimingPipeFlusher.<TimeFlushAsync>d__10.MoveNext() at offset 237
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at System.Net.Http.HttpConnection.<CopyFromBufferAsync>d__96.MoveNext() at offset 194
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at System.Net.Http.HttpConnection.<CopyToExactLengthAsync>d__99.MoveNext() at offset 499
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at System.Net.Http.ContentLengthReadStream.<CompleteCopyToAsync>d__4.MoveNext() at offset 137
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at ProxyKit.ProxyMiddleware.<CopyProxyHttpResponse>d__5.MoveNext() at offset 530
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task task) at offset 46
at ProxyKit.ProxyMiddleware.<Invoke>d__4.MoveNext() at offset 236
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at offset 12
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(System.Threading.Tasks.Task task) at offset 38

Do you have any clue for possible reason of this exception?

Modify HttpClient's Message Handler from within the context of RunProxy Possible?

I need to attach credentials to the HttpClientHandler if and only if the previous response from the server came back as 401 with an NTLM challenge. This due to a limitation with NetworkCredentials being attached where one is not being expected resulting an in error with .net core.

I don't seen an obvious way to accomplish this when using httpClientBuilder.ConfigurePrimaryHttpMessageHandler. Is there any way to update the httpclient's message handler?

ForwardTo Uri is incorrect when using .MapWhen()

Thanks for the cool project. Very useful.

I am loading proxy maps from a config file like this:

"UrlMaps": [
        {
            "path": "/sysa",
            "target": "https://localhost:5001/sysatest/",
            "workerName": "sys1-worker"
        },
        {
            "path": "/sysb",
            "target": "https://localhost:5001/sysbtest/",
            "workerName": "sys2-worker"
        },
        {
            "path": "/sysc",
            "target": "https://localhost:5001/sysctest/",
            "workerName": "sys3-worker"
        }
    ]

Then in my Startup.cs I do:

app.MapWhen(context => knownPaths.Contains(context.Request.Path), appInner =>
{
	appInner.RunProxy(context =>
	{
		var targetUrl = urlMaps[context.Request.Path];

		return context
			.ForwardTo(targetUrl)
			.AddXForwardedHeaders()
			.Send();
	});
});

The problem:

When I hit /sysb I would expect to be forwarded to https://localhost:5001/sysbtest/.
What actually happens is that I am forwarded to https://localhost:5001/sysbtest/sysb.

According to the docs: aspnet/core/fundamentals/middleware

When Map is used, the matched path segment(s) are removed from HttpRequest.Path and appended to HttpRequest.PathBase for each request.

The problem is here:

ProxyContextExtensions.cs#L24

So when you use .Map() the HttpRequest.Path is "" and everything works (ApplicationBuilderExtensions.cs#L54)

But that is not the case with .MapWhen(), HttpRequest.Path is not empty and the path is appended to the target url.

I can just do context.Request.Path = ""; before calling the .Send(), but it feels less than ideal :)

TaskCanceledException when in-process hosting is enabled

Hi! I'm investigating ProxyKit for a new project. Everything looks really great but I have not been able to solve one development time issue. Do you know why connecting to hot-module-replacement (HMR) endpoint fails over ProxyKit. Here's the exception from debug output:

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/2.0 GET https://localhost:44317/dist/__webpack_hmr  
System.Net.Http.HttpClient.ProxyKitClient.LogicalHandler:Information: Start processing HTTP request GET http://localhost:54469/dist/__webpack_hmr
System.Net.Http.HttpClient.ProxyKitClient.ClientHandler:Information: Sending HTTP request GET http://localhost:54469/dist/__webpack_hmr
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54469/dist/__webpack_hmr  
System.Net.Http.HttpClient.ProxyKitClient.ClientHandler:Information: Received HTTP response after 57.6952ms - OK
System.Net.Http.HttpClient.ProxyKitClient.LogicalHandler:Information: End processing HTTP request after 92.4618ms - OK
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 407907.3611ms 200 text/event-stream; charset=utf-8

.
.
.

Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll
Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware:Error: An unhandled exception has occurred while executing the request.

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request. ---> System.Net.Sockets.SocketException: The I/O operation has been aborted because of either a thread exit or an application request
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   at ProxyKit.ProxyMiddleware.CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage) in C:\Users\Jussi\Documents\GitHub\cNeuro\src\ProxyKit\ProxyMiddleware.cs:line 55
   at ProxyKit.ProxyMiddleware.Invoke(HttpContext context) in C:\Users\Jussi\Documents\GitHub\cNeuro\src\ProxyKit\ProxyMiddleware.cs:line 26
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware:Warning: The response has already started, the error page middleware will not be executed.
Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer:Error: Connection ID "18086456105130524772", Request ID "80000065-0000-fb00-b63f-84710c7967bb": An unhandled exception was thrown by the application.

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request. ---> System.Net.Sockets.SocketException: The I/O operation has been aborted because of either a thread exit or an application request
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.ChunkedEncodingReadStream.CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
   at ProxyKit.ProxyMiddleware.CopyProxyHttpResponse(HttpContext context, HttpResponseMessage responseMessage) in C:\Users\Jussi\Documents\GitHub\cNeuro\src\ProxyKit\ProxyMiddleware.cs:line 55
   at ProxyKit.ProxyMiddleware.Invoke(HttpContext context) in C:\Users\Jussi\Documents\GitHub\cNeuro\src\ProxyKit\ProxyMiddleware.cs:line 26
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContextOfT`1.ProcessRequestAsync()
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 29710.8822ms 200 text/event-stream; charset=utf-8
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 34543.7289ms 200 text/event-stream; charset=utf-8

Hot updates to code do not flow to the client. To reproduce, you need a webpack application with HMR enabled and a simple Proxy with just:

app.RunProxy(
    context => context
         .ForwardTo("http://localhost:54469/")
         .AddXForwardedHeaders()
         .Send());

When accessing the application through its own port, HMR works as intended. Logs show no exceptions, only this one request and any subsequent hot updates:

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54469/dist/__webpack_hmr  
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 30571.8054ms 200 text/event-stream; charset=utf-8

I can provide a minimal reproducible, if needed.

WebSocket on 2.0.4 Nuget

Hi,

I've included ProxyKit 2.0.4 via Nuget. However, I think proxying of WebSockets are not yet included. Do I need to build it from source?

Set ClientCertificate for HttpClient

Hello,
is there a way to pass an HttpClientHandler Object to the underlying HttpClient to add a ClientCertificate to the outgoing Request (ForwardContext)?
I tried to configure the HttpClient in the ConfigureServicesMethod, but I did not get it :)

tia

How to use HSTS with ProxyKit

I tried app.UseHsts(); with ProxyKit, but it doesn't working.

app.UseHsts();
app.RunProxy(context => context.ForwardTo(url).Execute());

How can I use ProxyKit with HSTS middleware?

ConsulServiceDiscovery with dynamic services

Great work on this project, thanks!

I am using a slight variation on the ConsulServiceDiscovery recipe. Except in my case the upstream services are more dynamic in that they can be added at runtime without code change (the application proxy also has registration APIs).

Because I can't spin up a proxy per service with IApplicationBuilder.Map() I can't get the middleware to set to strip the proxy part of the path out of Request.Path so it doesn't get forwarded to upstream services. E.g. /api/foo/some/api/resource needs to be forwarded on as /some/api/resource. I work around this by rewriting Request.Path with the stripped path. Was wondering if maybe the UpstreamHost would be a better place to capture this?

More of a discussion than issue, so feel free to close.

Drop incoming X-Forwarded headers by default & opt-in to forwarding them

  1. Currently ProxyKit will forward all incoming request headers to the upstream host, including X-Forwarded-*.
  2. Chaining proxies is not unusual and the X-Forwarded spec accomdates that.
  3. A developer can optionally apply proxy X-Forwarded headers to the upstream request using ApplyXForwardedHeaders().

The current design is that the first reverse proxy in a chain will accept external X-Forwarded-* headers and forwarding them by default. In order to mitigate against potential spooking attacks, the headers should be dropped by default and accepting them is an explicit opt-in decision.

The API for this could look like:

app.RunProxy(context => context
    .ForwardTo("http://localhost:5001")
    .ApplyXForwardedHeaders(append: true)
    .Execute());
  1. If X-Forwarded-* headers don't exist on incoming request, X-Forwarded-* are added.
  2. If X-Forwarded-* headers exist on incoming request and append:false (default), they are replaced.
  3. If X-Forwarded-* headers exist on incoming request and append:trye (opt-in), they are appended.

Pinging @leastprivilege @brockallen for feedback :)

Malformed User-Agent can cause IndexOutOfRangeException

The User-Agent is Mozilla/4.0 (compatible (compatible; MSIE 8.0; Windows NT 6.1; Trident/7.0). This is a malformed header value from a anonymous user in my production environment.

Here is the stack trace information:

System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Net.Http.HttpRuleParser.GetExpressionLength(String input, Int32 startIndex, Char openChar, Char closeChar, Boolean supportsNesting, Int32& nestedCount, Int32& length)
   at System.Net.Http.Headers.ProductInfoHeaderValue.GetProductInfoLength(String input, Int32 startIndex, ProductInfoHeaderValue& parsedValue)
   at System.Net.Http.Headers.ProductInfoHeaderParser.TryParseValue(String value, Object storeValue, Int32& index, Object& parsedValue)
   at System.Net.Http.Headers.HttpHeaders.TryParseAndAddRawHeaderValue(HeaderDescriptor descriptor, HeaderStoreItemInfo info, String value, Boolean addWhenInvalid)
   at System.Net.Http.Headers.HttpHeaders.ParseSingleRawHeaderValue(HeaderDescriptor descriptor, HeaderStoreItemInfo info)
   at System.Net.Http.Headers.HttpHeaders.ParseRawHeaderValues(HeaderDescriptor descriptor, HeaderStoreItemInfo info, Boolean removeEmptyHeader)
   at System.Net.Http.Headers.HttpHeaders.GetHeaderDescriptorsAndValuesCore()+MoveNext()
   at System.Net.Http.HttpConnection.WriteHeadersAsync(HttpHeaders headers, String cookiesFromContainer)
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
   at ProxyKit.ForwardContext.Send()

You might need to workaround this!

See also: https://github.com/dotnet/corefx/issues/34933 & #51

Support for ASP.NET Core 2.1

The project depends on v2.2.0 of ASP.NET Core, but v2.1 is the LTS version at the moment. Is there a way to target that, or multitarget for that?

Injecting services within HandleProxyRequest delegate

Hi,

I'd like to implement a proxy that supports templated request content and proxies the rendered template to the upstream services. A simplified example is:

            app.RunProxy(async context =>
            {
                string requestBody = await ReadRequestBodyAsync(context.Request);
                
                ForwardContext forwardContext = context
                    .ForwardTo("https://upstream")
                    .AddXForwardedHeaders();

                string rendered = await BindAsync(requestBody, new SomeModel
                {
                    Name = "Ben Foster"
                });

                forwardContext.UpstreamRequest.Content = new StringContent(rendered, Encoding.UTF8, context.Request.ContentType);

                return await forwardContext.Send();
            });

In the above example I'm hard-coding the SomeModel object but this would actually need to be loaded from a DB based on a value in one of the request headers.

What would the recommended approach be for injecting services into the delegate. I've considered:

  1. Using context.RequestServices
  2. Using another middleware component to load the required data and adds it to HttpContext.Items

Certain requests never return

Hi,

I want to replace a legacy app (ASP.NET) with a new one (ASP.NET Core). In order for the change to be transparent for my clients, I installed the new app in place of the old on in IIS and I'm using ProxyKit to selectively redirect the old requests to the old app, while the new one handles new requests. The configuration is as such:

app.UseWhen(
    // use proxy if requested path does not start with an item in paths collection
    context => !paths.Any(path => context.Request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase)),
    appInner => appInner.RunProxy(async context =>
    {
        var response = await context
            .ForwardTo(upstreamHost)
            .CopyXForwardedHeaders()
            .FixContentTypeOnGetRequests(context) // "adjusts" downstream request to support old clients
            .Send();

        // Replace upstream host ONLY in help pages
        if (context.Request.Path.Value.IndexOf("/help", StringComparison.OrdinalIgnoreCase) > -1)
        {
            await ReplaceHostnamesInProxiedResponse(upstreamHost, downstreamHost, response);
        }

        return response;
    }));

The configuration works fine most of the time and is fast enough that you can barely tell there is a proxy. Neither FixContentTypeOnGetRequests nor the if have any incidence on the situation described below.

For some reason I cannot explain, however, some calls to Send sometimes never return.

I have only been able to reproduce when calls happen in a very specific sequence:

There are 4 apps hosted in IIS on the same machine: PROXY, DOWNSTREAM, APP1, APP2

CLIENT               PROXY             DOWNSTREAM                 APP1                  APP2
   | ---- /path1 ----> |                   |                       |                      |
   |                   | ---- /path1 ----> |                       |                      |
   |                   |                   | ----- /extCall1 ----> |                      |
   |                   |                   |                       | ---- /extCall2 ----> |
   |                   | <-------------------------- /path2 ----------------------------- |
   |                   | ---- /path2 ----> |                       |                      |
   |                   |                   | ----- /extCall3 ----> |                      |
   |                   | <---------------- /path3 ---------------- |                      |
   |                   | ---- /path3 ----> |                       |                      |
   |                   | <--- R /path3 --- |                       |                      |
   |                   |                  TIMEOUT                  |                      |

I hope the diagram makes the situation clear. Basically, there is a series of calls which are all synchronous and needed to respond to the initial request. The client requests /path1 to the PROXY, which in turns triggers several other calls to other applications, some of which are further requests to the PROXY.

The third request to the PROXY, /path3, is handled quickly by the DOWNSTREAM app (~100ms), but the call to Send in the PROXY that's supposed to get that request and send it to the caller (APP1), never completes, triggering a series of time outs.

Calling making the exact same call directly, works just fine:

CLIENT               PROXY             DOWNSTREAM
   | ---- /path3 ----> |                   |
   |                   | ---- /path3 ----> |
   |                   | <--- R /path3 --- |
   | <--- R /path3 --- |                   |

Avoiding the PROXY in the last call, also works fine:

CLIENT               PROXY             DOWNSTREAM                 APP1                  APP2
   | ---- /path1 ----> |                   |                       |                      |
   |                   | ---- /path1 ----> |                       |                      |
   |                   |                   | ----- /extCall1 ----> |                      |
   |                   |                   |                       | ---- /extCall2 ----> |
   |                   | <-------------------------- /path2 ----------------------------- |
   |                   | ---- /path2 ----> |                       |                      |
   |                   |                   | ----- /extCall3 ----> |                      |
   |                   |                   | <------ /path3 ------ |                      |
   |                   |                   | ------ R /path3 ----> |                      |
   |                   |                   | <--- R /extCall3 ---- |                      |
   |                   | <--- R /path2 --- |                       |                      |
   |                   | -------------------------- R /path2 ---------------------------> |
etc.

However, reconfiguring our internal apps to hit DOWNSTREAM directly, totally beats the purpose of setting up the PROXY, so it's something I'd like to avoid.

Is there anything that would explain why this 3rd call to the PROXY simply never returns? It feels like some kind of deadlock, but I cannot figure out why.

Any insights are welcome.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.