Thursday, March 19, 2009

Making Witty Work With a Socks Proxy

I needed a decent Twitter client that supported a Socks proxy, but was unable to find one (if you know of one, please post a comment).  While there were some decent clients that have proxy settings, none of them supported Socks. So instead, I decided to take Witty and modify it instead.

Witty uses the HttpWebRequest class which does not appear to support socks proxies (again, if this is wrong, please post a comment).  This meant replacing all usages of that class with a custom implementation that I decided to call SocksHttpWebRequest (code to follow).  I built it on top of ProxySocket, which is a free .NET API that supports Socks 4 and 5 messaging.

The changes to Twitty itself were minimal.  The TwitterLib.TwitterNet class is where all the over-the-wire communications are handled, with the CreateTwitterRequest() method being the starting point for the modifications.

// private HttpWebRequest CreateTwitterRequest(string Uri)
private WebRequest CreateTwitterRequest(string Uri)
{
// Create the web request
// HttpWebRequest request = WebRequest.Create(Uri);
var request = SocksHttpWebRequest.Create(Uri);

// rest of method unchanged
...
}

From there it’s just a matter of changing the consuming methods to expect a WebRequest object back from CreateTwitterRequest().  For most of the methods, this meant the following changes:

// HttpWebRequest request = CreateTwitterRequest(requestURL);
var request = CreateTwitterRequest(pageRequestUrl);

// using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
using (var response = request.GetResponse())
{
// some code that utilizes the response
...
}

Ah, if only the var keyword had been embraced…

A few of the consuming methods also had the following line of code in them:

request.ServicePoint.Expect100Continue = false;

Since ServicePoint is not a property of the WebRequest class and thus does not apply to SocksHttpWebRequest, I simply commented it out.

I should note that the SocksHttpWebRequest and SocksHttpWebRsponse classes are not fully featured.  That is to say, most of the virtual members that WebRequest and WebResponse expose throw a NotImplementedException, and I only provided overrides for those that were needed to make Witty run properly.  However, much of the functionality is there, and they are certainly a better starting point than I had to work with.

And now, as promised, here’s the code:


using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Org.Mentalis.Network.ProxySocket;

namespace Ditrans
{
public class SocksHttpWebRequest : WebRequest
{

#region Member Variables

private readonly Uri _requestUri;
private WebHeaderCollection _requestHeaders;
private string _method;
private SocksHttpWebResponse _response;
private string _requestMessage;
private byte[] _requestContentBuffer;

// darn MS for making everything internal (yeah, I'm talking about you, System.net.KnownHttpVerb)
static readonly StringCollection validHttpVerbs =
new StringCollection { "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "OPTIONS" };

#endregion

#region Constructor

private SocksHttpWebRequest(Uri requestUri)
{
_requestUri = requestUri;
}

#endregion

#region WebRequest Members

public override WebResponse GetResponse()
{
if (Proxy == null)
{
throw new InvalidOperationException("Proxy property cannot be null.");
}
if (String.IsNullOrEmpty(Method))
{
throw new InvalidOperationException("Method has not been set.");
}

if (RequestSubmitted)
{
return _response;
}
_response = InternalGetResponse();
RequestSubmitted = true;
return _response;
}

public override Uri RequestUri
{
get { return _requestUri; }
}

public override IWebProxy Proxy { get; set; }

public override WebHeaderCollection Headers
{
get
{
if (_requestHeaders == null)
{
_requestHeaders = new WebHeaderCollection();
}
return _requestHeaders;
}
set
{
if (RequestSubmitted)
{
throw new InvalidOperationException("This operation cannot be performed after the request has been submitted.");
}
_requestHeaders = value;
}
}

public bool RequestSubmitted { get; private set; }

public override string Method
{
get
{
return _method ?? "GET";
}
set
{
if (validHttpVerbs.Contains(value))
{
_method = value;
}
else
{
throw new ArgumentOutOfRangeException("value", string.Format("'{0}' is not a known HTTP verb.", value));
}
}
}

public override long ContentLength { get; set; }

public override string ContentType { get; set; }

public override Stream GetRequestStream()
{
if (RequestSubmitted)
{
throw new InvalidOperationException("This operation cannot be performed after the request has been submitted.");
}

if (_requestContentBuffer == null)
{
_requestContentBuffer = new byte[ContentLength];
}
else if (ContentLength == default(long))
{
_requestContentBuffer = new byte[int.MaxValue];
}
else if (_requestContentBuffer.Length != ContentLength)
{
Array.Resize(ref _requestContentBuffer, (int) ContentLength);
}
return new MemoryStream(_requestContentBuffer);
}

#endregion

#region Methods

public static new WebRequest Create(string requestUri)
{
return new SocksHttpWebRequest(new Uri(requestUri));
}

public static new WebRequest Create(Uri requestUri)
{
return new SocksHttpWebRequest(requestUri);
}

private string BuildHttpRequestMessage()
{
if (RequestSubmitted)
{
throw new InvalidOperationException("This operation cannot be performed after the request has been submitted.");
}

var message = new StringBuilder();
message.AppendFormat("{0} {1} HTTP/1.0\r\nHost: {2}\r\n", Method, RequestUri.PathAndQuery, RequestUri.Host);

// add the headers
foreach (var key in Headers.Keys)
{
message.AppendFormat("{0}: {1}\r\n", key, Headers[key.ToString()]);
}

if (!string.IsNullOrEmpty(ContentType))
{
message.AppendFormat("Content-Type: {0}\r\n", ContentType);
}
if (ContentLength > 0)
{
message.AppendFormat("Content-Length: {0}\r\n", ContentLength);
}

// add a blank line to indicate the end of the headers
message.Append("\r\n");

// add content
if(_requestContentBuffer != null && _requestContentBuffer.Length > 0)
{
using (var stream = new MemoryStream(_requestContentBuffer, false))
{
using (var reader = new StreamReader(stream))
{
message.Append(reader.ReadToEnd());
}
}
}

return message.ToString();
}

private SocksHttpWebResponse InternalGetResponse()
{
var response = new StringBuilder();
using (var _socksConnection =
new ProxySocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
var proxyUri = Proxy.GetProxy(RequestUri);
var ipAddress = GetProxyIpAddress(proxyUri);
_socksConnection.ProxyEndPoint = new IPEndPoint(ipAddress, proxyUri.Port);
_socksConnection.ProxyType = ProxyTypes.Socks5;

// open connection
_socksConnection.Connect(RequestUri.Host, 80);
// send an HTTP request
_socksConnection.Send(Encoding.ASCII.GetBytes(RequestMessage));
// read the HTTP reply
var buffer = new byte[1024];

var bytesReceived = _socksConnection.Receive(buffer);
while (bytesReceived > 0)
{
response.Append(Encoding.ASCII.GetString(buffer, 0, bytesReceived));
bytesReceived = _socksConnection.Receive(buffer);
}
}
return new SocksHttpWebResponse(response.ToString());
}

private static IPAddress GetProxyIpAddress(Uri proxyUri)
{
IPAddress ipAddress;
if (!IPAddress.TryParse(proxyUri.Host, out ipAddress))
{
try
{
return Dns.GetHostEntry(proxyUri.Host).AddressList[0];
}
catch (Exception e)
{
throw new InvalidOperationException(
string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", proxyUri.Host), e);
}
}
return ipAddress;
}

#endregion

#region Properties

public string RequestMessage
{
get
{
if (string.IsNullOrEmpty(_requestMessage))
{
_requestMessage = BuildHttpRequestMessage();
}
return _requestMessage;
}
}

#endregion

}
}


using System;
using System.IO;
using System.Net;
using System.Text;

namespace Ditrans
{
public class SocksHttpWebResponse : WebResponse
{

#region Member Variables

private WebHeaderCollection _httpResponseHeaders;
private string _responseContent;

#endregion

#region Constructors

public SocksHttpWebResponse(string httpResponseMessage)
{
SetHeadersAndResponseContent(httpResponseMessage);
}

#endregion

#region WebResponse Members

public override Stream GetResponseStream()
{
return ResponseContent.Length == 0 ? Stream.Null : new MemoryStream(Encoding.UTF8.GetBytes(ResponseContent));
}

public override void Close() { /* the base implementation throws an exception */ }

public override WebHeaderCollection Headers
{
get
{
if (_httpResponseHeaders == null)
{
_httpResponseHeaders = new WebHeaderCollection();
}
return _httpResponseHeaders;
}
}

public override long ContentLength
{
get
{
return ResponseContent.Length;
}
set
{
throw new NotSupportedException();
}
}

#endregion

#region Methods

private void SetHeadersAndResponseContent(string responseMessage)
{
// the HTTP headers can be found before the first blank line
var indexOfFirstBlankLine = responseMessage.IndexOf("\r\n\r\n");

var headers = responseMessage.Substring(0, indexOfFirstBlankLine);
var headerValues = headers.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
// ignore the first line in the header since it is the HTTP response code
for (int i = 1; i < headerValues.Length; i++)
{
var headerEntry = headerValues[i].Split(new[] { ':' });
Headers.Add(headerEntry[0], headerEntry[1]);
}

ResponseContent = responseMessage.Substring(indexOfFirstBlankLine + 4);
}

#endregion

#region Properties

private string ResponseContent
{
get { return _responseContent ?? string.Empty; }
set { _responseContent = value; }
}

#endregion

}
}

4 comments:

Unknown said...

Good job!
I have a similar problem but and Your code is almost right for my solution. However I need to implement few more methods. For instance BeginGetResponse. It's a problem because it should be asynchronous. Is there some example how it is made in HttpWebReqest?

Chris Staley said...

@P4trykx,

To take a peek at the code in HttpWebRequest, you can either use Reflector or use debugging to pull down the actual source code.

Anonymous said...

Hi, is there any type of license associated with your code? I'd like to use it in a project of mine.

Chris Staley said...

#cszikszoy,

Feel free to use it anyway you wish.