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

}
}

Using SyntaxHighlighter 2.0 on Blogger

Updated 4/8/2009: Had originally posted wrong version of FindTagsByName() function.

Guogang Hu made a nice post a couple of years ago demonstrating how to use SyntaxHighlighter with Blogger, but since then the world has moved on.  While that post is still entirely relevant if you plan to use SyntaxHighlighter 1.5, a slight modification is required to the JavaScript Guogang supplied if you want to use version 2.0.

The key functionality in Guogang’s script is the removal of the <br> tags in the code blocks that are automatically inserted by the Blogger rendering engine in place of all line breaks. The script looks for the code blocks and then removes the extraneous <br> tags. However, due to the change in the way that code styling is indicated to SyntaxHighlighter 2.0, the script’s approach to locating the code blocks must be modified.

<!-- SyntaxHighlighter code styling changes -->
<!-- 1.5 method -->
<pre name="code" class="c-sharp">
... some code here ...
</pre>

<!-- 2.0 method -->
<pre class="brush: c#">
... some code here ...
</pre>

Whereas Guogang’s script looks for tags whose name attribute is “code,” we need to now be looking for tags whose class attribute begins with “brush:”.  And thus I give you the full, modified script:

</div></div> <!-- end outer-wrapper -->

<script type="text/javascript">
//<![CDATA[
function FindTagsByName(container, className, tagName)
{
var elements = document.getElementsByTagName(tagName);
for (var i = 0; i < elements.length; i++)
{
var tagClassName = elements[i].className;
if (tagClassName != null && tagClassName.search(className) == 0)
{
container.push(elements[i]);
}
}
}

var elements = [];
FindTagsByName(elements, "brush:", "pre");
FindTagsByName(elements, "brush:", "textarea");

for(var i=0; i < elements.length; i++)
{
if(elements[i].nodeName.toUpperCase() == "TEXTAREA") {
var childNode = elements[i].childNodes[0];
var newNode = document.createTextNode(childNode.nodeValue.replace(/<br\s*\/?>/gi,'\n'));
elements[i].replaceChild(newNode, childNode);
}
else if(elements[i].nodeName.toUpperCase() == "PRE") {
brs = elements[i].getElementsByTagName("br");
for(var j = 0, brLength = brs.length; j < brLength; j++)
{
var newNode = document.createTextNode("\n");
elements[i].replaceChild(newNode, brs[0]);
}
}
}
SyntaxHighlighter.all();
//]]>
</script>