Click here to Skip to main content
Click here to Skip to main content

Implementing a Multithreaded HTTP/HTTPS Debugging Proxy Server in C#

By , 3 Feb 2011
 

Introduction

This article will show you how to implement a multithreaded HTTP proxy server in C# with a non-standard proxy server feature of terminating and then proxying HTTPS traffic. I've added a simple caching mechanism, and have simplified the code by ignoring http/1.1 requests for keeping connections alive, etc.

Disclaimer: Understand that this code is for debugging and testing purposes only. The author does not intend for this code or the executable to be used in any way that may compromise someone's sensitive information. Do not use this server in any environment which has users that are unaware of its use. By using this code or the executable found in this article, you are taking responsibility for the data which may be collected through its use.

Background

If you are familiar with fiddler, then you already know how this proxy server works. It essentially performs a "man-in-the-middle" on the HTTP client to dump and debug HTTP traffic. The System.Net.Security.SslStream class is utilized to handle all the heavy lifting.

Using the Code

The most important part about this code is that when the client asks for a CONNECT, instead of just passing TCP traffic, we're going to handle an SSL handshake and establish an SSL session and receive a request from the client. In the mean time, we'll send the same request to the destination HTTPS server.

First, let's look at creating a server that can handle multiple concurrent TCP connections. We'll use the System.Threading.Thread object to start listening for connections in a separate thread. This thread's job will be to listen for incoming connections, and then spawn a new thread to handle processing, thus allowing the listening thread to continue listening for new connections without blocking while one client is processed.

public sealed class ProxyServer
{
   private TcpListener _listener;
   private Thread _listenerThread;

   public void Start()
   {
      _listener = new TcpListener(IPAddress.Loopback, 8888);
      _listenerThread = new Thread(new ParameterizedThreadStart(Listen));
      _listenerThread.Start(_listener);
   }
        
   public void Stop()
   {
      //stop listening for incoming connections
      _listener.Stop();
      //wait for server to finish processing current connections...
      _listenerThread.Abort();
      _listenerThread.Join();
   }

   private static void Listen(Object obj)
   {
      TcpListener listener = (TcpListener)obj;
      try
      {
         while (true)
         {
            TcpClient client = listener.AcceptTcpClient();
            while (!ThreadPool.QueueUserWorkItem
		(new WaitCallback(ProxyServer.ProcessClient), client)) ;
         }
      }
      catch (ThreadAbortException) { }
      catch (SocketException) { }
   }

   private static void ProcessClient(Object obj)
   {
      TcpClient client = (TcpClient)obj;
      try
      {
         //do your processing here
      }
      catch(Exception ex)
      {
         //handle exception
      }
      finally
      {
         client.Close();
      }
   }
}

And that's the beginning of the code to handle concurrent TCP clients in a multithreaded manner. Nothing really special there. The interesting bit is when we use SslStream to act as an HTTPS server and "trick" the client into believing it's talking to the destination server. Note that the browser should not actually be tricked because of the SSL certificate chain, but depending on their browser, it may or may not be apparent that the server's identity is in question.

Now let's take a look at the actual processing of the SSL request. Assume that we are somewhere inside the try block of the ProcessClient method shown above.

//read the first line HTTP command
Stream clientStream = client.GetStream();
StreamReader clientStreamReader = new StreamReader(clientStream);
String httpCmd = clientStreamReader.ReadLine();

//break up the line into three components
String[] splitBuffer = httpCmd.Split(spaceSplit, 3);
String method = splitBuffer[0];
String remoteUri = splitBuffer[1];
Version version = new Version(1, 0); //force everything to HTTP/1.0

//this will be the web request issued on behalf of the client
HttpWebRequest webReq;

if (method == "CONNECT")
{
   //Browser wants to create a secure tunnel
   //instead = we are going to perform a man in the middle
   //the user's browser should warn them of the certification errors however.
   //Please note: THIS IS ONLY FOR TESTING PURPOSES - 
   //you are responsible for the use of this code
   //this is the URI we'll request on behalf of the client
   remoteUri = "https://" + splitBuffer[1];
   //read and ignore headers
   while (!String.IsNullOrEmpty(clientStreamReader.ReadLine())) ;

   //tell the client that a tunnel has been established
   StreamWriter connectStreamWriter = new StreamWriter(clientStream);
   connectStreamWriter.WriteLine("HTTP/1.0 200 Connection established");
   connectStreamWriter.WriteLine
	(String.Format("Timestamp: {0}", DateTime.Now.ToString()));
   connectStreamWriter.WriteLine("Proxy-agent: matt-dot-net");
   connectStreamWriter.WriteLine();
   connectStreamWriter.Flush();

   //now-create an https "server"
   sslStream = new SslStream(clientStream, false);
   sslStream.AuthenticateAsServer(_certificate, 
	false, SslProtocols.Tls | SslProtocols.Ssl3 | SslProtocols.Ssl2, true);

   //HTTPS server created - we can now decrypt the client's traffic
   //.... 
}

