ReverseProxy.cs

From
Revision as of 07:29, 12 May 2006 by Wolfm (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
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);
		}
	}
}