You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
370 lines
18 KiB
370 lines
18 KiB
#if !BESTHTTP_DISABLE_PROXY |
|
using System; |
|
using System.IO; |
|
using System.Text; |
|
using BestHTTP.Authentication; |
|
using BestHTTP.Extensions; |
|
using BestHTTP.PlatformSupport.Memory; |
|
|
|
namespace BestHTTP |
|
{ |
|
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 |
|
} |
|
|
|
public sealed class SOCKSProxy : Proxy |
|
{ |
|
public SOCKSProxy(Uri address, Credentials credentials) |
|
: base(address, credentials) |
|
{ } |
|
|
|
internal override string GetRequestPath(Uri uri) |
|
{ |
|
return uri.GetRequestPathAndQueryURL(); |
|
} |
|
|
|
internal override bool SetupRequest(HTTPRequest request) |
|
{ |
|
return false; |
|
} |
|
|
|
internal override void Connect(Stream stream, HTTPRequest request) |
|
{ |
|
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.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.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Sending method negotiation - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.Context); |
|
|
|
// Write negotiation |
|
stream.Write(buffer, 0, count); |
|
// Read result |
|
count = stream.Read(buffer, 0, buffer.Length); |
|
|
|
if (HTTPManager.Logger.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Negotiation response - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.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(string.Format("SOCKS Proxy - Expected read count: 2! count: {0} buffer: {1}" + count.ToString(), BufferToHexStr(buffer, 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(), request.Context); |
|
switch (method) |
|
{ |
|
case SOCKSMethods.NoAuthenticationRequired: |
|
// nothing to do |
|
break; |
|
|
|
case SOCKSMethods.UsernameAndPassword: |
|
if (this.Credentials.UserName.Length > 255) |
|
throw new Exception(string.Format("SOCKS Proxy - Credentials.UserName too long! {0} > 255", this.Credentials.UserName.Length.ToString())); |
|
if (this.Credentials.Password.Length > 255) |
|
throw new Exception(string.Format("SOCKS Proxy - Credentials.Password too long! {0} > 255", this.Credentials.Password.Length.ToString())); |
|
|
|
// 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", request.Context); |
|
count = 0; |
|
buffer[count++] = 0x01; // version of sub negotiation |
|
|
|
WriteString(buffer, ref count, this.Credentials.UserName); |
|
WriteString(buffer, ref count, this.Credentials.Password); |
|
|
|
if (HTTPManager.Logger.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Sending username and password sub-negotiation - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.Context); |
|
|
|
// Write negotiation |
|
stream.Write(buffer, 0, count); |
|
// Read result |
|
count = stream.Read(buffer, 0, buffer.Length); |
|
|
|
if (HTTPManager.Logger.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Username and password sub-negotiation response - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.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(string.Format("SOCKS Proxy - Expected read count: 2! count: {0} buffer: {1}" + count.ToString(), BufferToHexStr(buffer, count))); |
|
else if (!success) |
|
throw new Exception("SOCKS proxy: username+password authentication failed!"); |
|
|
|
HTTPManager.Logger.Information("SOCKSProxy", "Authenticated!", request.Context); |
|
break; |
|
|
|
case SOCKSMethods.GSSAPI: |
|
throw new Exception("SOCKS proxy: GSSAPI not supported!"); |
|
|
|
case SOCKSMethods.NoAcceptableMethods: |
|
throw new Exception("SOCKS proxy: No acceptable method"); |
|
} |
|
} |
|
|
|
// 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 |
|
|
|
count = 0; |
|
buffer[count++] = (byte)SOCKSVersions.V5; // version: 5 |
|
buffer[count++] = 0x01; // command: connect |
|
buffer[count++] = 0x00; // reserved, bust be 0x00 |
|
|
|
if (request.CurrentUri.IsHostIsAnIPAddress()) |
|
{ |
|
bool isIPV4 = Extensions.Extensions.IsIpV4AddressValid(request.CurrentUri.Host); |
|
buffer[count++] = isIPV4 ? (byte)SOCKSAddressTypes.IPV4 : (byte)SOCKSAddressTypes.IPv6; |
|
|
|
var ipAddress = System.Net.IPAddress.Parse(request.CurrentUri.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, request.CurrentUri.Host); |
|
} |
|
|
|
// destination port in network octet order |
|
buffer[count++] = (byte)((request.CurrentUri.Port >> 8) & 0xFF); |
|
buffer[count++] = (byte)(request.CurrentUri.Port & 0xFF); |
|
|
|
if (HTTPManager.Logger.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Sending connect request - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.Context); |
|
|
|
stream.Write(buffer, 0, count); |
|
count = stream.Read(buffer, 0, buffer.Length); |
|
|
|
if (HTTPManager.Logger.Level == Logger.Loglevels.All) |
|
HTTPManager.Logger.Information("SOCKSProxy", string.Format("Connect response - count: {0} buffer: {1} ", count.ToString(), BufferToHexStr(buffer, count)), request.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'. |
|
|
|
version = (SOCKSVersions)buffer[0]; |
|
SOCKSReplies reply = (SOCKSReplies)buffer[1]; |
|
|
|
// at least 10 bytes expected as a result |
|
if (count < 10) |
|
throw new Exception(string.Format("SOCKS proxy: not enough data returned by the server. Expected count is at least 10 bytes, server returned {0} bytes! content: {1}", count.ToString(), BufferToHexStr(buffer, count))); |
|
else if (reply != SOCKSReplies.Succeeded) |
|
throw new Exception("SOCKS proxy error: " + reply.ToString()); |
|
|
|
HTTPManager.Logger.Information("SOCKSProxy", "Connected!", request.Context); |
|
} |
|
finally |
|
{ |
|
BufferPool.Release(buffer); |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
private string BufferToHexStr(byte[] buffer, int count) |
|
{ |
|
StringBuilder sb = new StringBuilder(count * 2); |
|
for (int i = 0; i < count; ++i) |
|
sb.AppendFormat("0x{0} ", buffer[i].ToString("X2")); |
|
return sb.ToString(); |
|
} |
|
} |
|
} |
|
#endif
|
|
|