From 5f2407ad3c2dd15c8441f6c2f3f90fee43d098f7 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 00:31:04 +0800 Subject: [PATCH 01/58] Separate method for proxy scheme validation. --- .../System.Net.Http/src/System/Net/Http/HttpUtilities.cs | 3 +++ .../Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs index e31e93080751e..0be1e108e18ff 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs @@ -34,6 +34,9 @@ internal static bool IsNonSecureWebSocketScheme(string scheme) => internal static bool IsSecureWebSocketScheme(string scheme) => string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase); + internal static bool IsSupportedProxyScheme(string scheme) => + string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase); + // Always specify TaskScheduler.Default to prevent us from using a user defined TaskScheduler.Current. // // Since we're not doing any CPU and/or I/O intensive operations, continue on the same thread. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index c7f70725895ec..848f619e2258f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -282,7 +282,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox if (proxyUri != null) { - Debug.Assert(HttpUtilities.IsSupportedNonSecureScheme(proxyUri.Scheme)); + Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme)); if (sslHostName == null) { if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme)) @@ -394,7 +394,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Exception from {_proxy.GetType().Name}.GetProxy({request.RequestUri}): {ex}"); } - if (proxyUri != null && proxyUri.Scheme != UriScheme.Http) + if (proxyUri != null && !HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme)) { throw new NotSupportedException(SR.net_http_invalid_proxy_scheme); } From 7a70c337e3a4544c26540deae2e2233c48c90d4f Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 02:44:36 +0800 Subject: [PATCH 02/58] Pass socks connection kind. --- .../src/System.Net.Http.csproj | 1 + .../src/System/Net/Http/HttpUtilities.cs | 5 ++- .../SocketsHttpHandler/HttpConnectionPool.cs | 22 ++++++++++- .../HttpConnectionPoolManager.cs | 37 +++++++++++++------ .../SocketsHttpHandler/SocksConnectionKind.cs | 11 ++++++ 5 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 43afb449b58aa..7f6b21d4cda14 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -176,6 +176,7 @@ + string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase); internal static bool IsSupportedProxyScheme(string scheme) => - string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase); + string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme); + + internal static bool IsSocksScheme(string scheme) => + string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase); // Always specify TaskScheduler.Default to prevent us from using a user defined TaskScheduler.Current. // diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index c60e5ca8a35f8..b33f20bd39ef6 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -29,6 +29,7 @@ internal sealed class HttpConnectionPool : IDisposable private readonly HttpConnectionPoolManager _poolManager; private readonly HttpConnectionKind _kind; + private readonly SocksConnectionKind _socksKind; private readonly Uri? _proxyUri; /// The origin authority used to construct the . @@ -101,15 +102,17 @@ internal sealed class HttpConnectionPool : IDisposable /// Initializes the pool. /// The manager associated with this pool. /// The kind of HTTP connections stored in this pool. + /// The kind of SOCKS connection to use in this pool. /// The host with which this pool is associated. /// The port with which this pool is associated. /// The SSL host with which this pool is associated. /// The proxy this pool targets (optional). /// The maximum number of connections allowed to be associated with the pool at any given time. - public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, int maxConnections) + public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, SocksConnectionKind socksKind, string? host, int port, string? sslHostName, Uri? proxyUri, int maxConnections) { _poolManager = poolManager; _kind = kind; + _socksKind = socksKind; _proxyUri = proxyUri; _maxConnections = maxConnections; @@ -179,7 +182,22 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK break; default: - Debug.Fail("Unkown HttpConnectionKind in HttpConnectionPool.ctor"); + Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor"); + break; + } + + switch (socksKind) + { + case SocksConnectionKind.None: + break; + + case SocksConnectionKind.Socks5: + Debug.Assert(kind == HttpConnectionKind.Http || kind == HttpConnectionKind.Https); + Debug.Assert(proxyUri != null); + break; + + default: + Debug.Fail("Unknown SocksConnectionKind in HttpConnectionPool.ctor"); break; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index 848f619e2258f..b01eca75e91b2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -260,7 +260,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox if (isProxyConnect) { Debug.Assert(uri == proxyUri); - return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri, GetIdentityIfDefaultCredentialsUsed(_settings._defaultCredentialsUsedForProxy)); + return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, proxyUri, GetIdentityIfDefaultCredentialsUsed(_settings._defaultCredentialsUsedForProxy)); } string? sslHostName = null; @@ -283,34 +283,46 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox if (proxyUri != null) { Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme)); - if (sslHostName == null) + if (HttpUtilities.IsSocksScheme(proxyUri.Scheme)) + { + // Socks proxy + if (sslHostName != null) + { + return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, sslHostName, null, identity); + } + else + { + return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, null, null, identity); + } + } + else if (sslHostName == null) { if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme)) { // Non-secure websocket connection through proxy to the destination. - return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, proxyUri, identity); } else { // Standard HTTP proxy usage for non-secure requests // The destination host and port are ignored here, since these connections // will be shared across any requests that use the proxy. - return new HttpConnectionKey(HttpConnectionKind.Proxy, null, 0, null, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.Proxy, SocksConnectionKind.None, null, 0, null, proxyUri, identity); } } else { // Tunnel SSL connection through proxy to the destination. - return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, SocksConnectionKind.None, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); } } else if (sslHostName != null) { - return new HttpConnectionKey(HttpConnectionKind.Https, uri.IdnHost, uri.Port, sslHostName, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.None, uri.IdnHost, uri.Port, sslHostName, null, identity); } else { - return new HttpConnectionKey(HttpConnectionKind.Http, uri.IdnHost, uri.Port, null, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, null, identity); } } @@ -321,7 +333,7 @@ public ValueTask SendAsyncCore(HttpRequestMessage request, HttpConnectionPool? pool; while (!_pools.TryGetValue(key, out pool)) { - pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri, _maxConnectionsPerServer); + pool = new HttpConnectionPool(this, key.Kind, key.SocksKind, key.Host, key.Port, key.SslHostName, key.ProxyUri, _maxConnectionsPerServer); if (_cleaningTimer == null) { @@ -516,15 +528,17 @@ private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredential internal readonly struct HttpConnectionKey : IEquatable { public readonly HttpConnectionKind Kind; + public readonly SocksConnectionKind SocksKind; public readonly string? Host; public readonly int Port; public readonly string? SslHostName; // null if not SSL public readonly Uri? ProxyUri; public readonly string Identity; - public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, string identity) + public HttpConnectionKey(HttpConnectionKind kind, SocksConnectionKind socksKind, string? host, int port, string? sslHostName, Uri? proxyUri, string identity) { Kind = kind; + SocksKind = socksKind; Host = host; Port = port; SslHostName = sslHostName; @@ -535,8 +549,8 @@ public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string // In the common case, SslHostName (when present) is equal to Host. If so, don't include in hash. public override int GetHashCode() => (SslHostName == Host ? - HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) : - HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity)); + HashCode.Combine(Kind, SocksKind, Host, Port, ProxyUri, Identity) : + HashCode.Combine(Kind, SocksKind, Host, Port, SslHostName, ProxyUri, Identity)); public override bool Equals([NotNullWhen(true)] object? obj) => obj is HttpConnectionKey hck && @@ -544,6 +558,7 @@ obj is HttpConnectionKey hck && public bool Equals(HttpConnectionKey other) => Kind == other.Kind && + SocksKind == other.SocksKind && Host == other.Host && Port == other.Port && ProxyUri == other.ProxyUri && diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs new file mode 100644 index 0000000000000..3bf5675c8fb62 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Http +{ + internal enum SocksConnectionKind : byte + { + None, + Socks5 + } +} From d0aa8fc70901f0b329511e05de26ab0ed5c79eb8 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 16:59:03 +0800 Subject: [PATCH 03/58] Unauthorized socks5 connection. --- .../src/System.Net.Http.csproj | 1 + .../SocketsHttpHandler/HttpConnectionPool.cs | 29 +++-- .../Http/SocketsHttpHandler/SocksHelper.cs | 118 ++++++++++++++++++ 3 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 7f6b21d4cda14..f0a4da9b03569 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -177,6 +177,7 @@ + - { - lock (altSvcBlocklist) - { - altSvcBlocklist.Remove(badAuthority); - } - }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) + .ContinueWith(t => + { + lock (altSvcBlocklist) + { + altSvcBlocklist.Remove(badAuthority); + } + }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } if (disabled) @@ -1252,7 +1252,18 @@ public ValueTask SendAsync(HttpRequestMessage request, bool case HttpConnectionKind.Https: case HttpConnectionKind.ProxyConnect: Debug.Assert(_originAuthority != null); - stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false); + switch (_socksKind) + { + case SocksConnectionKind.None: + stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false); + break; + + case SocksConnectionKind.Socks5: + Debug.Assert(_proxyUri != null); + stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); + await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, cancellationToken).ConfigureAwait(false); + break; + } break; case HttpConnectionKind.Proxy: 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 new file mode 100644 index 0000000000000..bbb20a89313cc --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http +{ + // https://tools.ietf.org/html/rfc1928 + internal static class SocksHelper + { + // socks protocol limits address length to 1 byte, thus the maximum possible buffer is 256+other fields + private const int BufferSize = 512; + private const int ProtocolVersion = 5; + private const byte METHOD_NO_AUTH = 0; + // private const byte METHOD_GSSAPI = 1; + // private const byte METHOD_USERNAME_PASSWORD = 2; + private const byte METHOD_NO_ACCEPTABLE = 0xFF; + private const byte CMD_CONNECT = 1; + // private const byte CMD_BIND = 2; + // private const byte CMD_UDP_ASSOCIATE = 3; + // private const byte ATYP_IPV4 = 1; + private const byte ATYP_DOMAIN_NAME = 3; + // private const byte ATYP_IPV6 = 4; + private const byte REP_SUCCESS = 0; + // private const byte REP_FAILURE = 1; + // private const byte REP_NOT_ALLOWED = 2; + // private const byte REP_NETWORK_UNREACHABLE = 3; + // private const byte REP_HOST_UNREACHABLE = 4; + // private const byte REP_CONNECTION_REFUSED = 5; + // private const byte REP_TTL_EXPIRED = 6; + // private const byte REP_CMD_NOT_SUPPORT = 7; + // private const byte REP_ATYP_NOT_SUPPORT = 8; + + public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, CancellationToken cancellationToken) + { + byte[] buffer = ArrayPool.Shared.Rent(BufferSize); + + try + { + // +----+----------+----------+ + // |VER | NMETHODS | METHODS | + // +----+----------+----------+ + // | 1 | 1 | 1 to 255 | + // +----+----------+----------+ + buffer[0] = ProtocolVersion; + buffer[1] = 1; + buffer[2] = METHOD_NO_AUTH; + await stream.WriteAsync(buffer.AsMemory(0, 3), cancellationToken).ConfigureAwait(false); + + // +----+--------+ + // |VER | METHOD | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); + if (buffer[0] != ProtocolVersion) + throw new Exception("Bad protocol version"); + + switch (buffer[1]) + { + case METHOD_NO_AUTH: + // continue + break; + + case METHOD_NO_ACCEPTABLE: + default: + throw new Exception("No acceptable auth method."); + } + + + // +----+-----+-------+------+----------+----------+ + // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + buffer[0] = ProtocolVersion; + buffer[1] = CMD_CONNECT; + buffer[2] = 0; + buffer[3] = ATYP_DOMAIN_NAME; + + int addressLength = Encoding.UTF8.GetByteCount(host); + buffer[4] = checked((byte)addressLength); + int bytesEncoded = Encoding.UTF8.GetBytes(host, buffer.AsSpan(5)); + Debug.Assert(bytesEncoded == addressLength); + + Debug.Assert(port > 0); + Debug.Assert(port < ushort.MaxValue); + buffer[addressLength + 5] = (byte)(port >> 8); + buffer[addressLength + 6] = (byte)port; + + await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7), cancellationToken).ConfigureAwait(false); + + // +----+-----+-------+------+----------+----------+ + // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | + // +----+-----+-------+------+----------+----------+ + // | 1 | 1 | X'00' | 1 | Variable | 2 | + // +----+-----+-------+------+----------+----------+ + await stream.ReadAsync(buffer.AsMemory(0, 5), cancellationToken).ConfigureAwait(false); + if (buffer[0] != ProtocolVersion) + throw new Exception("Bad protocol version"); + if (buffer[1] != REP_SUCCESS) + throw new Exception("Connection failed"); + addressLength = buffer[4]; + await stream.ReadAsync(buffer.AsMemory(0, addressLength + 2), cancellationToken).ConfigureAwait(false); + // response address not used + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } +} From b091355b9f2b19aece0255fe770011c874b129a0 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 17:44:44 +0800 Subject: [PATCH 04/58] Username and password auth. --- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../Http/SocketsHttpHandler/SocksHelper.cs | 55 +++++++++++++++++-- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index b84148f2e9f57..7a26b65cc85be 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1261,7 +1261,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool case SocksConnectionKind.Socks5: Debug.Assert(_proxyUri != null); stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); - await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, cancellationToken).ConfigureAwait(false); + await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); break; } break; 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 bbb20a89313cc..71e803fc59bd1 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 @@ -18,7 +18,7 @@ internal static class SocksHelper private const int ProtocolVersion = 5; private const byte METHOD_NO_AUTH = 0; // private const byte METHOD_GSSAPI = 1; - // private const byte METHOD_USERNAME_PASSWORD = 2; + private const byte METHOD_USERNAME_PASSWORD = 2; private const byte METHOD_NO_ACCEPTABLE = 0xFF; private const byte CMD_CONNECT = 1; // private const byte CMD_BIND = 2; @@ -36,7 +36,7 @@ internal static class SocksHelper // private const byte REP_CMD_NOT_SUPPORT = 7; // private const byte REP_ATYP_NOT_SUPPORT = 8; - public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, CancellationToken cancellationToken) + public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -48,9 +48,19 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // | 1 | 1 | 1 to 255 | // +----+----------+----------+ buffer[0] = ProtocolVersion; - buffer[1] = 1; - buffer[2] = METHOD_NO_AUTH; - await stream.WriteAsync(buffer.AsMemory(0, 3), cancellationToken).ConfigureAwait(false); + var credentials = proxyCredentials?.GetCredential(proxyUri, ""); + if (credentials != null) + { + buffer[1] = 1; + buffer[2] = METHOD_NO_AUTH; + } + else + { + buffer[1] = 2; + buffer[2] = METHOD_NO_AUTH; + buffer[3] = METHOD_USERNAME_PASSWORD; + } + await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2), cancellationToken).ConfigureAwait(false); // +----+--------+ // |VER | METHOD | @@ -67,6 +77,41 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // continue break; + case METHOD_USERNAME_PASSWORD: + { + // https://tools.ietf.org/html/rfc1929 + if (credentials == null) + throw new Exception("Server choses bad auth method."); + + // +----+------+----------+------+----------+ + // |VER | ULEN | UNAME | PLEN | PASSWD | + // +----+------+----------+------+----------+ + // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | + // +----+------+----------+------+----------+ + buffer[0] = ProtocolVersion; + int uLen = Encoding.UTF8.GetByteCount(credentials.UserName); + buffer[1] = checked((byte)uLen); + int uLenEncoded = Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2)); + Debug.Assert(uLen == uLenEncoded); + int pLen = Encoding.UTF8.GetByteCount(credentials.Password); + buffer[2 + uLen] = checked((byte)pLen); + int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); + Debug.Assert(pLen == pLenEncoded); + await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen), cancellationToken).ConfigureAwait(false); + + // +----+--------+ + // |VER | STATUS | + // +----+--------+ + // | 1 | 1 | + // +----+--------+ + await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); + if (buffer[0] != ProtocolVersion) + throw new Exception("Bad protocol version"); + if (buffer[1] != REP_SUCCESS) + throw new Exception("Authentication failed."); + break; + } + case METHOD_NO_ACCEPTABLE: default: throw new Exception("No acceptable auth method."); From 98a1d124891ea3985f44f7f3e8c672966e37120a Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 23:38:53 +0800 Subject: [PATCH 05/58] Fix response address. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) 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 71e803fc59bd1..a51092e47d4b3 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 @@ -23,9 +23,9 @@ internal static class SocksHelper private const byte CMD_CONNECT = 1; // private const byte CMD_BIND = 2; // private const byte CMD_UDP_ASSOCIATE = 3; - // private const byte ATYP_IPV4 = 1; + private const byte ATYP_IPV4 = 1; private const byte ATYP_DOMAIN_NAME = 3; - // private const byte ATYP_IPV6 = 4; + private const byte ATYP_IPV6 = 4; private const byte REP_SUCCESS = 0; // private const byte REP_FAILURE = 1; // private const byte REP_NOT_ALLOWED = 2; @@ -145,13 +145,28 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - await stream.ReadAsync(buffer.AsMemory(0, 5), cancellationToken).ConfigureAwait(false); + await stream.ReadAsync(buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) throw new Exception("Connection failed"); - addressLength = buffer[4]; - await stream.ReadAsync(buffer.AsMemory(0, addressLength + 2), cancellationToken).ConfigureAwait(false); + switch (buffer[3]) + { + case ATYP_IPV4: + await stream.ReadAsync(buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); + break; + case ATYP_IPV6: + await stream.ReadAsync(buffer.AsMemory(0, 16), cancellationToken).ConfigureAwait(false); + break; + case ATYP_DOMAIN_NAME: + await stream.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); + addressLength = buffer[0]; + await stream.ReadAsync(buffer.AsMemory(0, addressLength), cancellationToken).ConfigureAwait(false); + break; + default: + throw new Exception("Unknown address type"); + } + await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); // response address not used } finally From c4cdd8e16b8f8f33b97f46f94e0fba1438f6f4f8 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sun, 28 Feb 2021 23:44:17 +0800 Subject: [PATCH 06/58] Fix proxyUri value and assertion. --- .../System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 4 ++-- .../Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 7a26b65cc85be..e27dfd316ea05 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -130,7 +130,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(host != null); Debug.Assert(port != 0); Debug.Assert(sslHostName == null); - Debug.Assert(proxyUri == null); + Debug.Assert(socksKind == SocksConnectionKind.None || proxyUri == null); _http3Enabled = false; break; @@ -139,7 +139,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(host != null); Debug.Assert(port != 0); Debug.Assert(sslHostName != null); - Debug.Assert(proxyUri == null); + Debug.Assert(socksKind == SocksConnectionKind.None || proxyUri == null); break; case HttpConnectionKind.Proxy: diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index b01eca75e91b2..6c0f2b6dde527 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -288,11 +288,11 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox // Socks proxy if (sslHostName != null) { - return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, sslHostName, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); } else { - return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, null, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, null, proxyUri, identity); } } else if (sslHostName == null) From b5e37019874107a6511e94816d8fee8b1d2e83e4 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 1 Mar 2021 13:04:12 +0800 Subject: [PATCH 07/58] Use HttpConnectionKind for SOCKS. --- .../src/System.Net.Http.csproj | 1 - .../SocketsHttpHandler/HttpConnectionKind.cs | 4 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 53 +++++++++---------- .../HttpConnectionPoolManager.cs | 27 +++++----- .../SocketsHttpHandler/SocksConnectionKind.cs | 11 ---- 5 files changed, 40 insertions(+), 56 deletions(-) delete mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index f0a4da9b03569..3dd1ab6618727 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -176,7 +176,6 @@ - diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKind.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKind.cs index 873994bd03324..5e0b13a4ce891 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKind.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionKind.cs @@ -10,6 +10,8 @@ internal enum HttpConnectionKind : byte Proxy, // HTTP proxy usage for non-secure (HTTP) requests. ProxyTunnel, // Non-secure websocket (WS) connection using CONNECT tunneling through proxy. SslProxyTunnel, // HTTP proxy usage for secure (HTTPS/WSS) requests using SSL and proxy CONNECT. - ProxyConnect // Connection used for proxy CONNECT. Tunnel will be established on top of this. + ProxyConnect, // Connection used for proxy CONNECT. Tunnel will be established on top of this. + SocksTunnel, // SOCKS proxy usage for HTTP requests. + SslSocksTunnel // SOCKS proxy usage for HTTPS requests. } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index e27dfd316ea05..a16472c13c6e0 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -29,7 +29,6 @@ internal sealed class HttpConnectionPool : IDisposable private readonly HttpConnectionPoolManager _poolManager; private readonly HttpConnectionKind _kind; - private readonly SocksConnectionKind _socksKind; private readonly Uri? _proxyUri; /// The origin authority used to construct the . @@ -102,17 +101,15 @@ internal sealed class HttpConnectionPool : IDisposable /// Initializes the pool. /// The manager associated with this pool. /// The kind of HTTP connections stored in this pool. - /// The kind of SOCKS connection to use in this pool. /// The host with which this pool is associated. /// The port with which this pool is associated. /// The SSL host with which this pool is associated. /// The proxy this pool targets (optional). /// The maximum number of connections allowed to be associated with the pool at any given time. - public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, SocksConnectionKind socksKind, string? host, int port, string? sslHostName, Uri? proxyUri, int maxConnections) + public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, int maxConnections) { _poolManager = poolManager; _kind = kind; - _socksKind = socksKind; _proxyUri = proxyUri; _maxConnections = maxConnections; @@ -130,7 +127,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(host != null); Debug.Assert(port != 0); Debug.Assert(sslHostName == null); - Debug.Assert(socksKind == SocksConnectionKind.None || proxyUri == null); + Debug.Assert(proxyUri == null); _http3Enabled = false; break; @@ -139,7 +136,7 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK Debug.Assert(host != null); Debug.Assert(port != 0); Debug.Assert(sslHostName != null); - Debug.Assert(socksKind == SocksConnectionKind.None || proxyUri == null); + Debug.Assert(proxyUri == null); break; case HttpConnectionKind.Proxy: @@ -181,23 +178,26 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK _http3Enabled = false; break; - default: - Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor"); - break; - } + case HttpConnectionKind.SocksTunnel: + Debug.Assert(host != null); + Debug.Assert(port != 0); + Debug.Assert(sslHostName == null); + Debug.Assert(proxyUri != null); - switch (socksKind) - { - case SocksConnectionKind.None: + _http3Enabled = false; break; - case SocksConnectionKind.Socks5: - Debug.Assert(kind == HttpConnectionKind.Http || kind == HttpConnectionKind.Https); + case HttpConnectionKind.SslSocksTunnel: + Debug.Assert(host != null); + Debug.Assert(port != 0); + Debug.Assert(sslHostName != null); Debug.Assert(proxyUri != null); + + _http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3 break; default: - Debug.Fail("Unknown SocksConnectionKind in HttpConnectionPool.ctor"); + Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor"); break; } @@ -1252,18 +1252,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool case HttpConnectionKind.Https: case HttpConnectionKind.ProxyConnect: Debug.Assert(_originAuthority != null); - switch (_socksKind) - { - case SocksConnectionKind.None: - stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false); - break; - - case SocksConnectionKind.Socks5: - Debug.Assert(_proxyUri != null); - stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); - await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); - break; - } + stream = await ConnectToTcpHostAsync(_originAuthority.IdnHost, _originAuthority.Port, request, async, cancellationToken).ConfigureAwait(false); break; case HttpConnectionKind.Proxy: @@ -1281,6 +1270,14 @@ public ValueTask SendAsync(HttpRequestMessage request, bool return (null, null, response); } break; + + case HttpConnectionKind.SocksTunnel: + case HttpConnectionKind.SslSocksTunnel: + Debug.Assert(_originAuthority != null); + Debug.Assert(_proxyUri != null); + stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); + await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + break; } Debug.Assert(stream != null); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs index 6c0f2b6dde527..6dcb8a2949cbc 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPoolManager.cs @@ -260,7 +260,7 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox if (isProxyConnect) { Debug.Assert(uri == proxyUri); - return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, proxyUri, GetIdentityIfDefaultCredentialsUsed(_settings._defaultCredentialsUsedForProxy)); + return new HttpConnectionKey(HttpConnectionKind.ProxyConnect, uri.IdnHost, uri.Port, null, proxyUri, GetIdentityIfDefaultCredentialsUsed(_settings._defaultCredentialsUsedForProxy)); } string? sslHostName = null; @@ -288,11 +288,11 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox // Socks proxy if (sslHostName != null) { - return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); } else { - return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.Socks5, uri.IdnHost, uri.Port, null, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity); } } else if (sslHostName == null) @@ -300,29 +300,29 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme)) { // Non-secure websocket connection through proxy to the destination. - return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.ProxyTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity); } else { // Standard HTTP proxy usage for non-secure requests // The destination host and port are ignored here, since these connections // will be shared across any requests that use the proxy. - return new HttpConnectionKey(HttpConnectionKind.Proxy, SocksConnectionKind.None, null, 0, null, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.Proxy, null, 0, null, proxyUri, identity); } } else { // Tunnel SSL connection through proxy to the destination. - return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, SocksConnectionKind.None, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); + return new HttpConnectionKey(HttpConnectionKind.SslProxyTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity); } } else if (sslHostName != null) { - return new HttpConnectionKey(HttpConnectionKind.Https, SocksConnectionKind.None, uri.IdnHost, uri.Port, sslHostName, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Https, uri.IdnHost, uri.Port, sslHostName, null, identity); } else { - return new HttpConnectionKey(HttpConnectionKind.Http, SocksConnectionKind.None, uri.IdnHost, uri.Port, null, null, identity); + return new HttpConnectionKey(HttpConnectionKind.Http, uri.IdnHost, uri.Port, null, null, identity); } } @@ -333,7 +333,7 @@ public ValueTask SendAsyncCore(HttpRequestMessage request, HttpConnectionPool? pool; while (!_pools.TryGetValue(key, out pool)) { - pool = new HttpConnectionPool(this, key.Kind, key.SocksKind, key.Host, key.Port, key.SslHostName, key.ProxyUri, _maxConnectionsPerServer); + pool = new HttpConnectionPool(this, key.Kind, key.Host, key.Port, key.SslHostName, key.ProxyUri, _maxConnectionsPerServer); if (_cleaningTimer == null) { @@ -528,17 +528,15 @@ private static string GetIdentityIfDefaultCredentialsUsed(bool defaultCredential internal readonly struct HttpConnectionKey : IEquatable { public readonly HttpConnectionKind Kind; - public readonly SocksConnectionKind SocksKind; public readonly string? Host; public readonly int Port; public readonly string? SslHostName; // null if not SSL public readonly Uri? ProxyUri; public readonly string Identity; - public HttpConnectionKey(HttpConnectionKind kind, SocksConnectionKind socksKind, string? host, int port, string? sslHostName, Uri? proxyUri, string identity) + public HttpConnectionKey(HttpConnectionKind kind, string? host, int port, string? sslHostName, Uri? proxyUri, string identity) { Kind = kind; - SocksKind = socksKind; Host = host; Port = port; SslHostName = sslHostName; @@ -549,8 +547,8 @@ public HttpConnectionKey(HttpConnectionKind kind, SocksConnectionKind socksKind, // In the common case, SslHostName (when present) is equal to Host. If so, don't include in hash. public override int GetHashCode() => (SslHostName == Host ? - HashCode.Combine(Kind, SocksKind, Host, Port, ProxyUri, Identity) : - HashCode.Combine(Kind, SocksKind, Host, Port, SslHostName, ProxyUri, Identity)); + HashCode.Combine(Kind, Host, Port, ProxyUri, Identity) : + HashCode.Combine(Kind, Host, Port, SslHostName, ProxyUri, Identity)); public override bool Equals([NotNullWhen(true)] object? obj) => obj is HttpConnectionKey hck && @@ -558,7 +556,6 @@ obj is HttpConnectionKey hck && public bool Equals(HttpConnectionKey other) => Kind == other.Kind && - SocksKind == other.SocksKind && Host == other.Host && Port == other.Port && ProxyUri == other.ProxyUri && diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs deleted file mode 100644 index 3bf5675c8fb62..0000000000000 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksConnectionKind.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Net.Http -{ - internal enum SocksConnectionKind : byte - { - None, - Socks5 - } -} From 15b2a8f26d2d120d885d11cae072dfdd6e2af089 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 1 Mar 2021 13:11:04 +0800 Subject: [PATCH 08/58] Handle more connection kind assertions. --- .../Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index a16472c13c6e0..484e2112d4640 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -307,7 +307,7 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public HttpAuthority? OriginAuthority => _originAuthority; public HttpConnectionSettings Settings => _poolManager.Settings; public HttpConnectionKind Kind => _kind; - public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel; + public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel; public bool AnyProxyKind => (_proxyUri != null); public Uri? ProxyUri => _proxyUri; public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; @@ -330,10 +330,10 @@ public byte[] Http2AltSvcOriginUri Debug.Assert(_originAuthority != null); sb - .Append(_kind == HttpConnectionKind.Https ? "https://" : "http://") + .Append(IsSecure ? "https://" : "http://") .Append(_originAuthority.IdnHost); - if (_originAuthority.Port != (_kind == HttpConnectionKind.Https ? DefaultHttpsPort : DefaultHttpPort)) + if (_originAuthority.Port != (IsSecure ? DefaultHttpsPort : DefaultHttpPort)) { sb .Append(':') @@ -559,7 +559,7 @@ private static bool IsUsableHttp11Connection(HttpConnection connection, long now private async ValueTask<(HttpConnectionBase? connection, bool isNewConnection, HttpResponseMessage? failureResponse)> GetHttp2ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http); + Debug.Assert(_kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.Http || _kind == HttpConnectionKind.SocksTunnel || _kind == HttpConnectionKind.SslSocksTunnel); // See if we have an HTTP2 connection Http2Connection? http2Connection = GetExistingHttp2Connection(); @@ -621,7 +621,7 @@ private static bool IsUsableHttp11Connection(HttpConnection connection, long now sslStream = stream as SslStream; - if (_kind == HttpConnectionKind.Http) + if (!IsSecure) { http2Connection = await ConstructHttp2ConnectionAsync(stream, request, cancellationToken).ConfigureAwait(false); From 56bf28b7d3f4884ab779a5906c62a8b8f69e9dcc Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 1 Mar 2021 14:00:43 +0800 Subject: [PATCH 09/58] SOCKS4/4a support. --- .../src/System/Net/Http/HttpUtilities.cs | 4 +- .../SocketsHttpHandler/HttpConnectionPool.cs | 18 +++- .../Http/SocketsHttpHandler/SocksHelper.cs | 102 ++++++++++++++++-- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs index fb5f7de37bc93..8c9d786fd49d2 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpUtilities.cs @@ -38,7 +38,9 @@ internal static bool IsSupportedProxyScheme(string scheme) => string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase) || IsSocksScheme(scheme); internal static bool IsSocksScheme(string scheme) => - string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase); + string.Equals(scheme, "socks5", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "socks4a", StringComparison.OrdinalIgnoreCase) || + string.Equals(scheme, "socks4", StringComparison.OrdinalIgnoreCase); // Always specify TaskScheduler.Default to prevent us from using a user defined TaskScheduler.Current. // diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 484e2112d4640..65770d9eeda93 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1276,7 +1276,23 @@ public ValueTask SendAsync(HttpRequestMessage request, bool Debug.Assert(_originAuthority != null); Debug.Assert(_proxyUri != null); stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); - await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + + if (string.Equals(_proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + { + await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + } + else if (string.Equals(_proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) + { + await SocksHelper.EstablishSocks4TunnelAsync(stream, true, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + } + else if (string.Equals(_proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) + { + await SocksHelper.EstablishSocks4TunnelAsync(stream, false, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + } + else + { + Debug.Fail("Bad socks version."); + } break; } 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 a51092e47d4b3..c45795a15b3fb 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 @@ -10,12 +10,12 @@ namespace System.Net.Http { - // https://tools.ietf.org/html/rfc1928 internal static class SocksHelper { // socks protocol limits address length to 1 byte, thus the maximum possible buffer is 256+other fields private const int BufferSize = 512; - private const int ProtocolVersion = 5; + private const int ProtocolVersion4 = 4; + private const int ProtocolVersion5 = 5; private const byte METHOD_NO_AUTH = 0; // private const byte METHOD_GSSAPI = 1; private const byte METHOD_USERNAME_PASSWORD = 2; @@ -35,6 +35,7 @@ internal static class SocksHelper // private const byte REP_TTL_EXPIRED = 6; // private const byte REP_CMD_NOT_SUPPORT = 7; // private const byte REP_ATYP_NOT_SUPPORT = 8; + private const byte CD_SUCCESS = 90; public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) { @@ -42,12 +43,14 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h try { + // https://tools.ietf.org/html/rfc1928 + // +----+----------+----------+ // |VER | NMETHODS | METHODS | // +----+----------+----------+ // | 1 | 1 | 1 to 255 | // +----+----------+----------+ - buffer[0] = ProtocolVersion; + buffer[0] = ProtocolVersion5; var credentials = proxyCredentials?.GetCredential(proxyUri, ""); if (credentials != null) { @@ -68,7 +71,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // | 1 | 1 | // +----+--------+ await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); - if (buffer[0] != ProtocolVersion) + if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); switch (buffer[1]) @@ -88,7 +91,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - buffer[0] = ProtocolVersion; + buffer[0] = ProtocolVersion5; int uLen = Encoding.UTF8.GetByteCount(credentials.UserName); buffer[1] = checked((byte)uLen); int uLenEncoded = Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2)); @@ -105,7 +108,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // | 1 | 1 | // +----+--------+ await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); - if (buffer[0] != ProtocolVersion) + if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) throw new Exception("Authentication failed."); @@ -123,7 +126,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - buffer[0] = ProtocolVersion; + buffer[0] = ProtocolVersion5; buffer[1] = CMD_CONNECT; buffer[2] = 0; buffer[3] = ATYP_DOMAIN_NAME; @@ -146,7 +149,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ await stream.ReadAsync(buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); - if (buffer[0] != ProtocolVersion) + if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) throw new Exception("Connection failed"); @@ -174,5 +177,88 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h ArrayPool.Shared.Return(buffer); } } + + public static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + { + byte[] buffer = ArrayPool.Shared.Rent(BufferSize); + + try + { + // https://www.openssh.com/txt/socks4.protocol + + // +----+----+----+----+----+----+----+----+----+----+....+----+ + // | VN | CD | DSTPORT | DSTIP | USERID |NULL| + // +----+----+----+----+----+----+----+----+----+----+....+----+ + // 1 1 2 4 variable 1 + string? username = proxyCredentials?.GetCredential(proxyUri, "")?.UserName; + buffer[0] = ProtocolVersion4; + buffer[1] = CMD_CONNECT; + + Debug.Assert(port > 0); + Debug.Assert(port < ushort.MaxValue); + buffer[2] = (byte)(port >> 8); + buffer[3] = (byte)port; + + if (isVersion4a) + { + buffer[4] = 0; + buffer[5] = 0; + buffer[6] = 0; + buffer[7] = 255; + } + else + { + bool addressWritten = false; + foreach (var address in await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false)) + { + // SOCKS4 supports only IPv4 + if (address.AddressFamily == Sockets.AddressFamily.InterNetwork) + { + address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); + Debug.Assert(bytesWritten == 4); + addressWritten = true; + break; + } + } + if (!addressWritten) + { + throw new Exception("No suitable IPv4 address."); + } + } + + int uLen = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); + buffer[8 + uLen] = 0; + int totalLength = 9 + uLen; + + if (isVersion4a) + { + // https://www.openssh.com/txt/socks4a.protocol + int aLen = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + uLen)); + buffer[9 + uLen + aLen] = 0; + totalLength += aLen + 1; + } + + await stream.WriteAsync(buffer.AsMemory(0, totalLength), cancellationToken).ConfigureAwait(false); + + // +----+----+----+----+----+----+----+----+ + // | VN | CD | DSTPORT | DSTIP | + // +----+----+----+----+----+----+----+----+ + // 1 1 2 4 + await stream.ReadAsync(buffer.AsMemory(0, 8), cancellationToken).ConfigureAwait(false); + if (buffer[0] != ProtocolVersion4) + { + throw new Exception("Bad protocol version"); + } + if (buffer[1] != CD_SUCCESS) + { + throw new Exception("Connection failed"); + } + // response address not used + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } } } From a2a80f7cd13e87a53cf2b5ecb924032e5bd47404 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 1 Mar 2021 14:22:01 +0800 Subject: [PATCH 10/58] Move version selection into SocksHelper. --- .../SocketsHttpHandler/HttpConnectionPool.cs | 18 +------------ .../Http/SocketsHttpHandler/SocksHelper.cs | 25 +++++++++++++++++-- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 65770d9eeda93..632f12ab16021 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1276,23 +1276,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool Debug.Assert(_originAuthority != null); Debug.Assert(_proxyUri != null); stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); - - if (string.Equals(_proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) - { - await SocksHelper.EstablishSocks5TunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); - } - else if (string.Equals(_proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) - { - await SocksHelper.EstablishSocks4TunnelAsync(stream, true, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); - } - else if (string.Equals(_proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) - { - await SocksHelper.EstablishSocks4TunnelAsync(stream, false, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); - } - else - { - Debug.Fail("Bad socks version."); - } + await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); break; } 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 c45795a15b3fb..f3f5377393d50 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,28 @@ internal static class SocksHelper // private const byte REP_ATYP_NOT_SUPPORT = 8; private const byte CD_SUCCESS = 90; - public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + public static ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + { + if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + { + return EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, cancellationToken); + } + else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) + { + return EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, cancellationToken); + } + else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) + { + return EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, cancellationToken); + } + else + { + Debug.Fail("Bad socks version."); + return default; + } + } + + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -178,7 +199,7 @@ public static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string h } } - public static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); From 1d7fa76fe0e2f2fed23456326fcc347f961794ea Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 2 Mar 2021 19:25:47 +0800 Subject: [PATCH 11/58] Call sync version of read write. --- .../SocketsHttpHandler/HttpConnectionPool.cs | 2 +- .../Http/SocketsHttpHandler/SocksHelper.cs | 81 ++++++++++++++----- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 632f12ab16021..6d95fcc32f546 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1276,7 +1276,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool Debug.Assert(_originAuthority != null); Debug.Assert(_proxyUri != null); stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); - await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, cancellationToken).ConfigureAwait(false); + await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, async, cancellationToken).ConfigureAwait(false); break; } 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 f3f5377393d50..3ef70a6cc48ed 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,19 +37,19 @@ internal static class SocksHelper // private const byte REP_ATYP_NOT_SUPPORT = 8; private const byte CD_SUCCESS = 90; - public static ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + public static ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) { - return EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, cancellationToken); + return EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async, cancellationToken); } else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) { - return EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, cancellationToken); + return EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async, cancellationToken); } else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) { - return EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, cancellationToken); + return EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async, cancellationToken); } else { @@ -58,7 +58,7 @@ public static ValueTask EstablishSocksTunnelAsync(Stream stream, string host, in } } - private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -84,14 +84,21 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2] = METHOD_NO_AUTH; buffer[3] = METHOD_USERNAME_PASSWORD; } - await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2), cancellationToken).ConfigureAwait(false); + if (async) + { + await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2), cancellationToken).ConfigureAwait(false); + } + else + { + stream.Write(buffer.AsSpan(0, buffer[1] + 2)); + } // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ - await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); @@ -121,14 +128,21 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2 + uLen] = checked((byte)pLen); int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); Debug.Assert(pLen == pLenEncoded); - await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen), cancellationToken).ConfigureAwait(false); + if (async) + { + await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen), cancellationToken).ConfigureAwait(false); + } + else + { + stream.Write(buffer.AsSpan(0, 4 + uLen + pLen)); + } // +----+--------+ // |VER | STATUS | // +----+--------+ // | 1 | 1 | // +----+--------+ - await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -162,14 +176,21 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[addressLength + 5] = (byte)(port >> 8); buffer[addressLength + 6] = (byte)port; - await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7), cancellationToken).ConfigureAwait(false); + if (async) + { + await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7), cancellationToken).ConfigureAwait(false); + } + else + { + stream.Write(buffer.AsSpan(0, addressLength + 7)); + } // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - await stream.ReadAsync(buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -177,20 +198,20 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string switch (buffer[3]) { case ATYP_IPV4: - await stream.ReadAsync(buffer.AsMemory(0, 4), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async, cancellationToken).ConfigureAwait(false); break; case ATYP_IPV6: - await stream.ReadAsync(buffer.AsMemory(0, 16), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 16), async, cancellationToken).ConfigureAwait(false); break; case ATYP_DOMAIN_NAME: - await stream.ReadAsync(buffer.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 1), async, cancellationToken).ConfigureAwait(false); addressLength = buffer[0]; await stream.ReadAsync(buffer.AsMemory(0, addressLength), cancellationToken).ConfigureAwait(false); break; default: throw new Exception("Unknown address type"); } - await stream.ReadAsync(buffer.AsMemory(0, 2), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); // response address not used } finally @@ -199,7 +220,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } } - private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -259,13 +280,20 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is totalLength += aLen + 1; } - await stream.WriteAsync(buffer.AsMemory(0, totalLength), cancellationToken).ConfigureAwait(false); + if (async) + { + await stream.WriteAsync(buffer.AsMemory(0, totalLength), cancellationToken).ConfigureAwait(false); + } + else + { + stream.Write(buffer.AsSpan(0, totalLength)); + } // +----+----+----+----+----+----+----+----+ // | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // 1 1 2 4 - await stream.ReadAsync(buffer.AsMemory(0, 8), cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion4) { throw new Exception("Bad protocol version"); @@ -281,5 +309,22 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is ArrayPool.Shared.Return(buffer); } } + + private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) + { + while (!buffer.IsEmpty) + { + int bytesRead = async + ? await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false) + : stream.Read(buffer.Span); + + if (bytesRead == 0) + { + throw new Exception("Early EOF"); + } + + buffer = buffer[bytesRead..]; + } + } } } From 44cce2ed19b7d2d29e6a94ea80d1ae4451b18430 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 2 Mar 2021 19:33:09 +0800 Subject: [PATCH 12/58] Cancellation by disposing stream. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) 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 3ef70a6cc48ed..44085c019cfa6 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,28 +37,37 @@ internal static class SocksHelper // private const byte REP_ATYP_NOT_SUPPORT = 8; private const byte CD_SUCCESS = 90; - public static ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) + public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { - if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) - { - return EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async, cancellationToken); - } - else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) - { - return EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async, cancellationToken); - } - else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) + cancellationToken.Register(() => stream.Dispose()); + + try { - return EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async, cancellationToken); + if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else + { + Debug.Fail("Bad socks version."); + } } - else + catch { - Debug.Fail("Bad socks version."); - return default; + stream.Dispose(); + throw; } } - private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -86,7 +95,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } if (async) { - await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2), cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2)).ConfigureAwait(false); } else { @@ -98,7 +107,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); @@ -130,7 +139,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string Debug.Assert(pLen == pLenEncoded); if (async) { - await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen), cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen)).ConfigureAwait(false); } else { @@ -142,7 +151,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -178,7 +187,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string if (async) { - await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7), cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7)).ConfigureAwait(false); } else { @@ -190,7 +199,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -198,20 +207,20 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string switch (buffer[3]) { case ATYP_IPV4: - await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async).ConfigureAwait(false); break; case ATYP_IPV6: - await ReadToFillAsync(stream, buffer.AsMemory(0, 16), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 16), async).ConfigureAwait(false); break; case ATYP_DOMAIN_NAME: - await ReadToFillAsync(stream, buffer.AsMemory(0, 1), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 1), async).ConfigureAwait(false); addressLength = buffer[0]; - await stream.ReadAsync(buffer.AsMemory(0, addressLength), cancellationToken).ConfigureAwait(false); + await stream.ReadAsync(buffer.AsMemory(0, addressLength)).ConfigureAwait(false); break; default: throw new Exception("Unknown address type"); } - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); // response address not used } finally @@ -220,7 +229,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } } - private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -251,7 +260,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is else { bool addressWritten = false; - foreach (var address in await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false)) + foreach (var address in await Dns.GetHostAddressesAsync(host).ConfigureAwait(false)) { // SOCKS4 supports only IPv4 if (address.AddressFamily == Sockets.AddressFamily.InterNetwork) @@ -282,7 +291,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is if (async) { - await stream.WriteAsync(buffer.AsMemory(0, totalLength), cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer.AsMemory(0, totalLength)).ConfigureAwait(false); } else { @@ -293,7 +302,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // 1 1 2 4 - await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion4) { throw new Exception("Bad protocol version"); @@ -310,12 +319,12 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) + private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async) { while (!buffer.IsEmpty) { int bytesRead = async - ? await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false) + ? await stream.ReadAsync(buffer).ConfigureAwait(false) : stream.Read(buffer.Span); if (bytesRead == 0) From 140bbc193a832a35c30489c3adee0de936130f62 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 3 Mar 2021 17:08:12 +0800 Subject: [PATCH 13/58] Dispose cancellation registration. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) 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 44085c019cfa6..27d20c6b7f669 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 @@ -39,32 +39,33 @@ internal static class SocksHelper public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { - cancellationToken.Register(() => stream.Dispose()); - - try + using (cancellationToken.Register(() => stream.Dispose())) { - if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + try { - await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); - } - else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) - { - await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); - } - else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) - { - await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) + { + await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + } + else + { + Debug.Fail("Bad socks version."); + } } - else + catch { - Debug.Fail("Bad socks version."); + stream.Dispose(); + throw; } } - catch - { - stream.Dispose(); - throw; - } } private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) From 694b869324cbdbb9b70c6935fdee4bc858b7e2f4 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 3 Mar 2021 17:17:02 +0800 Subject: [PATCH 14/58] IP addressing for SOCKS5. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) 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 27d20c6b7f669..738d742563cae 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 @@ -174,12 +174,34 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[0] = ProtocolVersion5; buffer[1] = CMD_CONNECT; buffer[2] = 0; - buffer[3] = ATYP_DOMAIN_NAME; + int addressLength; - int addressLength = Encoding.UTF8.GetByteCount(host); - buffer[4] = checked((byte)addressLength); - int bytesEncoded = Encoding.UTF8.GetBytes(host, buffer.AsSpan(5)); - Debug.Assert(bytesEncoded == addressLength); + if (IPAddress.TryParse(host, out var hostIP)) + { + if (hostIP.AddressFamily == Sockets.AddressFamily.InterNetwork) + { + buffer[3] = ATYP_IPV4; + hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); + Debug.Assert(bytesWritten == 4); + addressLength = 3; + } + else + { + Debug.Assert(hostIP.AddressFamily == Sockets.AddressFamily.InterNetworkV6); + buffer[3] = ATYP_IPV6; + hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); + Debug.Assert(bytesWritten == 16); + addressLength = 15; + } + } + else + { + buffer[3] = ATYP_DOMAIN_NAME; + addressLength = Encoding.UTF8.GetByteCount(host); + buffer[4] = checked((byte)addressLength); + int bytesEncoded = Encoding.UTF8.GetBytes(host, buffer.AsSpan(5)); + Debug.Assert(bytesEncoded == addressLength); + } Debug.Assert(port > 0); Debug.Assert(port < ushort.MaxValue); From e68224a0ad1c4844593d28882729624bdb8344b1 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 3 Mar 2021 17:26:23 +0800 Subject: [PATCH 15/58] IP addressing for SOCKS4. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) 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 738d742563cae..bbe35a1089c77 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 @@ -273,38 +273,59 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is buffer[2] = (byte)(port >> 8); buffer[3] = (byte)port; - if (isVersion4a) + IPAddress? ipv4Address = null; + if (IPAddress.TryParse(host, out var hostIP)) { - buffer[4] = 0; - buffer[5] = 0; - buffer[6] = 0; - buffer[7] = 255; + if (hostIP.AddressFamily == Sockets.AddressFamily.InterNetwork) + { + ipv4Address = hostIP; + } + else if (hostIP.IsIPv4MappedToIPv6) + { + ipv4Address = hostIP.MapToIPv4(); + } + else + { + throw new Exception("SOCKS4 does not support IPv6."); + } } - else + else if (!isVersion4a) { - bool addressWritten = false; + // SOCKS4 requires DNS resolution locally foreach (var address in await Dns.GetHostAddressesAsync(host).ConfigureAwait(false)) { - // SOCKS4 supports only IPv4 if (address.AddressFamily == Sockets.AddressFamily.InterNetwork) { - address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); - Debug.Assert(bytesWritten == 4); - addressWritten = true; + ipv4Address = address; break; } } - if (!addressWritten) + } + + if (ipv4Address == null) + { + if (!isVersion4a) { + // Fails to get IPv4 address in SOCKS4 path throw new Exception("No suitable IPv4 address."); } + + buffer[4] = 0; + buffer[5] = 0; + buffer[6] = 0; + buffer[7] = 255; + } + else + { + ipv4Address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); + Debug.Assert(bytesWritten == 4); } int uLen = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); buffer[8 + uLen] = 0; int totalLength = 9 + uLen; - if (isVersion4a) + if (isVersion4a && ipv4Address == null) { // https://www.openssh.com/txt/socks4a.protocol int aLen = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + uLen)); From 24e1e2d5636b5616ab23379c1b5b02d40475f9e2 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 3 Mar 2021 17:33:41 +0800 Subject: [PATCH 16/58] Wrap write method. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) 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 bbe35a1089c77..35ba3121af114 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 @@ -94,14 +94,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2] = METHOD_NO_AUTH; buffer[3] = METHOD_USERNAME_PASSWORD; } - if (async) - { - await stream.WriteAsync(buffer.AsMemory(0, buffer[1] + 2)).ConfigureAwait(false); - } - else - { - stream.Write(buffer.AsSpan(0, buffer[1] + 2)); - } + await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false); // +----+--------+ // |VER | METHOD | @@ -138,14 +131,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2 + uLen] = checked((byte)pLen); int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); Debug.Assert(pLen == pLenEncoded); - if (async) - { - await stream.WriteAsync(buffer.AsMemory(0, 4 + uLen + pLen)).ConfigureAwait(false); - } - else - { - stream.Write(buffer.AsSpan(0, 4 + uLen + pLen)); - } + await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async).ConfigureAwait(false); // +----+--------+ // |VER | STATUS | @@ -208,14 +194,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[addressLength + 5] = (byte)(port >> 8); buffer[addressLength + 6] = (byte)port; - if (async) - { - await stream.WriteAsync(buffer.AsMemory(0, addressLength + 7)).ConfigureAwait(false); - } - else - { - stream.Write(buffer.AsSpan(0, addressLength + 7)); - } + await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async).ConfigureAwait(false); // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | @@ -333,14 +312,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is totalLength += aLen + 1; } - if (async) - { - await stream.WriteAsync(buffer.AsMemory(0, totalLength)).ConfigureAwait(false); - } - else - { - stream.Write(buffer.AsSpan(0, totalLength)); - } + await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false); // +----+----+----+----+----+----+----+----+ // | VN | CD | DSTPORT | DSTIP | @@ -363,6 +335,18 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } + private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async) + { + if (async) + { + await stream.WriteAsync(buffer).ConfigureAwait(false); + } + else + { + stream.Write(buffer.Span); + } + } + private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async) { while (!buffer.IsEmpty) From d1a8eae1f4460534c2cfcc0174f75d648f9ba989 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 3 Mar 2021 17:48:18 +0800 Subject: [PATCH 17/58] Cancellation and optimization. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 84 +++++++++---------- 1 file changed, 39 insertions(+), 45 deletions(-) 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 35ba3121af114..8c4be4e5bccc5 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 @@ -39,21 +39,22 @@ internal static class SocksHelper public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { + // in sync path, dispose the stream to cancel using (cancellationToken.Register(() => stream.Dispose())) { try { if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); } else { @@ -68,7 +69,7 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho } } - private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -94,14 +95,14 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2] = METHOD_NO_AUTH; buffer[3] = METHOD_USERNAME_PASSWORD; } - await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async, cancellationToken).ConfigureAwait(false); // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); @@ -131,14 +132,14 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2 + uLen] = checked((byte)pLen); int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); Debug.Assert(pLen == pLenEncoded); - await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async, cancellationToken).ConfigureAwait(false); // +----+--------+ // |VER | STATUS | // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -194,35 +195,26 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[addressLength + 5] = (byte)(port >> 8); buffer[addressLength + 6] = (byte)port; - await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async, cancellationToken).ConfigureAwait(false); // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) throw new Exception("Connection failed"); - switch (buffer[3]) + int bytesToSkip = buffer[3] switch { - case ATYP_IPV4: - await ReadToFillAsync(stream, buffer.AsMemory(0, 4), async).ConfigureAwait(false); - break; - case ATYP_IPV6: - await ReadToFillAsync(stream, buffer.AsMemory(0, 16), async).ConfigureAwait(false); - break; - case ATYP_DOMAIN_NAME: - await ReadToFillAsync(stream, buffer.AsMemory(0, 1), async).ConfigureAwait(false); - addressLength = buffer[0]; - await stream.ReadAsync(buffer.AsMemory(0, addressLength)).ConfigureAwait(false); - break; - default: - throw new Exception("Unknown address type"); - } - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); + ATYP_IPV4 => 5, + ATYP_IPV6 => 17, + ATYP_DOMAIN_NAME => buffer[4] + 2, + _ => throw new Exception("Unknown address type") + }; + await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async, cancellationToken).ConfigureAwait(false); // response address not used } finally @@ -231,7 +223,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } } - private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) + private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -271,24 +263,20 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is else if (!isVersion4a) { // SOCKS4 requires DNS resolution locally - foreach (var address in await Dns.GetHostAddressesAsync(host).ConfigureAwait(false)) + var addresses = async + ? await Dns.GetHostAddressesAsync(host, Sockets.AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) + : Dns.GetHostAddresses(host); + + if (addresses.Length == 0) { - if (address.AddressFamily == Sockets.AddressFamily.InterNetwork) - { - ipv4Address = address; - break; - } + throw new Exception("No suitable IPv4 address."); } + + ipv4Address = addresses[0]; } if (ipv4Address == null) { - if (!isVersion4a) - { - // Fails to get IPv4 address in SOCKS4 path - throw new Exception("No suitable IPv4 address."); - } - buffer[4] = 0; buffer[5] = 0; buffer[6] = 0; @@ -298,6 +286,12 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is { ipv4Address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); Debug.Assert(bytesWritten == 4); + if (buffer[4] == 0 && buffer[5] == 0 && buffer[6] == 0) + { + // Invalid IP address used by SOCKS4a to represent remote DNS. + // In case we don't have a domain name, throwing. + throw new Exception("Invalid ip address."); + } } int uLen = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); @@ -312,13 +306,13 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is totalLength += aLen + 1; } - await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, totalLength), async, cancellationToken).ConfigureAwait(false); // +----+----+----+----+----+----+----+----+ // | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // 1 1 2 4 - await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async, cancellationToken).ConfigureAwait(false); if (buffer[0] != ProtocolVersion4) { throw new Exception("Bad protocol version"); @@ -335,11 +329,11 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async) + private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) { if (async) { - await stream.WriteAsync(buffer).ConfigureAwait(false); + await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); } else { @@ -347,12 +341,12 @@ private static async ValueTask WriteAsync(Stream stream, Memory buffer, bo } } - private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async) + private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) { while (!buffer.IsEmpty) { int bytesRead = async - ? await stream.ReadAsync(buffer).ConfigureAwait(false) + ? await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false) : stream.Read(buffer.Span); if (bytesRead == 0) From 78362310f7aaf54770c80e82da4de550de6f4eb3 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 4 Mar 2021 19:57:28 +0800 Subject: [PATCH 18/58] Optimize. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) 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 8c4be4e5bccc5..4c61a61a31487 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 @@ -40,13 +40,13 @@ internal static class SocksHelper public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { // in sync path, dispose the stream to cancel - using (cancellationToken.Register(() => stream.Dispose())) + using (cancellationToken.Register(s => ((Stream)s!).Dispose(), stream)) { try { if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); + await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) { @@ -69,7 +69,7 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho } } - private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); @@ -95,14 +95,14 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2] = METHOD_NO_AUTH; buffer[3] = METHOD_USERNAME_PASSWORD; } - await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async, cancellationToken).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false); // +----+--------+ // |VER | METHOD | // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); @@ -132,14 +132,14 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2 + uLen] = checked((byte)pLen); int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); Debug.Assert(pLen == pLenEncoded); - await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async, cancellationToken).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async).ConfigureAwait(false); // +----+--------+ // |VER | STATUS | // +----+--------+ // | 1 | 1 | // +----+--------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -195,14 +195,14 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[addressLength + 5] = (byte)(port >> 8); buffer[addressLength + 6] = (byte)port; - await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async, cancellationToken).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async).ConfigureAwait(false); // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | // +----+-----+-------+------+----------+----------+ // | 1 | 1 | X'00' | 1 | Variable | 2 | // +----+-----+-------+------+----------+----------+ - await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion5) throw new Exception("Bad protocol version"); if (buffer[1] != REP_SUCCESS) @@ -214,7 +214,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string ATYP_DOMAIN_NAME => buffer[4] + 2, _ => throw new Exception("Unknown address type") }; - await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async).ConfigureAwait(false); // response address not used } finally @@ -306,13 +306,13 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is totalLength += aLen + 1; } - await WriteAsync(stream, buffer.AsMemory(0, totalLength), async, cancellationToken).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false); // +----+----+----+----+----+----+----+----+ // | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // 1 1 2 4 - await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async, cancellationToken).ConfigureAwait(false); + await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); if (buffer[0] != ProtocolVersion4) { throw new Exception("Bad protocol version"); @@ -329,11 +329,11 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) + private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async) { if (async) { - await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer).ConfigureAwait(false); } else { @@ -341,12 +341,12 @@ private static async ValueTask WriteAsync(Stream stream, Memory buffer, bo } } - private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async, CancellationToken cancellationToken) + private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async) { while (!buffer.IsEmpty) { int bytesRead = async - ? await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false) + ? await stream.ReadAsync(buffer).ConfigureAwait(false) : stream.Read(buffer.Span); if (bytesRead == 0) From da3ef586c84119a94dce4ca1f369190b6965d945 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Fri, 5 Mar 2021 00:38:51 +0800 Subject: [PATCH 19/58] Apply suggestions from code review Co-authored-by: Miha Zupan --- .../Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 10 +--------- .../System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 10 +++++----- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 6d95fcc32f546..8d12508533137 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -179,18 +179,10 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK break; case HttpConnectionKind.SocksTunnel: - Debug.Assert(host != null); - Debug.Assert(port != 0); - Debug.Assert(sslHostName == null); - Debug.Assert(proxyUri != null); - - _http3Enabled = false; - break; - case HttpConnectionKind.SslSocksTunnel: Debug.Assert(host != null); Debug.Assert(port != 0); - Debug.Assert(sslHostName != null); + Debug.Assert(sslHostName == null); Debug.Assert(proxyUri != null); _http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3 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 4c61a61a31487..df11fedc27f64 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 @@ -39,7 +39,6 @@ internal static class SocksHelper public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { - // in sync path, dispose the stream to cancel using (cancellationToken.Register(s => ((Stream)s!).Dispose(), stream)) { try @@ -302,7 +301,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is { // https://www.openssh.com/txt/socks4a.protocol int aLen = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + uLen)); - buffer[9 + uLen + aLen] = 0; + buffer[totalLength + aLen] = 0; totalLength += aLen + 1; } @@ -329,15 +328,16 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - private static async ValueTask WriteAsync(Stream stream, Memory buffer, bool async) + private static ValueTask WriteAsync(Stream stream, Memory buffer, bool async) { if (async) { - await stream.WriteAsync(buffer).ConfigureAwait(false); + return stream.WriteAsync(buffer); } else { stream.Write(buffer.Span); + return default; } } @@ -351,7 +351,7 @@ private static async ValueTask ReadToFillAsync(Stream stream, Memory buffe if (bytesRead == 0) { - throw new Exception("Early EOF"); + throw new IOException(SR.net_http_invalid_response_premature_eof); } buffer = buffer[bytesRead..]; From 73714073a49375aef3d8cad4a39221a8c3e355dd Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Fri, 5 Mar 2021 00:49:43 +0800 Subject: [PATCH 20/58] Clarify logic. --- .../Http/SocketsHttpHandler/SocksHelper.cs | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) 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 df11fedc27f64..b5d368edf6463 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 @@ -123,15 +123,15 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ buffer[0] = ProtocolVersion5; - int uLen = Encoding.UTF8.GetByteCount(credentials.UserName); - buffer[1] = checked((byte)uLen); - int uLenEncoded = Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2)); - Debug.Assert(uLen == uLenEncoded); - int pLen = Encoding.UTF8.GetByteCount(credentials.Password); - buffer[2 + uLen] = checked((byte)pLen); - int pLenEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + uLen)); - Debug.Assert(pLen == pLenEncoded); - await WriteAsync(stream, buffer.AsMemory(0, 4 + uLen + pLen), async).ConfigureAwait(false); + int usernameLength = Encoding.UTF8.GetByteCount(credentials.UserName); + buffer[1] = checked((byte)usernameLength); + int usernameLengthEncoded = Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2)); + Debug.Assert(usernameLength == usernameLengthEncoded); + int passwordLength = Encoding.UTF8.GetByteCount(credentials.Password); + buffer[2 + usernameLength] = checked((byte)passwordLength); + int passwordLengthEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + usernameLength)); + Debug.Assert(passwordLength == passwordLengthEncoded); + await WriteAsync(stream, buffer.AsMemory(0, 4 + usernameLength + passwordLength), async).ConfigureAwait(false); // +----+--------+ // |VER | STATUS | @@ -169,7 +169,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[3] = ATYP_IPV4; hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); Debug.Assert(bytesWritten == 4); - addressLength = 3; + addressLength = 4; } else { @@ -177,24 +177,23 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[3] = ATYP_IPV6; hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); Debug.Assert(bytesWritten == 16); - addressLength = 15; + addressLength = 16; } } else { buffer[3] = ATYP_DOMAIN_NAME; - addressLength = Encoding.UTF8.GetByteCount(host); - buffer[4] = checked((byte)addressLength); + int hostLength = Encoding.UTF8.GetByteCount(host); + buffer[4] = checked((byte)hostLength); int bytesEncoded = Encoding.UTF8.GetBytes(host, buffer.AsSpan(5)); - Debug.Assert(bytesEncoded == addressLength); + Debug.Assert(bytesEncoded == hostLength); + addressLength = hostLength + 1; } - Debug.Assert(port > 0); - Debug.Assert(port < ushort.MaxValue); - buffer[addressLength + 5] = (byte)(port >> 8); - buffer[addressLength + 6] = (byte)port; + buffer[addressLength + 4] = (byte)(port >> 8); + buffer[addressLength + 5] = (byte)port; - await WriteAsync(stream, buffer.AsMemory(0, addressLength + 7), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, addressLength + 6), async).ConfigureAwait(false); // +----+-----+-------+------+----------+----------+ // |VER | REP | RSV | ATYP | DST.ADDR | DST.PORT | @@ -238,8 +237,6 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is buffer[0] = ProtocolVersion4; buffer[1] = CMD_CONNECT; - Debug.Assert(port > 0); - Debug.Assert(port < ushort.MaxValue); buffer[2] = (byte)(port >> 8); buffer[3] = (byte)port; @@ -293,16 +290,16 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - int uLen = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); - buffer[8 + uLen] = 0; - int totalLength = 9 + uLen; + int usernameLength = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); + buffer[8 + usernameLength] = 0; + int totalLength = 9 + usernameLength; if (isVersion4a && ipv4Address == null) { // https://www.openssh.com/txt/socks4a.protocol - int aLen = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + uLen)); - buffer[totalLength + aLen] = 0; - totalLength += aLen + 1; + int hostLength = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + usernameLength)); + buffer[totalLength + hostLength] = 0; + totalLength += hostLength + 1; } await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false); From c007769f711ce0554bd681b3ecbe12768d66f941 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Fri, 5 Mar 2021 00:55:22 +0800 Subject: [PATCH 21/58] Remove ssl assertion. --- .../src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 8d12508533137..05123374364bd 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -182,7 +182,6 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK case HttpConnectionKind.SslSocksTunnel: Debug.Assert(host != null); Debug.Assert(port != 0); - Debug.Assert(sslHostName == null); Debug.Assert(proxyUri != null); _http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3 From 0517a87aa23c43380dbc1d2fbd35da2547b4e248 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Fri, 5 Mar 2021 01:39:20 +0800 Subject: [PATCH 22/58] SocksException. --- .../src/Resources/Strings.resx | 82 ++++++++++++------- .../src/System.Net.Http.csproj | 1 + .../Http/SocketsHttpHandler/SocksException.cs | 12 +++ .../Http/SocketsHttpHandler/SocksHelper.cs | 46 ++++++----- 4 files changed, 93 insertions(+), 48 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 2c03bfb4e7390..fa8a186c92fe3 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -406,7 +406,7 @@ Client certificate was not found in the personal (\"MY\") certificate store. In UWP, client certificates are only supported if they have been added to that certificate store. - Only the 'http' scheme is allowed for proxies. + Only the 'http', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies. Request headers must contain only ASCII characters. @@ -606,4 +606,28 @@ Synchronous reads are not supported, use ReadAsync instead. - + + SOCKS authentication failed. + + + Address type returned from SOCKS server cannot be determined. + + + SOCKS server failed to connect to destination. + + + The destination ip is invalid for SOCKS4 protocol. + + + SOCKS4 does not support IPv6. + + + SOCKS server does not return suitable authentication method. + + + Cannot resolve IPv4 address for host. + + + Unexpected SOCKS protocol version. Required {0}, got {1}. + + \ No newline at end of file diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 3dd1ab6618727..4826be3f2297a 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -177,6 +177,7 @@ + 5, ATYP_IPV6 => 17, ATYP_DOMAIN_NAME => buffer[4] + 2, - _ => throw new Exception("Unknown address type") + _ => throw new SocksException(SR.net_socks_bad_address_type) }; await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async).ConfigureAwait(false); // response address not used @@ -253,7 +256,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } else { - throw new Exception("SOCKS4 does not support IPv6."); + throw new SocksException(SR.net_socks_ipv6_notsupported); } } else if (!isVersion4a) @@ -265,7 +268,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is if (addresses.Length == 0) { - throw new Exception("No suitable IPv4 address."); + throw new SocksException(SR.net_socks_no_ipv4_address); } ipv4Address = addresses[0]; @@ -286,7 +289,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is { // Invalid IP address used by SOCKS4a to represent remote DNS. // In case we don't have a domain name, throwing. - throw new Exception("Invalid ip address."); + throw new SocksException(SR.net_socks_ipv4_invalid); } } @@ -309,13 +312,10 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // +----+----+----+----+----+----+----+----+ // 1 1 2 4 await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); - if (buffer[0] != ProtocolVersion4) - { - throw new Exception("Bad protocol version"); - } + VerifyProtocolVersion(ProtocolVersion4, buffer[0]); if (buffer[1] != CD_SUCCESS) { - throw new Exception("Connection failed"); + throw new SocksException(SR.net_socks_connection_failed); } // response address not used } @@ -325,6 +325,14 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } + private static void VerifyProtocolVersion(byte expected, byte version) + { + if (expected != version) + { + throw new SocksException(SR.Format(SR.net_socks_unexpected_version, expected, version)); + } + } + private static ValueTask WriteAsync(Stream stream, Memory buffer, bool async) { if (async) From ca9babed46836d21ee52978e092eadb857e6a88d Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Sat, 6 Mar 2021 15:45:57 +0800 Subject: [PATCH 23/58] Make SocksException derive from IOException. --- .../src/System/Net/Http/SocketsHttpHandler/SocksException.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs index d8d31474b150f..835a78f5636ac 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs @@ -1,9 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; + namespace System.Net.Http { - internal class SocksException : Exception + internal class SocksException : IOException { public SocksException(string message) : base(message) { From 50a723a0a81a7b5c877f966c6ab2e179382f5c8b Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 11 Mar 2021 16:07:41 +0800 Subject: [PATCH 24/58] Use binary primitives to write port in BE. --- .../src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 1207441b8f8b3..e73cc3c84ade4 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 @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Buffers.Binary; using System.Diagnostics; using System.IO; using System.Text; @@ -192,8 +193,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string addressLength = hostLength + 1; } - buffer[addressLength + 4] = (byte)(port >> 8); - buffer[addressLength + 5] = (byte)port; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port); await WriteAsync(stream, buffer.AsMemory(0, addressLength + 6), async).ConfigureAwait(false); @@ -240,8 +240,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is buffer[0] = ProtocolVersion4; buffer[1] = CMD_CONNECT; - buffer[2] = (byte)(port >> 8); - buffer[3] = (byte)port; + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), (ushort)port); IPAddress? ipv4Address = null; if (IPAddress.TryParse(host, out var hostIP)) From 3b6eb5faa20ab835bb8d080066f4dac7859e5df2 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 11 Mar 2021 18:02:27 +0800 Subject: [PATCH 25/58] Socks loopback test. --- .../Socks/LoopbackSocksServer.cs | 318 ++++++++++++++++++ .../FunctionalTests/Socks/SocksProxyTest.cs | 40 +++ .../System.Net.Http.Functional.Tests.csproj | 5 + 3 files changed, 363 insertions(+) create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs create mode 100644 src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs new file mode 100644 index 0000000000000..b2f777ec852b0 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.IO; +using System.Net.Sockets; +using System.Net.Test.Common; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.Http.Functional.Tests.Socks +{ + /// + /// Provides a test-only SOCKS4/5 proxy. + /// + internal class LoopbackSocksServer : IDisposable + { + private readonly Socket _listener; + private readonly ManualResetEvent _serverStopped; + private bool _disposed; + + private int _connections; + public int Connections => _connections; + + public int Port { get; } + + private LoopbackSocksServer() + { + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + _listener.Listen(int.MaxValue); + + var ep = (IPEndPoint)_listener.LocalEndPoint; + Port = ep.Port; + + _serverStopped = new ManualResetEvent(false); + } + + private void Start() + { + Task.Run(async () => + { + var activeTasks = new ConcurrentDictionary(); + + try + { + while (true) + { + Socket s = await _listener.AcceptAsync().ConfigureAwait(false); + + var connectionTask = Task.Run(async () => + { + try + { + await ProcessConnection(s).ConfigureAwait(false); + } + catch (Exception ex) + { + EventSourceTestLogging.Log.TestAncillaryError(ex); + } + }); + + activeTasks.TryAdd(connectionTask, 0); + _ = connectionTask.ContinueWith(t => activeTasks.TryRemove(connectionTask, out _), TaskContinuationOptions.ExecuteSynchronously); + } + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.OperationAborted) + { + // caused during Dispose() to cancel the loop. ignore. + } + catch (Exception ex) + { + EventSourceTestLogging.Log.TestAncillaryError(ex); + } + + try + { + await Task.WhenAll(activeTasks.Keys).ConfigureAwait(false); + } + catch (Exception ex) + { + EventSourceTestLogging.Log.TestAncillaryError(ex); + } + + _serverStopped.Set(); + }); + } + + private async Task ProcessConnection(Socket s) + { + Interlocked.Increment(ref _connections); + + using (var ns = new NetworkStream(s, ownsSocket: true)) + { + await ProcessRequest(s, ns).ConfigureAwait(false); + } + } + + private async Task ProcessRequest(Socket clientSocket, NetworkStream ns) + { + int version = await ns.ReadByteAsync().ConfigureAwait(false); + + await (version switch + { + 4 => ProcessSocks4Request(clientSocket, ns), + 5 => ProcessSocks5Request(clientSocket, ns), + -1 => throw new Exception("Early EOF"), + _ => throw new Exception("Bad request version") + }).ConfigureAwait(false); + } + + private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) + { + byte[] buffer = new byte[7]; + await ReadToFillAsync(ns, buffer).ConfigureAwait(false); + + if (buffer[0] != 1) + throw new Exception("Only CONNECT is supported."); + + int port = (buffer[1] << 8) + buffer[2]; + // formats ip into string to ensure we get the correct order + string remoteHost = $"{buffer[3]}.{buffer[4]}.{buffer[5]}.{buffer[6]}"; + + while (true) + { + int usernameByte = await ns.ReadByteAsync().ConfigureAwait(false); + if (usernameByte == 0) + break; + if (usernameByte == -1) + throw new Exception("Early EOF"); + } + + if (remoteHost.StartsWith("0.0.0") && remoteHost != "0.0.0.0") + { + byte[] hostBuffer = new byte[1024]; + int hostnameBytes = 0; + + while (true) + { + int b = await ns.ReadByteAsync().ConfigureAwait(false); + if (b == -1) + throw new Exception("Early EOF"); + if (b == 0) + break; + + hostBuffer[hostnameBytes++] = (byte)b; + } + + remoteHost = Encoding.UTF8.GetString(hostBuffer.AsSpan(hostnameBytes)); + } + + ns.WriteByte(4); + buffer[0] = 90; + await ns.WriteAsync(buffer).ConfigureAwait(false); + + await ProcessConnect(clientSocket, ns, remoteHost, port).ConfigureAwait(false); + } + + private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) + { + int nMethods = await ns.ReadByteAsync().ConfigureAwait(false); + if (nMethods == -1) + throw new Exception("Early EOF"); + + byte[] buffer = new byte[1024]; + await ReadToFillAsync(ns, buffer.AsMemory(0, nMethods)).ConfigureAwait(false); + + if (!buffer.AsSpan(0, nMethods).Contains((byte)0)) + { + await ns.WriteAsync(new byte[] { 5, 0xFF }).ConfigureAwait(false); + return; + } + + await ns.WriteAsync(new byte[] { 5, 0 }).ConfigureAwait(false); + + await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false); + if (buffer[0] != 5) + throw new Exception("Bad protocol version."); + if (buffer[1] != 1) + throw new Exception("Only CONNECT is supported."); + + string remoteHost; + switch (buffer[3]) + { + case 1: + await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false); + remoteHost = $"{buffer[0]}.{buffer[1]}.{buffer[2]}.{buffer[3]}"; + break; + case 4: + await ReadToFillAsync(ns, buffer.AsMemory(0, 16)).ConfigureAwait(false); + remoteHost = new IPAddress(buffer.AsSpan(0, 16)).ToString(); + break; + case 3: + int length = await ns.ReadByteAsync().ConfigureAwait(false); + if (length == -1) + throw new Exception("Early EOF"); + await ReadToFillAsync(ns, buffer.AsMemory(0, length)).ConfigureAwait(false); + remoteHost = Encoding.UTF8.GetString(buffer.AsSpan(0, length)); + break; + + default: + throw new Exception("Unknown address type."); + } + + await ReadToFillAsync(ns, buffer.AsMemory(0, 2)).ConfigureAwait(false); + int port = (buffer[0] << 8) + buffer[1]; + + await ns.WriteAsync(new byte[] { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }).ConfigureAwait(false); + + await ProcessConnect(clientSocket, ns, remoteHost, port).ConfigureAwait(false); + } + + private async Task ProcessConnect(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort) + { + + // Open connection to destination server. + using Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + await serverSocket.ConnectAsync(remoteHost, remotePort).ConfigureAwait(false); + NetworkStream serverStream = new NetworkStream(serverSocket); + + // Relay traffic to/from client and destination server. + Task clientCopyTask = Task.Run(async () => + { + try + { + await clientStream.CopyToAsync(serverStream).ConfigureAwait(false); + serverSocket.Shutdown(SocketShutdown.Send); + } + catch (Exception ex) + { + HandleExceptions(ex); + } + }); + + Task serverCopyTask = Task.Run(async () => + { + try + { + await serverStream.CopyToAsync(clientStream).ConfigureAwait(false); + clientSocket.Shutdown(SocketShutdown.Send); + } + catch (Exception ex) + { + HandleExceptions(ex); + } + }); + + await Task.WhenAll(new[] { clientCopyTask, serverCopyTask }).ConfigureAwait(false); + + /// Closes sockets to cause both tasks to end, and eats connection reset/aborted errors. + void HandleExceptions(Exception ex) + { + SocketError sockErr = (ex.InnerException as SocketException)?.SocketErrorCode ?? SocketError.Success; + + // If aborted, the other task failed and is asking this task to end. + if (sockErr == SocketError.OperationAborted) + { + return; + } + + // Ask the other task to end by disposing, causing OperationAborted. + try + { + clientSocket.Close(); + } + catch (ObjectDisposedException) + { + } + + try + { + serverSocket.Close(); + } + catch (ObjectDisposedException) + { + } + + // Eat reset/abort. + if (sockErr != SocketError.ConnectionReset && sockErr != SocketError.ConnectionAborted) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + } + } + + private async ValueTask ReadToFillAsync(Stream stream, Memory buffer) + { + while (!buffer.IsEmpty) + { + int bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false); + if (bytesRead == 0) + throw new Exception("Incomplete request"); + + buffer = buffer.Slice(bytesRead); + } + } + + public static LoopbackSocksServer Create() + { + var server = new LoopbackSocksServer(); + server.Start(); + + return server; + } + + public void Dispose() + { + if (!_disposed) + { + _listener.Dispose(); + _serverStopped.WaitOne(); + _disposed = true; + } + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs new file mode 100644 index 0000000000000..c05af174f75e8 --- /dev/null +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Test.Common; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Http.Functional.Tests.Socks +{ + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public sealed class SocksProxyTest : HttpClientHandlerTestBase + { + public SocksProxyTest(ITestOutputHelper helper) : base(helper) + { + } + + [Theory] + [InlineData("socks4")] + [InlineData("socks4a")] + [InlineData("socks5")] + public async Task TestLoopbackHttp1Async(string schema) + { + await LoopbackServerFactory.CreateClientAndServerAsync(async url => + { + using (var proxy = LoopbackSocksServer.Create()) + { + using (var handler = CreateHttpClientHandler()) + using (var client = CreateHttpClient()) + { + handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + string response = await client.GetStringAsync(url); + Assert.Equal("Echo", response); + } + } + }, + async server => await server.HandleRequestAsync(content: "Echo")); + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 2799f14f74475..f1d5256e55e3f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -194,6 +194,11 @@ + + + + + From 488218118a5ae2867ec47a81bc6b230e15f1f071 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 11 Mar 2021 20:58:00 +0800 Subject: [PATCH 26/58] Expand test matrix. --- .../FunctionalTests/Socks/SocksProxyTest.cs | 62 ++++++++++++++++++- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index c05af174f75e8..653f7be1636e5 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -2,14 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Test.Common; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; namespace System.Net.Http.Functional.Tests.Socks { - [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - public sealed class SocksProxyTest : HttpClientHandlerTestBase + public abstract class SocksProxyTest : HttpClientHandlerTestBase { public SocksProxyTest(ITestOutputHelper helper) : base(helper) { @@ -19,7 +19,7 @@ public SocksProxyTest(ITestOutputHelper helper) : base(helper) [InlineData("socks4")] [InlineData("socks4a")] [InlineData("socks5")] - public async Task TestLoopbackHttp1Async(string schema) + public async Task TestLoopbackAsync(string schema) { await LoopbackServerFactory.CreateClientAndServerAsync(async url => { @@ -28,7 +28,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => using (var handler = CreateHttpClientHandler()) using (var client = CreateHttpClient()) { + client.DefaultRequestVersion = UseVersion; handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + string response = await client.GetStringAsync(url); Assert.Equal("Echo", response); } @@ -36,5 +38,59 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => }, async server => await server.HandleRequestAsync(content: "Echo")); } + + [Theory] + [InlineData("socks4")] + [InlineData("socks4a")] + [InlineData("socks5")] + public async Task TestLoopbackHttpsAsync(string schema) + { + using var cert = TestHelper.CreateServerSelfSignedCertificate(); + + await LoopbackServerFactory.CreateClientAndServerAsync(async url => + { + using (var proxy = LoopbackSocksServer.Create()) + { + using (var handler = CreateHttpClientHandler()) + using (var client = CreateHttpClient()) + { + client.DefaultRequestVersion = UseVersion; + handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + handler.ServerCertificateCustomValidationCallback = (_1, _2, _3, _4) => true; + + string response = await client.GetStringAsync($"https://{cert.GetNameInfo(X509NameType.SimpleName, false)}:{url.Port}/"); + Assert.Equal("Echo", response); + } + } + }, + async server => await server.HandleRequestAsync(content: "Echo"), + options: new GenericLoopbackOptions + { + UseSsl = true, + Certificate = cert + }); + } + } + + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public sealed class SocksProxyTest_Http1 : SocksProxyTest + { + public SocksProxyTest_Http1(ITestOutputHelper helper) : base(helper) + { + } + + protected override Version UseVersion => HttpVersion.Version11; + } + + + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public sealed class SocksProxyTest_Http2 : SocksProxyTest + { + public SocksProxyTest_Http2(ITestOutputHelper helper) : base(helper) + { + } + + protected override Version UseVersion => HttpVersion.Version20; } } From 5d21f2b8b2a4c99be686d6154b51d755d350079e Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Fri, 12 Mar 2021 00:17:59 +0800 Subject: [PATCH 27/58] Try to solve certificate issue. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 653f7be1636e5..2cff5c003a6f6 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -56,7 +56,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => { client.DefaultRequestVersion = UseVersion; handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); - handler.ServerCertificateCustomValidationCallback = (_1, _2, _3, _4) => true; + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; string response = await client.GetStringAsync($"https://{cert.GetNameInfo(X509NameType.SimpleName, false)}:{url.Port}/"); Assert.Equal("Echo", response); From 5ef161c73c3653802b545f165f8629064e3fa309 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 16 Mar 2021 20:24:43 +0800 Subject: [PATCH 28/58] Pass handler to httpclient. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 2cff5c003a6f6..849ce41fabe44 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -26,7 +26,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => using (var proxy = LoopbackSocksServer.Create()) { using (var handler = CreateHttpClientHandler()) - using (var client = CreateHttpClient()) + using (var client = CreateHttpClient(handler)) { client.DefaultRequestVersion = UseVersion; handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); @@ -52,7 +52,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => using (var proxy = LoopbackSocksServer.Create()) { using (var handler = CreateHttpClientHandler()) - using (var client = CreateHttpClient()) + using (var client = CreateHttpClient(handler)) { client.DefaultRequestVersion = UseVersion; handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); From 87ef694c3abf79d943777a200e56eaa06c2baf43 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 16 Mar 2021 20:58:20 +0800 Subject: [PATCH 29/58] Update ConnectToTcpHostAsync. --- .../System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 1a117285c2f58..401cbc1b7e1a6 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1268,7 +1268,7 @@ public ValueTask SendAsync(HttpRequestMessage request, bool case HttpConnectionKind.SslSocksTunnel: Debug.Assert(_originAuthority != null); Debug.Assert(_proxyUri != null); - stream = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); + (socket, stream) = await ConnectToTcpHostAsync(_proxyUri.IdnHost, _proxyUri.Port, request, async, cancellationToken).ConfigureAwait(false); await SocksHelper.EstablishSocksTunnelAsync(stream, _originAuthority.IdnHost, _originAuthority.Port, _proxyUri, ProxyCredentials, async, cancellationToken).ConfigureAwait(false); break; } From b487d1faf57fd157d4c4e3d17e6a4bce63a9df0e Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 24 Mar 2021 18:24:52 +0100 Subject: [PATCH 30/58] Remove custom self-signed cert use from Socks test --- .../FunctionalTests/Socks/SocksProxyTest.cs | 85 ++++++------------- 1 file changed, 28 insertions(+), 57 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 849ce41fabe44..c65d34e2fdaab 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Test.Common; -using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -11,64 +10,40 @@ namespace System.Net.Http.Functional.Tests.Socks { public abstract class SocksProxyTest : HttpClientHandlerTestBase { - public SocksProxyTest(ITestOutputHelper helper) : base(helper) - { - } + public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } [Theory] - [InlineData("socks4")] - [InlineData("socks4a")] - [InlineData("socks5")] - public async Task TestLoopbackAsync(string schema) + [InlineData("socks4", true)] + [InlineData("socks4", false)] + [InlineData("socks4a", true)] + [InlineData("socks4a", false)] + [InlineData("socks5", true)] + [InlineData("socks5", false)] + public async Task TestLoopbackAsync(string schema, bool useSsl) { - await LoopbackServerFactory.CreateClientAndServerAsync(async url => - { - using (var proxy = LoopbackSocksServer.Create()) + await LoopbackServerFactory.CreateClientAndServerAsync( + async url => { - using (var handler = CreateHttpClientHandler()) - using (var client = CreateHttpClient(handler)) - { - client.DefaultRequestVersion = UseVersion; - handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + using LoopbackSocksServer proxy = LoopbackSocksServer.Create(); + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); - string response = await client.GetStringAsync(url); - Assert.Equal("Echo", response); - } - } - }, - async server => await server.HandleRequestAsync(content: "Echo")); - } - - [Theory] - [InlineData("socks4")] - [InlineData("socks4a")] - [InlineData("socks5")] - public async Task TestLoopbackHttpsAsync(string schema) - { - using var cert = TestHelper.CreateServerSelfSignedCertificate(); + handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; - await LoopbackServerFactory.CreateClientAndServerAsync(async url => - { - using (var proxy = LoopbackSocksServer.Create()) - { - using (var handler = CreateHttpClientHandler()) - using (var client = CreateHttpClient(handler)) + var request = new HttpRequestMessage(HttpMethod.Get, url) { - client.DefaultRequestVersion = UseVersion; - handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + Version = UseVersion, + VersionPolicy = HttpVersionPolicy.RequestVersionExact // Needed for H2C + }; + + using HttpResponseMessage response = await client.SendAsync(request); + string responseString = await response.Content.ReadAsStringAsync(); - string response = await client.GetStringAsync($"https://{cert.GetNameInfo(X509NameType.SimpleName, false)}:{url.Port}/"); - Assert.Equal("Echo", response); - } - } - }, - async server => await server.HandleRequestAsync(content: "Echo"), - options: new GenericLoopbackOptions - { - UseSsl = true, - Certificate = cert - }); + Assert.Equal("Echo", responseString); + }, + async server => await server.HandleRequestAsync(content: "Echo"), + options: new GenericLoopbackOptions { UseSsl = useSsl }); } } @@ -76,9 +51,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async url => [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public sealed class SocksProxyTest_Http1 : SocksProxyTest { - public SocksProxyTest_Http1(ITestOutputHelper helper) : base(helper) - { - } + public SocksProxyTest_Http1(ITestOutputHelper helper) : base(helper) { } protected override Version UseVersion => HttpVersion.Version11; } @@ -87,9 +60,7 @@ public SocksProxyTest_Http1(ITestOutputHelper helper) : base(helper) [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public sealed class SocksProxyTest_Http2 : SocksProxyTest { - public SocksProxyTest_Http2(ITestOutputHelper helper) : base(helper) - { - } + public SocksProxyTest_Http2(ITestOutputHelper helper) : base(helper) { } protected override Version UseVersion => HttpVersion.Version20; } From de78efc908beed4f1b6c5f441aa8674910e519e4 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 24 Mar 2021 18:47:24 +0100 Subject: [PATCH 31/58] Fix LoopbackSocksServer's parsing of Socks4a domain name --- .../tests/FunctionalTests/Socks/LoopbackSocksServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index b2f777ec852b0..d9f739a64b494 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -148,7 +148,7 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) hostBuffer[hostnameBytes++] = (byte)b; } - remoteHost = Encoding.UTF8.GetString(hostBuffer.AsSpan(hostnameBytes)); + remoteHost = Encoding.UTF8.GetString(hostBuffer.AsSpan(0, hostnameBytes)); } ns.WriteByte(4); From df1d77fbabdf0d5c377a855714efa00e9134cbfc Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 24 Mar 2021 20:03:04 +0100 Subject: [PATCH 32/58] Only set RequestVersionExact for H2C Setting it in general breaks H2 => H1.1 downgrade on platforms without ALPN --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index c65d34e2fdaab..318ca539a9f83 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -31,11 +31,12 @@ await LoopbackServerFactory.CreateClientAndServerAsync( handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; - var request = new HttpRequestMessage(HttpMethod.Get, url) + var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; + + if (UseVersion == HttpVersion.Version20 && !useSsl) { - Version = UseVersion, - VersionPolicy = HttpVersionPolicy.RequestVersionExact // Needed for H2C - }; + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; // H2C + } using HttpResponseMessage response = await client.SendAsync(request); string responseString = await response.Content.ReadAsStringAsync(); From ee02582d5db39c166d9253131f4855a1ab1ac39e Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 7 Apr 2021 15:58:13 +0800 Subject: [PATCH 33/58] Add auth test. --- .../Socks/LoopbackSocksServer.cs | 51 +++++++++++++++++-- .../FunctionalTests/Socks/SocksProxyTest.cs | 37 +++++++++++--- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index d9f739a64b494..05909b11d3682 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -26,8 +26,18 @@ internal class LoopbackSocksServer : IDisposable public int Port { get; } - private LoopbackSocksServer() + private string? _username, _password; + + private LoopbackSocksServer(string? username = null, string? password = null) { + if (password != null && username == null) + { + throw new ArgumentException("Password must be used together with username.", nameof(password)); + } + + _username = username; + _password = password; + _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); _listener.Listen(int.MaxValue); @@ -123,6 +133,8 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) // formats ip into string to ensure we get the correct order string remoteHost = $"{buffer[3]}.{buffer[4]}.{buffer[5]}.{buffer[6]}"; + byte[] usernameBuffer = new byte[1024]; + int usernameBytes = 0; while (true) { int usernameByte = await ns.ReadByteAsync().ConfigureAwait(false); @@ -130,6 +142,16 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) break; if (usernameByte == -1) throw new Exception("Early EOF"); + usernameBuffer[usernameBytes++] = (byte)usernameByte; + } + + if (_username != null) + { + string username = Encoding.UTF8.GetString(usernameBuffer.AsSpan(0, usernameBytes)); + if (username != _username) + { + throw new Exception("Bad username."); + } } if (remoteHost.StartsWith("0.0.0") && remoteHost != "0.0.0.0") @@ -167,13 +189,34 @@ private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) byte[] buffer = new byte[1024]; await ReadToFillAsync(ns, buffer.AsMemory(0, nMethods)).ConfigureAwait(false); - if (!buffer.AsSpan(0, nMethods).Contains((byte)0)) + byte expectedAuthMethod = _username == null ? (byte)0 : (byte)2; + if (!buffer.AsSpan(0, nMethods).Contains(expectedAuthMethod)) { await ns.WriteAsync(new byte[] { 5, 0xFF }).ConfigureAwait(false); return; } - await ns.WriteAsync(new byte[] { 5, 0 }).ConfigureAwait(false); + await ns.WriteAsync(new byte[] { 5, expectedAuthMethod }).ConfigureAwait(false); + + if (_username != null) + { + await ReadToFillAsync(ns, buffer.AsMemory(0, 2)).ConfigureAwait(false); + if (buffer[0] != 5) + throw new Exception("Bad protocol version."); + + int uLen = buffer[1]; + await ReadToFillAsync(ns, buffer.AsMemory(0, uLen)).ConfigureAwait(false); + if (Encoding.UTF8.GetString(buffer.AsSpan(0, uLen)) != _username) + throw new Exception("Bad username."); + + await ReadToFillAsync(ns, buffer.AsMemory(0, 1)).ConfigureAwait(false); + int pLen = buffer[1]; + await ReadToFillAsync(ns, buffer.AsMemory(0, pLen)).ConfigureAwait(false); + if (_password != null && Encoding.UTF8.GetString(buffer.AsSpan(0, pLen)) != _password) + throw new Exception("Bad password."); + + await ns.WriteAsync(new byte[] { 5, 0 }).ConfigureAwait(false); + } await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false); if (buffer[0] != 5) @@ -297,7 +340,7 @@ private async ValueTask ReadToFillAsync(Stream stream, Memory buffer) } } - public static LoopbackSocksServer Create() + public static LoopbackSocksServer Create(string? username = null, string? password = null) { var server = new LoopbackSocksServer(); server.Start(); diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 318ca539a9f83..2b82c744e4609 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -10,27 +10,48 @@ namespace System.Net.Http.Functional.Tests.Socks { public abstract class SocksProxyTest : HttpClientHandlerTestBase { + private class Credentials : ICredentials + { + private readonly string _username, _password; + + public Credentials(string username, string password) + { + _username = username; + _password = password; + } + + public NetworkCredential? GetCredential(Uri uri, string authType) + => new NetworkCredential(_username, _password); + } + public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } [Theory] - [InlineData("socks4", true)] - [InlineData("socks4", false)] - [InlineData("socks4a", true)] - [InlineData("socks4a", false)] - [InlineData("socks5", true)] - [InlineData("socks5", false)] - public async Task TestLoopbackAsync(string schema, bool useSsl) + [InlineData("socks4", true, false)] + [InlineData("socks4", false, false)] + [InlineData("socks4", false, true)] + [InlineData("socks4a", true, false)] + [InlineData("socks4a", false, false)] + [InlineData("socks5", true, false)] + [InlineData("socks5", false, false)] + [InlineData("socks5", false, true)] + public async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth) { await LoopbackServerFactory.CreateClientAndServerAsync( async url => { - using LoopbackSocksServer proxy = LoopbackSocksServer.Create(); + using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); using HttpClientHandler handler = CreateHttpClientHandler(); using HttpClient client = CreateHttpClient(handler); handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; + if (useAuth) + { + handler.Proxy.Credentials = new Credentials("DOTNET", "424242"); + } + var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; if (UseVersion == HttpVersion.Version20 && !useSsl) From efc32b8fa4835519071b9048cbf7ecf5eb21c9a4 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 7 Apr 2021 16:29:37 +0800 Subject: [PATCH 34/58] Add IP in test matrix. --- .../FunctionalTests/Socks/SocksProxyTest.cs | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 2b82c744e4609..7d612c6d8f781 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -26,16 +26,40 @@ public Credentials(string username, string password) public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } - [Theory] - [InlineData("socks4", true, false)] - [InlineData("socks4", false, false)] - [InlineData("socks4", false, true)] - [InlineData("socks4a", true, false)] - [InlineData("socks4a", false, false)] - [InlineData("socks5", true, false)] - [InlineData("socks5", false, false)] - [InlineData("socks5", false, true)] - public async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth) + [Fact] + public async Task TestSocks4Async() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks4IPAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "127.0.0.1"); + + [Fact] + public async Task TestSocks4SslAsync() => await TestLoopbackAsync("socks4", useSsl: true, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks4AuthAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: true, "localhost"); + + [Fact] + public async Task TestSocks4aAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks4aIPAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "127.0.0.1"); + + [Fact] + public async Task TestSocks4aSslAsync() => await TestLoopbackAsync("socks4a", useSsl: true, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks5Async() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks5IPAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "127.0.0.1"); + + [Fact] + public async Task TestSocks5SslAsync() => await TestLoopbackAsync("socks5", useSsl: true, useAuth: false, "localhost"); + + [Fact] + public async Task TestSocks5AuthAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: true, "localhost"); + + private async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string host) { await LoopbackServerFactory.CreateClientAndServerAsync( async url => @@ -52,7 +76,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( handler.Proxy.Credentials = new Credentials("DOTNET", "424242"); } - var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; + var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder(url) { Host = host }.Uri) { Version = UseVersion }; if (UseVersion == HttpVersion.Version20 && !useSsl) { From ddb2f8de0a84943ccfa29e6fb3b7201500119508 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Wed, 7 Apr 2021 16:33:44 +0800 Subject: [PATCH 35/58] Only override host when required. --- .../FunctionalTests/Socks/SocksProxyTest.cs | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 7d612c6d8f781..e27308e442354 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -27,40 +27,42 @@ public Credentials(string username, string password) public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } [Fact] - public async Task TestSocks4Async() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "localhost"); + public async Task TestSocks4HostAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "localhost"); [Fact] public async Task TestSocks4IPAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "127.0.0.1"); [Fact] - public async Task TestSocks4SslAsync() => await TestLoopbackAsync("socks4", useSsl: true, useAuth: false, "localhost"); + public async Task TestSocks4SslAsync() => await TestLoopbackAsync("socks4", useSsl: true, useAuth: false); [Fact] - public async Task TestSocks4AuthAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: true, "localhost"); + public async Task TestSocks4AuthAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: true); [Fact] - public async Task TestSocks4aAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "localhost"); + public async Task TestSocks4aHostAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "localhost"); [Fact] public async Task TestSocks4aIPAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "127.0.0.1"); [Fact] - public async Task TestSocks4aSslAsync() => await TestLoopbackAsync("socks4a", useSsl: true, useAuth: false, "localhost"); + public async Task TestSocks4aSslAsync() => await TestLoopbackAsync("socks4a", useSsl: true, useAuth: false); [Fact] - public async Task TestSocks5Async() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "localhost"); + public async Task TestSocks5HostAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "localhost"); [Fact] public async Task TestSocks5IPAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "127.0.0.1"); [Fact] - public async Task TestSocks5SslAsync() => await TestLoopbackAsync("socks5", useSsl: true, useAuth: false, "localhost"); + public async Task TestSocks5SslAsync() => await TestLoopbackAsync("socks5", useSsl: true, useAuth: false); [Fact] - public async Task TestSocks5AuthAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: true, "localhost"); + public async Task TestSocks5AuthAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: true); - private async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string host) + private async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string? overrideHost = null) { + Assert.False(useSsl && (overrideHost != null)); + await LoopbackServerFactory.CreateClientAndServerAsync( async url => { @@ -76,7 +78,12 @@ await LoopbackServerFactory.CreateClientAndServerAsync( handler.Proxy.Credentials = new Credentials("DOTNET", "424242"); } - var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder(url) { Host = host }.Uri) { Version = UseVersion }; + if (overrideHost != null) + { + url = new UriBuilder(url) { Host = overrideHost }.Uri; + } + + var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; if (UseVersion == HttpVersion.Version20 && !useSsl) { From e30ee4e28e6376ffd78b9c4f3b32ad39353690b2 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 17:51:42 +0200 Subject: [PATCH 36/58] Don't attempt NT Auth for Socks proxies --- .../System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 8286df2fd5d2b..aecc95069acfb 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -299,7 +299,6 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection public HttpConnectionSettings Settings => _poolManager.Settings; public HttpConnectionKind Kind => _kind; public bool IsSecure => _kind == HttpConnectionKind.Https || _kind == HttpConnectionKind.SslProxyTunnel || _kind == HttpConnectionKind.SslSocksTunnel; - public bool AnyProxyKind => (_proxyUri != null); public Uri? ProxyUri => _proxyUri; public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials; public byte[]? HostHeaderValueBytes => _hostHeaderValueBytes; @@ -1160,9 +1159,9 @@ public async Task SendWithNtConnectionAuthAsync(HttpConnect public Task SendWithNtProxyAuthAsync(HttpConnection connection, HttpRequestMessage request, bool async, CancellationToken cancellationToken) { - if (AnyProxyKind && ProxyCredentials != null) + if (ProxyUri != null && ProxyUri.Scheme == Uri.UriSchemeHttp && ProxyCredentials != null) { - return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, connection, this, cancellationToken); + return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri, async, ProxyCredentials, connection, this, cancellationToken); } return connection.SendAsync(request, async, cancellationToken); From 7b68507aa2f38255acf140c138025e3f545f9eb0 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 17:55:10 +0200 Subject: [PATCH 37/58] Skip HTTP2 ssl test on platforms without ALPN support --- .../FunctionalTests/Socks/SocksProxyTest.cs | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index e27308e442354..8ac14518bc43f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Linq; using System.Net.Test.Common; using System.Threading.Tasks; using Xunit; @@ -10,6 +12,8 @@ namespace System.Net.Http.Functional.Tests.Socks { public abstract class SocksProxyTest : HttpClientHandlerTestBase { + public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } + private class Credentials : ICredentials { private readonly string _username, _password; @@ -24,52 +28,31 @@ public Credentials(string username, string password) => new NetworkCredential(_username, _password); } - public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } - - [Fact] - public async Task TestSocks4HostAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "localhost"); - - [Fact] - public async Task TestSocks4IPAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: false, "127.0.0.1"); - - [Fact] - public async Task TestSocks4SslAsync() => await TestLoopbackAsync("socks4", useSsl: true, useAuth: false); - - [Fact] - public async Task TestSocks4AuthAsync() => await TestLoopbackAsync("socks4", useSsl: false, useAuth: true); - - [Fact] - public async Task TestSocks4aHostAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "localhost"); - - [Fact] - public async Task TestSocks4aIPAsync() => await TestLoopbackAsync("socks4a", useSsl: false, useAuth: false, "127.0.0.1"); - - [Fact] - public async Task TestSocks4aSslAsync() => await TestLoopbackAsync("socks4a", useSsl: true, useAuth: false); - - [Fact] - public async Task TestSocks5HostAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "localhost"); - - [Fact] - public async Task TestSocks5IPAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: false, "127.0.0.1"); + public static IEnumerable TestLoopbackAsync_MemberData() => + from scheme in new[] { "socks4", "socks4a", "socks5" } + from useSsl in BoolValues + from useAuth in BoolValues + from host in new[] { "localhost", IPAddress.Loopback.ToString() } + select new object[] { scheme, useSsl, useAuth, host }; - [Fact] - public async Task TestSocks5SslAsync() => await TestLoopbackAsync("socks5", useSsl: true, useAuth: false); - - [Fact] - public async Task TestSocks5AuthAsync() => await TestLoopbackAsync("socks5", useSsl: false, useAuth: true); - - private async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string? overrideHost = null) + [Theory] + [MemberData(nameof(TestLoopbackAsync_MemberData))] + public async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string host) { - Assert.False(useSsl && (overrideHost != null)); + if (useSsl && UseVersion == HttpVersion.Version20 && !PlatformDetection.SupportsAlpn) + { + return; + } await LoopbackServerFactory.CreateClientAndServerAsync( - async url => + async uri => { using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); using HttpClientHandler handler = CreateHttpClientHandler(); using HttpClient client = CreateHttpClient(handler); + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -78,22 +61,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync( handler.Proxy.Credentials = new Credentials("DOTNET", "424242"); } - if (overrideHost != null) - { - url = new UriBuilder(url) { Host = overrideHost }.Uri; - } - - var request = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; - - if (UseVersion == HttpVersion.Version20 && !useSsl) - { - request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; // H2C - } - - using HttpResponseMessage response = await client.SendAsync(request); - string responseString = await response.Content.ReadAsStringAsync(); + uri = new UriBuilder(uri) { Host = host }.Uri; - Assert.Equal("Echo", responseString); + Assert.Equal("Echo", await client.GetStringAsync(uri)); }, async server => await server.HandleRequestAsync(content: "Echo"), options: new GenericLoopbackOptions { UseSsl = useSsl }); From dfb737c10eda00bc0324da7a9783b89d89729d88 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 17:57:42 +0200 Subject: [PATCH 38/58] Use NetworkCredential directly --- .../FunctionalTests/Socks/SocksProxyTest.cs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 8ac14518bc43f..580c3e305776d 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -14,20 +14,6 @@ public abstract class SocksProxyTest : HttpClientHandlerTestBase { public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } - private class Credentials : ICredentials - { - private readonly string _username, _password; - - public Credentials(string username, string password) - { - _username = username; - _password = password; - } - - public NetworkCredential? GetCredential(Uri uri, string authType) - => new NetworkCredential(_username, _password); - } - public static IEnumerable TestLoopbackAsync_MemberData() => from scheme in new[] { "socks4", "socks4a", "socks5" } from useSsl in BoolValues @@ -58,7 +44,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( if (useAuth) { - handler.Proxy.Credentials = new Credentials("DOTNET", "424242"); + handler.Proxy.Credentials = new NetworkCredential("DOTNET", "424242"); } uri = new UriBuilder(uri) { Host = host }.Uri; From 1bd2c0e555c5451be6dfebb0bee4a216df9ba6ec Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 18:01:20 +0200 Subject: [PATCH 39/58] Pass AddressFamily to sync Dns resolution too --- .../System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 e73cc3c84ade4..9f7f97cd4c4ba 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 @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Diagnostics; using System.IO; +using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -167,7 +168,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string if (IPAddress.TryParse(host, out var hostIP)) { - if (hostIP.AddressFamily == Sockets.AddressFamily.InterNetwork) + if (hostIP.AddressFamily == AddressFamily.InterNetwork) { buffer[3] = ATYP_IPV4; hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); @@ -176,7 +177,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } else { - Debug.Assert(hostIP.AddressFamily == Sockets.AddressFamily.InterNetworkV6); + Debug.Assert(hostIP.AddressFamily == AddressFamily.InterNetworkV6); buffer[3] = ATYP_IPV6; hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); Debug.Assert(bytesWritten == 16); @@ -245,7 +246,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is IPAddress? ipv4Address = null; if (IPAddress.TryParse(host, out var hostIP)) { - if (hostIP.AddressFamily == Sockets.AddressFamily.InterNetwork) + if (hostIP.AddressFamily == AddressFamily.InterNetwork) { ipv4Address = hostIP; } @@ -262,8 +263,8 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is { // SOCKS4 requires DNS resolution locally var addresses = async - ? await Dns.GetHostAddressesAsync(host, Sockets.AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) - : Dns.GetHostAddresses(host); + ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) + : Dns.GetHostAddresses(host, AddressFamily.InterNetwork); if (addresses.Length == 0) { From 5c26033f8f27e52598755280f786b214e987ce02 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 18:29:39 +0200 Subject: [PATCH 40/58] Consistently check encoded string lengths --- .../Http/SocketsHttpHandler/SocksHelper.cs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) 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 9f7f97cd4c4ba..da84bdfe551b0 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 @@ -126,14 +126,10 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ buffer[0] = ProtocolVersion5; - int usernameLength = Encoding.UTF8.GetByteCount(credentials.UserName); - buffer[1] = checked((byte)usernameLength); - int usernameLengthEncoded = Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2)); - Debug.Assert(usernameLength == usernameLengthEncoded); - int passwordLength = Encoding.UTF8.GetByteCount(credentials.Password); - buffer[2 + usernameLength] = checked((byte)passwordLength); - int passwordLengthEncoded = Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + usernameLength)); - Debug.Assert(passwordLength == passwordLengthEncoded); + byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2))); + buffer[1] = usernameLength; + byte passwordLength = checked((byte)Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + usernameLength))); + buffer[2 + usernameLength] = passwordLength; await WriteAsync(stream, buffer.AsMemory(0, 4 + usernameLength + passwordLength), async).ConfigureAwait(false); // +----+--------+ @@ -187,11 +183,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string else { buffer[3] = ATYP_DOMAIN_NAME; - int hostLength = Encoding.UTF8.GetByteCount(host); - buffer[4] = checked((byte)hostLength); - int bytesEncoded = Encoding.UTF8.GetBytes(host, buffer.AsSpan(5)); - Debug.Assert(bytesEncoded == hostLength); - addressLength = hostLength + 1; + byte bytesEncoded = checked((byte)Encoding.UTF8.GetBytes(host, buffer.AsSpan(5))); + buffer[4] = bytesEncoded; + addressLength = bytesEncoded + 1; } BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port); @@ -276,6 +270,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is if (ipv4Address == null) { + Debug.Assert(isVersion4a); buffer[4] = 0; buffer[5] = 0; buffer[6] = 0; @@ -293,14 +288,14 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } - int usernameLength = Encoding.UTF8.GetBytes(username, buffer.AsSpan(8)); + byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(username, buffer.AsSpan(8))); buffer[8 + usernameLength] = 0; int totalLength = 9 + usernameLength; - if (isVersion4a && ipv4Address == null) + if (ipv4Address == null) { // https://www.openssh.com/txt/socks4a.protocol - int hostLength = Encoding.UTF8.GetBytes(host, buffer.AsSpan(9 + usernameLength)); + byte hostLength = checked((byte)Encoding.UTF8.GetBytes(host, buffer.AsSpan(totalLength))); buffer[totalLength + hostLength] = 0; totalLength += hostLength + 1; } From 1308e14f3158d8e5494eb1623330c3534ce086cb Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 19:18:18 +0200 Subject: [PATCH 41/58] Fix Socks5 user/pass auth --- .../Http/SocketsHttpHandler/SocksHelper.cs | 10 ++++----- .../Socks/LoopbackSocksServer.cs | 22 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) 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 da84bdfe551b0..4796c55321e0e 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 @@ -18,6 +18,7 @@ internal static class SocksHelper private const int BufferSize = 512; private const int ProtocolVersion4 = 4; private const int ProtocolVersion5 = 5; + private const int SubnegotiationVersion = 1; private const byte METHOD_NO_AUTH = 0; // private const byte METHOD_GSSAPI = 1; private const byte METHOD_USERNAME_PASSWORD = 2; @@ -85,7 +86,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+----------+----------+ buffer[0] = ProtocolVersion5; var credentials = proxyCredentials?.GetCredential(proxyUri, ""); - if (credentials != null) + if (credentials is null) { buffer[1] = 1; buffer[2] = METHOD_NO_AUTH; @@ -125,12 +126,12 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+------+----------+------+----------+ // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ - buffer[0] = ProtocolVersion5; + buffer[0] = SubnegotiationVersion; byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2))); buffer[1] = usernameLength; byte passwordLength = checked((byte)Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + usernameLength))); buffer[2 + usernameLength] = passwordLength; - await WriteAsync(stream, buffer.AsMemory(0, 4 + usernameLength + passwordLength), async).ConfigureAwait(false); + await WriteAsync(stream, buffer.AsMemory(0, 3 + usernameLength + passwordLength), async).ConfigureAwait(false); // +----+--------+ // |VER | STATUS | @@ -138,8 +139,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | // +----+--------+ await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); - VerifyProtocolVersion(ProtocolVersion5, buffer[0]); - if (buffer[1] != REP_SUCCESS) + if (buffer[0] != SubnegotiationVersion || buffer[1] != REP_SUCCESS) { throw new SocksException(SR.net_socks_auth_failed); } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index 05909b11d3682..64a442a2e4465 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -200,22 +200,20 @@ private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) if (_username != null) { - await ReadToFillAsync(ns, buffer.AsMemory(0, 2)).ConfigureAwait(false); - if (buffer[0] != 5) - throw new Exception("Bad protocol version."); + if (await ns.ReadByteAsync().ConfigureAwait(false) != 1) + throw new Exception("Bad subnegotiation version."); - int uLen = buffer[1]; - await ReadToFillAsync(ns, buffer.AsMemory(0, uLen)).ConfigureAwait(false); - if (Encoding.UTF8.GetString(buffer.AsSpan(0, uLen)) != _username) + int usernameLength = await ns.ReadByteAsync().ConfigureAwait(false); + await ReadToFillAsync(ns, buffer.AsMemory(0, usernameLength)).ConfigureAwait(false); + if (Encoding.UTF8.GetString(buffer.AsSpan(0, usernameLength)) != _username) throw new Exception("Bad username."); - await ReadToFillAsync(ns, buffer.AsMemory(0, 1)).ConfigureAwait(false); - int pLen = buffer[1]; - await ReadToFillAsync(ns, buffer.AsMemory(0, pLen)).ConfigureAwait(false); - if (_password != null && Encoding.UTF8.GetString(buffer.AsSpan(0, pLen)) != _password) + int passwordLength = await ns.ReadByteAsync().ConfigureAwait(false); + await ReadToFillAsync(ns, buffer.AsMemory(0, passwordLength)).ConfigureAwait(false); + if (Encoding.UTF8.GetString(buffer.AsSpan(0, passwordLength)) != _password) throw new Exception("Bad password."); - await ns.WriteAsync(new byte[] { 5, 0 }).ConfigureAwait(false); + await ns.WriteAsync(new byte[] { 1, 0 }).ConfigureAwait(false); } await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false); @@ -342,7 +340,7 @@ private async ValueTask ReadToFillAsync(Stream stream, Memory buffer) public static LoopbackSocksServer Create(string? username = null, string? password = null) { - var server = new LoopbackSocksServer(); + var server = new LoopbackSocksServer(username, password); server.Start(); return server; From 76b61ce48638b8f24c2f7e4fc5920de2376d07ec Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Wed, 7 Apr 2021 23:16:38 +0200 Subject: [PATCH 42/58] Add IPv6 test for socks5 --- .../FunctionalTests/Socks/LoopbackSocksServer.cs | 13 ++++++------- .../FunctionalTests/Socks/SocksProxyTest.cs | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index 64a442a2e4465..9ad0372a5c224 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -177,7 +177,7 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) buffer[0] = 90; await ns.WriteAsync(buffer).ConfigureAwait(false); - await ProcessConnect(clientSocket, ns, remoteHost, port).ConfigureAwait(false); + await RelayHttpTraffic(clientSocket, ns, remoteHost, port).ConfigureAwait(false); } private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) @@ -227,7 +227,7 @@ private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) { case 1: await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false); - remoteHost = $"{buffer[0]}.{buffer[1]}.{buffer[2]}.{buffer[3]}"; + remoteHost = new IPAddress(buffer.AsSpan(0, 4)).ToString(); break; case 4: await ReadToFillAsync(ns, buffer.AsMemory(0, 16)).ConfigureAwait(false); @@ -250,16 +250,15 @@ private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) await ns.WriteAsync(new byte[] { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }).ConfigureAwait(false); - await ProcessConnect(clientSocket, ns, remoteHost, port).ConfigureAwait(false); + await RelayHttpTraffic(clientSocket, ns, remoteHost, port).ConfigureAwait(false); } - private async Task ProcessConnect(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort) + private async Task RelayHttpTraffic(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort) { - // Open connection to destination server. - using Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + using var serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; await serverSocket.ConnectAsync(remoteHost, remotePort).ConfigureAwait(false); - NetworkStream serverStream = new NetworkStream(serverSocket); + var serverStream = new NetworkStream(serverSocket); // Relay traffic to/from client and destination server. Task clientCopyTask = Task.Run(async () => diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 580c3e305776d..b731792f00ba4 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -14,16 +14,20 @@ public abstract class SocksProxyTest : HttpClientHandlerTestBase { public SocksProxyTest(ITestOutputHelper helper) : base(helper) { } + private static string[] Hosts(string socksScheme) => socksScheme == "socks5" + ? 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 useSsl in BoolValues from useAuth in BoolValues - from host in new[] { "localhost", IPAddress.Loopback.ToString() } + from host in Hosts(scheme) select new object[] { scheme, useSsl, useAuth, host }; [Theory] [MemberData(nameof(TestLoopbackAsync_MemberData))] - public async Task TestLoopbackAsync(string schema, bool useSsl, bool useAuth, string host) + public async Task TestLoopbackAsync(string scheme, bool useSsl, bool useAuth, string host) { if (useSsl && UseVersion == HttpVersion.Version20 && !PlatformDetection.SupportsAlpn) { @@ -39,7 +43,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; - handler.Proxy = new WebProxy($"{schema}://localhost:{proxy.Port}"); + handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}"); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; if (useAuth) @@ -52,7 +56,11 @@ await LoopbackServerFactory.CreateClientAndServerAsync( Assert.Equal("Echo", await client.GetStringAsync(uri)); }, async server => await server.HandleRequestAsync(content: "Echo"), - options: new GenericLoopbackOptions { UseSsl = useSsl }); + options: new GenericLoopbackOptions + { + UseSsl = useSsl, + Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback + }); } } From 54bde5124600a7338c9664057f5a705a78533539 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Thu, 8 Apr 2021 00:51:08 +0200 Subject: [PATCH 43/58] Exception nits --- .../src/Resources/Strings.resx | 72 +++++++++---------- .../Http/SocketsHttpHandler/SocksHelper.cs | 45 ++++-------- 2 files changed, 49 insertions(+), 68 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index 8f5dec97a9ff5..b8aa30616b8fb 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -607,29 +607,29 @@ Synchronous reads are not supported, use ReadAsync instead. - SOCKS authentication failed. + Failed to authenticate with the SOCKS server. - Address type returned from SOCKS server cannot be determined. + SOCKS server returned an unknown address type. - SOCKS server failed to connect to destination. - - - The destination ip is invalid for SOCKS4 protocol. + SOCKS server failed to connect to the destination. - SOCKS4 does not support IPv6. + SOCKS4 does not support IPv6 addresses. - SOCKS server does not return suitable authentication method. + SOCKS server did not return a suitable authentication method. - Cannot resolve IPv4 address for host. + Failed to resolve the destination host to an IPv4 address. Unexpected SOCKS protocol version. Required {0}, got {1}. + + SOCKS server requested username & password authentication. + The proxy tunnel request to proxy '{0}' failed with status code '{1}'." 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 4796c55321e0e..db1b444720232 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 @@ -18,27 +18,15 @@ internal static class SocksHelper private const int BufferSize = 512; private const int ProtocolVersion4 = 4; private const int ProtocolVersion5 = 5; - private const int SubnegotiationVersion = 1; + private const int SubnegotiationVersion = 1; // Socks5 username/password auth private const byte METHOD_NO_AUTH = 0; - // private const byte METHOD_GSSAPI = 1; private const byte METHOD_USERNAME_PASSWORD = 2; - private const byte METHOD_NO_ACCEPTABLE = 0xFF; private const byte CMD_CONNECT = 1; - // private const byte CMD_BIND = 2; - // private const byte CMD_UDP_ASSOCIATE = 3; private const byte ATYP_IPV4 = 1; private const byte ATYP_DOMAIN_NAME = 3; private const byte ATYP_IPV6 = 4; - private const byte REP_SUCCESS = 0; - // private const byte REP_FAILURE = 1; - // private const byte REP_NOT_ALLOWED = 2; - // private const byte REP_NETWORK_UNREACHABLE = 3; - // private const byte REP_HOST_UNREACHABLE = 4; - // private const byte REP_CONNECTION_REFUSED = 5; - // private const byte REP_TTL_EXPIRED = 6; - // private const byte REP_CMD_NOT_SUPPORT = 7; - // private const byte REP_ATYP_NOT_SUPPORT = 8; - private const byte CD_SUCCESS = 90; + private const byte Socks5_Success = 0; + private const byte Socks4_Success = 90; public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { @@ -85,7 +73,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | 1 to 255 | // +----+----------+----------+ buffer[0] = ProtocolVersion5; - var credentials = proxyCredentials?.GetCredential(proxyUri, ""); + NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, ""); if (credentials is null) { buffer[1] = 1; @@ -116,9 +104,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string case METHOD_USERNAME_PASSWORD: { // https://tools.ietf.org/html/rfc1929 - if (credentials == null) + if (credentials is null) { - throw new SocksException(SR.net_socks_no_auth_method); + throw new SocksException(SR.net_socks_auth_required); } // +----+------+----------+------+----------+ @@ -139,14 +127,13 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | // +----+--------+ await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false); - if (buffer[0] != SubnegotiationVersion || buffer[1] != REP_SUCCESS) + if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5_Success) { throw new SocksException(SR.net_socks_auth_failed); } break; } - case METHOD_NO_ACCEPTABLE: default: throw new SocksException(SR.net_socks_no_auth_method); } @@ -162,7 +149,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string buffer[2] = 0; int addressLength; - if (IPAddress.TryParse(host, out var hostIP)) + if (IPAddress.TryParse(host, out IPAddress? hostIP)) { if (hostIP.AddressFamily == AddressFamily.InterNetwork) { @@ -199,7 +186,7 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // +----+-----+-------+------+----------+----------+ await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async).ConfigureAwait(false); VerifyProtocolVersion(ProtocolVersion5, buffer[0]); - if (buffer[1] != REP_SUCCESS) + if (buffer[1] != Socks5_Success) { throw new SocksException(SR.net_socks_connection_failed); } @@ -255,7 +242,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } else if (!isVersion4a) { - // SOCKS4 requires DNS resolution locally + // Socks4 does not support domain names - try to resolve it here var addresses = async ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) : Dns.GetHostAddresses(host, AddressFamily.InterNetwork); @@ -268,7 +255,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is ipv4Address = addresses[0]; } - if (ipv4Address == null) + if (ipv4Address is null) { Debug.Assert(isVersion4a); buffer[4] = 0; @@ -280,19 +267,13 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is { ipv4Address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten); Debug.Assert(bytesWritten == 4); - if (buffer[4] == 0 && buffer[5] == 0 && buffer[6] == 0) - { - // Invalid IP address used by SOCKS4a to represent remote DNS. - // In case we don't have a domain name, throwing. - throw new SocksException(SR.net_socks_ipv4_invalid); - } } byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(username, buffer.AsSpan(8))); buffer[8 + usernameLength] = 0; int totalLength = 9 + usernameLength; - if (ipv4Address == null) + if (ipv4Address is null) { // https://www.openssh.com/txt/socks4a.protocol byte hostLength = checked((byte)Encoding.UTF8.GetBytes(host, buffer.AsSpan(totalLength))); @@ -308,7 +289,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // 1 1 2 4 await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); VerifyProtocolVersion(ProtocolVersion4, buffer[0]); - if (buffer[1] != CD_SUCCESS) + if (buffer[1] != Socks4_Success) { throw new SocksException(SR.net_socks_connection_failed); } From 4d7a5456c34fbb277945afa22cf3a24a1de86fce Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 8 Apr 2021 15:30:57 +0800 Subject: [PATCH 44/58] Add exceptional tests. --- .../Socks/LoopbackSocksServer.cs | 15 +++-- .../FunctionalTests/Socks/SocksProxyTest.cs | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index 9ad0372a5c224..9ae20927dd80c 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -150,6 +150,9 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) string username = Encoding.UTF8.GetString(usernameBuffer.AsSpan(0, usernameBytes)); if (username != _username) { + ns.WriteByte(4); + buffer[0] = 93; + await ns.WriteAsync(buffer).ConfigureAwait(false); throw new Exception("Bad username."); } } @@ -205,13 +208,17 @@ private async Task ProcessSocks5Request(Socket clientSocket, NetworkStream ns) int usernameLength = await ns.ReadByteAsync().ConfigureAwait(false); await ReadToFillAsync(ns, buffer.AsMemory(0, usernameLength)).ConfigureAwait(false); - if (Encoding.UTF8.GetString(buffer.AsSpan(0, usernameLength)) != _username) - throw new Exception("Bad username."); + string username = Encoding.UTF8.GetString(buffer.AsSpan(0, usernameLength)); int passwordLength = await ns.ReadByteAsync().ConfigureAwait(false); await ReadToFillAsync(ns, buffer.AsMemory(0, passwordLength)).ConfigureAwait(false); - if (Encoding.UTF8.GetString(buffer.AsSpan(0, passwordLength)) != _password) - throw new Exception("Bad password."); + string password = Encoding.UTF8.GetString(buffer.AsSpan(0, passwordLength)); + + if (username != _username || password != _password) + { + await ns.WriteAsync(new byte[] { 1, 1 }).ConfigureAwait(false); + throw new Exception("Invalid credentials."); + } await ns.WriteAsync(new byte[] { 1, 0 }).ConfigureAwait(false); } diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index b731792f00ba4..57f069b2b0a29 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Test.Common; using System.Threading.Tasks; @@ -62,6 +63,60 @@ await LoopbackServerFactory.CreateClientAndServerAsync( Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback }); } + + public static IEnumerable TestExceptionalAsync_MemberData() + { + foreach (string scheme in new[] { "socks4", "socks4a" }) + yield return new object[] { scheme, "::1", false, null }; + foreach (string scheme in new[] { "socks4", "socks4a", "socks5" }) + { + yield return new object[] { scheme, "localhost", true, null }; + yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password") }; + } + } + + [Theory] + [MemberData(nameof(TestExceptionalAsync_MemberData))] + public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials) + { + using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); + + handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}") + { + Credentials = credentials + }; + + // SocksException is not public + await Assert.ThrowsAnyAsync(() => client.GetStringAsync($"http://{host}")); + + await LoopbackServerFactory.CreateClientAndServerAsync( + async uri => + { + using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); + + client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + + handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}") + { + Credentials = credentials + }; + + uri = new UriBuilder(uri) { Host = host }.Uri; + + // SocksException is not public + var ex = await Assert.ThrowsAnyAsync(() => client.GetStringAsync(uri)); + Assert.Equal("SocksException", ex.GetType().Name); + }, + async server => await server.HandleRequestAsync(content: "Echo"), + options: new GenericLoopbackOptions + { + Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback + }); + } } From 1e5cde49bed55cbb0075d4bfd7740e8bf80ad651 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Mon, 12 Apr 2021 23:46:07 +0800 Subject: [PATCH 45/58] Fix exceptional test. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 57f069b2b0a29..d8d69bb941b4f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -79,18 +79,6 @@ public static IEnumerable TestExceptionalAsync_MemberData() [MemberData(nameof(TestExceptionalAsync_MemberData))] public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials) { - using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); - using HttpClientHandler handler = CreateHttpClientHandler(); - using HttpClient client = CreateHttpClient(handler); - - handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}") - { - Credentials = credentials - }; - - // SocksException is not public - await Assert.ThrowsAnyAsync(() => client.GetStringAsync($"http://{host}")); - await LoopbackServerFactory.CreateClientAndServerAsync( async uri => { From 8bbb9e85781b83fde04459211259839651b1b9ee Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Tue, 13 Apr 2021 17:01:50 +0800 Subject: [PATCH 46/58] Fix NRT compilation Co-authored-by: Miha Zupan --- .../System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index 8434a8cc76ce7..fc4454044d606 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1167,7 +1167,7 @@ public Task SendWithNtProxyAuthAsync(HttpConnection connect { if (DoProxyAuth && ProxyCredentials is not null) { - return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri, async, ProxyCredentials, connection, this, cancellationToken); + return AuthenticationHelper.SendWithNtProxyAuthAsync(request, ProxyUri!, async, ProxyCredentials, connection, this, cancellationToken); } return connection.SendAsync(request, async, cancellationToken); From 32a36b5464139fb4ccc036c2d2310f552a805d2e Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 15 Apr 2021 16:55:49 +0800 Subject: [PATCH 47/58] Server shouldn't wait for request in exceptional test. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index d8d69bb941b4f..907578215d875 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -99,7 +99,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( var ex = await Assert.ThrowsAnyAsync(() => client.GetStringAsync(uri)); Assert.Equal("SocksException", ex.GetType().Name); }, - async server => await server.HandleRequestAsync(content: "Echo"), + server => Task.CompletedTask, options: new GenericLoopbackOptions { Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback From 9ed36861cd0c6c12ec16a69e36fbd27554a64532 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 15 Apr 2021 17:13:43 +0800 Subject: [PATCH 48/58] Add exception message to test. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 907578215d875..56482bb3d69c3 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -67,17 +67,17 @@ await LoopbackServerFactory.CreateClientAndServerAsync( public static IEnumerable TestExceptionalAsync_MemberData() { foreach (string scheme in new[] { "socks4", "socks4a" }) - yield return new object[] { scheme, "::1", false, null }; + yield return new object[] { scheme, "::1", false, null, "SOCKS4 does not support IPv6 addresses." }; foreach (string scheme in new[] { "socks4", "socks4a", "socks5" }) { - yield return new object[] { scheme, "localhost", true, null }; - yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password") }; + yield return new object[] { scheme, "localhost", true, null, "SOCKS server did not return a suitable authentication method." }; + yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; } } [Theory] [MemberData(nameof(TestExceptionalAsync_MemberData))] - public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials) + public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials, string exceptionMessage) { await LoopbackServerFactory.CreateClientAndServerAsync( async uri => @@ -98,6 +98,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync( // SocksException is not public var ex = await Assert.ThrowsAnyAsync(() => client.GetStringAsync(uri)); Assert.Equal("SocksException", ex.GetType().Name); + Assert.Equal(exceptionMessage, ex.Message); }, server => Task.CompletedTask, options: new GenericLoopbackOptions From e70eb6266f210b8a9432667bbf49955ea635d242 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 15 Apr 2021 17:29:16 +0800 Subject: [PATCH 49/58] Update auth failure handling. --- .../Net/Http/SocketsHttpHandler/SocksHelper.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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 db1b444720232..99b151c572f94 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 @@ -27,6 +27,7 @@ internal static class SocksHelper private const byte ATYP_IPV6 = 4; private const byte Socks5_Success = 0; private const byte Socks4_Success = 90; + private const byte Socks4_AuthFailed = 93; public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) { @@ -106,6 +107,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // https://tools.ietf.org/html/rfc1929 if (credentials is null) { + // If the server is behaving well, it shouldn't pick username and password auth + // because we don't claim to support it when we don't have credentials. + // Just being defensive here. throw new SocksException(SR.net_socks_auth_required); } @@ -289,9 +293,16 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // 1 1 2 4 await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); VerifyProtocolVersion(ProtocolVersion4, buffer[0]); - if (buffer[1] != Socks4_Success) + + switch (buffer[1]) { - throw new SocksException(SR.net_socks_connection_failed); + case Socks4_Success: + // Nothing to do + break; + case Socks4_AuthFailed: + throw new SocksException(SR.net_socks_auth_failed); + default: + throw new SocksException(SR.net_socks_connection_failed); } // response address not used } From c79979c4ae9634795c1e9d59a6cc1485e26d3bea Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 15 Apr 2021 17:38:01 +0800 Subject: [PATCH 50/58] SOCKS4 and 5 uses different auth model, requires different error message. --- .../tests/FunctionalTests/Socks/SocksProxyTest.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 56482bb3d69c3..e3e9f9836ef0e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -67,12 +67,14 @@ await LoopbackServerFactory.CreateClientAndServerAsync( public static IEnumerable TestExceptionalAsync_MemberData() { foreach (string scheme in new[] { "socks4", "socks4a" }) - yield return new object[] { scheme, "::1", false, null, "SOCKS4 does not support IPv6 addresses." }; - foreach (string scheme in new[] { "socks4", "socks4a", "socks5" }) { - yield return new object[] { scheme, "localhost", true, null, "SOCKS server did not return a suitable authentication method." }; + yield return new object[] { scheme, "::1", false, null, "SOCKS4 does not support IPv6 addresses." }; + yield return new object[] { scheme, "localhost", true, null, "Failed to authenticate with the SOCKS server." }; yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; } + + yield return new object[] { "socks5", "localhost", true, null, "SOCKS server did not return a suitable authentication method." }; + yield return new object[] { "socks5", "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; } [Theory] From bca15fa1278b8f1140d96f9a56a39960d867c5c0 Mon Sep 17 00:00:00 2001 From: Huo Yaoyuan Date: Thu, 15 Apr 2021 17:48:31 +0800 Subject: [PATCH 51/58] Revert accidental indent change. --- .../Http/SocketsHttpHandler/HttpConnectionPool.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs index fc4454044d606..ad40acd7e887e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnectionPool.cs @@ -1111,13 +1111,13 @@ internal void BlocklistAuthority(HttpAuthority badAuthority) if (added) { _ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds) - .ContinueWith(t => - { - lock (altSvcBlocklist) - { - altSvcBlocklist.Remove(badAuthority); - } - }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + .ContinueWith(t => + { + lock (altSvcBlocklist) + { + altSvcBlocklist.Remove(badAuthority); + } + }, _altSvcBlocklistTimerCancellation.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } if (disabled) From 0ae546d9d9c259887bb7594c973d1a6e106376a1 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Thu, 15 Apr 2021 16:11:52 +0200 Subject: [PATCH 52/58] Expand test matrix to include Sync HTTP1 --- .../FunctionalTests/Socks/SocksProxyTest.cs | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index e3e9f9836ef0e..60e144aedb79e 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -42,8 +42,6 @@ await LoopbackServerFactory.CreateClientAndServerAsync( using HttpClientHandler handler = CreateHttpClientHandler(); using HttpClient client = CreateHttpClient(handler); - client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; - handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}"); handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates; @@ -54,7 +52,11 @@ await LoopbackServerFactory.CreateClientAndServerAsync( uri = new UriBuilder(uri) { Host = host }.Uri; - Assert.Equal("Echo", await client.GetStringAsync(uri)); + HttpRequestMessage request = CreateRequest(HttpMethod.Get, uri, UseVersion, exactVersion: true); + + using HttpResponseMessage response = await client.SendAsync(TestAsync, request); + string responseString = await response.Content.ReadAsStringAsync(); + Assert.Equal("Echo", responseString); }, async server => await server.HandleRequestAsync(content: "Echo"), options: new GenericLoopbackOptions @@ -68,7 +70,7 @@ public static IEnumerable TestExceptionalAsync_MemberData() { foreach (string scheme in new[] { "socks4", "socks4a" }) { - yield return new object[] { scheme, "::1", false, null, "SOCKS4 does not support IPv6 addresses." }; + yield return new object[] { scheme, "[::1]", false, null, "SOCKS4 does not support IPv6 addresses." }; yield return new object[] { scheme, "localhost", true, null, "Failed to authenticate with the SOCKS server." }; yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; } @@ -81,50 +83,44 @@ public static IEnumerable TestExceptionalAsync_MemberData() [MemberData(nameof(TestExceptionalAsync_MemberData))] public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials, string exceptionMessage) { - await LoopbackServerFactory.CreateClientAndServerAsync( - async uri => - { - using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); - using HttpClientHandler handler = CreateHttpClientHandler(); - using HttpClient client = CreateHttpClient(handler); - - client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact; + using LoopbackSocksServer proxy = useAuth ? LoopbackSocksServer.Create("DOTNET", "424242") : LoopbackSocksServer.Create(); + using HttpClientHandler handler = CreateHttpClientHandler(); + using HttpClient client = CreateHttpClient(handler); - handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}") - { - Credentials = credentials - }; + handler.Proxy = new WebProxy($"{scheme}://localhost:{proxy.Port}") + { + Credentials = credentials + }; - uri = new UriBuilder(uri) { Host = host }.Uri; + HttpRequestMessage request = CreateRequest(HttpMethod.Get, new Uri($"http://{host}/"), UseVersion, exactVersion: true); - // SocksException is not public - var ex = await Assert.ThrowsAnyAsync(() => client.GetStringAsync(uri)); - Assert.Equal("SocksException", ex.GetType().Name); - Assert.Equal(exceptionMessage, ex.Message); - }, - server => Task.CompletedTask, - options: new GenericLoopbackOptions - { - Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback - }); + // SocksException is not public + var ex = await Assert.ThrowsAnyAsync(() => client.SendAsync(TestAsync, request)); + Assert.Equal(exceptionMessage, ex.Message); + Assert.Equal("SocksException", ex.GetType().Name); } } [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - public sealed class SocksProxyTest_Http1 : SocksProxyTest + public sealed class SocksProxyTest_Http1_Async : SocksProxyTest { - public SocksProxyTest_Http1(ITestOutputHelper helper) : base(helper) { } - + public SocksProxyTest_Http1_Async(ITestOutputHelper helper) : base(helper) { } protected override Version UseVersion => HttpVersion.Version11; } + [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + public sealed class SocksProxyTest_Http1_Sync : SocksProxyTest + { + public SocksProxyTest_Http1_Sync(ITestOutputHelper helper) : base(helper) { } + protected override Version UseVersion => HttpVersion.Version11; + protected override bool TestAsync => false; + } [ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] public sealed class SocksProxyTest_Http2 : SocksProxyTest { public SocksProxyTest_Http2(ITestOutputHelper helper) : base(helper) { } - protected override Version UseVersion => HttpVersion.Version20; } } From b06f14508e8cea6ecdab1eb55d53599fd6362831 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 07:22:04 +0200 Subject: [PATCH 53/58] Read received bytes before returning error response in Socks4 loopback --- .../Socks/LoopbackSocksServer.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs index 9ae20927dd80c..0649f4d6bafd0 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/LoopbackSocksServer.cs @@ -145,18 +145,6 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) usernameBuffer[usernameBytes++] = (byte)usernameByte; } - if (_username != null) - { - string username = Encoding.UTF8.GetString(usernameBuffer.AsSpan(0, usernameBytes)); - if (username != _username) - { - ns.WriteByte(4); - buffer[0] = 93; - await ns.WriteAsync(buffer).ConfigureAwait(false); - throw new Exception("Bad username."); - } - } - if (remoteHost.StartsWith("0.0.0") && remoteHost != "0.0.0.0") { byte[] hostBuffer = new byte[1024]; @@ -176,6 +164,18 @@ private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns) remoteHost = Encoding.UTF8.GetString(hostBuffer.AsSpan(0, hostnameBytes)); } + if (_username != null) + { + string username = Encoding.UTF8.GetString(usernameBuffer.AsSpan(0, usernameBytes)); + if (username != _username) + { + ns.WriteByte(4); + buffer[0] = 93; + await ns.WriteAsync(buffer).ConfigureAwait(false); + return; + } + } + ns.WriteByte(4); buffer[0] = 90; await ns.WriteAsync(buffer).ConfigureAwait(false); From 7f201916ba6021f676cd807870cb9c8fbba2efe8 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 07:23:32 +0200 Subject: [PATCH 54/58] Use named bool arguments --- .../src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 99b151c572f94..3d926f244a65d 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 @@ -41,11 +41,11 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho } else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, true, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, isVersion4a: true, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, false, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); } else { From 2ff779ab089debc9ec566db1f8677c7e677fd82b Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 08:25:55 +0200 Subject: [PATCH 55/58] Improve exception messages --- .../src/Resources/Strings.resx | 3 ++ .../Http/SocketsHttpHandler/SocksHelper.cs | 49 ++++++++++++------- .../FunctionalTests/Socks/SocksProxyTest.cs | 8 +++ 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx index b8aa30616b8fb..897207df43ddc 100644 --- a/src/libraries/System.Net.Http/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx @@ -627,6 +627,9 @@ Unexpected SOCKS protocol version. Required {0}, got {1}. + + Encoding the {0} took more than the maximum of 255 bytes. + SOCKS server requested username & password authentication. 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 3d926f244a65d..487cbd86b93b6 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 @@ -14,11 +14,11 @@ namespace System.Net.Http { internal static class SocksHelper { - // socks protocol limits address length to 1 byte, thus the maximum possible buffer is 256+other fields - private const int BufferSize = 512; + // Largest possible message size is 513 bytes (Socks5 username & password auth) + private const int BufferSize = 513; private const int ProtocolVersion4 = 4; private const int ProtocolVersion5 = 5; - private const int SubnegotiationVersion = 1; // Socks5 username/password auth + private const int SubnegotiationVersion = 1; // Socks5 username & password auth private const byte METHOD_NO_AUTH = 0; private const byte METHOD_USERNAME_PASSWORD = 2; private const byte CMD_CONNECT = 1; @@ -35,17 +35,19 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho { try { + NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, proxyUri.Scheme); + if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, proxyCredentials, async).ConfigureAwait(false); + await EstablishSocks5TunnelAsync(stream, host, port, proxyUri, credentials, async).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, isVersion4a: true, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, isVersion4a: true, host, port, proxyUri, credentials, async, cancellationToken).ConfigureAwait(false); } else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase)) { - await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, proxyUri, proxyCredentials, async, cancellationToken).ConfigureAwait(false); + await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, proxyUri, credentials, async, cancellationToken).ConfigureAwait(false); } else { @@ -60,10 +62,9 @@ public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string ho } } - private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async) + private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, NetworkCredential? credentials, bool async) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); - try { // https://tools.ietf.org/html/rfc1928 @@ -74,7 +75,6 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | 1 to 255 | // +----+----------+----------+ buffer[0] = ProtocolVersion5; - NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, ""); if (credentials is null) { buffer[1] = 1; @@ -119,9 +119,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | // +----+------+----------+------+----------+ buffer[0] = SubnegotiationVersion; - byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(credentials.UserName, buffer.AsSpan(2))); + byte usernameLength = EncodeString(credentials.UserName, buffer.AsSpan(2), nameof(credentials.UserName)); buffer[1] = usernameLength; - byte passwordLength = checked((byte)Encoding.UTF8.GetBytes(credentials.Password, buffer.AsSpan(3 + usernameLength))); + byte passwordLength = EncodeString(credentials.Password, buffer.AsSpan(3 + usernameLength), nameof(credentials.Password)); buffer[2 + usernameLength] = passwordLength; await WriteAsync(stream, buffer.AsMemory(0, 3 + usernameLength + passwordLength), async).ConfigureAwait(false); @@ -174,9 +174,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string else { buffer[3] = ATYP_DOMAIN_NAME; - byte bytesEncoded = checked((byte)Encoding.UTF8.GetBytes(host, buffer.AsSpan(5))); - buffer[4] = bytesEncoded; - addressLength = bytesEncoded + 1; + byte hostLength = EncodeString(host, buffer.AsSpan(5), nameof(host)); + buffer[4] = hostLength; + addressLength = hostLength + 1; } BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port); @@ -210,10 +210,9 @@ private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string } } - private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, ICredentials? proxyCredentials, bool async, CancellationToken cancellationToken) + private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, NetworkCredential? credentials, bool async, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(BufferSize); - try { // https://www.openssh.com/txt/socks4.protocol @@ -222,7 +221,6 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // | VN | CD | DSTPORT | DSTIP | USERID |NULL| // +----+----+----+----+----+----+----+----+----+----+....+----+ // 1 1 2 4 variable 1 - string? username = proxyCredentials?.GetCredential(proxyUri, "")?.UserName; buffer[0] = ProtocolVersion4; buffer[1] = CMD_CONNECT; @@ -273,14 +271,14 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is Debug.Assert(bytesWritten == 4); } - byte usernameLength = checked((byte)Encoding.UTF8.GetBytes(username, buffer.AsSpan(8))); + byte usernameLength = EncodeString(credentials?.UserName, buffer.AsSpan(8), nameof(credentials.UserName)); buffer[8 + usernameLength] = 0; int totalLength = 9 + usernameLength; if (ipv4Address is null) { // https://www.openssh.com/txt/socks4a.protocol - byte hostLength = checked((byte)Encoding.UTF8.GetBytes(host, buffer.AsSpan(totalLength))); + byte hostLength = EncodeString(host, buffer.AsSpan(totalLength), nameof(host)); buffer[totalLength + hostLength] = 0; totalLength += hostLength + 1; } @@ -312,6 +310,19 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is } } + private static byte EncodeString(ReadOnlySpan chars, Span buffer, string parameterName) + { + try + { + return checked((byte)Encoding.UTF8.GetBytes(chars, buffer)); + } + catch + { + Debug.Assert(Encoding.UTF8.GetByteCount(chars) > 255); + throw new SocksException(SR.Format(SR.net_socks_string_too_long, parameterName)); + } + } + private static void VerifyProtocolVersion(byte expected, byte version) { if (expected != version) diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 60e144aedb79e..23d779b069845 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -73,10 +73,18 @@ public static IEnumerable TestExceptionalAsync_MemberData() yield return new object[] { scheme, "[::1]", false, null, "SOCKS4 does not support IPv6 addresses." }; yield return new object[] { scheme, "localhost", true, null, "Failed to authenticate with the SOCKS server." }; yield return new object[] { scheme, "localhost", true, new NetworkCredential("bad_username", "bad_password"), "Failed to authenticate with the SOCKS server." }; + yield return new object[] { scheme, "localhost", true, new NetworkCredential(new string('a', 256), "foo"), "Encoding the UserName took more than the maximum of 255 bytes." }; + } + + foreach (string scheme in new[] { "socks4a", "socks5" }) + { + yield return new object[] { scheme, new string('a', 256), false, null, "Encoding the host took more than the maximum of 255 bytes." }; } yield return new object[] { "socks5", "localhost", true, null, "SOCKS server did not return a suitable authentication method." }; 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." }; } [Theory] From c08e717cc45089e53bf6e761b6a6c9c30ed277a7 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 08:26:41 +0200 Subject: [PATCH 56/58] !IsEmpty => Length != 0 --- .../src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 487cbd86b93b6..d0ebde1b5e881 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 @@ -346,7 +346,7 @@ private static ValueTask WriteAsync(Stream stream, Memory buffer, bool asy private static async ValueTask ReadToFillAsync(Stream stream, Memory buffer, bool async) { - while (!buffer.IsEmpty) + while (buffer.Length != 0) { int bytesRead = async ? await stream.ReadAsync(buffer).ConfigureAwait(false) From 52df3626ef4391e27b013316e511921d88325036 Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 08:57:15 +0200 Subject: [PATCH 57/58] Improve exception messages 2 --- .../Http/SocketsHttpHandler/SocksException.cs | 6 +++--- .../Net/Http/SocketsHttpHandler/SocksHelper.cs | 16 ++++++++++++---- .../FunctionalTests/Socks/SocksProxyTest.cs | 2 ++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs index 835a78f5636ac..c9312c23d57c3 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksException.cs @@ -7,8 +7,8 @@ namespace System.Net.Http { internal class SocksException : IOException { - public SocksException(string message) : base(message) - { - } + public SocksException(string message) : base(message) { } + + public SocksException(string message, Exception innerException) : base(message, innerException) { } } } 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 d0ebde1b5e881..84a3c52ce758f 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 @@ -227,7 +227,7 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), (ushort)port); IPAddress? ipv4Address = null; - if (IPAddress.TryParse(host, out var hostIP)) + if (IPAddress.TryParse(host, out IPAddress? hostIP)) { if (hostIP.AddressFamily == AddressFamily.InterNetwork) { @@ -245,9 +245,17 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is else if (!isVersion4a) { // Socks4 does not support domain names - try to resolve it here - var addresses = async - ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) - : Dns.GetHostAddresses(host, AddressFamily.InterNetwork); + IPAddress[] addresses; + try + { + addresses = async + ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false) + : Dns.GetHostAddresses(host, AddressFamily.InterNetwork); + } + catch (Exception ex) + { + throw new SocksException(SR.net_socks_no_ipv4_address, ex); + } if (addresses.Length == 0) { diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs index 23d779b069845..b82d091a90d91 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/Socks/SocksProxyTest.cs @@ -76,6 +76,8 @@ public static IEnumerable TestExceptionalAsync_MemberData() yield return new object[] { scheme, "localhost", true, new NetworkCredential(new string('a', 256), "foo"), "Encoding the UserName took more than the maximum of 255 bytes." }; } + 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" }) { yield return new object[] { scheme, new string('a', 256), false, null, "Encoding the host took more than the maximum of 255 bytes." }; From 95400e21568c1c1c583235e5e49dc00d911222ed Mon Sep 17 00:00:00 2001 From: MihaZupan Date: Fri, 16 Apr 2021 10:42:18 +0200 Subject: [PATCH 58/58] Avoid enforing Socks4 VN value --- .../src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs | 1 - 1 file changed, 1 deletion(-) 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 84a3c52ce758f..8e7408410e2f4 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 @@ -298,7 +298,6 @@ private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool is // +----+----+----+----+----+----+----+----+ // 1 1 2 4 await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false); - VerifyProtocolVersion(ProtocolVersion4, buffer[0]); switch (buffer[1]) {