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
}
}