-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Configuration/Connections: Allow HTTP tunneling #2274
Merged
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
2abb9fa
experimental hack to allow HTTP tunneling
mgravell 6d89559
ForAwait
mgravell 6ffbc6c
keep the buffer until we're done!
mgravell c991ad9
fixup connect target
mgravell e18865b
mark test inconclusive if missing env-var
mgravell ba8b70e
add latency/bandwidth tests for http-tunnel
mgravell f3c1482
check for trailing \n
mgravell 8d8d10c
fix http message format (#2275)
maksimkim 6aa99f9
provide tunnel functionality via abstract base class
mgravell 08bdb73
implement tunnel as async
mgravell a161fcf
duplicate BeforeSocketConnect API on Tunnel
mgravell c0c9c63
release notes
mgravell 4d3cf11
docs
mgravell bdaecbd
remove obsolete comments
mgravell 9fb8b7a
Merge branch 'main' into http-tunnel
NickCraver bfca10f
Tidy up!
NickCraver 1e88d19
More tweaks
NickCraver 49e6f71
Last error tweak
NickCraver File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
using System; | ||
using System.Buffers; | ||
using System.IO; | ||
using System.Net; | ||
using System.Net.Sockets; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Pipelines.Sockets.Unofficial; | ||
|
||
namespace StackExchange.Redis.Configuration | ||
{ | ||
/// <summary> | ||
/// Allows interception of the transport used to communicate with Redis. | ||
/// </summary> | ||
public abstract class Tunnel | ||
{ | ||
/// <summary> | ||
/// Gets the underlying socket endpoint to use when connecting to a logical endpoint. | ||
/// </summary> | ||
/// <remarks><c>null</c> should be returned if a socket is not required for this endpoint.</remarks> | ||
public virtual ValueTask<EndPoint?> GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) => new(endpoint); | ||
|
||
/// <summary> | ||
/// Allows modification of a <see cref="Socket"/> between creation and connection. | ||
/// Passed in is the endpoint we're connecting to, which type of connection it is, and the socket itself. | ||
/// For example, a specific local IP endpoint could be bound, linger time altered, etc. | ||
/// </summary> | ||
public virtual ValueTask BeforeSocketConnectAsync(EndPoint endPoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) => default; | ||
|
||
/// <summary> | ||
/// Invoked on a connected endpoint before server authentication and other handshakes occur, allowing pre-redis handshakes. By returning a custom <see cref="Stream"/>, | ||
/// the entire data flow can be intercepted, providing entire custom transports. | ||
/// </summary> | ||
public virtual ValueTask<Stream?> BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) => default; | ||
/// <inheritdoc/> | ||
public abstract override string ToString(); | ||
|
||
private sealed class HttpProxyTunnel : Tunnel | ||
{ | ||
public EndPoint Proxy { get; } | ||
public HttpProxyTunnel(EndPoint proxy) => Proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); | ||
|
||
public override ValueTask<EndPoint?> GetSocketConnectEndpointAsync(EndPoint endpoint, CancellationToken cancellationToken) => new(Proxy); | ||
|
||
public override async ValueTask<Stream?> BeforeAuthenticateAsync(EndPoint endpoint, ConnectionType connectionType, Socket? socket, CancellationToken cancellationToken) | ||
{ | ||
if (socket is not null) | ||
{ | ||
var encoding = Encoding.ASCII; | ||
var ep = Format.ToString(endpoint); | ||
const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse = "HTTP/1.1 200 OK\r\n\r\n"; | ||
byte[] chunk = ArrayPool<byte>.Shared.Rent(Math.Max( | ||
encoding.GetByteCount(Prefix) + encoding.GetByteCount(ep) + encoding.GetByteCount(Suffix), | ||
encoding.GetByteCount(ExpectedResponse) | ||
)); | ||
var offset = 0; | ||
offset += encoding.GetBytes(Prefix, 0, Prefix.Length, chunk, offset); | ||
offset += encoding.GetBytes(ep, 0, ep.Length, chunk, offset); | ||
offset += encoding.GetBytes(Suffix, 0, Suffix.Length, chunk, offset); | ||
|
||
static void SafeAbort(object? obj) | ||
{ | ||
try | ||
{ | ||
(obj as SocketAwaitableEventArgs)?.Abort(SocketError.TimedOut); | ||
} | ||
catch { } // best effort only | ||
} | ||
|
||
using (var args = new SocketAwaitableEventArgs()) | ||
using (cancellationToken.Register(static s => SafeAbort(s), args)) | ||
{ | ||
args.SetBuffer(chunk, 0, offset); | ||
if (!socket.SendAsync(args)) args.Complete(); | ||
await args; | ||
|
||
// we expect to see: "HTTP/1.1 200 OK\n"; note our buffer is definitely big enough already | ||
int toRead = encoding.GetByteCount(ExpectedResponse), read; | ||
offset = 0; | ||
|
||
while (toRead > 0) | ||
{ | ||
args.SetBuffer(chunk, offset, toRead); | ||
if (!socket.ReceiveAsync(args)) args.Complete(); | ||
read = await args; | ||
|
||
if (read <= 0) break; // EOF (since we're never doing zero-length reads) | ||
toRead -= read; | ||
offset += read; | ||
} | ||
if (toRead != 0) throw new EndOfStreamException("EOF negotiating HTTP tunnel"); | ||
// lazy | ||
var actualResponse = encoding.GetString(chunk, 0, offset); | ||
if (ExpectedResponse != actualResponse) | ||
{ | ||
throw new InvalidOperationException("Unexpected response negotiating HTTP tunnel"); | ||
} | ||
ArrayPool<byte>.Shared.Return(chunk); | ||
} | ||
} | ||
return default; // no need for custom stream wrapper here | ||
} | ||
|
||
public override string ToString() => "http:" + Format.ToString(Proxy); | ||
} | ||
|
||
/// <summary> | ||
/// Create a tunnel via an HTTP proxy server. | ||
/// </summary> | ||
/// <param name="proxy">The endpoint to use as an HTTP proxy server.</param> | ||
public static Tunnel HttpProxy(EndPoint proxy) => new HttpProxyTunnel(proxy); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgravell According to https://datatracker.ietf.org/doc/html/draft-luotonen-web-proxy-tunneling-01#section-3.2, the expected response should be
HTTP/1.1 200 Connection established
instead ofHTTP/1.1 200 OK
. Currently it's not possible to use a HTTP proxy that correctly implements this behavior (e.g. Squid).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created a PR: #2448