123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- #if !UNITY_WEBGL || UNITY_EDITOR
- using System;
- using System.Text;
- using System.Threading;
- using Best.HTTP.Shared;
- using Best.HTTP.Shared.Extensions;
- using Best.HTTP.Shared.PlatformSupport.Memory;
- using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
- using Best.HTTP.Shared.Streams;
- namespace Best.HTTP.Proxies.Implementations
- {
- internal enum SOCKSVersions : byte
- {
- Unknown = 0x00,
- V5 = 0x05
- }
- /// <summary>
- /// https://tools.ietf.org/html/rfc1928
- /// The values currently defined for METHOD are:
- /// o X'00' NO AUTHENTICATION REQUIRED
- /// o X'01' GSSAPI
- /// o X'02' USERNAME/PASSWORD
- /// o X'03' to X'7F' IANA ASSIGNED
- /// o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
- /// o X'FF' NO ACCEPTABLE METHODS
- /// </summary>
- internal enum SOCKSMethods : byte
- {
- NoAuthenticationRequired = 0x00,
- GSSAPI = 0x01,
- UsernameAndPassword = 0x02,
- NoAcceptableMethods = 0xFF
- }
- internal enum SOCKSReplies : byte
- {
- Succeeded = 0x00,
- GeneralSOCKSServerFailure = 0x01,
- ConnectionNotAllowedByRuleset = 0x02,
- NetworkUnreachable = 0x03,
- HostUnreachable = 0x04,
- ConnectionRefused = 0x05,
- TTLExpired = 0x06,
- CommandNotSupported = 0x07,
- AddressTypeNotSupported = 0x08
- }
- internal enum SOCKSAddressTypes
- {
- IPV4 = 0x00,
- DomainName = 0x03,
- IPv6 = 0x04
- }
- internal sealed class SOCKSV5Negotiator : IContentConsumer
- {
- public PeekableContentProviderStream ContentProvider { get; private set; }
- enum NegotiationStates
- {
- MethodSelection,
- ExpectAuthenticationResponse,
- ConnectResponse
- }
- NegotiationStates _state;
- SOCKSProxy _proxy;
- ProxyConnectParameters _parameters;
- public SOCKSV5Negotiator(SOCKSProxy proxy, ProxyConnectParameters parameters)
- {
- this._proxy = proxy;
- this._parameters = parameters;
- //(this._parameters.stream as IPeekableContentProvider).Consumer = this;
- (this._parameters.stream as PeekableContentProviderStream).SetTwoWayBinding(this);
- SendHandshake();
- }
- public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
- public void UnsetBinding() => this.ContentProvider = null;
- public void OnConnectionClosed()
- {
- CallOnError(new Exception($"{nameof(SOCKSV5Negotiator)}: connection closed unexpectedly!"));
- }
- public void OnError(Exception ex)
- {
- CallOnError(ex);
- }
- void SendHandshake()
- {
- var buffer = BufferPool.Get(1024, true);
- try
- {
- int count = 0;
- // https://tools.ietf.org/html/rfc1928
- // The client connects to the server, and sends a version
- // identifier/method selection message:
- //
- // +----+----------+----------+
- // |VER | NMETHODS | METHODS |
- // +----+----------+----------+
- // | 1 | 1 | 1 to 255 |
- // +----+----------+----------+
- //
- // The VER field is set to X'05' for this version of the protocol. The
- // NMETHODS field contains the number of method identifier octets that
- // appear in the METHODS field.
- //
- buffer[count++] = (byte)SOCKSVersions.V5;
- if (this._proxy.Credentials != null)
- {
- buffer[count++] = 0x02; // method count
- buffer[count++] = (byte)SOCKSMethods.UsernameAndPassword;
- buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
- }
- else
- {
- buffer[count++] = 0x01; // method count
- buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
- }
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Sending method negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
- // enqueue buffer and move its ownership to the tcp streamer
- this._parameters.stream.Write(buffer.AsBuffer(count));
- // null out the buffer so it won't be released
- buffer = null;
- }
- catch (Exception ex)
- {
- CallOnError(ex);
- }
- finally
- {
- BufferPool.Release(buffer);
- }
- }
- void SendConnect()
- {
- // The SOCKS request is formed as follows:
- //
- // +----+-----+-------+------+----------+----------+
- // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
- // +----+-----+-------+------+----------+----------+
- // | 1 | 1 | X'00' | 1 | Variable | 2 |
- // +----+-----+-------+------+----------+----------+
- //
- // Where:
- //
- // o VER protocol version: X'05'
- // o CMD
- // o CONNECT X'01'
- // o BIND X'02'
- // o UDP ASSOCIATE X'03'
- // o RSV RESERVED
- // o ATYP address type of following address
- // o IP V4 address: X'01'
- // o DOMAINNAME: X'03'
- // o IP V6 address: X'04'
- // o DST.ADDR desired destination address
- // o DST.PORT desired destination port in network octet
- // order
- var buffer = BufferPool.Get(512, true);
- int count = 0;
- buffer[count++] = (byte)SOCKSVersions.V5; // version: 5
- buffer[count++] = 0x01; // command: connect
- buffer[count++] = 0x00; // reserved, bust be 0x00
- if (this._parameters.uri.IsHostIsAnIPAddress())
- {
- bool isIPV4 = Extensions.IsIpV4AddressValid(this._parameters.uri.Host);
- buffer[count++] = isIPV4 ? (byte)SOCKSAddressTypes.IPV4 : (byte)SOCKSAddressTypes.IPv6;
- var ipAddress = System.Net.IPAddress.Parse(this._parameters.uri.Host);
- var ipBytes = ipAddress.GetAddressBytes();
- WriteBytes(buffer, ref count, ipBytes); // destination address
- }
- else
- {
- buffer[count++] = (byte)SOCKSAddressTypes.DomainName;
- // The first octet of the address field contains the number of octets of name that
- // follow, there is no terminating NUL octet.
- WriteString(buffer, ref count, this._parameters.uri.Host);
- }
- // destination port in network octet order
- buffer[count++] = (byte)((this._parameters.uri.Port >> 8) & 0xFF);
- buffer[count++] = (byte)(this._parameters.uri.Port & 0xFF);
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Sending connect request - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
- this._parameters.stream.Write(buffer.AsBuffer(count));
- this._state = NegotiationStates.ConnectResponse;
- }
- public void OnContent()
- {
- try
- {
- switch (this._state)
- {
- case NegotiationStates.MethodSelection:
- {
- if (this.ContentProvider.Length < 2)
- return;
- // Read method selection result
- //count = stream.Read(buffer, 0, buffer.Length);
- var buffer = BufferPool.Get(BufferPool.MinBufferSize, true);
- int count = this.ContentProvider.Read(buffer, 0, buffer.Length);
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Negotiation response - count: {count} buffer: {buffer.AsBuffer(count)}", this._parameters.context);
- // The server selects from one of the methods given in METHODS, and
- // sends a METHOD selection message:
- //
- // +----+--------+
- // |VER | METHOD |
- // +----+--------+
- // | 1 | 1 |
- // +----+--------+
- //
- // If the selected METHOD is X'FF', none of the methods listed by the
- // client are acceptable, and the client MUST close the connection.
- //
- // The values currently defined for METHOD are:
- //
- // o X'00' NO AUTHENTICATION REQUIRED
- // o X'01' GSSAPI
- // o X'02' USERNAME/PASSWORD
- // o X'03' to X'7F' IANA ASSIGNED
- // o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
- // o X'FF' NO ACCEPTABLE METHODS
- //
- // The client and server then enter a method-specific sub-negotiation.
- SOCKSVersions version = (SOCKSVersions)buffer[0];
- SOCKSMethods method = (SOCKSMethods)buffer[1];
- // Expected result:
- // 1.) Received bytes' count is 2: version + preferred method
- // 2.) Version must be 5
- // 3.) Preferred method must NOT be 0xFF
- if (count != 2)
- throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
- else if (version != SOCKSVersions.V5)
- throw new Exception("SOCKS Proxy - Expected version: 5, received version: " + buffer[0].ToString("X2"));
- else if (method == SOCKSMethods.NoAcceptableMethods)
- throw new Exception("SOCKS Proxy - Received 'NO ACCEPTABLE METHODS' (0xFF)");
- else
- {
- HTTPManager.Logger.Information("SOCKSProxy", "Method negotiation over. Method: " + method.ToString(), this._parameters.context);
- switch (method)
- {
- case SOCKSMethods.NoAuthenticationRequired:
- SendConnect();
- break;
- case SOCKSMethods.UsernameAndPassword:
- if (this._proxy.Credentials.UserName.Length > 255)
- throw new Exception($"SOCKS Proxy - Credentials.UserName too long! {this._proxy.Credentials.UserName.Length} > 255");
- if (this._proxy.Credentials.Password.Length > 255)
- throw new Exception($"SOCKS Proxy - Credentials.Password too long! {this._proxy.Credentials.Password.Length} > 255");
- // https://tools.ietf.org/html/rfc1929 : Username/Password Authentication for SOCKS V5
- // Once the SOCKS V5 server has started, and the client has selected the
- // Username/Password Authentication protocol, the Username/Password
- // subnegotiation begins. This begins with the client producing a
- // Username/Password request:
- //
- // +----+------+----------+------+----------+
- // |VER | ULEN | UNAME | PLEN | PASSWD |
- // +----+------+----------+------+----------+
- // | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
- // +----+------+----------+------+----------+
- HTTPManager.Logger.Information("SOCKSProxy", "starting sub-negotiation", this._parameters.context);
- count = 0;
- buffer[count++] = 0x01; // version of sub negotiation
- WriteString(buffer, ref count, this._proxy.Credentials.UserName);
- WriteString(buffer, ref count, this._proxy.Credentials.Password);
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Sending username and password sub-negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
- // Write negotiation and transfer ownership of buffer
- this._parameters.stream.Write(buffer.AsBuffer(count));
- this._state = NegotiationStates.ExpectAuthenticationResponse;
- break;
- case SOCKSMethods.GSSAPI:
- throw new Exception("SOCKS proxy: GSSAPI not supported!");
- case SOCKSMethods.NoAcceptableMethods:
- throw new Exception("SOCKS proxy: No acceptable method");
- }
- }
- break;
- }
- case NegotiationStates.ExpectAuthenticationResponse:
- {
- if (this.ContentProvider.Length < 2)
- return;
- // Read result
- var buffer = BufferPool.Get(512, true);
- var count = this._parameters.stream.Read(buffer, 0, buffer.Length);
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Username and password sub-negotiation response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
- // The server verifies the supplied UNAME and PASSWD, and sends the
- // following response:
- //
- // +----+--------+
- // |VER | STATUS |
- // +----+--------+
- // | 1 | 1 |
- // +----+--------+
- // A STATUS field of X'00' indicates success. If the server returns a
- // `failure' (STATUS value other than X'00') status, it MUST close the
- // connection.
- bool success = buffer[1] == 0;
- if (count != 2)
- throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
- else if (!success)
- throw new Exception("SOCKS proxy: username+password authentication failed!");
- HTTPManager.Logger.Information("SOCKSProxy", "Authenticated!", this._parameters.context);
- // Send connect
- SendConnect();
- break;
- }
- case NegotiationStates.ConnectResponse:
- {
- if (this.ContentProvider.Length < 10)
- return;
- var buffer = BufferPool.Get(512, true);
- var count = this._parameters.stream.Read(buffer, 0, buffer.Length);
- if (HTTPManager.Logger.IsDiagnostic)
- HTTPManager.Logger.Information("SOCKSProxy", $"Connect response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
- // The SOCKS request information is sent by the client as soon as it has
- // established a connection to the SOCKS server, and completed the
- // authentication negotiations. The server evaluates the request, and
- // returns a reply formed as follows:
- //
- // +----+-----+-------+------+----------+----------+
- // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
- // +----+-----+-------+------+----------+----------+
- // | 1 | 1 | X'00' | 1 | Variable | 2 |
- // +----+-----+-------+------+----------+----------+
- //
- // Where:
- // o VER protocol version: X'05'
- // o REP Reply field:
- // o X'00' succeeded
- // o X'01' general SOCKS server failure
- // o X'02' connection not allowed by ruleset
- // o X'03' Network unreachable
- // o X'04' Host unreachable
- // o X'05' Connection refused
- // o X'06' TTL expired
- // o X'07' Command not supported
- // o X'08' Address type not supported
- // o X'09' to X'FF' unassigned
- // o RSV RESERVED
- // o ATYP address type of following address
- // o IP V4 address: X'01'
- // o DOMAINNAME: X'03'
- // o IP V6 address: X'04'
- // o BND.ADDR server bound address
- // o BND.PORT server bound port in network octet order
- //
- // Fields marked RESERVED (RSV) must be set to X'00'.
- SOCKSVersions version = (SOCKSVersions)buffer[0];
- SOCKSReplies reply = (SOCKSReplies)buffer[1];
- // at least 10 bytes expected as a result
- if (count < 10)
- throw new Exception($"SOCKS proxy: not enough data returned by the server. Expected count is at least 10 bytes, server returned {count} bytes! content: {buffer.AsBuffer(count)}");
- else if (reply != SOCKSReplies.Succeeded)
- throw new Exception("SOCKS proxy error: " + reply.ToString());
- HTTPManager.Logger.Information("SOCKSProxy", "Connected!", this._parameters.context);
- CallOnSuccess();
- break;
- }
- }
- }
- catch(Exception ex)
- {
- CallOnError(ex);
- }
- }
- void CallOnError(Exception ex)
- {
- var callback = Interlocked.Exchange(ref this._parameters.OnError, null);
- Interlocked.Exchange(ref this._parameters.OnSuccess, null);
- this.ContentProvider.Unbind();
- callback?.Invoke(this._parameters, ex, false);
- }
- void CallOnSuccess()
- {
- var callback = Interlocked.Exchange(ref this._parameters.OnSuccess, null);
- Interlocked.Exchange(ref this._parameters.OnError, null);
- this.ContentProvider.Unbind();
- callback?.Invoke(this._parameters);
- }
- private void WriteString(byte[] buffer, ref int count, string str)
- {
- // Get the bytes
- int byteCount = Encoding.UTF8.GetByteCount(str);
- if (byteCount > 255)
- throw new Exception(string.Format("SOCKS Proxy - String is too large ({0}) to fit in 255 bytes!", byteCount.ToString()));
- // number of bytes
- buffer[count++] = (byte)byteCount;
- // and the bytes itself
- Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, count);
- count += byteCount;
- }
- private void WriteBytes(byte[] buffer, ref int count, byte[] bytes)
- {
- Array.Copy(bytes, 0, buffer, count, bytes.Length);
- count += bytes.Length;
- }
- }
- }
- #endif
|