未验证 提交 32d1a256 编写于 作者: H Huo Yaoyuan 提交者: GitHub

SOCKS4/4a/5 proxy support in SocketsHttpHandler (#48883)

* Separate method for proxy scheme validation.

* Pass socks connection kind.

* Unauthorized socks5 connection.

* Username and password auth.

* Fix response address.

* Fix proxyUri value and assertion.

* Use HttpConnectionKind for SOCKS.

* Handle more connection kind assertions.

* SOCKS4/4a support.

* Move version selection into SocksHelper.

* Call sync version of read write.

* Cancellation by disposing stream.

* Dispose cancellation registration.

* IP addressing for SOCKS5.

* IP addressing for SOCKS4.

* Wrap write method.

* Cancellation and optimization.

* Optimize.

* Apply suggestions from code review
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>

* Clarify logic.

* Remove ssl assertion.

* SocksException.

* Make SocksException derive from IOException.

* Use binary primitives to write port in BE.

* Socks loopback test.

* Expand test matrix.

* Try to solve certificate issue.

* Pass handler to httpclient.

* Update ConnectToTcpHostAsync.

* Remove custom self-signed cert use from Socks test

* Fix LoopbackSocksServer's parsing of Socks4a domain name

* Only set RequestVersionExact for H2C

Setting it in general breaks H2 => H1.1 downgrade on platforms without ALPN

* Add auth test.

* Add IP in test matrix.

* Only override host when required.

* Don't attempt NT Auth for Socks proxies

* Skip HTTP2 ssl test on platforms without ALPN support

* Use NetworkCredential directly

* Pass AddressFamily to sync Dns resolution too

* Consistently check encoded string lengths

* Fix Socks5 user/pass auth

* Add IPv6 test for socks5

* Exception nits

* Add exceptional tests.

* Fix exceptional test.

* Fix NRT compilation
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>

* Server shouldn't wait for request in exceptional test.

* Add exception message to test.

* Update auth failure handling.

* SOCKS4 and 5 uses different auth model, requires different error message.

* Revert accidental indent change.

* Expand test matrix to include Sync HTTP1

* Read received bytes before returning error response in Socks4 loopback

* Use named bool arguments

* Improve exception messages

* !IsEmpty => Length != 0

* Improve exception messages 2

* Avoid enforing Socks4 VN value
Co-authored-by: NMiha Zupan <mihazupan.zupan1@gmail.com>
上级 1132baae
......@@ -406,7 +406,7 @@
<value>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.</value>
</data>
<data name="net_http_invalid_proxy_scheme" xml:space="preserve">
<value>Only the 'http' scheme is allowed for proxies.</value>
<value>Only the 'http', 'socks4', 'socks4a' and 'socks5' schemes are allowed for proxies.</value>
</data>
<data name="net_http_request_invalid_char_encoding" xml:space="preserve">
<value>Request headers must contain only ASCII characters.</value>
......@@ -606,6 +606,33 @@
<data name="net_http_synchronous_reads_not_supported" xml:space="preserve">
<value>Synchronous reads are not supported, use ReadAsync instead.</value>
</data>
<data name="net_socks_auth_failed" xml:space="preserve">
<value>Failed to authenticate with the SOCKS server.</value>
</data>
<data name="net_socks_bad_address_type" xml:space="preserve">
<value>SOCKS server returned an unknown address type.</value>
</data>
<data name="net_socks_connection_failed" xml:space="preserve">
<value>SOCKS server failed to connect to the destination.</value>
</data>
<data name="net_socks_ipv6_notsupported" xml:space="preserve">
<value>SOCKS4 does not support IPv6 addresses.</value>
</data>
<data name="net_socks_no_auth_method" xml:space="preserve">
<value>SOCKS server did not return a suitable authentication method.</value>
</data>
<data name="net_socks_no_ipv4_address" xml:space="preserve">
<value>Failed to resolve the destination host to an IPv4 address.</value>
</data>
<data name="net_socks_unexpected_version" xml:space="preserve">
<value>Unexpected SOCKS protocol version. Required {0}, got {1}.</value>
</data>
<data name="net_socks_string_too_long" xml:space="preserve">
<value>Encoding the {0} took more than the maximum of 255 bytes.</value>
</data>
<data name="net_socks_auth_required" xml:space="preserve">
<value>SOCKS server requested username &amp; password authentication.</value>
</data>
<data name="net_http_proxy_tunnel_returned_failure_status_code" xml:space="preserve">
<value>The proxy tunnel request to proxy '{0}' failed with status code '{1}'."</value>
</data>
......
......@@ -179,6 +179,8 @@
<Compile Include="System\Net\Http\HttpTelemetry.AnyOS.cs" />
<Compile Include="System\Net\Http\HttpUtilities.AnyOS.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SystemProxyInfo.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocksHelper.cs" />
<Compile Include="System\Net\Http\SocketsHttpHandler\SocksException.cs" />
<Compile Include="$(CommonPath)System\Net\NTAuthentication.Common.cs"
Link="Common\System\Net\NTAuthentication.Common.cs" />
<Compile Include="$(CommonPath)System\Net\ContextFlagsPal.cs"
......
......@@ -34,6 +34,14 @@ internal static bool IsHttpUri(Uri uri)
internal static bool IsSecureWebSocketScheme(string scheme) =>
string.Equals(scheme, "wss", StringComparison.OrdinalIgnoreCase);
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, "socks4a", StringComparison.OrdinalIgnoreCase) ||
string.Equals(scheme, "socks4", 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.
......
......@@ -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.
}
}
......@@ -190,8 +190,17 @@ public HttpConnectionPool(HttpConnectionPoolManager poolManager, HttpConnectionK
_http3Enabled = false;
break;
case HttpConnectionKind.SocksTunnel:
case HttpConnectionKind.SslSocksTunnel:
Debug.Assert(host != null);
Debug.Assert(port != 0);
Debug.Assert(proxyUri != null);
_http3Enabled = false; // TODO: SOCKS supports UDP and may be used for HTTP3
break;
default:
Debug.Fail("Unkown HttpConnectionKind in HttpConnectionPool.ctor");
Debug.Fail("Unknown HttpConnectionKind in HttpConnectionPool.ctor");
break;
}
......@@ -317,7 +326,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 Uri? ProxyUri => _proxyUri;
public ICredentials? ProxyCredentials => _poolManager.ProxyCredentials;
public byte[]? HostHeaderValueBytes => _hostHeaderValueBytes;
......@@ -339,10 +348,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(':')
......@@ -547,7 +556,7 @@ private async ValueTask<HttpConnectionBase> GetHttp11ConnectionAsync(HttpRequest
private async ValueTask<HttpConnectionBase> 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();
......@@ -603,7 +612,7 @@ private async ValueTask<HttpConnectionBase> GetHttp2ConnectionAsync(HttpRequestM
sslStream = stream as SslStream;
if (_kind == HttpConnectionKind.Http)
if (!IsSecure)
{
http2Connection = await ConstructHttp2ConnectionAsync(stream, request, cancellationToken).ConfigureAwait(false);
......@@ -1148,7 +1157,7 @@ internal void BlocklistAuthority(HttpAuthority badAuthority)
Debug.Assert(_altSvcBlocklistTimerCancellation != null);
if (added)
{
_ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds)
_ = Task.Delay(AltSvcBlocklistTimeoutInMilliseconds)
.ContinueWith(t =>
{
lock (altSvcBlocklist)
......@@ -1264,6 +1273,14 @@ private async ValueTask<(Socket?, Stream, TransportContext?)> ConnectAsync(HttpR
case HttpConnectionKind.SslProxyTunnel:
stream = await EstablishProxyTunnelAsync(async, request.HasHeaders ? request.Headers : null, cancellationToken).ConfigureAwait(false);
break;
case HttpConnectionKind.SocksTunnel:
case HttpConnectionKind.SslSocksTunnel:
Debug.Assert(_originAuthority != null);
Debug.Assert(_proxyUri != null);
(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;
}
Debug.Assert(stream != null);
......
......@@ -282,8 +282,20 @@ private HttpConnectionKey GetConnectionKey(HttpRequestMessage request, Uri? prox
if (proxyUri != null)
{
Debug.Assert(HttpUtilities.IsSupportedNonSecureScheme(proxyUri.Scheme));
if (sslHostName == null)
Debug.Assert(HttpUtilities.IsSupportedProxyScheme(proxyUri.Scheme));
if (HttpUtilities.IsSocksScheme(proxyUri.Scheme))
{
// Socks proxy
if (sslHostName != null)
{
return new HttpConnectionKey(HttpConnectionKind.SslSocksTunnel, uri.IdnHost, uri.Port, sslHostName, proxyUri, identity);
}
else
{
return new HttpConnectionKey(HttpConnectionKind.SocksTunnel, uri.IdnHost, uri.Port, null, proxyUri, identity);
}
}
else if (sslHostName == null)
{
if (HttpUtilities.IsNonSecureWebSocketScheme(uri.Scheme))
{
......@@ -394,7 +406,7 @@ public ValueTask<HttpResponseMessage> 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);
}
......
// 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 : IOException
{
public SocksException(string message) : base(message) { }
public SocksException(string message, Exception innerException) : base(message, innerException) { }
}
}
// 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.Buffers.Binary;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace System.Net.Http
{
internal static class SocksHelper
{
// 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 byte METHOD_NO_AUTH = 0;
private const byte METHOD_USERNAME_PASSWORD = 2;
private const byte CMD_CONNECT = 1;
private const byte ATYP_IPV4 = 1;
private const byte ATYP_DOMAIN_NAME = 3;
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)
{
using (cancellationToken.Register(s => ((Stream)s!).Dispose(), stream))
{
try
{
NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, proxyUri.Scheme);
if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase))
{
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, credentials, async, cancellationToken).ConfigureAwait(false);
}
else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase))
{
await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, proxyUri, credentials, async, cancellationToken).ConfigureAwait(false);
}
else
{
Debug.Fail("Bad socks version.");
}
}
catch
{
stream.Dispose();
throw;
}
}
}
private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, Uri proxyUri, NetworkCredential? credentials, bool async)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
// https://tools.ietf.org/html/rfc1928
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
buffer[0] = ProtocolVersion5;
if (credentials is null)
{
buffer[1] = 1;
buffer[2] = METHOD_NO_AUTH;
}
else
{
buffer[1] = 2;
buffer[2] = METHOD_NO_AUTH;
buffer[3] = METHOD_USERNAME_PASSWORD;
}
await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false);
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
switch (buffer[1])
{
case METHOD_NO_AUTH:
// continue
break;
case METHOD_USERNAME_PASSWORD:
{
// 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);
}
// +----+------+----------+------+----------+
// |VER | ULEN | UNAME | PLEN | PASSWD |
// +----+------+----------+------+----------+
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
// +----+------+----------+------+----------+
buffer[0] = SubnegotiationVersion;
byte usernameLength = EncodeString(credentials.UserName, buffer.AsSpan(2), nameof(credentials.UserName));
buffer[1] = 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);
// +----+--------+
// |VER | STATUS |
// +----+--------+
// | 1 | 1 |
// +----+--------+
await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5_Success)
{
throw new SocksException(SR.net_socks_auth_failed);
}
break;
}
default:
throw new SocksException(SR.net_socks_no_auth_method);
}
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
buffer[0] = ProtocolVersion5;
buffer[1] = CMD_CONNECT;
buffer[2] = 0;
int addressLength;
if (IPAddress.TryParse(host, out IPAddress? hostIP))
{
if (hostIP.AddressFamily == AddressFamily.InterNetwork)
{
buffer[3] = ATYP_IPV4;
hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
Debug.Assert(bytesWritten == 4);
addressLength = 4;
}
else
{
Debug.Assert(hostIP.AddressFamily == AddressFamily.InterNetworkV6);
buffer[3] = ATYP_IPV6;
hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
Debug.Assert(bytesWritten == 16);
addressLength = 16;
}
}
else
{
buffer[3] = ATYP_DOMAIN_NAME;
byte hostLength = EncodeString(host, buffer.AsSpan(5), nameof(host));
buffer[4] = hostLength;
addressLength = hostLength + 1;
}
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port);
await WriteAsync(stream, buffer.AsMemory(0, addressLength + 6), 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).ConfigureAwait(false);
VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
if (buffer[1] != Socks5_Success)
{
throw new SocksException(SR.net_socks_connection_failed);
}
int bytesToSkip = buffer[3] switch
{
ATYP_IPV4 => 5,
ATYP_IPV6 => 17,
ATYP_DOMAIN_NAME => buffer[4] + 2,
_ => throw new SocksException(SR.net_socks_bad_address_type)
};
await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async).ConfigureAwait(false);
// response address not used
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port, Uri proxyUri, NetworkCredential? credentials, bool async, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
try
{
// https://www.openssh.com/txt/socks4.protocol
// +----+----+----+----+----+----+----+----+----+----+....+----+
// | VN | CD | DSTPORT | DSTIP | USERID |NULL|
// +----+----+----+----+----+----+----+----+----+----+....+----+
// 1 1 2 4 variable 1
buffer[0] = ProtocolVersion4;
buffer[1] = CMD_CONNECT;
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), (ushort)port);
IPAddress? ipv4Address = null;
if (IPAddress.TryParse(host, out IPAddress? hostIP))
{
if (hostIP.AddressFamily == AddressFamily.InterNetwork)
{
ipv4Address = hostIP;
}
else if (hostIP.IsIPv4MappedToIPv6)
{
ipv4Address = hostIP.MapToIPv4();
}
else
{
throw new SocksException(SR.net_socks_ipv6_notsupported);
}
}
else if (!isVersion4a)
{
// Socks4 does not support domain names - try to resolve it here
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)
{
throw new SocksException(SR.net_socks_no_ipv4_address);
}
ipv4Address = addresses[0];
}
if (ipv4Address is null)
{
Debug.Assert(isVersion4a);
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);
}
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 = EncodeString(host, buffer.AsSpan(totalLength), nameof(host));
buffer[totalLength + hostLength] = 0;
totalLength += hostLength + 1;
}
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).ConfigureAwait(false);
switch (buffer[1])
{
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
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static byte EncodeString(ReadOnlySpan<char> chars, Span<byte> 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)
{
throw new SocksException(SR.Format(SR.net_socks_unexpected_version, expected, version));
}
}
private static ValueTask WriteAsync(Stream stream, Memory<byte> buffer, bool async)
{
if (async)
{
return stream.WriteAsync(buffer);
}
else
{
stream.Write(buffer.Span);
return default;
}
}
private static async ValueTask ReadToFillAsync(Stream stream, Memory<byte> buffer, bool async)
{
while (buffer.Length != 0)
{
int bytesRead = async
? await stream.ReadAsync(buffer).ConfigureAwait(false)
: stream.Read(buffer.Span);
if (bytesRead == 0)
{
throw new IOException(SR.net_http_invalid_response_premature_eof);
}
buffer = buffer[bytesRead..];
}
}
}
}
// 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
{
/// <summary>
/// Provides a test-only SOCKS4/5 proxy.
/// </summary>
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 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);
var ep = (IPEndPoint)_listener.LocalEndPoint;
Port = ep.Port;
_serverStopped = new ManualResetEvent(false);
}
private void Start()
{
Task.Run(async () =>
{
var activeTasks = new ConcurrentDictionary<Task, int>();
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]}";
byte[] usernameBuffer = new byte[1024];
int usernameBytes = 0;
while (true)
{
int usernameByte = await ns.ReadByteAsync().ConfigureAwait(false);
if (usernameByte == 0)
break;
if (usernameByte == -1)
throw new Exception("Early EOF");
usernameBuffer[usernameBytes++] = (byte)usernameByte;
}
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(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);
await RelayHttpTraffic(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);
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, expectedAuthMethod }).ConfigureAwait(false);
if (_username != null)
{
if (await ns.ReadByteAsync().ConfigureAwait(false) != 1)
throw new Exception("Bad subnegotiation version.");
int usernameLength = await ns.ReadByteAsync().ConfigureAwait(false);
await ReadToFillAsync(ns, buffer.AsMemory(0, usernameLength)).ConfigureAwait(false);
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);
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);
}
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 = new IPAddress(buffer.AsSpan(0, 4)).ToString();
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 RelayHttpTraffic(clientSocket, ns, remoteHost, port).ConfigureAwait(false);
}
private async Task RelayHttpTraffic(Socket clientSocket, NetworkStream clientStream, string remoteHost, int remotePort)
{
// Open connection to destination server.
using var serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
await serverSocket.ConnectAsync(remoteHost, remotePort).ConfigureAwait(false);
var 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);
/// <summary>Closes sockets to cause both tasks to end, and eats connection reset/aborted errors.</summary>
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<byte> 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(string? username = null, string? password = null)
{
var server = new LoopbackSocksServer(username, password);
server.Start();
return server;
}
public void Dispose()
{
if (!_disposed)
{
_listener.Dispose();
_serverStopped.WaitOne();
_disposed = true;
}
}
}
}
// 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.IO;
using System.Linq;
using System.Net.Test.Common;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace System.Net.Http.Functional.Tests.Socks
{
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<object[]> TestLoopbackAsync_MemberData() =>
from scheme in new[] { "socks4", "socks4a", "socks5" }
from useSsl in BoolValues
from useAuth in BoolValues
from host in Hosts(scheme)
select new object[] { scheme, useSsl, useAuth, host };
[Theory]
[MemberData(nameof(TestLoopbackAsync_MemberData))]
public async Task TestLoopbackAsync(string scheme, bool useSsl, bool useAuth, string host)
{
if (useSsl && UseVersion == HttpVersion.Version20 && !PlatformDetection.SupportsAlpn)
{
return;
}
await LoopbackServerFactory.CreateClientAndServerAsync(
async uri =>
{
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}");
handler.ServerCertificateCustomValidationCallback = TestHelper.AllowAllCertificates;
if (useAuth)
{
handler.Proxy.Credentials = new NetworkCredential("DOTNET", "424242");
}
uri = new UriBuilder(uri) { Host = host }.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
{
UseSsl = useSsl,
Address = host == "::1" ? IPAddress.IPv6Loopback : IPAddress.Loopback
});
}
public static IEnumerable<object[]> 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, "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." };
}
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." };
}
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]
[MemberData(nameof(TestExceptionalAsync_MemberData))]
public async Task TestExceptionalAsync(string scheme, string host, bool useAuth, ICredentials? credentials, string exceptionMessage)
{
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
};
HttpRequestMessage request = CreateRequest(HttpMethod.Get, new Uri($"http://{host}/"), UseVersion, exactVersion: true);
// SocksException is not public
var ex = await Assert.ThrowsAnyAsync<IOException>(() => 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_Async : SocksProxyTest
{
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;
}
}
......@@ -191,6 +191,11 @@
<Compile Include="$(CommonPath)Interop\OSX\Interop.Libraries.cs"
Link="Common\Interop\OSX\Interop.Libraries.cs" />
</ItemGroup>
<!-- Requires SocketsHttpHandler -->
<ItemGroup Condition="'$(TargetsBrowser)' != 'true'">
<Compile Include="Socks\LoopbackSocksServer.cs" />
<Compile Include="Socks\SocksProxyTest.cs" />
</ItemGroup>
<ItemGroup>
<Compile Include="CustomContent.netcore.cs" />
<Compile Include="HPackTest.cs" />
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册