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

Reverse Proxy in C# .NET v2.0

By , 20 Apr 2007
 

Screenshot - Article.gif

Introduction

This article illustrates how a reverse proxy server can be developed in C# .NET v2.0 using HTTP Handler in IIS. The idea is to intercept and manipulate incoming HTTP requests to the IIS web server. I've developed a simple server with a basic, HTTP Reverse Proxy functionality, but there is still a lot more to add.

Background

A reverse proxy differs from an ordinary forward proxy. A forward proxy is an intermediate server that accepts requests addressed to it. It then requests content from the origin server and returns it to the client. The client must be configured to use the forward proxy. The reverse proxy does not require any special configuration by the client. The client requests content from the reverse proxy. The reverse proxy then decides where to send those requests, and returns the content as if it was itself the origin. Reverse proxies can be used to bring several servers into the same URL space.

Using the code

The below code is the core of the Reverse Proxy Server. I've used a HTML parser written by Jeff Heaton for rewriting the URLs in the HTML page rendered by the Reverse Proxy Server.

Points to Ponder

  1. IHttpHandler, the abstract HTTP Handler base class of .NET must be inherited to define custom HTTP Handler.
  2. IRequiresSessionState must be inherited if we are required to manipulate session variables.
  3. IsReusable is a read-only property which can be used to define where the application instance can be pooled/reused.
/*
 * REVERSE PROXY SERVER - IIS HTTP HANDLER - C# .NET v2.0
 * 
 * FILE NAME    :    Program.cs
 * 
 * DATE CREATED :    March 26, 2007, 16:15:05
 * CREATED BY   :    Gunasekaran Paramesh
 * 
 * LAST UPDATED :    April 16, 2007, 3:10:09 PM
 * UPDATED BY   :    Gunasekaran Paramesh
 * 
 * DESCRIPTION  :    Implementation of Reverse Proxy Server through IIS HTTP 
                     handler in C# .NET v2.0
*/

using System;
using System.IO;
using System.Net;
using System.Web;
using System.Text;
using System.Web.Mail;
using System.Collections;
using System.Configuration;
using System.Web.SessionState;
using System.DirectoryServices;

namespace TryHTTPHandler
{
public class SyncHandler : IHttpHandler, IRequiresSessionState
{
    public bool IsReusable { get { return true; } }

