< Summary

Information
Class: System.Net.Http.SocksHelper
Assembly: System.Net.Http
File(s): D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\SocksHelper.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 203
Coverable lines: 203
Total lines: 366
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 61
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
EstablishSocksTunnelAsync()0%880%
EstablishSocks5TunnelAsync()0%23230%
EstablishSocks4TunnelAsync()0%22220%
EncodeString(...)100%110%
VerifyProtocolVersion(...)0%220%
WriteAsync(...)0%220%
ReadToFillAsync()0%440%

File(s)

D:\runner\runtime\src\libraries\System.Net.Http\src\System\Net\Http\SocketsHttpHandler\SocksHelper.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Buffers;
 5using System.Buffers.Binary;
 6using System.Diagnostics;
 7using System.IO;
 8using System.Net.Sockets;
 9using System.Text;
 10using System.Threading;
 11using System.Threading.Tasks;
 12
 13namespace System.Net.Http
 14{
 15    internal static class SocksHelper
 16    {
 17        // Largest possible message size is 513 bytes (Socks5 username & password auth)
 18        private const int BufferSize = 513;
 19        private const int ProtocolVersion4 = 4;
 20        private const int ProtocolVersion5 = 5;
 21        private const int SubnegotiationVersion = 1; // Socks5 username & password auth
 22        private const byte METHOD_NO_AUTH = 0;
 23        private const byte METHOD_USERNAME_PASSWORD = 2;
 24        private const byte CMD_CONNECT = 1;
 25        private const byte ATYP_IPV4 = 1;
 26        private const byte ATYP_DOMAIN_NAME = 3;
 27        private const byte ATYP_IPV6 = 4;
 28        private const byte Socks5_Success = 0;
 29        private const byte Socks4_Success = 90;
 30        private const byte Socks4_AuthFailed = 93;
 31
 32        public static async ValueTask EstablishSocksTunnelAsync(Stream stream, string host, int port, Uri proxyUri, ICre
 033        {
 034            using (cancellationToken.Register(s => ((Stream)s!).Dispose(), stream))
 035            {
 36                try
 037                {
 038                    NetworkCredential? credentials = proxyCredentials?.GetCredential(proxyUri, proxyUri.Scheme);
 39
 040                    if (string.Equals(proxyUri.Scheme, "socks5", StringComparison.OrdinalIgnoreCase))
 041                    {
 042                        await EstablishSocks5TunnelAsync(stream, host, port, credentials, async).ConfigureAwait(false);
 043                    }
 044                    else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase))
 045                    {
 046                        await EstablishSocks4TunnelAsync(stream, isVersion4a: true, host, port, credentials, async, canc
 047                    }
 048                    else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase))
 049                    {
 050                        await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, credentials, async, can
 051                    }
 52                    else
 053                    {
 054                        Debug.Fail("Bad socks version.");
 55                    }
 056                }
 057                catch
 058                {
 059                    stream.Dispose();
 060                    throw;
 61                }
 062            }
 063        }
 64
 65        private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, NetworkCredentia
 066        {
 067            byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 68            try
 069            {
 70                // https://tools.ietf.org/html/rfc1928
 71
 72                // +----+----------+----------+
 73                // |VER | NMETHODS | METHODS  |
 74                // +----+----------+----------+
 75                // | 1  |    1     | 1 to 255 |
 76                // +----+----------+----------+
 077                buffer[0] = ProtocolVersion5;
 078                if (credentials is null)
 079                {
 080                    buffer[1] = 1;
 081                    buffer[2] = METHOD_NO_AUTH;
 082                }
 83                else
 084                {
 085                    buffer[1] = 2;
 086                    buffer[2] = METHOD_NO_AUTH;
 087                    buffer[3] = METHOD_USERNAME_PASSWORD;
 088                }
 089                await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false);
 90
 91                // +----+--------+
 92                // |VER | METHOD |
 93                // +----+--------+
 94                // | 1  |   1    |
 95                // +----+--------+
 096                await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
 097                VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
 98
 099                switch (buffer[1])
 100                {
 101                    case METHOD_NO_AUTH:
 102                        // continue
 0103                        break;
 104
 105                    case METHOD_USERNAME_PASSWORD:
 0106                        {
 107                            // https://tools.ietf.org/html/rfc1929
 0108                            if (credentials is null)
 0109                            {
 110                                // If the server is behaving well, it shouldn't pick username and password auth
 111                                // because we don't claim to support it when we don't have credentials.
 112                                // Just being defensive here.
 0113                                throw new SocksException(SR.net_socks_auth_required);
 114                            }
 115
 116                            // +----+------+----------+------+----------+
 117                            // |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
 118                            // +----+------+----------+------+----------+
 119                            // | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
 120                            // +----+------+----------+------+----------+
 0121                            buffer[0] = SubnegotiationVersion;
 0122                            byte usernameLength = EncodeString(credentials.UserName, buffer.AsSpan(2), nameof(credential
 0123                            buffer[1] = usernameLength;
 0124                            byte passwordLength = EncodeString(credentials.Password, buffer.AsSpan(3 + usernameLength), 
 0125                            buffer[2 + usernameLength] = passwordLength;
 0126                            await WriteAsync(stream, buffer.AsMemory(0, 3 + usernameLength + passwordLength), async).Con
 127
 128                            // +----+--------+
 129                            // |VER | STATUS |
 130                            // +----+--------+
 131                            // | 1  |   1    |
 132                            // +----+--------+
 0133                            await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
 0134                            if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5_Success)
 0135                            {
 0136                                throw new SocksException(SR.net_socks_auth_failed);
 137                            }
 0138                            break;
 139                        }
 140
 141                    default:
 0142                        throw new SocksException(SR.net_socks_no_auth_method);
 143                }
 144
 145
 146                // +----+-----+-------+------+----------+----------+
 147                // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
 148                // +----+-----+-------+------+----------+----------+
 149                // | 1  |  1  | X'00' |  1   | Variable |    2     |
 150                // +----+-----+-------+------+----------+----------+
 0151                buffer[0] = ProtocolVersion5;
 0152                buffer[1] = CMD_CONNECT;
 0153                buffer[2] = 0;
 154                int addressLength;
 155
 0156                if (IPAddress.TryParse(host, out IPAddress? hostIP))
 0157                {
 0158                    if (hostIP.AddressFamily == AddressFamily.InterNetwork)
 0159                    {
 0160                        buffer[3] = ATYP_IPV4;
 0161                        hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0162                        Debug.Assert(bytesWritten == 4);
 0163                        addressLength = 4;
 0164                    }
 165                    else
 0166                    {
 0167                        Debug.Assert(hostIP.AddressFamily == AddressFamily.InterNetworkV6);
 0168                        buffer[3] = ATYP_IPV6;
 0169                        hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0170                        Debug.Assert(bytesWritten == 16);
 0171                        addressLength = 16;
 0172                    }
 0173                }
 174                else
 0175                {
 0176                    buffer[3] = ATYP_DOMAIN_NAME;
 0177                    byte hostLength = EncodeString(host, buffer.AsSpan(5), nameof(host));
 0178                    buffer[4] = hostLength;
 0179                    addressLength = hostLength + 1;
 0180                }
 181
 0182                BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port);
 183
 0184                await WriteAsync(stream, buffer.AsMemory(0, addressLength + 6), async).ConfigureAwait(false);
 185
 186                // +----+-----+-------+------+----------+----------+
 187                // |VER | REP |  RSV  | ATYP | DST.ADDR | DST.PORT |
 188                // +----+-----+-------+------+----------+----------+
 189                // | 1  |  1  | X'00' |  1   | Variable |    2     |
 190                // +----+-----+-------+------+----------+----------+
 0191                await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async).ConfigureAwait(false);
 0192                VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
 0193                if (buffer[1] != Socks5_Success)
 0194                {
 0195                    throw new SocksException(SR.Format(SR.net_socks_connection_failed, buffer[1].ToString("X2")));
 196                }
 0197                int bytesToSkip = buffer[3] switch
 0198                {
 0199                    ATYP_IPV4 => 5,
 0200                    ATYP_IPV6 => 17,
 0201                    ATYP_DOMAIN_NAME => buffer[4] + 2,
 0202                    _ => throw new SocksException(SR.net_socks_bad_address_type)
 0203                };
 0204                await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async).ConfigureAwait(false);
 205                // response address not used
 0206            }
 207            finally
 0208            {
 0209                ArrayPool<byte>.Shared.Return(buffer);
 0210            }
 0211        }
 212
 213        private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port
 0214        {
 0215            byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 216            try
 0217            {
 218                // https://www.openssh.com/txt/socks4.protocol
 219
 220                // +----+----+----+----+----+----+----+----+----+----+....+----+
 221                // | VN | CD | DSTPORT |      DSTIP        | USERID       |NULL|
 222                // +----+----+----+----+----+----+----+----+----+----+....+----+
 223                //    1    1      2              4           variable       1
 0224                buffer[0] = ProtocolVersion4;
 0225                buffer[1] = CMD_CONNECT;
 226
 0227                BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), (ushort)port);
 228
 0229                IPAddress? ipv4Address = null;
 0230                if (IPAddress.TryParse(host, out IPAddress? hostIP))
 0231                {
 0232                    if (hostIP.AddressFamily == AddressFamily.InterNetwork)
 0233                    {
 0234                        ipv4Address = hostIP;
 0235                    }
 0236                    else if (hostIP.IsIPv4MappedToIPv6)
 0237                    {
 0238                        ipv4Address = hostIP.MapToIPv4();
 0239                    }
 240                    else
 0241                    {
 0242                        throw new SocksException(SR.net_socks_ipv6_notsupported);
 243                    }
 0244                }
 0245                else if (!isVersion4a)
 0246                {
 247                    // Socks4 does not support domain names - try to resolve it here
 248                    IPAddress[] addresses;
 249                    try
 0250                    {
 0251                        addresses = async
 0252                            ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).Confi
 0253                            : Dns.GetHostAddresses(host, AddressFamily.InterNetwork);
 0254                    }
 0255                    catch (Exception ex)
 0256                    {
 0257                        throw new SocksException(SR.net_socks_no_ipv4_address, ex);
 258                    }
 259
 0260                    if (addresses.Length == 0)
 0261                    {
 0262                        throw new SocksException(SR.net_socks_no_ipv4_address);
 263                    }
 264
 0265                    ipv4Address = addresses[0];
 0266                }
 267
 0268                if (ipv4Address is null)
 0269                {
 0270                    Debug.Assert(isVersion4a);
 0271                    buffer[4] = 0;
 0272                    buffer[5] = 0;
 0273                    buffer[6] = 0;
 0274                    buffer[7] = 255;
 0275                }
 276                else
 0277                {
 0278                    ipv4Address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0279                    Debug.Assert(bytesWritten == 4);
 0280                }
 281
 0282                byte usernameLength = EncodeString(credentials?.UserName, buffer.AsSpan(8), nameof(credentials.UserName)
 0283                buffer[8 + usernameLength] = 0;
 0284                int totalLength = 9 + usernameLength;
 285
 0286                if (ipv4Address is null)
 0287                {
 288                    // https://www.openssh.com/txt/socks4a.protocol
 0289                    byte hostLength = EncodeString(host, buffer.AsSpan(totalLength), nameof(host));
 0290                    buffer[totalLength + hostLength] = 0;
 0291                    totalLength += hostLength + 1;
 0292                }
 293
 0294                await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false);
 295
 296                // +----+----+----+----+----+----+----+----+
 297                // | VN | CD | DSTPORT |      DSTIP        |
 298                // +----+----+----+----+----+----+----+----+
 299                //    1    1      2              4
 0300                await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false);
 301
 0302                switch (buffer[1])
 303                {
 304                    case Socks4_Success:
 305                        // Nothing to do
 0306                        break;
 307                    case Socks4_AuthFailed:
 0308                        throw new SocksException(SR.net_socks_auth_failed);
 309                    default:
 0310                        throw new SocksException(SR.Format(SR.net_socks_connection_failed, buffer[1].ToString("X2")));
 311                }
 312                // response address not used
 0313            }
 314            finally
 0315            {
 0316                ArrayPool<byte>.Shared.Return(buffer);
 0317            }
 0318        }
 319
 320        private static byte EncodeString(ReadOnlySpan<char> chars, Span<byte> buffer, string parameterName)
 0321        {
 322            try
 0323            {
 0324                return checked((byte)Encoding.UTF8.GetBytes(chars, buffer));
 325            }
 0326            catch
 0327            {
 0328                Debug.Assert(Encoding.UTF8.GetByteCount(chars) > 255);
 0329                throw new SocksException(SR.Format(SR.net_socks_string_too_long, parameterName));
 330            }
 0331        }
 332
 333        private static void VerifyProtocolVersion(byte expected, byte version)
 0334        {
 0335            if (expected != version)
 0336            {
 0337                throw new SocksException(SR.Format(SR.net_socks_unexpected_version, expected, version));
 338            }
 0339        }
 340
 341        private static ValueTask WriteAsync(Stream stream, Memory<byte> buffer, bool async)
 0342        {
 0343            if (async)
 0344            {
 0345                return stream.WriteAsync(buffer);
 346            }
 347            else
 0348            {
 0349                stream.Write(buffer.Span);
 0350                return default;
 351            }
 0352        }
 353
 354        private static async ValueTask ReadToFillAsync(Stream stream, Memory<byte> buffer, bool async)
 0355        {
 0356            int bytesRead = async
 0357                ? await stream.ReadAtLeastAsync(buffer, buffer.Length, throwOnEndOfStream: false).ConfigureAwait(false)
 0358                : stream.ReadAtLeast(buffer.Span, buffer.Length, throwOnEndOfStream: false);
 359
 0360            if (bytesRead < buffer.Length)
 0361            {
 0362                throw new IOException(SR.net_http_invalid_response_premature_eof);
 363            }
 0364        }
 365    }
 366}