Points of Interest

You can see that I have an X509Certificate2 _certificate defined elsewhere in the code. To get a certificate for this, you need to use a tool like makecert.exe to create a self-signed certificate. I found makecert.exe in the Windows SDK and it also comes with Fiddler. I have included a certificate file in the source files to allow the server to run, but because it does not include the private key, to actually handle SSL traffic, You will need to run makecert.exe

Here is the syntax I used for makecert.exe:

makecert.exe cert.cer -a sha1 -n "CN=matt-dot-net" -sr LocalMachine -ss My -sky signature -pe -len 2048

One thing to note is that on your HttpWebRequest object, you will need to set the Proxy property to null if you are using Windows internet options to specify using your proxy server. This is because your HttpWebRequest will default to the Windows internet settings and you'll have a proxy server that is trying to use itself as a proxy server!

Another interesting and frustrating hang-up was the handling of cookies. When I thought that I was processing requests/responses perfectly, I found that I could not maintain state with any websites because cookies were not properly being sent by the client. I determined (from using fiddler and firefox live HTTP headers) that cookies needed to be set individually. The server was returning several cookies in one Set-Cookie header, but I needed to parse them out and return an individual Set-Cookie header for each one. I haven't researched to find out why this is, or what is the proper handling of cookies in HTTP, but all I can assume was that when a browser is set to use a proxy server, it expects the proxy server to process cookies into individual Set-Cookie headers and if not configured to use a proxy, the browser does this itself.

History

I threw together this code on a Sunday afternoon, so I expect it to have errors and problems. Also, I make no claim that this is properly handling errors, cleaning up objects, or is in any way the best way to do anything.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

matt-dot-net
Software Developer (Senior)
United States United States
No Biography provided
Follow on   Twitter

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionHow can I bypass the certificate warnings shown on the IE or any browser?memberbeinblack17-Jun-13 23:42 
Hi,
 
The article is great, But I have an issue that 'How can I bypass the certificate warnings shown on the IE or any browser?"
 
or how can I respond with the original certificate as the website has? I mean when we hit the website without using this proxy server there are no errors in the browser, so how can we do the same thing using this proxy server.
 
I would really appreciate your assistance.
 
Thanks,
 
Shahid
QuestionWhy Image are not upload?memberGAJERA22-May-13 4:44 
Hi ,
Here i am trying to upload image ,but it is not able to uploaded .
Can u help me how to upload image with this proxy?
 
Thanks,
QuestionHow to use HTTP/1.1 ?memberGAJERA3-May-13 22:55 
Hi Matt,
It is great example for the learning HTTP proxy.
 
Here i am passing the HTTP version what i am getting from client request ,
for that i update this function like,
string method = splitBuffer[0];
               string remoteUri = splitBuffer[1];
               string protocolversion =  splitBuffer[2].ToString().Split('/')[1];
 
webReq.ProtocolVersion = (protocolversion.EndsWith(".0") ? HttpVersion.Version10 : HttpVersion.Version11);
 
  private string writeResponseStatus(HttpStatusCode code, string description, Version version,StreamWriter stremResponseWriter)
        {
            string s = string.Format("HTTP/{0} {1} {2}",version.ToString(),(Int32)code, description);
            stremResponseWriter.WriteLine(s);
            return s + "\r\n";
        }
 
It Work fine ,but some of the link are not working like,also give the right SSL certificate .
 
like http://mail.yahoo.com/;_ylt=A86.JoIIp4RRWm0B90KuitIF?.intl=in[^]
 
In above function if write "HTTP/1.0 {1} {2}",(Int32)code, description" though it work fine for any request.
 
Browser doesn't redirect the next request .Can you help me to resolve this issue.
In short i wants to use HTTP version what i am getting from client .
Can u help me to resolve this issue?.I am using Browser IE-9 also tested in Mozilla and chrome.
 
Thanks,
Mahendra
QuestionSave encoded certificate to store failed => 0x5 (5)memberpara para20-Mar-13 12:15 
Hi I get this error when I tried to execute the mkcert command
C:\Users\PARA\Downloads\HTTPProxy-src>makecert.exe cert.cer -a sha1 -n "CN=matt-
dot-net" -sr LocalMachine -ss My -sky signature -pe -len 2048
Error: Save encoded certificate to store failed => 0x5 (5)
hello

AnswerRe: Save encoded certificate to store failed => 0x5 (5)memberpara para21-Mar-13 11:33 
Ok, it worked when I ran cmd.exe as Administrator, my bad
hello

QuestionCode does not work for HTTPSmemberaditya_bokade5-Mar-13 0:13 
Hi Matt,
After referring to your following comment:
This is all unnecessary. Use this commandline:
 
makecert.exe "c:\temp\cert.cer" -a sha1 -n "CN=matt-dot-net" -sr LocalMachine -ss My -sky signature -pe -len 2048
 
Then copy the cert to the proxy server's working directory.
 
Note, you can replace the CN with whatever you want.
I Executed the makecert command provided by you on my machine where my HTTPS proxy server is running.
However, when I try to brows any HTTPS url on IE, it says:
 
There is a problem with this website's security certificate. 
  
Security certificate problems may indicate an attempt to fool you or intercept any data you send to the server.  
  We recommend that you close this webpage and do not continue to this website 
 
Please help me out of this problem.
QuestionMultiple cookies in a responsememberNir Asis31-Jan-13 5:14 
I want to refer to the last paragraph of the article (quoted below). I tried to reproduce that but noticed that even when not using a proxy the browser does not identify that a single Set-Cookie header contains more than one cookie and uses only the first. Can anyhow else add his/hers experience with this issue?
 
The header I used was
Set-Cookie: FIRST=1; SECOND=1; path=/; domain=localhost
but only FIRST was acknowledged by the browser.
 
"Another interesting and frustrating hang-up was the handling of cookies. When I thought that I was processing requests/responses perfectly, I found that I could not maintain state with any websites because cookies were not properly being sent by the client. I determined (from using fiddler and firefox live HTTP headers) that cookies needed to be set individually. The server was returning several cookies in one Set-Cookie header, but I needed to parse them out and return an individual Set-Cookie header for each one. I haven't researched to find out why this is, or what is the proper handling of cookies in HTTP, but all I can assume was that when a browser is set to use a proxy server, it expects the proxy server to process cookies into individual Set-Cookie headers and if not configured to use a proxy, the browser does this itself."
QuestionProblem with HttpsmemberNnamani Uchenna19-Nov-12 22:23 
Nice coding but it giving me this error "The server mode SSL must use a certificate with the associated private key."
 
@
try
                   {
                       sslStream.AuthenticateAsServer(_certificate, false, SslProtocols.Tls | SslProtocols.Ssl3 , true);
                       //sslStream.AuthenticateAsServer(_certificate1);
                   }
                   catch (Exception)
                   {
                       sslStream.Close();
                       clientStreamReader.Close();
                       connectStreamWriter.Close();
                       clientStream.Close();
                       return;
                   }

AnswerRe: Problem with Httpsmembermatt-dot-net20-Nov-12 8:37 
This has been answered several times.
QuestionDose not play viseomemberbr68br687-Nov-12 0:30 
hi man your proxy dosen't play flv in webpage
AnswerRe: Dose not play viseomemberHaggis7727-Apr-13 10:44 
Turn on keep alive:
 
case "proxy-connection":
                    case "connection":
                    case "keep-alive":
                        webReq.KeepAlive = true;
                        //ignore these
                        break;

BugHangs while browsingmemberajhvdb13-Feb-12 0:13 
If I start this project in debug mode with
Server.DumpHeaders = true;
Server.DumpPostData = true;
 
It doesn't respond after a few requests.
 
I got messages which said something like "could not close because it's open in another thread"
 

Im working now without these settings and it's ok.
 
Any ideas?
QuestionMy vote of 5 [modified]memberTimur Akhmedov25-Jan-12 7:53 
Works excellent. And really help me out. But could you please elaborate more about http protocol version? Why only 1.0 and not 1.1. Thanks in advanced. Timur.

modified 5-Feb-12 9:49am.

QuestionTroubles with HTTPSmemberMember 469052012-Dec-11 17:30 
Hey Matt,
 
Great article, exactly what I was looking for.
 
