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

Simple Reverse Proxy in C# 2.0 (description and deployment)

By , 28 Nov 2008
 

Structure of Reverse Proxy

Introduction

This article describes how to develop a Reverse Proxy in C# using the IIS HTTPHandlers, not manipulating incoming HTTP requests, but only by transferring all requests to the internal server (Remote Server).

Background

Wikipedia says, "A reverse proxy or surrogate is a proxy server that is installed within the neighborhood of one or more servers. Typically, reverse proxies are used in front of Web servers. All connections coming from the Internet addressed to one of the Web servers are routed through the proxy server, which may either deal with the request itself, or pass the request wholly or partially to the main web servers. A reverse proxy dispatches in-bound network traffic to a set of servers, presenting a single interface to the caller. (...)".

To write the code, I was inspired by articles of Vincent Brossier and Paramesh Gunasekaran here at CodeProject.

Using the Code

The code below is the core of the reverse proxy server.

The class ReverseProxy is the core of the application managing the communication between the navigator and the proxy server... points [1] and [4] on the above picture. The class RemoteServer manages communication between the proxy server (front-end to Internet) and the remote server (internal server)... points [2] and [3] on the above picture.

The class ReverseProxy:

  1. Creates a connection to the remote server to redirect all requests.
  2. Creates a request with the same data in the navigator request.
  3. Sends the request to the remote server and returns the response.
  4. Sends the response to the client (and handles cookies, if available).
  5. Closes streams
namespace ReverseProxy
{
    /// <summary>
    /// Handler all Client's requests and deliver the web site
    /// </summary>
    public class ReverseProxy : IHttpHandler, 
                 System.Web.SessionState.IRequiresSessionState
    {
        /// <summary>
        /// Method calls when client request the server
        /// </summary>
        /// <param name="context">HTTP context for client</param>
        public void ProcessRequest(HttpContext context)
        {
        
            // Create a connexion to the Remote Server to redirect all requests
            RemoteServer server = new RemoteServer(context);
            
            // Create a request with same data in navigator request
            HttpWebRequest request = server.GetRequest();

            // Send the request to the remote server and return the response
            HttpWebResponse response = server.GetResponse(request);
            byte[] responseData = server.GetResponseStreamBytes(response);

            // Send the response to client
            context.Response.ContentEncoding = Encoding.UTF8;
            context.Response.ContentType = response.ContentType;
            context.Response.OutputStream.Write(responseData, 0, 
                             responseData.Length);

            // Handle cookies to navigator
            server.SetContextCookies(response);
            
            // Close streams
            response.Close();
            context.Response.End();

        }
        
        public bool IsReusable
        {
            get { return true; }
        }

    }
}

The class RemoteServer contains many methods:

  • Constructor to initialize the communication with the remote server (defined by the URL).
  • GetRequest creates an HttpWebRequest object connected to the remote server and sends all "parameters" (arguments, POST data, cookies, ...).
  • GetResponse uses the previous object and gets the response from the remote server.
  • GetResponseStreamBytes converts the HttpWebResponse (returned by the previous method) to an array of bytes.
  • SetContextCookies sends cookies to the navigator context.
namespace ReverseProxy
{
    /// <summary>
    /// Manage communication between the proxy server and the remote server
    /// </summary>
    internal class RemoteServer
    {
        string _remoteUrl;
        HttpContext _context;

        /// <summary>
        /// Initialize the communication with the Remote Server
        /// </summary>
        /// <param name="context">Context </param>
        public RemoteServer(HttpContext context)
        {
            _context = context;

            // Convert the URL received from navigator to URL for server
            string serverUrl = ConfigurationSettings.AppSettings["RemoteWebSite"];
            _remoteUrl = context.Request.Url.AbsoluteUri.Replace("http://" + 
                         context.Request.Url.Host + 
                         context.Request.ApplicationPath, serverUrl);
        }

        /// <summary>
        /// Return address to communicate to the remote server
        /// </summary>
        public string RemoteUrl
        {
            get
            {
                return _remoteUrl;
            }
        }

        /// <summary>
        /// Create a request the remote server
        /// </summary>
        /// <returns>Request to send to the server </returns>
        public HttpWebRequest GetRequest()
        {
            CookieContainer cookieContainer = new CookieContainer();

            // Create a request to the server
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_remoteUrl);

            // Set some options
            request.Method = _context.Request.HttpMethod;
            request.UserAgent = _context.Request.UserAgent;
            request.KeepAlive = true;
            request.CookieContainer = cookieContainer;

            // Send Cookie extracted from the incoming request
            for (int i = 0; i < _context.Request.Cookies.Count; i++)
            {
                HttpCookie navigatorCookie = _context.Request.Cookies[i];
                Cookie c = new Cookie(navigatorCookie.Name, navigatorCookie.Value);
                c.Domain = new Uri(_remoteUrl).Host;
                c.Expires = navigatorCookie.Expires;
                c.HttpOnly = navigatorCookie.HttpOnly;
                c.Path = navigatorCookie.Path;
                c.Secure = navigatorCookie.Secure;
                cookieContainer.Add(c);
            }

            // For POST, write the post data extracted from the incoming request
            if (request.Method == "POST")
            {
                Stream clientStream = _context.Request.InputStream;
                byte[] clientPostData = new byte[_context.Request.InputStream.Length];
                clientStream.Read(clientPostData, 0, 
                                 (int)_context.Request.InputStream.Length);

                request.ContentType = _context.Request.ContentType;
                request.ContentLength = clientPostData.Length;
                Stream stream = request.GetRequestStream();
                stream.Write(clientPostData, 0, clientPostData.Length);
                stream.Close();
            }

            return request;

        }

        /// <summary>
        /// Send the request to the remote server and return the response
        /// </summary>
        /// <param name="request">Request to send to the server </param>
        /// <returns>Response received from the remote server
        ///           or null if page not found </returns>
        public HttpWebResponse GetResponse(HttpWebRequest request)
        {
            HttpWebResponse response;

            try
            {
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (System.Net.WebException)
            {                
                // Send 404 to client 
                _context.Response.StatusCode = 404;
                _context.Response.StatusDescription = "Page Not Found";
                _context.Response.Write("Page not found");
                _context.Response.End();
                return null;
            }

            return response;
        }

        /// <summary>
        /// Return the response in bytes array format
        /// </summary>
        /// <param name="response">Response received
        ///             from the remote server </param>
        /// <returns>Response in bytes </returns>
        public byte[] GetResponseStreamBytes(HttpWebResponse response)
        {            
            int bufferSize = 256;
            byte[] buffer = new byte[bufferSize];
            Stream responseStream;
            MemoryStream memoryStream = new MemoryStream();
            int remoteResponseCount;
            byte[] responseData;

            responseStream = response.GetResponseStream();
            remoteResponseCount = responseStream.Read(buffer, 0, bufferSize);

            while (remoteResponseCount > 0)
            {
                memoryStream.Write(buffer, 0, remoteResponseCount);
                remoteResponseCount = responseStream.Read(buffer, 0, bufferSize);
            }

            responseData = memoryStream.ToArray();

            memoryStream.Close();            
            responseStream.Close();

            memoryStream.Dispose();
            responseStream.Dispose();

            return responseData;
        }

        /// <summary>
        /// Set cookies received from remote server to response of navigator
        /// </summary>
        /// <param name="response">Response received
        ///                 from the remote server</param>
        public void SetContextCookies(HttpWebResponse response)
        {
            _context.Response.Cookies.Clear();

            foreach (Cookie receivedCookie in response.Cookies)
            {                
                HttpCookie c = new HttpCookie(receivedCookie.Name, 
                                   receivedCookie.Value);
                c.Domain = _context.Request.Url.Host;
                c.Expires = receivedCookie.Expires;
                c.HttpOnly = receivedCookie.HttpOnly;
                c.Path = receivedCookie.Path;
                c.Secure = receivedCookie.Secure;
                _context.Response.Cookies.Add(c);
            }
        }
    }
}

Sample "Web.Config" File

The config file sets where all the requests received by the reverse proxy server will be sent (for example: 192.168.1.90 on port 81).

<configuration>
  
  <appSettings>    
    <add key="RemoteWebSite" value="http://192.168.1.90:81" />
  </appSettings>
  
  <system.web>
    <httpHandlers>
      <add verb="*" path="*" 
          type="ReverseProxy.ReverseProxy, ReverseProxy"/>
    </httpHandlers>
  </system.web>
  
</configuration>

Deployment

In order to setup the reverse proxy server in IIS, the following steps need to be performed:

  1. Compile the project to get the .NET assemblies, and create a web.config configuration file.
  2. Create a new virtual directory (or a new website) in IIS, and copy the .NET assemblies into the "bin" folder, and the web.config to the root folder.
  3. Right-click the virtual directory just created, and go to "Properties / Home Directory / Configuration > Mappings" (see picture below), and add "wildcard application maps" to "aspnet_isapi.dll" (uncheck Verify that file exists).
  4. Click "OK" until you close the "Properties" dialog box.
  5. Set the correct IP of the remote server in web.config.

IIS Configuration

Points of Interest

This article explains how HTTPHandler works and how to capture the flow received by the web server IIS and transfer it (unchanged) to another server.

History

  • November 21, 2008 - 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

Denis Voituron
Team Leader Trasys
Belgium Belgium
Member
I am a trained civil engineer in computer science. After a few years as project manager of multimedia applications, I set up my IT company for almost 10 years. We created a CMS, software and websites for companies and public administrations. We received the award for “best company” for this job.
 
So, I obtained skills in several areas, and expertise in architecture, development and methodologies (MSF, Oracle, SQL Server, .NET).
 
I also have several years of experience in training for architecture and design software, database administration and network architecture of Windows.

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   
QuestionNice work, but how do you handle 304 code?memberHardy Wang5 Dec '12 - 14:56 
If I request static contents, (image, CSS or JS)
 
			try {
				response = (HttpWebResponse)request.GetResponse();
			} catch (WebException ex) {
			}
 
The exception handler will capture protocol error, with error code 304. In this case we should send back to broswer the same code. But not handled in your version yet.
My open source project Sea Turtle Batch Image Processor
Hardy

QuestionHow is this working?memberTech12329 Nov '12 - 10:53 
How we can access this site. Where should we insert the ReverseProxyDemo.dll ?
I played with HttpHandlers in HandlerMappings of the newly created site. But no use.
 
Can you please provide step by step how to access the remotesite ?
QuestionError could not load type 'ReverseProxy,.ReverseProxy' from assemblymemberRussellJacobs12 Apr '12 - 4:48 
I have followed the steps and I receive this error could not load type 'ReverseProxy,.ReverseProxy' from assembly. Relatively new to attempting this but could use assistance.
AnswerRe: Error could not load type 'ReverseProxy,.ReverseProxy' from assemblymemberGio Bejarasco8 Jun '12 - 9:01 
Oops, I think there's a typo over there.
Shouldn't it be "ReverseProxy, ReverseProxy"?
 
In my experience, it's always safe to specify the full name + assembly. Something like this
type="MyNamespace.MyType, MyAssembly"
QuestionIssues with the urlmemberMaggi121 Sep '11 - 5:02 
Hello,
 
This article provides a very good insight. I did follow the instructions to set up the reverse proxy,but, unable to get it working. My issue is with identifying the correct url.
 
For example, the Reverse Proxy (handler) resides on server1:8080 and the remote server on which my application resides is server2:8080. What should be the final url?
 
http://server1:8080/handler/server2:8080/WebApp/Default.aspx
 
Is this correct assuming that WebApp/Default.aspx is the link to my application. Any suggestions in this context will help.
 
Thanks
AnswerRe: Issues with the urlmemberTech12329 Nov '12 - 10:51 
How we can access this site. Where should we insert the ReverseProxyDemo.dll ?
I played with HttpHandlers in HandlerMappings of the newly created site. But no use.
 
Can you please provide step by step how to access the remotesite ?
GeneralMy vote of 5memberMember 35036912 Aug '10 - 8:16 
Code is clean, follows good coding standards, and the design looks good. An improvement over similar examples of reverse HTTP proxies.
GeneralAdd/Edit this code to make error messages much more descriptivemembertwebb721 Jul '09 - 18:42 
If you love this software proxy (which is great for mocking what happens in hardware) then you've probably seen that whenever an web exception like 500 happens, all you get is the hard coded 404 message defined in the try...catch... in this article.
 
        public HttpWebResponse GetResponse(HttpWebRequest request)
        {
            HttpWebResponse response = null;
 
            try
            {
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (System.Net.WebException e)
            {                
                // Send exception to client 
                _context.Response.StatusCode = (int)((HttpWebResponse)e.Response).StatusCode;
                _context.Response.StatusDescription = e.Status.ToString();
                Stream tResponseStream = e.Response.GetResponseStream();
                StreamReader tResponseStreamReader = new StreamReader(tResponseStream);
                _context.Response.Write(tResponseStreamReader.ReadToEnd());
                _context.Response.End();
 
                return response;
            }
 
            return response;
        }
 
Change the response code to this and the errors from the proxy target will come back to your browser.
 
What is a Jim?

GeneralRe: Add/Edit this code to make error messages much more descriptivememberAndreag711 Jun '11 - 1:19 
        public HttpWebResponse GetResponse(HttpWebRequest request)
        {
            HttpWebResponse response;
 
            try
            {
                response = (HttpWebResponse)request.GetResponse();
            }
            catch (System.Net.WebException ex)
            {
                response = ex.Response as HttpWebResponse;
            }
 
            return response;
        }

QuestionDifferent portmemberDeanBlans26 May '09 - 9:35 
I need to server this on a different port, say 88. It does not work from a different port, any ideas?
AnswerRe: Different portmemberDenis Voituron26 May '09 - 10:18 
You must configure IIS with this port : "WebSite Properties / Advanced".
GeneralRe: Different port [modified]memberDeanBlans26 May '09 - 22:39 
Done that.
 
For example I have setup http://66.39.178.157/Dell to proxy the Dell website. I have also added a certificate to it and https://66.39.178.157/Dell does not work. When trying https I don't see any attempt to retrieve Dell's website to proxy it. Side note the certificate does not match the site. If I change IIS to 88 for http then http://66.39.178.157:88/Dell still will not work. Also setup an additional virtual directory "Photos" which is just http on another box with browsing.
 
I have also setup another site called “test” with it’s own file structure and virtual directories “Dell” and “Photos”. It is on port 99 for http and 444 for https. So http://66.39.178.157:99 and https://66.39.178.157:444 will show the default iisstart.htm page. But http://66.39.178.157:99/Dell will not work.
 
Thanks for looking at this.
 
Screenshots are available at http://66.39.178.157/Screenshots
 
I have taken 66.39.178.157 offline.
 
modified on Sunday, June 14, 2009 12:16 AM

Generalhttpsmembertoronja7230 Apr '09 - 2:18 
does it work for https too?
thank you
GeneralRe: httpsmemberDenis Voituron30 Apr '09 - 9:35 
I've don't try this, but yes... All encryption / decryption processes are executed by IIS (and not by this code)... So, try it, and let's a message here Wink | ;-)
QuestionWorks with aspx, asmx -- but not gif, jpg, css, etc... [modified]membertwebb723 Feb '09 - 9:42 
Any ideas as to why I'm getting a 404 error on any file other than aspx, asmx..
I modified the code so a subdirectory "proxy/" forwards all requests onto another instance of IIS
I can call aspx, asmx pages all day long and it works properly, however if I request "proxy/image.gif" I get a 404 (and it looks like a 404 from IIS hosting the proxy, and not the Response.Write("Page not found"); as listed in your code)
Any help would be immensely appreciated.
Thanks,
Tim
*Edit* PS. Inside the Visual Studio 2008 debugger, all requests get passed properly (I have two web projects in the same solution, operating on different ports to simulate two different servers). This points me to the direction of IIS not passing the request properly, or not handling the response. Like most apps, it works great in the debugger, then craps out in production. (The wildcard was configured per your instruction)
QuestionRe: Works with aspx, asmx -- but not gif, jpg, css, etc...memberDenis Voituron3 Feb '09 - 10:13 
Are you sure to have in your web.config and to exactly use the deploiement method (in article)? With these items, you set IIS to capture all requests (include gif, ...) to the new DLL.
QuestionRe: Works with aspx, asmx -- but not gif, jpg, css, etc... [modified]membertwebb723 Feb '09 - 15:16 
Well, I think I'm closer. The only step I cannot recreate is the "verify if file exists" step in IIS. I'm in IIS 7 (arg).
IIS 7 must be checking to see if the *.gif (etc) files exist before even looking at the handler.
If you can offer any feedback on how to get this to work in 7, let me know.
I will absolutely post my solution if/when it works.
 
*Edit* The httphandler section has a property for "validate=false" but still no luck
 
What is a Jim?
modified on Tuesday, February 3, 2009 9:37 PM

AnswerRe: Works with aspx, asmx -- but not gif, jpg, css, etc...membertwebb723 Feb '09 - 15:54 
Bingo...
My web.config looks like this now...
Please note, I had to unlock IIS 7 to override the handler according to:
http://forums.asp.net/t/1240344.aspx
 

     <system.web>
          <httphandlers>
               <add validate="false" verb="*" path="*" type="ReverseProxy.ReverseProxy, ReverseProxy" />
          </httphandlers>
          <customerrors mode="Off" />
     </system.web>
      <system.webserver>
            <handlers accesspolicy="Read, Execute, Script">
                  <add name="Wildcard" path="*" verb="*" modules="IsapiModule" scriptprocessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourcetype="Unspecified" requireaccess="None" precondition="classicMode,runtimeVersionv2.0,bitness32" />
            </handlers>
      </system.webserver>
 

I hope this helps Smile | :)
 
What is a Jim?
GeneralRe: Works with aspx, asmx -- but not gif, jpg, css, etc...memberGio Bejarasco7 Jun '12 - 20:12 
Hi. I'm experiencing weird behavior wherein CSS is basically broken for any site. Is this similar to your situation?
GeneralRe: Works with aspx, asmx -- but not gif, jpg, css, etc...memberGio Bejarasco8 Jun '12 - 11:45 
Handlers has nothing to do with my problem. It's actually the path to other resources like css, gifs, etc. I'm good now.
AnswerRe: Works with aspx, asmx -- but not gif, jpg, css, etc... [modified]memberTech12329 Nov '12 - 10:51 
How we can access this site. Where should we insert the ReverseProxyDemo.dll ?
I played with HttpHandlers in HandlerMappings of the newly created site. But no use.
 
Can you please provide step by step how to access the remotesite ?
General404memberMoshe Katz8 Jan '09 - 13:52 
For some reason, all I'm getting from this is 404 errors. I have IIS 6 on server 2003 on one IP address and Apache on the same server with a different IP. I am trying to proxy over to apache.
When I look in the IIS logs, they have a 404 for each time I tried.
 
Any ideas?
 
Does it make a difference that the requests are coming in to apache on a non-standard port but getting forwarded to 80?
GeneralRe: 404membertwebb721 Jul '09 - 18:44 
read my response titled:
"Add/Edit this code to make error messages much more descriptive"
Hope that helps.
 
What is a Jim?

GeneralDeployment on Win Server 2003 and IIS 6memberRombolt15 Dec '08 - 9:53 
Hello Denis, great code by the way. That's exactly what I needed.
 
I used your code and changed a few things since I need to modify the message's body before relaying it to the Remote Host and it works great on my workstation.
 
I'm on XP Pro with IIS 5.
 
When I deployed the ReverseProxy on it's dedicated server, which is running Windows Server 2003 and IIS 6 it's a different story. The server accepts the request, sends it to the remote host, gets the answer back but I never get the final answer in the calling app.
 
I get a "Connection with the server has been reset" messasge. This message is generated by the Power Tcp librairy we use to communicate. I even tried installing the calling app on the server itself and I get the same result.
 
I timed every calls and it seems that as soon as the proxy turns around to relay the post to the remote host the connexion is droppep between the server and my calling app.
 
It's been 2 days now that I'm trying to figure out the source of this and I'm not making any progress what so ever.
 
I'm now turning to you... Do you have a hint about what could be causing this?
 
Thank you very much.
 
Rombolt
GeneralRe: Deployment on Win Server 2003 and IIS 6memberDenis Voituron16 Dec '08 - 8:55 
Hello,
 
Thanks... but I've also installed this code on Windows Server 2003 with IIS6 and everything works perfectly.
Do you have a firewall between your reverse proxy and your remote server?
 
Denis

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 28 Nov 2008
Article Copyright 2008 by Denis Voituron
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid