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
:
- Creates a connection to the remote server to redirect all requests.
- Creates a request with the same data in the navigator request.
- Sends the request to the remote server and returns the response.
- Sends the response to the client (and handles cookies, if available).
- Closes streams
namespace ReverseProxy
{
public class ReverseProxy : IHttpHandler,
System.Web.SessionState.IRequiresSessionState
{
public void ProcessRequest(HttpContext context)
{
RemoteServer server = new RemoteServer(context);
HttpWebRequest request = server.GetRequest();
HttpWebResponse response = server.GetResponse(request);
byte[] responseData = server.GetResponseStreamBytes(response);
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.ContentType = response.ContentType;
context.Response.OutputStream.Write(responseData, 0,
responseData.Length);
server.SetContextCookies(response);
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
{
internal class RemoteServer
{
string _remoteUrl;
HttpContext _context;
public RemoteServer(HttpContext context)
{
_context = context;
string serverUrl = ConfigurationSettings.AppSettings["RemoteWebSite"];
_remoteUrl = context.Request.Url.AbsoluteUri.Replace("http://" +
context.Request.Url.Host +
context.Request.ApplicationPath, serverUrl);
}
public string RemoteUrl
{
get
{
return _remoteUrl;
}
}
public HttpWebRequest GetRequest()
{
CookieContainer cookieContainer = new CookieContainer();
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_remoteUrl);
request.Method = _context.Request.HttpMethod;
request.UserAgent = _context.Request.UserAgent;
request.KeepAlive = true;
request.CookieContainer = cookieContainer;
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);
}
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;
}
public HttpWebResponse GetResponse(HttpWebRequest request)
{
HttpWebResponse response;
try
{
response = (HttpWebResponse)request.GetResponse();
}
catch (System.Net.WebException)
{
_context.Response.StatusCode = 404;
_context.Response.StatusDescription = "Page Not Found";
_context.Response.Write("Page not found");
_context.Response.End();
return null;
}
return response;
}
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;
}
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:
- Compile the project to get the .NET assemblies, and create a web.config configuration file.
- 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.
- 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).
- Click "OK" until you close the "Properties" dialog box.
- Set the correct IP of the remote server in web.config.
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).