Unfortunatley I am also having troubles with HTTPS. I have gone through the makecert procedure and can see the cert in the certificate store and have copied the .cer file to the working directory but when ever I browse to a SSL site I am getting an exception when the procedure AuthenticateAsServer is being called. The Exception is 'The credentials supplied to the package were not recognized.'
 
Any ideas?
 

TIA Ben.
AnswerDid you find the solution?memberaditya_bokade6-Mar-13 3:43 
Hi Tia Ben,
Did you manage to solve HTTPS problem? Please respond and answer here. I am in deadly need of this.
Thanks.
Questioncan you please tell me the command line how did you create the certificate?memberXinyiChen9-Nov-11 22:09 
I tried many times to use makecert.exe as below command line:
 
makecert -a sha1 -n "CN=https" -b 01/01/2011 -e 01/01/2088 https.cert
 
the certificate was created successfully, but when i load it into your program via
_certificate = new X509Certificate2(certFilePath);
, it threw exception, why?
AnswerRe: can you please tell me the command line how did you create the certificate?membermatt-dot-net10-Nov-11 2:36 
What's the exception?
GeneralRe: can you please tell me the command line how did you create the certificate?memberXinyiChen10-Nov-11 14:10 
the exception is "Could not create the certificate from file from..", see below code block:
any ideas?
 
                try
                {
                    _certificate = new X509Certificate2(certFilePath, "https");
                }
                catch (Exception ex)
                {
                    throw new ConfigurationErrorsException(String.Format("Could not create the certificate from file from {0}",certFilePath), ex);
                }

AnswerRe: can you please tell me the command line how did you create the certificate?memberXinyiChen11-Nov-11 3:35 
Actually, I solved this problem by myself, F.Y.I
The exception that reason is that I didn't put https.cer into the directory where's same as the executable file -
HTTPProxyServer.exe

 
As you mentioned in article, the private key must be associated with the self-signed certificate, otherwise, https won't be working well, how to create a self-signed certificate associated with private key. I did some investigation on internet.
 
Currently, I have already created that certificate - https.cer and private key - https.pvk.
Now, we need to run two commands as below, it will finally generate a self-signed certificate associated with private key.
 
// this command will create a Software Publisher Certificate (SPC)
cert2spc.exe https.cer https.spc
// Then we need to convert the SPC to the final self-signed certificate associated with private key.
pvkimprt.exe -pfx https.spc https.pvk
 
PS. you can find cert2spc.exe & pvkimprt.exe from Visual studio IDE directory or MSDN downloading area.
 
when you run the last command line, there is a wizard dialog it will first ask you the password with private key(i.e, the 1st step you typed in for created certificate), then it will ask you Yes/No to export private key, make sure that you choose Yes to export private key and next step is normally choose Personal Information Exchange PKCS #12 (.pfx), finally, for my side, I save the certificate as https.pfx, and with app.config, modifying the certificate path to https.pfx.
 
// Again, I put the https.pfx into the directory where's same as executable file 
HTTPProxyServer.exe

Now, HTTPS connection is working well in my IE browser.
when I start IE browser and type in
 
https://www.paypal.com/
 
it gave me a warning:
 
There is a problem with this website's security certificate.
 
it doesn't matter , I choose "Continue to the web site(not recommended)" Laugh | :laugh:
GeneralRe: can you please tell me the command line how did you create the certificate?membermatt-dot-net14-Nov-11 4:05 
This is all unnecessary. Use this commandline:
 
makecert.exe "c:\temp\cert.cer" -a sha1 -n "CN=matt-dot-net" -sr LocalMachine -ss My -sky signature -pe -len 2048
 
Then copy the cert to the proxy server's working directory.
 
Note, you can replace the CN with whatever you want.
Generalthis is greatmemberfisura20-Oct-11 18:27 
this is great, i haven't testes on HTTPS yet, but i like when it goes very well through HTTP..
thank you for sharing the source code
QuestionHTTPS doesn't workmemberrtnylw18-Aug-11 3:38 
Your code works great with standart http, but whenever I try to come accross https, it gives broken page. Do you have an idea why this might be happening? Thanks!
GeneralMy vote of 5memberalexjanjic9-Jul-11 6:12 
Thanks. Now I can debug SSL Pages.
QuestionWhich Options do I need to use to make the certificate?memberalexjanjic9-Jul-11 6:07 
As the title says I dont know which arguments I should use in makecert to make the Certificate right.
GeneralMy vote of 5memberKushan Ratnayake8-Jul-11 20:32 
Code works as I wanted

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130617.1 | Last Updated 3 Feb 2011
Article Copyright 2010 by matt-dot-net
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid