Skip to content

Commit aa1da30

Browse files
authored
Merge pull request #26 from twitchax/ws-support
WebSocket Support
2 parents 40a7763 + ef261c6 commit aa1da30

17 files changed

+902
-187
lines changed

README.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,29 @@ public void ConfigureServices(IServiceCollection services)
4242
}
4343
```
4444

45+
#### Run a Proxy
46+
47+
You can run a proxy over all endpoints.
48+
49+
```csharp
50+
app.RunProxy("https://google.com");
51+
```
52+
53+
In addition, you can route this proxy depending on the context.
54+
55+
```csharp
56+
app.RunProxy(context =>
57+
{
58+
if(context.WebSockets.IsWebSocketRequest)
59+
return "wss://mysite.com/ws";
60+
61+
return "https://mysite.com";
62+
});
63+
```
64+
4565
#### Existing Controller
4666

47-
You can use the proxy functionality on an existing `Controller` by leveraging the `Proxy` extension method.
67+
You can define a proxy over a specific endpoint on an existing `Controller` by leveraging the `Proxy` extension method.
4868

4969
```csharp
5070
public class MyController : Controller
@@ -57,6 +77,19 @@ public class MyController : Controller
5777
}
5878
```
5979

80+
In addition, you can proxy to WebSocket endpoints.
81+
82+
```csharp
83+
public class MyController : Controller
84+
{
85+
[Route("ws")]
86+
public Task OpenWs()
87+
{
88+
return this.ProxyAsync($"wss://myendpoint.com/ws");
89+
}
90+
}
91+
```
92+
6093
You can also pass special options that apply when the proxy operation occurs.
6194

6295
```csharp
@@ -67,32 +100,48 @@ public class MyController : Controller
67100
{
68101
var options = ProxyOptions.Instance
69102
.WithShouldAddForwardedHeaders(false)
103+
.WithHttpClientName("MyCustomClient")
104+
.WithIntercept(async context =>
105+
{
106+
if(c.Connection.RemotePort == 7777)
107+
{
108+
c.Response.StatusCode = 300;
109+
await c.Response.WriteAsync("I don't like this port, so I am not proxying this request!");
110+
return true;
111+
}
112+
113+
return false;
114+
})
70115
.WithBeforeSend((c, hrm) =>
71116
{
72117
// Set something that is needed for the downstream endpoint.
73118
hrm.Headers.Authorization = new AuthenticationHeaderValue("Basic");
119+
120+
return Task.CompletedTask;
74121
})
75122
.WithAfterReceive((c, hrm) =>
76123
{
77124
// Alter the content in some way before sending back to client.
78125
var newContent = new StringContent("It's all greek...er, Latin...to me!");
79126
hrm.Content = newContent;
127+
128+
return Task.CompletedTask;
80129
})
81-
.WithHandleFailure((c, e) =>
130+
.WithHandleFailure(async (c, e) =>
82131
{
83132
// Return a custom error response.
84133
c.Response.StatusCode = 403;
85-
c.Response.WriteAsync("Things borked.");
134+
await c.Response.WriteAsync("Things borked.");
86135
});
87136

88-
return this.ProxyAsync($"https://jsonplaceholder.typicode.com/posts/{postId}");
137+
return this.ProxyAsync($"https://jsonplaceholder.typicode.com/posts/{postId}", options);
89138
}
90139
}
91140
```
92141

93142
#### Application Builder
94143

95-
You can define a proxy in `Configure(IApplicationBuilder app, IHostingEnvironment env)`. The arguments are passed to the underlying lambda as a `Dictionary`.
144+
You can define a proxy over a specific endpoint in `Configure(IApplicationBuilder app, IHostingEnvironment env)`. The arguments are passed to the underlying lambda as a `Dictionary`.
96145

97146
```csharp
98147
app.UseProxy("api/{arg1}/{arg2}", async (args) => {

TODO.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
TODO:
2+
* Should UseProxy require the user to set all of the proxies at once? YES, in 4.0.0...`UseProxies` with builders.
3+
* Remove the [ProxyRoute] attribute? Maybe, in 4.0.0. If we keep it, change it to `UseStaticProxies`, and somehow return options?
4+
* Round robin helper, and protocol helper for `RunProxy`? Maybe in 4.0.0.
5+
* Add options for WebSocket calls.
6+
* Make options handlers called `Async`?
7+
* Allow the user to set options via a lambda for builder purposes?
8+
* Add a `RunProxy` that takes a `getProxiedAddress` as a `Task<string>`.
9+
10+
Some ideas of how `UseProxies` should work in 4.0.0.
11+
12+
```csharp
13+
14+
// Custom top-level extension method.
15+
app.UseProxies(proxies =>
16+
{
17+
proxies.Map("/route/thingy")
18+
.ToHttp("http://mysite.com/") // OR To(http, ws)
19+
.WithOption1();
20+
21+
// OR
22+
23+
proxies.Map("/route/thingy", proxy =>
24+
{
25+
// Make sure the proxy builder has HttpContext on it.
26+
proxy.ToHttp("http://mysite.com")
27+
.WithOption1(...);
28+
29+
proxy.ToWs(...);
30+
});
31+
});
32+
33+
// OR?
34+
35+
// Piggy-back on the ASP.NET Core 3 endpoints pattern.
36+
app.UseEndpoints(endpoints =>
37+
{
38+
endpoints.Map("/my/path", context =>
39+
{
40+
return context.ProxyAsync("http://mysite.com", options =>
41+
{
42+
options.WithOption1();
43+
});
44+
45+
// OR?
46+
47+
return context.HttpProxyTo("http://mysite.com", options =>
48+
{
49+
options.WithOption1();
50+
});
51+
52+
// OR, maybe there is an `HttpProxyTo` and `WsProxyTo`, and a `ProxyTo` that does its best to decide.
53+
});
54+
})
55+
```

src/Core/AspNetCore.Proxy.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
<ItemGroup>
1616
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
1717
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
18-
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.0.0" />
1918
<PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0" />
2019
</ItemGroup>
2120

src/Core/Extensions.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Microsoft.AspNetCore.Http;
2+
using System;
3+
using System.Threading.Tasks;
4+
5+
namespace AspNetCore.Proxy
6+
{
7+
internal static class Extensions
8+
{
9+
internal static async Task ExecuteProxyOperationAsync(this HttpContext context, string uri, ProxyOptions options = null)
10+
{
11+
try
12+
{
13+
if (context.WebSockets.IsWebSocketRequest)
14+
{
15+
if(!uri.StartsWith("ws", System.StringComparison.OrdinalIgnoreCase))
16+
throw new InvalidOperationException("A WebSocket request must forward to a WebSocket (ws[s]) endpoint.");
17+
18+
await context.ExecuteWsProxyOperationAsync(uri, options).ConfigureAwait(false);
19+
return;
20+
}
21+
22+
// Assume HTTP if not WebSocket.
23+
if(!uri.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
24+
throw new InvalidOperationException("An HTTP request must forward to an HTTP (http[s]) endpoint.");
25+
26+
await context.ExecuteHttpProxyOperationAsync(uri, options).ConfigureAwait(false);
27+
}
28+
catch (Exception e)
29+
{
30+
if(!context.Response.HasStarted)
31+
{
32+
if (options?.HandleFailure == null)
33+
{
34+
// If the failures are not caught, then write a generic response.
35+
context.Response.StatusCode = 502;
36+
await context.Response.WriteAsync($"Request could not be proxied.\n\n{e.Message}\n\n{e.StackTrace}").ConfigureAwait(false);
37+
return;
38+
}
39+
40+
await options.HandleFailure(context, e).ConfigureAwait(false);
41+
}
42+
}
43+
}
44+
}
45+
}

src/Core/Helpers.cs

Lines changed: 4 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
using Microsoft.AspNetCore.Http;
2-
using Microsoft.Extensions.DependencyInjection;
3-
using Microsoft.Extensions.DependencyModel;
41
using System;
52
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Net.Http;
8-
using System.Net.Sockets;
93
using System.Reflection;
10-
using System.Text;
11-
using System.Threading.Tasks;
4+
using Microsoft.Extensions.DependencyModel;
125

136
namespace AspNetCore.Proxy
147
{
158
internal static class Helpers
169
{
17-
internal static readonly string ProxyClientName = "AspNetCore.Proxy.ProxyClient";
10+
internal static readonly string HttpProxyClientName = "AspNetCore.Proxy.HttpProxyClient";
11+
internal static readonly string[] WebSocketNotForwardedHeaders = new[] { "Connection", "Host", "Upgrade", "Sec-WebSocket-Accept", "Sec-WebSocket-Protocol", "Sec-WebSocket-Key", "Sec-WebSocket-Version", "Sec-WebSocket-Extensions" };
1812

1913
internal static IEnumerable<Assembly> GetReferencingAssemblies()
2014
{
@@ -31,142 +25,5 @@ internal static IEnumerable<Assembly> GetReferencingAssemblies()
3125
}
3226
return assemblies;
3327
}
34-
35-
internal static async Task ExecuteProxyOperation(HttpContext context, string uri, ProxyOptions options = null)
36-
{
37-
try
38-
{
39-
var proxiedRequest = context.CreateProxiedHttpRequest(uri, options?.ShouldAddForwardedHeaders ?? true);
40-
41-
if(options?.BeforeSend != null)
42-
await options.BeforeSend(context, proxiedRequest).ConfigureAwait(false);
43-
var proxiedResponse = await context
44-
.SendProxiedHttpRequest(proxiedRequest, options?.HttpClientName ?? Helpers.ProxyClientName)
45-
.ConfigureAwait(false);
46-
47-
if(options?.AfterReceive != null)
48-
await options.AfterReceive(context, proxiedResponse).ConfigureAwait(false);
49-
await context.WriteProxiedHttpResponse(proxiedResponse).ConfigureAwait(false);
50-
}
51-
catch (Exception e)
52-
{
53-
if (options?.HandleFailure == null)
54-
{
55-
// If the failures are not caught, then write a generic response.
56-
context.Response.StatusCode = 502;
57-
await context.Response.WriteAsync($"Request could not be proxied.\n\n{e.Message}\n\n{e.StackTrace}.").ConfigureAwait(false);
58-
return;
59-
}
60-
61-
await options.HandleFailure(context, e).ConfigureAwait(false);
62-
}
63-
}
64-
}
65-
66-
internal static class Extensions
67-
{
68-
internal static HttpRequestMessage CreateProxiedHttpRequest(this HttpContext context, string uriString, bool shouldAddForwardedHeaders)
69-
{
70-
var uri = new Uri(uriString);
71-
var request = context.Request;
72-
73-
var requestMessage = new HttpRequestMessage();
74-
var requestMethod = request.Method;
75-
76-
// Write to request content, when necessary.
77-
if (!HttpMethods.IsGet(requestMethod) &&
78-
!HttpMethods.IsHead(requestMethod) &&
79-
!HttpMethods.IsDelete(requestMethod) &&
80-
!HttpMethods.IsTrace(requestMethod))
81-
{
82-
var streamContent = new StreamContent(request.Body);
83-
requestMessage.Content = streamContent;
84-
}
85-
86-
// Copy the request headers.
87-
foreach (var header in context.Request.Headers)
88-
if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
89-
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
90-
91-
// Add forwarded headers.
92-
if(shouldAddForwardedHeaders)
93-
AddForwardedHeadersToRequest(context, requestMessage);
94-
95-
// Set destination and method.
96-
requestMessage.Headers.Host = uri.Authority;
97-
requestMessage.RequestUri = uri;
98-
requestMessage.Method = new HttpMethod(request.Method);
99-
100-
return requestMessage;
101-
}
102-
103-
internal static Task<HttpResponseMessage> SendProxiedHttpRequest(this HttpContext context, HttpRequestMessage message, string httpClientName)
104-
{
105-
return context.RequestServices
106-
.GetService<IHttpClientFactory>()
107-
.CreateClient(httpClientName)
108-
.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
109-
}
110-
111-
internal static Task WriteProxiedHttpResponse(this HttpContext context, HttpResponseMessage responseMessage)
112-
{
113-
var response = context.Response;
114-
115-
response.StatusCode = (int)responseMessage.StatusCode;
116-
foreach (var header in responseMessage.Headers)
117-
{
118-
response.Headers[header.Key] = header.Value.ToArray();
119-
}
120-
121-
foreach (var header in responseMessage.Content.Headers)
122-
{
123-
response.Headers[header.Key] = header.Value.ToArray();
124-
}
125-
126-
response.Headers.Remove("transfer-encoding");
127-
128-
return responseMessage.Content.CopyToAsync(response.Body);
129-
}
130-
131-
private static void AddForwardedHeadersToRequest(HttpContext context, HttpRequestMessage requestMessage)
132-
{
133-
var request = context.Request;
134-
var connection = context.Connection;
135-
136-
var host = request.Host.ToString();
137-
var protocol = request.Scheme;
138-
139-
var localIp = connection.LocalIpAddress?.ToString();
140-
var isLocalIpV6 = connection.LocalIpAddress?.AddressFamily == AddressFamily.InterNetworkV6;
141-
142-
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
143-
var isRemoteIpV6 = connection.RemoteIpAddress?.AddressFamily == AddressFamily.InterNetworkV6;
144-
145-
if(remoteIp != null)
146-
requestMessage.Headers.TryAddWithoutValidation("X-Forwarded-For", remoteIp);
147-
requestMessage.Headers.TryAddWithoutValidation("X-Forwarded-Proto", protocol);
148-
requestMessage.Headers.TryAddWithoutValidation("X-Forwarded-Host", host);
149-
150-
// Fix IPv6 IPs for the `Forwarded` header.
151-
var forwardedHeader = new StringBuilder($"proto={protocol};host={host};");
152-
153-
if(localIp != null)
154-
{
155-
if(isLocalIpV6)
156-
localIp = $"\"[{localIp}]\"";
157-
158-
forwardedHeader.Append($"by={localIp};");
159-
}
160-
161-
if(remoteIp != null)
162-
{
163-
if(isRemoteIpV6)
164-
remoteIp = $"\"[{remoteIp}]\"";
165-
166-
forwardedHeader.Append($"for={remoteIp};");
167-
}
168-
169-
requestMessage.Headers.TryAddWithoutValidation("Forwarded", forwardedHeader.ToString());
170-
}
17128
}
172-
}
29+
}

0 commit comments

Comments
 (0)