    // Process incoming request
    public void ProcessRequest(HttpContext Context)
    {            
        string ServerURL = "";            

        try
        {
            // Parsing incoming URL and extracting original server URL
            char[] URL_Separator = { '/' };
            string[] URL_List = Context.Request.Url.AbsoluteUri.Remove(0, 
                7).Split(URL_Separator);
            ServerURL = "http://" + 
                URL_List[2].Remove(URL_List[2].Length - 5, 5) + @"/";
            string URLPrefix = @"/" + URL_List[1] + @"/" + 
                URL_List[2]; // Eg. "/handler/stg2web.sync";
            for ( int i = 3; i < URL_List.Length; i++ )
                ServerURL += URL_List[i] + @"/";
            ServerURL = ServerURL.Remove(ServerURL.Length -1, 1);
            WriteLog(ServerURL + " (" + 
                Context.Request.Url.ToString() + ")");

            // Extracting POST data from incoming request
            Stream RequestStream = Context.Request.InputStream;
            byte[] PostData = new byte[Context.Request.InputStream.Length];
            RequestStream.Read(PostData, 0,
                (int) Context.Request.InputStream.Length);

            // Creating proxy web request
            HttpWebRequest ProxyRequest = (
                HttpWebRequest) WebRequest.Create(ServerURL);
            if ( ConfigurationManager.AppSettings["UpchainProxy"] == 
                "true" )
                ProxyRequest.Proxy = new WebProxy(
                    ConfigurationManager.AppSettings["Proxy"], true);

            ProxyRequest.Method = Context.Request.HttpMethod;
            ProxyRequest.UserAgent = Context.Request.UserAgent;                
            CookieContainer ProxyCookieContainer = new CookieContainer();
            ProxyRequest.CookieContainer = new CookieContainer();
            ProxyRequest.CookieContainer.Add(
                ProxyCookieContainer.GetCookies(new Uri(ServerURL)));
            ProxyRequest.KeepAlive = true;

            //For POST, write the post data extracted from the incoming request
            if ( ProxyRequest.Method == "POST" )
            {
                ProxyRequest.ContentType = 
                    "application/x-www-form-urlencoded";
                ProxyRequest.ContentLength = PostData.Length;
                Stream ProxyRequestStream = ProxyRequest.GetRequestStream();
                ProxyRequestStream.Write(PostData, 0, PostData.Length);
                ProxyRequestStream.Close();
            }

            // Getting response from the proxy request                
            HttpWebResponse ProxyResponse = (
                HttpWebResponse) ProxyRequest.GetResponse();

            if (ProxyRequest.HaveResponse)
            {
                // Handle cookies
                foreach(Cookie ReturnCookie in ProxyResponse.Cookies)
                {
                    bool CookieFound = false;
                    foreach(Cookie OldCookie in 
                        ProxyCookieContainer.GetCookies(new Uri(ServerURL)))
                    {
                        if (ReturnCookie.Name.Equals(OldCookie.Name))
                        {
                            OldCookie.Value = ReturnCookie.Value;
                            CookieFound = true;
                        }
                    }
                    if (!CookieFound)
                        ProxyCookieContainer.Add(ReturnCookie);
                }
            }

            Stream StreamResponse = ProxyResponse.GetResponseStream();
            int ResponseReadBufferSize = 256;
            byte[] ResponseReadBuffer = new byte[ResponseReadBufferSize];
            MemoryStream MemoryStreamResponse = new MemoryStream();

            int ResponseCount = StreamResponse.Read(ResponseReadBuffer, 0, 
                ResponseReadBufferSize);
            while ( ResponseCount > 0 )
            {
                MemoryStreamResponse.Write(ResponseReadBuffer, 0, 
                    ResponseCount);
                ResponseCount = StreamResponse.Read(ResponseReadBuffer, 0, 
                    ResponseReadBufferSize);
            }

            byte[] ResponseData = MemoryStreamResponse.ToArray();
            string ResponseDataString = Encoding.ASCII.GetString(ResponseData);

            // While rendering HTML, parse and modify the URLs present
            if ( ProxyResponse.ContentType.StartsWith("text/html") )
            {
                HTML.ParseHTML Parser = new HTML.ParseHTML();
                Parser.Source = ResponseDataString;                    
                while( !Parser.Eof() )
                {
                    char ch = Parser.Parse();
                    if ( ch == 0 )
                    {
                        HTML.AttributeList Tag = Parser.GetTag();
                        if ( Tag["href"] != null )
                        {
                            if ( Tag["href"].Value.StartsWith(
                                @"/") )
                            {
                                WriteLog("URL " +  
                                    Tag["href"].Value + 
                                    " modified to " + URLPrefix + 
                                    Tag["href"].Value);
                                ResponseDataString = 
                                    ResponseDataString.Replace(
                                    "\"" + 
                                    Tag["href"].Value + 
                                    "\"", "\"" + 
                                    URLPrefix + Tag["href"].Value + 
                                    "\"");
                            }
                        }

                        if ( Tag["src"] != null )
                        {
                            if ( Tag["src"].Value.StartsWith(
                                @"/") )
                            {
                                WriteLog("URL " +  
                                    Tag["src"].Value + 
                                    " modified to " + 
                                    URLPrefix + Tag["src"].Value);
                                ResponseDataString = 
                                    ResponseDataString.Replace(
                                    "\"" + 
                                    Tag["src"].Value + 
                                    "\"", "\"" + 
                                    URLPrefix + Tag["src"].Value + 
                                    "\"");
                            }
                        }
                    }
                }
                Context.Response.Write(ResponseDataString);
            }
            else
                Context.Response.OutputStream.Write(ResponseData, 0, 
                    ResponseData.Length);

            MemoryStreamResponse.Close();
            StreamResponse.Close();
            ProxyResponse.Close();
        }
        catch ( Exception Ex )
        {
            Context.Response.Write(Ex.Message.ToString());
            WriteLog("An error has occurred while requesting the URL 
                " + ServerURL + "(" + 
                Context.Request.Url.ToString() + ")\n" + 
                Ex.ToString());
        }
    }        

    // Write debug log message
    private void WriteLog(string Message)
    {
        FileStream FS = new FileStream(ConfigurationManager.AppSettings[
            "Log"], FileMode.Append, FileAccess.Write);
        string DateTimeString = DateTime.Now.ToString();
        Message = "[" + DateTimeString + "] " + Message + 
            "\n";
        byte[] FileBuffer = Encoding.ASCII.GetBytes(Message);
        FS.Write(FileBuffer, 0, (int)FileBuffer.Length);
        FS.Flush(); FS.Close();
    }
}
}

Sample web.config Configuration File

<configuration>
 <system.web>
  <httpHandlers>
   <add verb="*" path="*.sync" type="TryHTTPHandler.SyncHandler, 
       TryHTTPHandler" />
  </httpHandlers>
  <customErrors mode="Off" />
  <appSettings>
   <add key="UpchainProxy" value="true"/>
   <add key="Proxy" value="proxy1:80"/>
   <add key="Log" value="D:\\HTTPHandlerLog.rtf"/>
  </appSettings>
 </system.web>
</configuration>

Reverse Proxy Server Setup

In order to setup the Reverse Proxy Server in IIS, the following steps need to be performed.

  1. Compile the project to get .NET assemblies and create a web.config configuration file.
  2. Create a virtual directory in IIS, say "Handler" and copy the .NET assemblies into the "bin" folder of the virtual directory.
  3. Also copy the web.config configuration file to the virtual directory.
  4. Right-click the virtual directory just created and go to Properties>Directory>Configuration>Mappings>Add
  5. Specify the new application extension that will be handled by the Reverse Proxy Server, say ".sync" in the "Extension" field.
  6. In the "Add/Edit Applications Mapping" dialog box, browse and specify aspnet_isapi.dll in the "Executable" field. (For example: c:\windows\microsoft.net\framework\v2.0.50727\aspnet_isapi.dll)
  7. Set the "Verbs" to "All Verbs".
  8. Ensure that "Verify that files exist" is unchecked.
  9. Click "OK" until you close the "Properties" dialog box.
  10. Navigate to the reverse proxy URL in IE. (For example: http://localhost/handler/stg2web.sync)
  11. The filename with ".sync" extension will be taken as the back-end server name.

If you navigate to say, http://localhost/handler/stg2web.sync/tso5/logon.cfm, you will get the response from the back-end server, http://stg2web/tso5/logon.cfm

Specifications

Please note that these proxy features HTTP specifications and DO NOT support HTTPS. The following features are supported.

  1. HTTP GET
  2. HTTP POST
  3. HTTP Cookies
  4. URL Rewriting/Remapping
  5. Debug Logging

History

April 18, 2007 - Baseline (Version 1.0)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Paramesh Gunasekaran
Technical Lead HCL Technologies
India India
Member
Paramesh Gunasekaran is currently working as a Software Engineer in HCL Technologies, India. He obtained his Bachelor's degree in Information Technology from Anna University, India. His research areas include Computational Biology, Artificial Neural Networks and Network Engineering. He has also received international acclaim for authoring industry papers in these areas. He is a Microsoft Certified Professional in ASP.NET/C# and has also been working in .NET technologies for more than 8 years.
 
Web: http://www.paramg.com

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   
QuestionIssues with the sub directory folder access in websitememberMember 812823420 Sep '11 - 9:29 
Hi,
 
The article gave a good insight, although, I am facing an issue when i navigate within the website. When I looked at the log file, it indicates that the CSS files and other Telerik dlls required to display UI controls could not be accessed. Is there some configuration changes that I need to to make in order to access the sub folder containing the CSS or dlls?
 
Any thoughts in this context will help.
 
Thanks
QuestionNothing after ".sync/" worksmemberLondon Bentley7 Sep '11 - 10:24 
I have this code configured and its working as is, if I hit my proxied server's root:
i.e.
http://localhost/handler/stg2web.sync
 
BUT, if I attempt to get a page beyond that, it doesnt work
i.e.
http://localhost/handler/stg2web.sync/tso5/logon.cfm
 
For that I would get a 404. Again I dont have these specific urls, I'm just using your samples. For the page I'm testing, I proxy back the root of my website just perfectly, and its correctly updating links within the html to prepend the proxy server url. But if I attempt to get to anything beyond .sync it throws a 404. Is there something in my application config that needs to be updated? Its as if its not using the handler on anything but that root url
GeneralQueries : Reverse Proxy in C# .NET 2.0memberlihkink12 Oct '10 - 1:48 
I really loved this article of yours, precisly put the points.
 
Just wanted to inform you about the web.config as it needs some modifications and then it worked at my end.
 
BTW, wanted some more information regarding this.
1. How to debug this ?
Is there any way of debugging this Handler ?
 
2. I tried in my company for my company intranet and it worked, I am not sure how.
Any guesses then ?
 
3. Could you please let me know where can I get information about Handlers in ASP .NET ?
GeneralRe: Queries : Reverse Proxy in C# .NET 2.0memberParamesh Gunasekaran29 Nov '10 - 6:39 
to debug, attach VS to inetinfo.exe or aspnet_wp.exe
 
you can get to know about http handlers and modules here
http://msdn.microsoft.com/en-us/library/bb398986.aspx[^]
Param

GeneralMy vote of 1memberSyed Javed26 Aug '10 - 0:38 
poor
GeneralRe: My vote of 1memberBill Seddon12 Nov '11 - 1:44 
It's always great when commenters offer detailed constructive criticism so other can learn from their breadth of experience and so avoid potential pitfalls. Thanks.
QuestionHow to bypass the processrequest?membervitiris4 Jan '10 - 18:10 
I'm using this proxy by the following:
 
google.mydomain.com will get the google.com website
yahoo.mydomain.com will get the yahoo.com website
and so on like that.
 
if i want to go to mydomain.com/login/default.aspx or some other page, how can I bypass the request and go directly to this page ignoring all of the reverseproxy code?
thanks
GeneralOpen Source Reverse ProxymemberParamesh Gunasekaran9 Dec '08 - 3:02 
Can this be developed as a open source reverse proxy in C#.NET? Guys, you're comments are welcome...
 
Param

Generalproblem with appSettingsmemberPrateek Bahl14 Sep '08 - 15:01 
Hi,
When I tried going to http:/localhost/handler I got the following error:
 
Configuration Error
Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.
 
Parser Error Message: Unrecognized configuration section system.web/appSettings.
 
Source Error:
 
Line 7: customerrors mode="Off" />
Line 8:
Line 9: appsettings>
Line 10:
Line 11: add key="UpchainProxy" value="true" />
 

Source File: c:\inetpub\wwwroot\handler\web.config Line: 9
 

(had to get rid of '<' in order to show the error)
I know the reason this error has something to do with appSettings, but not sure why. It would be great if someone can tell me why this is happening and how to solve it.
 
Thanks,
 
Prateek
QuestionRegarding Host Headermemberpmselvan_20005 Jul '08 - 8:27 
My Application requires that the reverse proxy server does not change the host header. It must be forwarded fully transparently from the browser to the server. This is necessary to be able to differentiate between Internet requests and intranet requests.
 
For example, Apache reverse proxy (mod_proxy) has the configuration option 'ProxyPreserveHost' to support this feature.
 
This reverse proxy support this above feature. Pls reply as early. Thanks in advance.
 
Paul
GeneralContent Typemembertauchert22 May '08 - 3:18 
This is similar to http://www.codeproject.com/KB/ajax/ajaxproxy.aspx[^], but there is an important feature needed for XML-Support: content type.
I just added the line
 
Context.Response.ContentType = ProxyResponse.ContentType;
 
before I write the stream.
 
Hope, this helps.
 
Frank
QuestionAjax supportmembereasyal13 Mar '08 - 5:46 
Hi,
 
That piece of code works great.
But since I use some Ajax in my pages, I get errors.
 
Do you have an idea how it could handle ajax requests ?
 
Thanks
Questioncan you develope it if get paid?membernolovelust20 Oct '07 - 9:50 
hi i need this project developed a bit. could you do it if i pay for it? contact via email pls (copluk at gmail.com)
GeneralStrange results with google.commemberneil young13 Oct '07 - 12:42 
- Invalid links
- Google image is not shown
- Problems with content encoding
 
Does not really work.
 
Got better results with http://www.saltypickle.com/Home/16[^]
 
And btw afaik the appsettings have to be specified outside system.web in order to make it run.
 
Regards

GeneralThe proxy name could not be resolved: 'proxy1'membermatchupsports26 Jun '07 - 9:37 

I get it to work for the host stg2web like this:
http://localhost/handler/stg2web.sync/tso5/logon.cfm
 
but with a host name like stg2web.somedomain.com I get:
The proxy name could not be resolved: 'proxy1'
 
Why does this nice code not work with a subdomain and domain as the destination server?
 
Thank you in advance!
 


GeneralRe: The proxy name could not be resolved: 'proxy1'memberGunasekaran Paramesh26 Jun '07 - 12:33 
This code can be configured to use a up-chain proxy server and by default it is configured to use 'proxy1' as the up-chain proxy server. You modify this in the .config file.
 
Param

GeneralRe: The proxy name could not be resolved: 'proxy1'membermatchupsports26 Jun '07 - 13:22 
Yes, I understand this is configurable. Is the up-chain proxy server something I need to install? or configure? Can you provide me the details of what is needed or point me to some references?
 
Why does it work for "hostname" but not "hostname.domain.com"?
 
Thanks, Brad
GeneralRe: The proxy name could not be resolved: 'proxy1'memberGunasekaran Paramesh13 Jul '07 - 5:36 
If you install this reverse proxy in a machine which is not directly connected to internet but via a proxy server, then you need to specify the up-chain proxy. Else you can disable it.
 
Param

QuestionWhy IRequireSessionState?memberHolger Hansen21 Jun '07 - 23:40 
...from a quick glance I didn't see any session involved. - So you just slow down things by having this interface.
 
Thanks for your solution, anyway. Good work!
AnswerRe: Why IRequireSessionState?memberGunasekaran Paramesh13 Jul '07 - 5:38 
I just thought of handling session also. But later I changed my mind and pushed it for future enhancement. Anyways, as of now IRequireSessionState interface is not required and can be removed.
 
Param

QuestionOT, where did you take the images?memberSimone Busoli25 Apr '07 - 1:45 
Sorry for the OT question, I just wanted to ask you where you took the images for the drawing at the top since I'd need some for some articles I'm going to write and can't find any.
 
Simone Busoli

AnswerRe: OT, where did you take the images?memberGunasekaran Paramesh29 May '07 - 0:02 
I jus googled...
 
Param

QuestionHow to use itmemberKhaled Al-Noami23 Apr '07 - 19:29 
Hi,
 
How I can use it. I got message says: unable to find proxy1
 
What is the problem?
 

AnswerRe: How to use itmemberGunasekaran Paramesh23 Apr '07 - 23:04 
Hey buddy,
 
I've redirected the incoming request to the reverse proxy to the upchain proxy (proxy1:80), if you dont want to upchain it, you can modify it.
 
Param

Generalwill it handle httpsmemberDaveAnand23 Apr '07 - 15:35 
will it handle https
 
Dave Anand
DVA Systems Inc.,
www.dvssys.com.com
 


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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 20 Apr 2007
Article Copyright 2007 by Paramesh Gunasekaran
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid