Click here to Skip to main content
15,888,527 members
Articles / Web Development / ASP.NET
Article

Simple HTTP Reverse Proxy with ASP.NET and IIS

Rate me:
Please Sign up or sign in to vote.
4.27/5 (25 votes)
22 May 20042 min read 598.8K   10.8K   91   82
Learn how easy it is to create HTTP Reverse Proxy in .NET using IIS.

Introduction

Sample Image - reverse-proxy.png

A reverse proxy is the same as a proxy except instead of delivering pages for internal users, it delivers them for external users. It can be used to take some load off web servers and provide an additional layer of protection. If you have a content server that has sensitive information that must remain secure, you can set up a proxy outside the firewall as a stand-in for your content server. When outside clients try to access the content server, they are sent to the proxy server instead. The real content resides on your content server, safely inside the firewall. The proxy server resides outside the firewall, and appears to the client to be the content server.

Where to use?

If you have an intranet with IP filtered security, you can offer the functionality of external consultation. You must just add the authentication system of your choice.

Functionality

This reverse proxy can run in two modes:

  • Mode 0: all web sites can be requested by clients with an URL like http://RevrseProxyURL/http//www.site.com/
  • Mode 1: only one web site can be requested. When the user requests the web application address, the proxy returns the content of this web site.

You can setup this mode in the web configuration file of you web application:

XML
<appSettings>
    <!-- PROXY Mode
  0 : all web site can be requested by a client with 
      an url like <A href="http://reverseproxyurl/http//www.site.com/">http://ReverseProxyURL/http//www.site.com/</A>
  1 : Only on web site can be resuested by clients, \
      In this case Web Application uses RemoteWebSite variable 
      to deliver the content of the web site.
    -->
   <add key="ProxyMode" value="1" />
   <add key="RemoteWebSite" value="<A href="http://www.codeproject.com/">http://www.codeproject.com/</A>" />
</appSettings>

The code

Create an HttpHandler to intercept all requests:

C#
using System;
using System.Configuration;
using System.Web;
using System.Net;
using System.Text;
using System.IO; 
namespace ReverseProxy
{
  /// <summary>
  /// Handler that intercept Client's request and deliver the web site
  /// </summary>

  public class ReverseProxy: IHttpHandler
  {
    /// <summary>
    /// Method calls when client request the server
    /// </summary>
    /// &;lt;param name="context">HTTP context for client</param>

    public void ProcessRequest(HttpContext context)
    {
      //read values from configuration 
      fileint proxyMode = 
        Convert.ToInt32(ConfigurationSettings.AppSettings["ProxyMode"]);
      string remoteWebSite = 
        ConfigurationSettings.AppSettings["RemoteWebSite"];
      string remoteUrl;
      if (proxyMode==0)
        remoteUrl= ParseURL(context.Request.Url.AbsoluteUri);
      //all site 
      acceptedelseremoteUrl= 
        context.Request.Url.AbsoluteUri.Replace("http://"+
        context.Request.Url.Host+
        context.Request.ApplicationPath,remoteWebSite);
      //only one site accepted
      //create the web request to get the remote stream
      HttpWebRequest request = 
        (HttpWebRequest)WebRequest.Create(remoteUrl);
      //TODO : you can add your own credentials system
      //request.Credentials = CredentialCache.DefaultCredentials;
      HttpWebResponse response;
      try
      {
        response = (HttpWebResponse)request.GetResponse ();
      }
      catch(System.Net.WebException we)
      {
        //remote url not found, send 404 to client 
        context.Response.StatusCode = 404;
        context.Response.StatusDescription = "Not Found";
        context.Response.Write("<h2>Page not found</h2>");
        context.Response.End();
        return;
      }
      Stream receiveStream = response.GetResponseStream();

      if ((response.ContentType.ToLower().IndexOf("html")>=0) 
        ||(response.ContentType.ToLower().IndexOf("javascript")>=0))
      {
        //this response is HTML Content, so we must parse it
        StreamReader readStream = 
          new StreamReader (receiveStream, Encoding.Default);
        Uri test = new Uri(remoteUrl);
        string content;
        if (proxyMode==0)
          content= ParseHtmlResponse(readStream.ReadToEnd(), 
            context.Request.ApplicationPath+"/http//"+test.Host);
        else
          content= ParseHtmlResponse(readStream.ReadToEnd(),
            context.Request.ApplicationPath);
        //write the updated HTML to the client
        context.Response.Write(content);
        //close streamsreadStream.Close();
        response.Close();
        context.Response.End();
      }
      else
      {
        //the response is not HTML 
        Contentbyte[] buff = new byte[1024];
        int bytes = 0;
        while( ( bytes = receiveStream.Read( buff, 0, 1024 ) ) > 0 )
        {
          //Write the stream directly to the client 
          context.Response.OutputStream.Write (buff, 0, bytes );
        }
        //close streams
        response.Close();
        context.Response.End();
      }
    }

    /// <summary>
    /// Get the remote URL to call
    /// </summary>
    /// <param name="url">URL get by client</param>
    /// <returns>Remote URL to return to the client</returns>

    public string ParseURL(string url)
    {
      if (url.IndexOf("http/")>=0)
      {
        string externalUrl=url.Substring(url.IndexOf("http/"));
        return externalUrl.Replace("http/","http://") ;
      }
      else
        return url;
    }

    /// <summary>
    /// Parse HTML response for update links and images sources
    /// </summary>
    /// <param name="html">HTML response</param>
    /// <param name="appPath">Path of application for replacement</param>
    /// <returns>HTML updated</returns>
    public string ParseHtmlResponse(string html,string appPath)
    {
      html=html.Replace("\"/","\""+appPath+"/");
      html=html.Replace("'/","'"+appPath+"/");
      html=html.Replace("=/","="+appPath+"/");
      return html;
    }
    ///
    /// Specifies whether this instance is reusable by other Http requests
    ///
    public bool IsReusable
    {
      get
      {
        return true;
      }
    }
  }
}

Configure the handler in web.config

You must add these lines in web.config file to redirect all user queries to the HTTPHandler:

XML
<httpHandlers>
   <add verb="*" path="*" type="ReverseProxy.ReverseProxy, ReverseProxy" />
</httpHandlers>

Configure IIS

If you want to process a request with any file extension, then you need to change IIS to pass all requests through to the ASP.NET ISAPI extension. Add the HEAD, GET and POST verbs to all files with .* file extension and map those to the ASP.NET ISAPI extension - aspnet_isapi.dll (in your .NET framework directory). The complete range of mappings, includes the new .* mapping.

TODO's

Now, you can develop your own security system, based on form, Windows or passport authentication.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
France France
Vincent Brossier is a software engineer from Paris, FRANCE, specializing in .Net.
Vincent is in charge of development of the Parisian university's Intranet, I take part in a nationnal project of numerical campus.
His role is to seek best technicals solutions to satisfy the 35000 users of this Intranet. Specialized in the development of distributed applications, vincent work primarily with technologies microsoft .NET (C#, ASP.NET, Webservices, SQLServer) even if he have also work with java technologies (Peer-to-peer sharing documents tool).
Vincent have also set up Open-Source solutions such as SPIP in nursery schools.
When he's not programming, he enjoys playing Guitar.

Comments and Discussions

 
QuestionNot Work in internet Pin
Dothanhhai2-Nov-12 9:57
Dothanhhai2-Nov-12 9:57 
QuestionWhen size of page to display too big, hangs briefly, renders incomplete page Pin
Jim Cross23-Nov-11 8:53
Jim Cross23-Nov-11 8:53 
QuestionSpurious null character added to end of stream? Pin
Steven Hirschorn25-Aug-11 1:46
Steven Hirschorn25-Aug-11 1:46 
Questionreverse proxy and WCF-Service Pin
walter.anna24-Jan-10 1:56
walter.anna24-Jan-10 1:56 
Questionhow can i bypass the processrequest method? Pin
vitiris4-Jan-10 18:11
vitiris4-Jan-10 18:11 
QuestionHow to set it up? Pin
maria_mir7-Jul-09 20:48
maria_mir7-Jul-09 20:48 
AnswerRe: How to set it up? Pin
maria_mir12-Jul-09 22:18
maria_mir12-Jul-09 22:18 
AnswerRe: How to set it up? Pin
eric_ruck4-Aug-11 5:44
eric_ruck4-Aug-11 5:44 
GeneralSome changes Pin
rickleb21-Mar-09 6:16
rickleb21-Mar-09 6:16 
I used this code to set up another IIS 6 website in parallel with my public site. This is a secure (SSL) site. So I wanted to use https externally to access internal http servers (Nagios running on Apache on Linux). Here is a summary of the changes that I made. First, note that I did not test the "all sites" proxy mode, just the individual site mode.

So my IIS 6 setup looks like this:

Default site (my primary HTTPS site)
nagios1 (my first nagios server)
nagios2 (my second nagios server)

nagios1 and nagios2 each have the ReverseProxy application installed in the root. On IIS 5 & 6, if you want to map all mime types to the ihttphandler, you must do it for the entire site. That is why I didn't just use a virtual directory. IIS 7 removes this restraint.

The first change was to this section:

string remoteUrl;
if (proxyMode==0)
   remoteUrl= ParseURL(context.Request.Url.AbsoluteUri); //all site accepted
else
  remoteUrl= context.Request.Url.AbsoluteUri.Replace("http://"+
        context.Request.Url.Host+context.Request.ApplicationPath,remoteWebSite);



There were 2 issues. The first was that "http://" was hard coded. Wouldn't work for https. In my case, the external site was https and internally it was http. After the following changes, this wasn't an issue. The second issue was that sometimes "proxy.aspx" was in the URL, sometimes not. I put some code in to check for this as well:

string remoteUrl;

string sProtocol = context.Request.Url.Scheme + "://" +
          context.Request.Url.Host +  context.Request.ApplicationPath;
remoteUrl = context.Request.Url.AbsoluteUri.Replace(sProtocol, remoteWebSite); //only one site accepted
int iLength = remoteUrl.LastIndexOf("proxy.aspx");
if (iLength > 0)
   remoteUrl = remoteUrl.Substring(0, iLength);


A second issue was that on any error, "404" was returned, which made it very hard to determine what the real back end error was. I changed the exception block to pass along the backend error with a description:

Original code:

catch(System.Net.WebException we)
{
//remote url not found, send 404 to client
   context.Response.StatusCode = 404;
   context.Response.StatusDescription = "Not Found";
   context.Response.Write("<h2>Page not found</h2>");
   context.Response.End();
   return;
}


New code:
catch(System.Net.WebException we)
{
   int     iStatus;
   string  sStatus;
   //remote url not found, send status to client
   sStatus = we.ToString();
   int iIndex = we.ToString().IndexOf('(');
   iStatus = 491; // Our own error code if theirs is invalid - unlikely
   if (iIndex != -1)
           iStatus = Convert.ToInt32(we.ToString().Substring(++iIndex, 3));
   context.Response.StatusCode = iStatus;
   context.Response.StatusDescription = we.Message;
   context.Response.Write("<h2>From ReverseProxy - " + we.Message +
           "</h2><br><center><h3><br>" + we.Data + "<br>URL: " +
                  we.Response.ResponseUri + "</br></br></h3></center>");
   context.Response.End();
   return;
}</br>



Lastly, the remapping of the links didn't work if the application was in the web site's root directory. This was because the replacement string was basically a single '/'. In this case, the browser would not have the correct relative path or the absolute path.

The solution was easy. Pass the absolute path. So

content= ParseHtmlResponse(readStream.ReadToEnd(),context.Request.ApplicationPath);


becomes

content = ParseHtmlResponse(readStream.ReadToEnd(), sProtocol);


Note that "sProtocol" was calculated above.

And finally, change ParseHtmlResponse from

public string ParseHtmlResponse(string html,string appPath)
{
   html=html.Replace("\"/","\""+appPath+"/");
   html=html.Replace("'/","'"+appPath+"/");
   html=html.Replace("=/","="+appPath+"/");
   return html;
}


to

public string ParseHtmlResponse(string html,string appPath)
{
   html = html.Replace("\"/", "\"" + appPath);
   html = html.Replace("'/", "'" + appPath);
   html = html.Replace("=/", "=" + appPath);
   return html;
}


Since the trailing backslash is already included in sProtocol.

These changes did not break the way the code worked originally, but they do make it more robust and work in more environments.

Finally, to save others a lot of heartache, here is a link to an article on how to create a server certificate that can be shared by different sites on the same machine using SSL host headers. This is non-obvious, and you can't have multiple SSL sites without it.

http://thelazyadmin.com/blogs/thelazyadmin/archive/2006/06/16/IIS-6.0-and-SSL-Host-Headers.aspx[^]

Also, remember that the "common name" for each site must be in DNS so IIS knows which site to direct the request to.

Now you can host multiple servers from a single IIS servers, using https externally and http internally ig you desire.

Hope this helps others.
Rick Bross

Hope this helps others.
GeneralRe: Some changes Pin
MunirS30-Aug-09 2:04
MunirS30-Aug-09 2:04 
GeneralPour Infos Pin
Member 210144124-Sep-08 3:34
Member 210144124-Sep-08 3:34 
GeneralInstallation Instructions Pin
Chris Spencer15-Jul-08 3:17
Chris Spencer15-Jul-08 3:17 
GeneralRegarding Host Header Pin
pmselvan_20005-Jul-08 8:29
pmselvan_20005-Jul-08 8:29 
GeneralRe: Regarding Host Header Pin
MunirS30-Aug-09 1:56
MunirS30-Aug-09 1:56 
GeneralRe: Regarding Host Header Pin
Stimphy21-Dec-09 15:03
Stimphy21-Dec-09 15:03 
GeneralError Pin
VJLaks16-Jun-08 20:39
VJLaks16-Jun-08 20:39 
GeneralDerivative Work Pin
Paul Johnston4-May-08 4:35
Paul Johnston4-May-08 4:35 
GeneralErrors Pin
hanisacsouka13-Apr-08 2:32
hanisacsouka13-Apr-08 2:32 
Generalhttp://www.site.com:8080/ Pin
hanisacsouka12-Apr-08 8:05
hanisacsouka12-Apr-08 8:05 
GeneralIIS 7.0 Pin
brice2nice13-Nov-07 10:50
brice2nice13-Nov-07 10:50 
Generalenabling file download Pin
Tarak Vandara5-Sep-07 19:49
Tarak Vandara5-Sep-07 19:49 
GeneralVery usefull for me. View my project for download files from rapidshare.com based on this sample [modified] Pin
W.M. Spon29-Jun-07 22:54
W.M. Spon29-Jun-07 22:54 
GeneralI couldnt get it work Pin
jeanmoura12-Apr-07 5:01
jeanmoura12-Apr-07 5:01 
Questionimage path doesnt work Pin
nolovelust5-Apr-07 10:45
nolovelust5-Apr-07 10:45 
AnswerRe: image path doesnt work Pin
MySuperName11-Jan-09 21:51
MySuperName11-Jan-09 21:51 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.