< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 204
Coverable lines: 204
Total lines: 368
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 63
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%10100%
EstablishSocks5TunnelAsync(...)0%23230%
EstablishSocks4TunnelAsync(...)0%22220%
EncodeString(...)100%110%
VerifyProtocolVersion(...)0%220%
WriteAsync(...)0%220%
ReadToFillAsync(...)0%440%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/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                        string.Equals(proxyUri.Scheme, "socks5h", StringComparison.OrdinalIgnoreCase))
 042                    {
 043                        await EstablishSocks5TunnelAsync(stream, host, port, credentials, async).ConfigureAwait(false);
 044                    }
 045                    else if (string.Equals(proxyUri.Scheme, "socks4a", StringComparison.OrdinalIgnoreCase))
 046                    {
 047                        await EstablishSocks4TunnelAsync(stream, isVersion4a: true, host, port, credentials, async, canc
 048                    }
 049                    else if (string.Equals(proxyUri.Scheme, "socks4", StringComparison.OrdinalIgnoreCase))
 050                    {
 051                        await EstablishSocks4TunnelAsync(stream, isVersion4a: false, host, port, credentials, async, can
 052                    }
 53                    else
 054                    {
 055                        Debug.Fail("Bad socks version.");
 56                    }
 057                }
 058                catch
 059                {
 060                    stream.Dispose();
 061                    throw;
 62                }
 063            }
 064        }
 65
 66        private static async ValueTask EstablishSocks5TunnelAsync(Stream stream, string host, int port, NetworkCredentia
 067        {
 068            byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 69            try
 070            {
 71                // https://tools.ietf.org/html/rfc1928
 72
 73                // +----+----------+----------+
 74                // |VER | NMETHODS | METHODS  |
 75                // +----+----------+----------+
 76                // | 1  |    1     | 1 to 255 |
 77                // +----+----------+----------+
 078                buffer[0] = ProtocolVersion5;
 079                if (credentials is null)
 080                {
 081                    buffer[1] = 1;
 082                    buffer[2] = METHOD_NO_AUTH;
 083                }
 84                else
 085                {
 086                    buffer[1] = 2;
 087                    buffer[2] = METHOD_NO_AUTH;
 088                    buffer[3] = METHOD_USERNAME_PASSWORD;
 089                }
 090                await WriteAsync(stream, buffer.AsMemory(0, buffer[1] + 2), async).ConfigureAwait(false);
 91
 92                // +----+--------+
 93                // |VER | METHOD |
 94                // +----+--------+
 95                // | 1  |   1    |
 96                // +----+--------+
 097                await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
 098                VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
 99
 0100                switch (buffer[1])
 101                {
 102                    case METHOD_NO_AUTH:
 103                        // continue
 0104                        break;
 105
 106                    case METHOD_USERNAME_PASSWORD:
 0107                        {
 108                            // https://tools.ietf.org/html/rfc1929
 0109                            if (credentials is null)
 0110                            {
 111                                // If the server is behaving well, it shouldn't pick username and password auth
 112                                // because we don't claim to support it when we don't have credentials.
 113                                // Just being defensive here.
 0114                                throw new SocksException(SR.net_socks_auth_required);
 115                            }
 116
 117                            // +----+------+----------+------+----------+
 118                            // |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
 119                            // +----+------+----------+------+----------+
 120                            // | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
 121                            // +----+------+----------+------+----------+
 0122                            buffer[0] = SubnegotiationVersion;
 0123                            byte usernameLength = EncodeString(credentials.UserName, buffer.AsSpan(2), nameof(credential
 0124                            buffer[1] = usernameLength;
 0125                            byte passwordLength = EncodeString(credentials.Password, buffer.AsSpan(3 + usernameLength), 
 0126                            buffer[2 + usernameLength] = passwordLength;
 0127                            await WriteAsync(stream, buffer.AsMemory(0, 3 + usernameLength + passwordLength), async).Con
 128
 129                            // +----+--------+
 130                            // |VER | STATUS |
 131                            // +----+--------+
 132                            // | 1  |   1    |
 133                            // +----+--------+
 0134                            await ReadToFillAsync(stream, buffer.AsMemory(0, 2), async).ConfigureAwait(false);
 0135                            if (buffer[0] != SubnegotiationVersion || buffer[1] != Socks5_Success)
 0136                            {
 0137                                throw new SocksException(SR.net_socks_auth_failed);
 138                            }
 0139                            break;
 140                        }
 141
 142                    default:
 0143                        throw new SocksException(SR.net_socks_no_auth_method);
 144                }
 145
 146
 147                // +----+-----+-------+------+----------+----------+
 148                // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
 149                // +----+-----+-------+------+----------+----------+
 150                // | 1  |  1  | X'00' |  1   | Variable |    2     |
 151                // +----+-----+-------+------+----------+----------+
 0152                buffer[0] = ProtocolVersion5;
 0153                buffer[1] = CMD_CONNECT;
 0154                buffer[2] = 0;
 155                int addressLength;
 156
 0157                if (IPAddress.TryParse(host, out IPAddress? hostIP))
 0158                {
 0159                    if (hostIP.AddressFamily == AddressFamily.InterNetwork)
 0160                    {
 0161                        buffer[3] = ATYP_IPV4;
 0162                        hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0163                        Debug.Assert(bytesWritten == 4);
 0164                        addressLength = 4;
 0165                    }
 166                    else
 0167                    {
 0168                        Debug.Assert(hostIP.AddressFamily == AddressFamily.InterNetworkV6);
 0169                        buffer[3] = ATYP_IPV6;
 0170                        hostIP.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0171                        Debug.Assert(bytesWritten == 16);
 0172                        addressLength = 16;
 0173                    }
 0174                }
 175                else
 0176                {
 0177                    buffer[3] = ATYP_DOMAIN_NAME;
 0178                    byte hostLength = EncodeString(host, buffer.AsSpan(5), nameof(host));
 0179                    buffer[4] = hostLength;
 0180                    addressLength = hostLength + 1;
 0181                }
 182
 0183                BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(addressLength + 4), (ushort)port);
 184
 0185                await WriteAsync(stream, buffer.AsMemory(0, addressLength + 6), async).ConfigureAwait(false);
 186
 187                // +----+-----+-------+------+----------+----------+
 188                // |VER | REP |  RSV  | ATYP | DST.ADDR | DST.PORT |
 189                // +----+-----+-------+------+----------+----------+
 190                // | 1  |  1  | X'00' |  1   | Variable |    2     |
 191                // +----+-----+-------+------+----------+----------+
 0192                await ReadToFillAsync(stream, buffer.AsMemory(0, 5), async).ConfigureAwait(false);
 0193                VerifyProtocolVersion(ProtocolVersion5, buffer[0]);
 0194                if (buffer[1] != Socks5_Success)
 0195                {
 0196                    throw new SocksException(SR.Format(SR.net_socks_connection_failed, buffer[1].ToString("X2")));
 197                }
 0198                int bytesToSkip = buffer[3] switch
 0199                {
 0200                    ATYP_IPV4 => 5,
 0201                    ATYP_IPV6 => 17,
 0202                    ATYP_DOMAIN_NAME => buffer[4] + 2,
 0203                    _ => throw new SocksException(SR.net_socks_bad_address_type)
 0204                };
 0205                await ReadToFillAsync(stream, buffer.AsMemory(0, bytesToSkip), async).ConfigureAwait(false);
 206                // response address not used
 0207            }
 208            finally
 0209            {
 0210                ArrayPool<byte>.Shared.Return(buffer);
 0211            }
 0212        }
 213
 214        private static async ValueTask EstablishSocks4TunnelAsync(Stream stream, bool isVersion4a, string host, int port
 0215        {
 0216            byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
 217            try
 0218            {
 219                // https://www.openssh.com/txt/socks4.protocol
 220
 221                // +----+----+----+----+----+----+----+----+----+----+....+----+
 222                // | VN | CD | DSTPORT |      DSTIP        | USERID       |NULL|
 223                // +----+----+----+----+----+----+----+----+----+----+....+----+
 224                //    1    1      2              4           variable       1
 0225                buffer[0] = ProtocolVersion4;
 0226                buffer[1] = CMD_CONNECT;
 227
 0228                BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2), (ushort)port);
 229
 0230                IPAddress? ipv4Address = null;
 0231                if (IPAddress.TryParse(host, out IPAddress? hostIP))
 0232                {
 0233                    if (hostIP.AddressFamily == AddressFamily.InterNetwork)
 0234                    {
 0235                        ipv4Address = hostIP;
 0236                    }
 0237                    else if (hostIP.IsIPv4MappedToIPv6)
 0238                    {
 0239                        ipv4Address = hostIP.MapToIPv4();
 0240                    }
 241                    else
 0242                    {
 0243                        throw new SocksException(SR.net_socks_ipv6_notsupported);
 244                    }
 0245                }
 0246                else if (!isVersion4a)
 0247                {
 248                    // Socks4 does not support domain names - try to resolve it here
 249                    IPAddress[] addresses;
 250                    try
 0251                    {
 0252                        addresses = async
 0253                            ? await Dns.GetHostAddressesAsync(host, AddressFamily.InterNetwork, cancellationToken).Confi
 0254                            : Dns.GetHostAddresses(host, AddressFamily.InterNetwork);
 0255                    }
 0256                    catch (Exception ex)
 0257                    {
 0258                        throw new SocksException(SR.net_socks_no_ipv4_address, ex);
 259                    }
 260
 0261                    if (addresses.Length == 0)
 0262                    {
 0263                        throw new SocksException(SR.net_socks_no_ipv4_address);
 264                    }
 265
 0266                    ipv4Address = addresses[0];
 0267                }
 268
 0269                if (ipv4Address is null)
 0270                {
 0271                    Debug.Assert(isVersion4a);
 0272                    buffer[4] = 0;
 0273                    buffer[5] = 0;
 0274                    buffer[6] = 0;
 0275                    buffer[7] = 255;
 0276                }
 277                else
 0278                {
 0279                    ipv4Address.TryWriteBytes(buffer.AsSpan(4), out int bytesWritten);
 0280                    Debug.Assert(bytesWritten == 4);
 0281                }
 282
 0283                byte usernameLength = EncodeString(credentials?.UserName, buffer.AsSpan(8), nameof(credentials.UserName)
 0284                buffer[8 + usernameLength] = 0;
 0285                int totalLength = 9 + usernameLength;
 286
 0287                if (ipv4Address is null)
 0288                {
 289                    // https://www.openssh.com/txt/socks4a.protocol
 0290                    byte hostLength = EncodeString(host, buffer.AsSpan(totalLength), nameof(host));
 0291                    buffer[totalLength + hostLength] = 0;
 0292                    totalLength += hostLength + 1;
 0293                }
 294
 0295                await WriteAsync(stream, buffer.AsMemory(0, totalLength), async).ConfigureAwait(false);
 296
 297                // +----+----+----+----+----+----+----+----+
 298                // | VN | CD | DSTPORT |      DSTIP        |
 299                // +----+----+----+----+----+----+----+----+
 300                //    1    1      2              4
 0301                await ReadToFillAsync(stream, buffer.AsMemory(0, 8), async).ConfigureAwait(false);
 302
 0303                switch (buffer[1])
 304                {
 305                    case Socks4_Success:
 306                        // Nothing to do
 0307                        break;
 308                    case Socks4_AuthFailed:
 0309                        throw new SocksException(SR.net_socks_auth_failed);
 310                    default:
 0311                        throw new SocksException(SR.Format(SR.net_socks_connection_failed, buffer[1].ToString("X2")));
 312                }
 313                // response address not used
 0314            }
 315            finally
 0316            {
 0317                ArrayPool<byte>.Shared.Return(buffer);
 0318            }
 0319        }
 320
 321        private static byte EncodeString(ReadOnlySpan<char> chars, Span<byte> buffer, string parameterName)
 0322        {
 323            try
 0324            {
 0325                return checked((byte)Encoding.UTF8.GetBytes(chars, buffer));
 326            }
 0327            catch
 0328            {
 0329                Debug.Assert(Encoding.UTF8.GetByteCount(chars) > 255);
 0330                throw new SocksException(SR.Format(SR.net_socks_string_too_long, parameterName));
 331            }
 0332        }
 333
 334        private static void VerifyProtocolVersion(byte expected, byte version)
 0335        {
 0336            if (expected != version)
 0337            {
 0338                throw new SocksException(SR.Format(SR.net_socks_unexpected_version, expected, version));
 339            }
 0340        }
 341
 342        private static ValueTask WriteAsync(Stream stream, Memory<byte> buffer, bool async)
 0343        {
 0344            if (async)
 0345            {
 0346                return stream.WriteAsync(buffer);
 347            }
 348            else
 0349            {
 0350                stream.Write(buffer.Span);
 0351                return default;
 352            }
 0353        }
 354
 355        private static async ValueTask ReadToFillAsync(Stream stream, Memory<byte> buffer, bool async)
 0356        {
 0357            int bytesRead = async
 0358                ? await stream.ReadAtLeastAsync(buffer, buffer.Length, throwOnEndOfStream: false).ConfigureAwait(false)
 0359                : stream.ReadAtLeast(buffer.Span, buffer.Length, throwOnEndOfStream: false);
 360
 0361            if (bytesRead < buffer.Length)
 0362            {
 0363                throw new IOException(SR.net_http_invalid_response_premature_eof);
 364            }
 0365        }
 366    }
 367}
 368