diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 1da773e718e25c..1b0be33fd7cb95 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -367,7 +367,7 @@ Cannot access a closed stream. - Only the 'http', 'https', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies. + Only the 'http', 'https', 'socks4', 'socks4a', 'socks5', and 'socks5h' schemes are allowed for proxies. Request headers must contain only ASCII characters. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs index dab4ad49f7b0dd..09b9f7698f627b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpEnvironmentProxy.cs @@ -146,6 +146,11 @@ private HttpEnvironmentProxy(Uri? httpProxy, Uri? httpsProxy, string? bypassList hostIndex = 9; protocol = "socks5"; } + else if (value.StartsWith("socks5h://", StringComparison.OrdinalIgnoreCase)) + { + hostIndex = 10; + protocol = "socks5h"; + } else if (value.StartsWith("socks4a://", StringComparison.OrdinalIgnoreCase)) { hostIndex = 10; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs index ed5f1c14459716..9fa5682c080bd5 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpUtilities.SocketsHttpHandler.cs @@ -26,6 +26,7 @@ internal static bool IsSupportedProxyScheme(string scheme) => internal static bool IsSocksScheme(string scheme) => string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "socks5h", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) || string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs index e34382b49f8ce3..f864c504359a15 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs @@ -37,7 +37,8 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho { NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, proxyUri.Scheme); - if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase) || + string.Equals(proxyUri.Scheme, "socks5h", StringComparison.OrdinalIgnoreCase)) { await EstablishSocks5TunnelAsync(stream, host, port, credentials, async).ConfigureAwait(false); } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs index 300ed02aff41ce..3705d67b32a4d1 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocksProxyTest.cs @@ -15,12 +15,12 @@ public abstract class SocksProxyTest : HttpClientHandlerTestBase { public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } - private static string[] Hosts(string socksScheme) => socksScheme == "socks5" + private static string[] Hosts(string socksScheme) => socksScheme == "socks5" || socksScheme == "socks5h" ? new[] { "localhost", "127.0.0.1", "::1" } : new[] { "localhost", "127.0.0.1" }; public static IEnumerable TestLoopbackAsync_MemberData() => - from scheme in new[] { "socks4", "socks4a", "socks5" } + from scheme in new[] { "socks4", "socks4a", "socks5", "socks5h" } from useSsl in BoolValues from useAuth in BoolValues from host in Hosts(scheme) @@ -77,7 +77,7 @@ public static IEnumerable TestExceptionalAsync_MemberData() yield return new object[] { "socks4", new string('a', 256), false, null, "Failed to resolve the destination host to an IPv4 address." }; - foreach (string scheme in new[] { "socks4a", "socks5" }) + foreach (string scheme in new[] { "socks4a", "socks5", "socks5h" }) { yield return new object[] { scheme, new string('a', 256), false, null, "Encoding the host took more than the maximum of 255 bytes." }; } @@ -86,6 +86,10 @@ public static IEnumerable TestExceptionalAsync_MemberData() yield return new object[] { "socks5", "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; yield return new object[] { "socks5", "localhost", true, new NetworkCredential(new string('a', 256), "foo"), "Encoding the UserName took more than the maximum of 255 bytes." }; yield return new object[] { "socks5", "localhost", true, new NetworkCredential("foo", new string('a', 256)), "Encoding the Password took more than the maximum of 255 bytes." }; + yield return new object[] { "socks5h", "localhost", true, null, "SOCKS server did not return a suitable authentication method." }; + yield return new object[] { "socks5h", "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; + yield return new object[] { "socks5h", "localhost", true, new NetworkCredential(new string('a', 256), "foo"), "Encoding the UserName took more than the maximum of 255 bytes." }; + yield return new object[] { "socks5h", "localhost", true, new NetworkCredential("foo", new string('a', 256)), "Encoding the Password took more than the maximum of 255 bytes." }; } [Theory] diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs index fcadc1de12fc5f..755fd07d317d84 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/HttpEnvironmentProxyTest.cs @@ -149,6 +149,7 @@ await RemoteExecutor.Invoke(() => [InlineData("socks4://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] [InlineData("socks4a://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] [InlineData("socks5://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] + [InlineData("socks5h://1.2.3.4:8888/foo", "1.2.3.4", "8888", null, null)] [InlineData("https://1.1.1.5:3005", "1.1.1.5", "3005", null, null)] [InlineData("https://1.1.1.5", "1.1.1.5", "443", null, null)] // Everything before the last '@' is considered as user info (unlike regular Uri parsing).