Skip to content

Commit 9f3f76d

Browse files
authored
Fix HTTP tunnel (#2448)
According to https://datatracker.ietf.org/doc/html/draft-luotonen-web-proxy-tunneling-01#section-3.2, the expected response to a `CONNECT` should be `HTTP/1.1 200 Connection established` instead of `HTTP/1.1 200 OK`. Allow both responses for now (as still a lot of proxy implementations fail to follow the standard). A HTTP(S), SOCKS, etc. proxy is traditionally specified by an URI of form `scheme://[user:pass@]host[:port]`. Accept both formats for backwards compatibility.
1 parent 3ba8d2a commit 9f3f76d

File tree

4 files changed

+30
-19
lines changed

4 files changed

+30
-19
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Current package versions:
1111
- Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428))
1212
- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445))
1313
- Fix [#2449](https://github.com/StackExchange/StackExchange.Redis/issues/2449): Resolve AOT trim warnings in `TryGetAzureRoleInstanceIdNoThrow` ([#2451 by eerhardt](https://github.com/StackExchange/StackExchange.Redis/pull/2451))
14+
- Adds: Support for `HTTP/1.1 200 Connection established` in HTTP Tunnel ([#2448 by flobernd](https://github.com/StackExchange/StackExchange.Redis/pull/2448))
1415

1516
## 2.6.104
1617

src/StackExchange.Redis/Configuration/Tunnel.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ private sealed class HttpProxyTunnel : Tunnel
4949
{
5050
var encoding = Encoding.ASCII;
5151
var ep = Format.ToString(endpoint);
52-
const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse = "HTTP/1.1 200 OK\r\n\r\n";
52+
const string Prefix = "CONNECT ", Suffix = " HTTP/1.1\r\n\r\n", ExpectedResponse1 = "HTTP/1.1 200 OK\r\n\r\n", ExpectedResponse2 = "HTTP/1.1 200 Connection established\r\n\r\n";
5353
byte[] chunk = ArrayPool<byte>.Shared.Rent(Math.Max(
5454
encoding.GetByteCount(Prefix) + encoding.GetByteCount(ep) + encoding.GetByteCount(Suffix),
55-
encoding.GetByteCount(ExpectedResponse)
55+
Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2))
5656
));
5757
var offset = 0;
5858
offset += encoding.GetBytes(Prefix, 0, Prefix.Length, chunk, offset);
@@ -76,10 +76,11 @@ static void SafeAbort(object? obj)
7676
await args;
7777

7878
// we expect to see: "HTTP/1.1 200 OK\n"; note our buffer is definitely big enough already
79-
int toRead = encoding.GetByteCount(ExpectedResponse), read;
79+
int toRead = Math.Max(encoding.GetByteCount(ExpectedResponse1), encoding.GetByteCount(ExpectedResponse2)), read;
8080
offset = 0;
8181

82-
while (toRead > 0)
82+
var actualResponse = "";
83+
while (toRead > 0 && !actualResponse.EndsWith("\r\n\r\n"))
8384
{
8485
args.SetBuffer(chunk, offset, toRead);
8586
if (!socket.ReceiveAsync(args)) args.Complete();
@@ -88,11 +89,12 @@ static void SafeAbort(object? obj)
8889
if (read <= 0) break; // EOF (since we're never doing zero-length reads)
8990
toRead -= read;
9091
offset += read;
92+
93+
actualResponse = encoding.GetString(chunk, 0, offset);
9194
}
92-
if (toRead != 0) throw new EndOfStreamException("EOF negotiating HTTP tunnel");
95+
if (toRead != 0 && !actualResponse.EndsWith("\r\n\r\n")) throw new EndOfStreamException("EOF negotiating HTTP tunnel");
9396
// lazy
94-
var actualResponse = encoding.GetString(chunk, 0, offset);
95-
if (ExpectedResponse != actualResponse)
97+
if (ExpectedResponse1 != actualResponse && ExpectedResponse2 != actualResponse)
9698
{
9799
throw new InvalidOperationException("Unexpected response negotiating HTTP tunnel");
98100
}

src/StackExchange.Redis/ConfigurationOptions.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -912,19 +912,25 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
912912
{
913913
Tunnel = null;
914914
}
915-
else if (value.StartsWith("http:"))
915+
else
916916
{
917-
value = value.Substring(5);
918-
if (!Format.TryParseEndPoint(value, out var ep))
917+
// For backwards compatibility with `http:address_with_port`.
918+
if (value.StartsWith("http:") && !value.StartsWith("http://"))
919+
{
920+
value = value.Insert(5, "//");
921+
}
922+
923+
var uri = new Uri(value, UriKind.Absolute);
924+
if (uri.Scheme != "http")
925+
{
926+
throw new ArgumentException("Tunnel cannot be parsed: " + value);
927+
}
928+
if (!Format.TryParseEndPoint($"{uri.Host}:{uri.Port}", out var ep))
919929
{
920930
throw new ArgumentException("HTTP tunnel cannot be parsed: " + value);
921931
}
922932
Tunnel = Tunnel.HttpProxy(ep);
923933
}
924-
else
925-
{
926-
throw new ArgumentException("Tunnel cannot be parsed: " + value);
927-
}
928934
break;
929935
// Deprecated options we ignore...
930936
case OptionKeys.HighPrioritySocketThreads:

tests/StackExchange.Redis.Tests/ConfigTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -619,19 +619,21 @@ public async Task MutableOptions()
619619
Assert.Equal(newPass, conn.RawConfig.Password);
620620
}
621621

622-
[Fact]
623-
public void HttpTunnelCanRoundtrip()
622+
[Theory]
623+
[InlineData("http://somewhere:22", "http:somewhere:22")]
624+
[InlineData("http:somewhere:22", "http:somewhere:22")]
625+
public void HttpTunnelCanRoundtrip(string input, string expected)
624626
{
625-
var config = ConfigurationOptions.Parse("127.0.0.1:6380,tunnel=http:somewhere:22");
627+
var config = ConfigurationOptions.Parse($"127.0.0.1:6380,tunnel={input}");
626628
var ip = Assert.IsType<IPEndPoint>(Assert.Single(config.EndPoints));
627629
Assert.Equal(6380, ip.Port);
628630
Assert.Equal("127.0.0.1", ip.Address.ToString());
629631

630632
Assert.NotNull(config.Tunnel);
631-
Assert.Equal("http:somewhere:22", config.Tunnel.ToString());
633+
Assert.Equal(expected, config.Tunnel.ToString());
632634

633635
var cs = config.ToString();
634-
Assert.Equal("127.0.0.1:6380,tunnel=http:somewhere:22", cs);
636+
Assert.Equal($"127.0.0.1:6380,tunnel={expected}", cs);
635637
}
636638

637639
private class CustomTunnel : Tunnel { }

0 commit comments

Comments
 (0)