using System;
using System.Collections;
using System.Configuration;
using System.Diagnostics;
using System.Web;
using System.Web.SessionState;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
namespace ReverseProxy {
class HandlerFactory : IHttpHandlerFactory {
ReverseProxy reverseProxy;
public IHttpHandler GetHandler(HttpContext context, string requestType, String url, String pathTranslated) {
if (url.EndsWith("logon.aspx"))
return System.Web.UI.PageParser.GetCompiledPageInstance(url, pathTranslated, context);
else
return ReverseProxy;
}
public void ReleaseHandler(IHttpHandler handler) { }
public ReverseProxy ReverseProxy {
get {
if (reverseProxy == null)
reverseProxy = new ReverseProxy();
return reverseProxy;
}
}
}
public class ReverseProxy: IHttpHandler, IRequiresSessionState {
const int MaximumRedirections = 0;
string backEndSite {
get {
return ConfigurationSettings.AppSettings["BackEndSite"];
}
}
string convertBackEndToFrontEndHtml(string html, string frontEndServerName, string frontEndVirtualPath, string backEndSite) {
/*
prepend frontEndVirtualPath to fix up relative urls as follows: (avoiding "value=" tokens which
are most likely html input text boxes)
href="/targetPath" -> href="/frontEndVirtualPath/targetPath"
href='/targetPath' -> href="/frontEndVirtualPath/targetPath" (handle single quoted urls too)
*/
html = Regex.Replace(html, "(?<!value=)(?:\"|')/(\\S*?)(?:\"|')", "\"" + frontEndVirtualPath + "/" + "$1" + "\"", RegexOptions.Multiline | RegexOptions.IgnoreCase);
/*
also fixup url references in css as follows:
style="background-image: url(/targetPath/myimage.gif)" -> style="background-image: url(/frontEndVirtualPath/targetPath/myimage.gif)"
*/
html = Regex.Replace(html, @"(?:url\()/(\S*?)(?:\))", "url(" + frontEndVirtualPath + "/" + "$1)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
/*
also fixup absolute urls and absolute encoded urls as follows:
http://backEndSite/targetPath/images/myimage.gif --> http://frontEndServerName/frontEndVirtualPath/targetPath/images/myimage.gif
*/
html = Regex.Replace(html, backEndSite, frontEndServerName + frontEndVirtualPath, RegexOptions.Multiline | RegexOptions.IgnoreCase);
html = Regex.Replace(html, HttpContext.Current.Server.UrlEncode(backEndSite), HttpContext.Current.Server.UrlEncode(frontEndServerName + frontEndVirtualPath), RegexOptions.Multiline | RegexOptions.IgnoreCase);
/*
also fixes up strings with "src" and any other token to the left of the equal sign as follows:
src=/ -> src=frontEndVirtualPath/
*/
html = html.Replace("=/","=" + frontEndVirtualPath + "/");
return html;
}
void convertBackEndToFrontEndResponse(HttpWebResponse backEndResponse, HttpRequest frontEndRequest, HttpResponse frontEndResponse) {
try {
Stream backEndResponseStream = getStream(backEndResponse);
write("Content Type of response is [" + backEndResponse.ContentType + "]");
frontEndResponse.ContentType = backEndResponse.ContentType;
foreach(Cookie each in backEndResponse.Cookies) {
if (frontEndResponse.Cookies[each.Name] != null)
frontEndResponse.Cookies.Remove(each.Name);
HttpCookie cookie = new HttpCookie(each.Name, each.Value);
if (each.Domain.IndexOf('.') != -1) // Add domain only if it is dotted - IE doesn't send back the cookie if we set the domain otherwise
cookie.Domain = frontEndRequest.Url.Host;
cookie.Expires = each.Expires;
cookie.Path = each.Path;
cookie.Secure = each.Secure;
frontEndResponse.Cookies.Add(cookie);
}
if ((backEndResponse.ContentType.ToLower().IndexOf("html") >= 0) || (backEndResponse.ContentType.ToLower().IndexOf("javascript")>=0)) {
StreamReader backEndResponseStreamReader = new StreamReader(backEndResponseStream, Encoding.Default);
string backEndResponseHtml = backEndResponseStreamReader.ReadToEnd();
write("********* Start of Raw Backend Response *********");
write(backEndResponseHtml);
write("********* End of Raw Backend Response / Start of Converted Frontend Response *********");
try {
string frontEndHtml = convertBackEndToFrontEndHtml(backEndResponseHtml, frontEndRequest.Url.GetLeftPart(UriPartial.Authority), frontEndRequest.ApplicationPath, backEndSite);
write(frontEndHtml);
write("********* End of Converted Frontend Response *********");
frontEndResponse.ContentEncoding = encodingFor(backEndResponse.ContentEncoding);
write("Content Encoding for response is [" + frontEndResponse.ContentEncoding.ToString() + "]");
frontEndResponse.Write(frontEndHtml);
}
finally {
backEndResponseStreamReader.Close();
}
}
else {
write("Sending opaque content back without modification");
if (backEndResponse.ContentEncoding.Length > 0) {
write("Content Encoding for response is [" + backEndResponse.ContentEncoding + "]");
frontEndResponse.AppendHeader("Content-Encoding", backEndResponse.ContentEncoding);
}
copyStream(backEndResponseStream, frontEndResponse.OutputStream);
}
}
finally {
write("End processing of request");
backEndResponse.Close();
frontEndResponse.End();
}
}
Uri convertFrontEndToBackEndUrl(Uri frontEndUrl, string frontEndVirtualPath, string backEndSite) {
return new Uri(Regex.Replace(frontEndUrl.AbsoluteUri, frontEndUrl.GetLeftPart(UriPartial.Authority) + frontEndVirtualPath, backEndSite, RegexOptions.IgnoreCase));
}
void copyStream(Stream input, Stream output) {
Byte[] buffer = new byte[1024];
int bytes = 0;
while( (bytes = input.Read(buffer, 0, 1024) ) > 0 )
output.Write(buffer, 0, bytes);
}
HttpWebRequest createProxyRequest(HttpRequest originalRequest, Uri uri, string method) {
HttpWebRequest proxyRequest = (HttpWebRequest)WebRequest.Create(uri);
proxyRequest.Timeout = 3600000; // 1 hour max wait time for request to complete
proxyRequest.ContentType = originalRequest.ContentType;
proxyRequest.UserAgent = originalRequest.UserAgent;
proxyRequest.CookieContainer = new CookieContainer();
foreach(String each in originalRequest.Headers) {
if (!WebHeaderCollection.IsRestricted(each))
proxyRequest.Headers.Add(each, originalRequest.Headers.Get(each));
}
proxyRequest.Method = method;
if (method == "POST" && originalRequest.ContentLength > 0) {
write("Sending POST data");
Stream outputStream = proxyRequest.GetRequestStream();
copyStream(originalRequest.InputStream, outputStream);
outputStream.Close();
}
proxyRequest.AllowAutoRedirect = false;
if (HttpContext.Current.Session != null && HttpContext.Current.Session.Count == 2) {
write("Sending basic logon credentials stored in session");
// if we performed basic auth via this reverse proxy and the userid / passwd are stored in the current session then use these auth credentials
proxyRequest.PreAuthenticate = true;
proxyRequest.Credentials = new NetworkCredential(HttpContext.Current.Session["userid"].ToString(), HttpContext.Current.Session["passwd"].ToString());
}
else if(HttpContext.Current.User.Identity.IsAuthenticated) {
// user is already authenticated, therefore use the current ticket when accessing backend server -- should work with both Basic & NTLM auth
write("Sending current authentication ticket that is already in place with backend");
proxyRequest.PreAuthenticate = true;
proxyRequest.Credentials = CredentialCache.DefaultCredentials;
}
return proxyRequest;
}
Encoding encodingFor(string codeName) {
try {
return Encoding.GetEncoding(codeName);
}
catch(Exception) {
return Encoding.Default;
}
}
Stream getStream(HttpWebResponse response) {
Stream compressedStream = null;
if (response.ContentEncoding == "gzip") {
write("Decompressing gzipped response");
compressedStream = new GZipInputStream(response.GetResponseStream());
}
else if (response.ContentEncoding == "deflate") {
write("Decompressing deflated response");
compressedStream = new InflaterInputStream(response.GetResponseStream());
}
if (compressedStream != null) {
MemoryStream decompressedStream = new MemoryStream();
int size = 2048;
byte[] writeData = new byte[2048];
while (true) {
size = compressedStream.Read(writeData, 0, size);
if (size > 0)
decompressedStream.Write(writeData,0,size);
else
break;
}
decompressedStream.Seek(0, SeekOrigin.Begin);
return decompressedStream;
}
else
return response.GetResponseStream();
}
bool isRedirection(HttpStatusCode code) {
string statusCode = Enum.Format(typeof(HttpStatusCode), code, "d");
return statusCode.StartsWith("3");
}
public bool IsReusable {
get {
return true;
}
}
string methodToUse(HttpRequest originalRequest, HttpWebResponse response) {
if (response == null) {
write("Request is a " + originalRequest.HttpMethod);
return originalRequest.HttpMethod;
}
if (originalRequest.HttpMethod == "POST" && (response.StatusCode == HttpStatusCode.RedirectKeepVerb || response.StatusCode == HttpStatusCode.TemporaryRedirect)) {
write("Request is a POST");
return "POST";
}
else {
write("Request is a GET");
return "GET";
}
}
string parseRealm(string authHeader) {
Regex regex = new Regex(".*=\\\"(.*)\"");
Match match = regex.Match(authHeader);
if (match.Success)
return match.Groups[1].Value;
else
return "";
}
public void ProcessRequest(HttpContext context) {
HttpRequest frontEndRequest = context.Request;
HttpResponse frontEndResponse = context.Response;
Uri frontEndUrl = frontEndRequest.Url;
write("Receiving request for [" + frontEndUrl.AbsoluteUri + "]");
Uri backEndUrl = convertFrontEndToBackEndUrl(frontEndUrl, frontEndRequest.ApplicationPath, backEndSite);
write("Converting request to [" + backEndUrl.AbsoluteUri + "]");
HttpWebRequest proxyRequest = null;
HttpWebResponse backEndResponse = null;
try {
int timesRedirected = 0;
do {
proxyRequest = createProxyRequest(frontEndRequest, backEndUrl, methodToUse(frontEndRequest, backEndResponse));
if (frontEndRequest.UrlReferrer != null) {
Uri backEndReferUrl = convertFrontEndToBackEndUrl(frontEndRequest.UrlReferrer, frontEndRequest.ApplicationPath, backEndSite);
proxyRequest.Referer = backEndReferUrl.AbsoluteUri;
}
foreach(string each in frontEndRequest.Cookies) {
HttpCookie requestCookie = frontEndRequest.Cookies[each];
Cookie cookie = new Cookie(requestCookie.Name, requestCookie.Value);
if (requestCookie.Domain == null)
cookie.Domain = backEndUrl.Host;
cookie.Expires = requestCookie.Expires;
cookie.Path = requestCookie.Path;
cookie.Secure = requestCookie.Secure;
proxyRequest.CookieContainer.Add(cookie);
}
write("Sending request to backend and getting response");
backEndResponse = proxyRequest.GetResponse() as HttpWebResponse;
write("Status code of response is [" + backEndResponse.StatusCode.ToString() + "]");
if (isRedirection(backEndResponse.StatusCode)) {
timesRedirected++;
String newLocation = backEndResponse.Headers["Location"];
if(newLocation.IndexOf("://") == -1)
newLocation = backEndUrl.GetLeftPart(UriPartial.Authority) + newLocation;
backEndUrl = new Uri(newLocation);
write("Being redirected to [" + backEndUrl.AbsoluteUri + "]");
}
if (!isRedirection(backEndResponse.StatusCode) || timesRedirected >= MaximumRedirections) {
if (timesRedirected >= MaximumRedirections) warn("Exceeded maximum redirections");
break;
}
} while (true);
}
catch(System.Net.WebException webException) {
HttpWebResponse webResponse = webException.Response as HttpWebResponse;
if (webResponse != null) {
if (webResponse.StatusCode == HttpStatusCode.Unauthorized) {
string realm = parseRealm(webResponse.GetResponseHeader("WWW-AUTHENTICATE"));
warn("Unauthorized...redirecting to logon page");
frontEndResponse.Redirect(frontEndRequest.ApplicationPath + "/logon.aspx?Realm=" + context.Server.UrlEncode(realm) + "&ReturnUrl=" + context.Server.UrlEncode(frontEndUrl.PathAndQuery));
return;
}
frontEndResponse.StatusCode = (int)webResponse.StatusCode;
frontEndResponse.StatusDescription = webResponse.StatusDescription;
}
if (webException.Response == null) {
frontEndResponse.Write("<p>" + webException.Status + "</p>");
frontEndResponse.Write("<p>" + webException.Message + "</p>");
}
else {
frontEndResponse.ContentType = webException.Response.ContentType;
Stream responseStream = webException.Response.GetResponseStream();
copyStream(responseStream, frontEndResponse.OutputStream);
responseStream.Close();
}
warn(webException.Message, webException);
warn("Abnormal end to processing of request");
frontEndResponse.End();
return;
}
switch((int) backEndResponse.StatusCode) {
case 301:
case 302:
case 303:
case 307:
frontEndResponse.StatusCode = (int) backEndResponse.StatusCode;
string newLocation = backEndResponse.Headers["Location"];
newLocation = Regex.Replace(newLocation, backEndSite,
frontEndRequest.Url.GetLeftPart(UriPartial.Authority) + frontEndRequest.ApplicationPath,
RegexOptions.IgnoreCase);
frontEndResponse.RedirectLocation = newLocation;
break;
}
convertBackEndToFrontEndResponse(backEndResponse, frontEndRequest, frontEndResponse);
}
[Conditional("TRACE")]
void warn(string message) {
StackTrace stack = new StackTrace(1, true);
StackFrame frame = stack.GetFrame(0);
HttpContext.Current.Trace.Warn(frame.GetMethod().Name, message);
}
[Conditional("TRACE")]
void warn(string message, Exception exception) {
StackTrace stack = new StackTrace(1, true);
StackFrame frame = stack.GetFrame(0);
HttpContext.Current.Trace.Warn(frame.GetMethod().Name, message, exception);
}
[Conditional("TRACE")]
void write(string message) {
StackTrace stack = new StackTrace(1, true);
StackFrame frame = stack.GetFrame(0);
HttpContext.Current.Trace.Write(frame.GetMethod().Name, message);
}
}
}