/* * Authors: Benton Stark * * Copyright (c) 2007-2012 Starksoft, LLC (http://www.starksoft.com) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ using System; using System.Text; using System.Net; using System.Net.Sockets; using System.Globalization; using System.IO; using System.Threading; using System.ComponentModel; namespace Starksoft.Net.Proxy { /// /// Socks4 connection proxy class. This class implements the Socks4 standard proxy protocol. /// /// /// This class implements the Socks4 proxy protocol standard for TCP communciations. /// public class Socks4ProxyClient : IProxyClient { private const int WAIT_FOR_DATA_INTERVAL = 50; // 50 ms private const int WAIT_FOR_DATA_TIMEOUT = 15000; // 15 seconds private const string PROXY_NAME = "SOCKS4"; private TcpClient _tcpClient; private TcpClient _tcpClientCached; private string _proxyHost; private int _proxyPort; private string _proxyUserId; /// /// Default Socks4 proxy port. /// internal const int SOCKS_PROXY_DEFAULT_PORT = 1080; /// /// Socks4 version number. /// internal const byte SOCKS4_VERSION_NUMBER = 4; /// /// Socks4 connection command value. /// internal const byte SOCKS4_CMD_CONNECT = 0x01; /// /// Socks4 bind command value. /// internal const byte SOCKS4_CMD_BIND = 0x02; /// /// Socks4 reply request grant response value. /// internal const byte SOCKS4_CMD_REPLY_REQUEST_GRANTED = 90; /// /// Socks4 reply request rejected or failed response value. /// internal const byte SOCKS4_CMD_REPLY_REQUEST_REJECTED_OR_FAILED = 91; /// /// Socks4 reply request rejected becauase the proxy server can not connect to the IDENTD server value. /// internal const byte SOCKS4_CMD_REPLY_REQUEST_REJECTED_CANNOT_CONNECT_TO_IDENTD = 92; /// /// Socks4 reply request rejected because of a different IDENTD server. /// internal const byte SOCKS4_CMD_REPLY_REQUEST_REJECTED_DIFFERENT_IDENTD = 93; /// /// Create a Socks4 proxy client object. The default proxy port 1080 is used. /// public Socks4ProxyClient() { } /// /// Creates a Socks4 proxy client object using the supplied TcpClient object connection. /// /// A TcpClient connection object. public Socks4ProxyClient(TcpClient tcpClient) { if (tcpClient == null) throw new ArgumentNullException("tcpClient"); _tcpClientCached = tcpClient; } /// /// Create a Socks4 proxy client object. The default proxy port 1080 is used. /// /// Host name or IP address of the proxy server. /// Proxy user identification information. public Socks4ProxyClient(string proxyHost, string proxyUserId) { if (String.IsNullOrEmpty(proxyHost)) throw new ArgumentNullException("proxyHost"); if (proxyUserId == null) throw new ArgumentNullException("proxyUserId"); _proxyHost = proxyHost; _proxyPort = SOCKS_PROXY_DEFAULT_PORT; _proxyUserId = proxyUserId; } /// /// Create a Socks4 proxy client object. /// /// Host name or IP address of the proxy server. /// Port used to connect to proxy server. /// Proxy user identification information. public Socks4ProxyClient(string proxyHost, int proxyPort, string proxyUserId) { if (String.IsNullOrEmpty(proxyHost)) throw new ArgumentNullException("proxyHost"); if (proxyPort <= 0 || proxyPort > 65535) throw new ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535"); if (proxyUserId == null) throw new ArgumentNullException("proxyUserId"); _proxyHost = proxyHost; _proxyPort = proxyPort; _proxyUserId = proxyUserId; } /// /// Create a Socks4 proxy client object. The default proxy port 1080 is used. /// /// Host name or IP address of the proxy server. public Socks4ProxyClient(string proxyHost) { if (String.IsNullOrEmpty(proxyHost)) throw new ArgumentNullException("proxyHost"); _proxyHost = proxyHost; _proxyPort = SOCKS_PROXY_DEFAULT_PORT; } /// /// Create a Socks4 proxy client object. /// /// Host name or IP address of the proxy server. /// Port used to connect to proxy server. public Socks4ProxyClient(string proxyHost, int proxyPort) { if (String.IsNullOrEmpty(proxyHost)) throw new ArgumentNullException("proxyHost"); if (proxyPort <= 0 || proxyPort > 65535) throw new ArgumentOutOfRangeException("proxyPort", "port must be greater than zero and less than 65535"); _proxyHost = proxyHost; _proxyPort = proxyPort; } /// /// Gets or sets host name or IP address of the proxy server. /// public string ProxyHost { get { return _proxyHost; } set { _proxyHost = value; } } /// /// Gets or sets port used to connect to proxy server. /// public int ProxyPort { get { return _proxyPort; } set { _proxyPort = value; } } /// /// Gets String representing the name of the proxy. /// /// This property will always return the value 'SOCKS4' virtual public string ProxyName { get { return PROXY_NAME; } } /// /// Gets or sets proxy user identification information. /// public string ProxyUserId { get { return _proxyUserId; } set { _proxyUserId = value; } } /// /// Gets or sets the TcpClient object. /// This property can be set prior to executing CreateConnection to use an existing TcpClient connection. /// public TcpClient TcpClient { get { return _tcpClientCached; } set { _tcpClientCached = value; } } /// /// Creates a TCP connection to the destination host through the proxy server /// host. /// /// Destination host name or IP address of the destination server. /// Port number to connect to on the destination server. /// /// Returns an open TcpClient object that can be used normally to communicate /// with the destination server /// /// /// This method creates a connection to the proxy server and instructs the proxy server /// to make a pass through connection to the specified destination host on the specified /// port. /// public TcpClient CreateConnection(string destinationHost, int destinationPort) { if (String.IsNullOrEmpty(destinationHost)) throw new ArgumentNullException("destinationHost"); if (destinationPort <= 0 || destinationPort > 65535) throw new ArgumentOutOfRangeException("destinationPort", "port must be greater than zero and less than 65535"); try { // if we have no cached tcpip connection then create one if (_tcpClientCached == null) { if (String.IsNullOrEmpty(_proxyHost)) throw new ProxyException("ProxyHost property must contain a value."); if (_proxyPort <= 0 || _proxyPort > 65535) throw new ProxyException("ProxyPort value must be greater than zero and less than 65535"); // create new tcp client object to the proxy server _tcpClient = new TcpClient(); // attempt to open the connection _tcpClient.Connect(_proxyHost, _proxyPort); } else { _tcpClient = _tcpClientCached; } // send connection command to proxy host for the specified destination host and port SendCommand(_tcpClient.GetStream(), SOCKS4_CMD_CONNECT, destinationHost, destinationPort, _proxyUserId); // remove the private reference to the tcp client so the proxy object does not keep it // return the open proxied tcp client object to the caller for normal use TcpClient rtn = _tcpClient; _tcpClient = null; return rtn; } catch (Exception ex) { throw new ProxyException(String.Format(CultureInfo.InvariantCulture, "Connection to proxy host {0} on port {1} failed.", Utils.GetHost(_tcpClient), Utils.GetPort(_tcpClient)), ex); } } /// /// Sends a command to the proxy server. /// /// Proxy server data stream. /// Proxy byte command to execute. /// Destination host name or IP address. /// Destination port number /// IDENTD user ID value. internal virtual void SendCommand(NetworkStream proxy, byte command, string destinationHost, int destinationPort, string userId) { // PROXY SERVER REQUEST // The client connects to the SOCKS server and sends a CONNECT request when // it wants to establish a connection to an application server. The client // includes in the request packet the IP address and the port number of the // destination host, and userid, in the following format. // // +----+----+----+----+----+----+----+----+----+----+....+----+ // | VN | CD | DSTPORT | DSTIP | USERID |NULL| // +----+----+----+----+----+----+----+----+----+----+....+----+ // # of bytes: 1 1 2 4 variable 1 // // VN is the SOCKS protocol version number and should be 4. CD is the // SOCKS command code and should be 1 for CONNECT request. NULL is a byte // of all zero bits. // userId needs to be a zero length string so that the GetBytes method // works properly if (userId == null) userId = ""; byte[] destIp = GetIPAddressBytes(destinationHost); byte[] destPort = GetDestinationPortBytes(destinationPort); byte[] userIdBytes = ASCIIEncoding.ASCII.GetBytes(userId); byte[] request = new byte[9 + userIdBytes.Length]; // set the bits on the request byte array request[0] = SOCKS4_VERSION_NUMBER; request[1] = command; destPort.CopyTo(request, 2); destIp.CopyTo(request, 4); userIdBytes.CopyTo(request, 8); request[8 + userIdBytes.Length] = 0x00; // null (byte with all zeros) terminator for userId // send the connect request proxy.Write(request, 0, request.Length); // wait for the proxy server to respond WaitForData(proxy); // PROXY SERVER RESPONSE // The SOCKS server checks to see whether such a request should be granted // based on any combination of source IP address, destination IP address, // destination port number, the userid, and information it may obtain by // consulting IDENT, cf. RFC 1413. If the request is granted, the SOCKS // server makes a connection to the specified port of the destination host. // A reply packet is sent to the client when this connection is established, // or when the request is rejected or the operation fails. // // +----+----+----+----+----+----+----+----+ // | VN | CD | DSTPORT | DSTIP | // +----+----+----+----+----+----+----+----+ // # of bytes: 1 1 2 4 // // VN is the version of the reply code and should be 0. CD is the result // code with one of the following values: // // 90: request granted // 91: request rejected or failed // 92: request rejected becuase SOCKS server cannot connect to // identd on the client // 93: request rejected because the client program and identd // report different user-ids // // The remaining fields are ignored. // // The SOCKS server closes its connection immediately after notifying // the client of a failed or rejected request. For a successful request, // the SOCKS server gets ready to relay traffic on both directions. This // enables the client to do I/O on its connection as if it were directly // connected to the application server. // create an 8 byte response array byte[] response = new byte[8]; // read the resonse from the network stream proxy.Read(response, 0, 8); // evaluate the reply code for an error condition if (response[1] != SOCKS4_CMD_REPLY_REQUEST_GRANTED) HandleProxyCommandError(response, destinationHost, destinationPort); } /// /// Translate the host name or IP address to a byte array. /// /// Host name or IP address. /// Byte array representing IP address in bytes. internal byte[] GetIPAddressBytes(string destinationHost) { IPAddress ipAddr = null; // if the address doesn't parse then try to resolve with dns if (!IPAddress.TryParse(destinationHost, out ipAddr)) { try { ipAddr = Dns.GetHostEntry(destinationHost).AddressList[0]; } catch (Exception ex) { throw new ProxyException(String.Format(CultureInfo.InvariantCulture, "A error occurred while attempting to DNS resolve the host name {0}.", destinationHost), ex); } } // return address bytes return ipAddr.GetAddressBytes(); } /// /// Translate the destination port value to a byte array. /// /// Destination port. /// Byte array representing an 16 bit port number as two bytes. internal byte[] GetDestinationPortBytes(int value) { byte[] array = new byte[2]; array[0] = Convert.ToByte(value / 256); array[1] = Convert.ToByte(value % 256); return array; } /// /// Receive a byte array from the proxy server and determine and handle and errors that may have occurred. /// /// Proxy server command response as a byte array. /// Destination host. /// Destination port number. internal void HandleProxyCommandError(byte[] response, string destinationHost, int destinationPort) { if (response == null) throw new ArgumentNullException("response"); // extract the reply code byte replyCode = response[1]; // extract the ip v4 address (4 bytes) byte[] ipBytes = new byte[4]; for (int i = 0; i < 4; i++) ipBytes[i] = response[i + 4]; // convert the ip address to an IPAddress object IPAddress ipAddr = new IPAddress(ipBytes); // extract the port number big endian (2 bytes) byte[] portBytes = new byte[2]; portBytes[0] = response[3]; portBytes[1] = response[2]; Int16 port = BitConverter.ToInt16(portBytes, 0); // translate the reply code error number to human readable text string proxyErrorText; switch (replyCode) { case SOCKS4_CMD_REPLY_REQUEST_REJECTED_OR_FAILED: proxyErrorText = "connection request was rejected or failed"; break; case SOCKS4_CMD_REPLY_REQUEST_REJECTED_CANNOT_CONNECT_TO_IDENTD: proxyErrorText = "connection request was rejected because SOCKS destination cannot connect to identd on the client"; break; case SOCKS4_CMD_REPLY_REQUEST_REJECTED_DIFFERENT_IDENTD: proxyErrorText = "connection request rejected because the client program and identd report different user-ids"; break; default: proxyErrorText = String.Format(CultureInfo.InvariantCulture, "proxy client received an unknown reply with the code value '{0}' from the proxy destination", replyCode.ToString(CultureInfo.InvariantCulture)); break; } // build the exeception message string string exceptionMsg = String.Format(CultureInfo.InvariantCulture, "The {0} concerning destination host {1} port number {2}. The destination reported the host as {3} port {4}.", proxyErrorText, destinationHost, destinationPort, ipAddr.ToString(), port.ToString(CultureInfo.InvariantCulture)); // throw a new application exception throw new ProxyException(exceptionMsg); } internal void WaitForData(NetworkStream stream) { int sleepTime = 0; while (!stream.DataAvailable) { Thread.Sleep(WAIT_FOR_DATA_INTERVAL); sleepTime += WAIT_FOR_DATA_INTERVAL; if (sleepTime > WAIT_FOR_DATA_TIMEOUT) throw new ProxyException("A timeout while waiting for the proxy destination to respond."); } } #region "Async Methods" private BackgroundWorker _asyncWorker; private Exception _asyncException; bool _asyncCancelled; /// /// Gets a value indicating whether an asynchronous operation is running. /// /// Returns true if an asynchronous operation is running; otherwise, false. /// public bool IsBusy { get { return _asyncWorker == null ? false : _asyncWorker.IsBusy; } } /// /// Gets a value indicating whether an asynchronous operation is cancelled. /// /// Returns true if an asynchronous operation is cancelled; otherwise, false. /// public bool IsAsyncCancelled { get { return _asyncCancelled; } } /// /// Cancels any asychronous operation that is currently active. /// public void CancelAsync() { if (_asyncWorker != null && !_asyncWorker.CancellationPending && _asyncWorker.IsBusy) { _asyncCancelled = true; _asyncWorker.CancelAsync(); } } private void CreateAsyncWorker() { if (_asyncWorker != null) _asyncWorker.Dispose(); _asyncException = null; _asyncWorker = null; _asyncCancelled = false; _asyncWorker = new BackgroundWorker(); } /// /// Event handler for CreateConnectionAsync method completed. /// public event EventHandler CreateConnectionAsyncCompleted; /// /// Asynchronously creates a remote TCP connection through a proxy server to the destination host on the destination port /// using the supplied open TcpClient object with an open connection to proxy server. /// /// Destination host name or IP address. /// Port number to connect to on the destination host. /// /// Returns TcpClient object that can be used normally to communicate /// with the destination server. /// /// /// This instructs the proxy server to make a pass through connection to the specified destination host on the specified /// port. /// public void CreateConnectionAsync(string destinationHost, int destinationPort) { if (_asyncWorker != null && _asyncWorker.IsBusy) throw new InvalidOperationException("The Socks4/4a object is already busy executing another asynchronous operation. You can only execute one asychronous method at a time."); CreateAsyncWorker(); _asyncWorker.WorkerSupportsCancellation = true; _asyncWorker.DoWork += new DoWorkEventHandler(CreateConnectionAsync_DoWork); _asyncWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(CreateConnectionAsync_RunWorkerCompleted); Object[] args = new Object[2]; args[0] = destinationHost; args[1] = destinationPort; _asyncWorker.RunWorkerAsync(args); } private void CreateConnectionAsync_DoWork(object sender, DoWorkEventArgs e) { try { Object[] args = (Object[])e.Argument; e.Result = CreateConnection((string)args[0], (int)args[1]); } catch (Exception ex) { _asyncException = ex; } } private void CreateConnectionAsync_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (CreateConnectionAsyncCompleted != null) CreateConnectionAsyncCompleted(this, new CreateConnectionAsyncCompletedEventArgs(_asyncException, _asyncCancelled, (TcpClient)e.Result)); } #endregion } }