├── appveyor.yml ├── LICENSE.md ├── README.md └── code └── ReverseProxy.cs /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | build_script: 3 | - cmd: build.cmd 4 | artifacts: 5 | - path: out\*.* 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 1998 - 2009, Paul Johnston & Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | IIS Reverse Proxy 3 | ================= 4 | 5 | [![Build status](https://ci.appveyor.com/api/projects/status/v4rbwgojpj3bsmq7/branch/master?svg=true)](https://ci.appveyor.com/project/kveretennicov/iisproxy/branch/master) 6 | 7 | By Paul Johnston, http://pajhome.org.uk/ 8 | 9 | This is a simple C# script to add reverse proxy functionality to the IIS web 10 | server. It has been very useful for hosting TurboGears applications behind 11 | IIS, and could be used for other frameworks like Ruby on Rails and Django. 12 | 13 | 14 | Using The Script 15 | ---------------- 16 | 17 | 1. Create the directory you want to be proxied, within the webroot. We'll call 18 | this the "proxy directory". 19 | 2. Copy the "bin" subdirectory from the IIS proxy souce into the proxy 20 | directory. 21 | 3. In IIS Manager, locate the proxy directory and select "Properties" 22 | 4. In the "Directory" tab, under "Application Settings", select "Create" 23 | 5. Click "Configuration" 24 | 6. In the "Mappings" tab, under "Wildcard application maps", select "Insert" 25 | 7. Enter "C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll" 26 | 8. Uncheck "Verify that file exists" 27 | 9. Click "Ok" three times to return to IIS Manager 28 | 10. In the proxy directory, create a file web.config with the following 29 | content, replacing {URL} with the URL of the back-end site. Note that the 30 | URL must not have any path component, or a trailing slash. 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Directory Layout 45 | ---------------- 46 | 47 | The directory layout on the back-end site must exactly match that on the 48 | front-end site. For example, if the front-end site is configured so that 49 | http://front/mydir is proxied to http://back/ then a request for 50 | http://front/mydir/myfile is proxies to http://back/mydir/myfile 51 | The reason for this is to avoid the proxy doing any link rewriting. While 52 | some proxies attempt this, I've found it unreliable. Reconfiguring the 53 | back-end is a better option. 54 | 55 | 56 | User Identity 57 | ------------- 58 | 59 | By default, the script uses its default credentials for authentication to the 60 | back-end. If the directory is configured to use Windows authentication and 61 | impersonation, the script will authenticate to the back-end with the identity 62 | of the client user. Otherwise, it will be the user the script is running as. 63 | 64 | There is an option to enable a different authentication model. 65 | 66 | 67 | 68 | If this option is specified, the script with present basic authentication to 69 | the back-end. The user name will be the user the script is running as, and 70 | the password the value specified. This can be useful for fronting a legacy 71 | application with a single sign-on interface. 72 | 73 | The also adds the current user name into the back-end request, as the 74 | Remote-User header. Relying on this for security can carry some risks - a 75 | header is potentially spoofable. The back-end application must be deployed so 76 | it can only be accessed from trusted sources, usually by making it only listen 77 | on the 127.0.0.1 interface. 78 | 79 | 80 | Tracing Redirects 81 | ----------------- 82 | 83 | To aid debugging, the script has the ability to record redirect locations 84 | generated by the back end. These are recorded in the event log, in the 85 | application section and with the source "iisproxy". To enable this option, add 86 | the following to the appSettings: 87 | 88 | 89 | 90 | iisproxy may need to be given permission to access the error log. If you 91 | encounter problems, see http://support.microsoft.com/kb/329291 92 | 93 | Why IIS? 94 | -------- 95 | 96 | Serving behind IIS is useful for IIS-specific features, and also if an 97 | existing server is already using IIS. I found the IIS implementation of 98 | integrated Windows authentication to be very reliable, while mod_auth_sspi 99 | for Apache has some problems, including issues with inter-forest trusts. 100 | 101 | 102 | Aims of Script 103 | -------------- 104 | 105 | The script aims to be simple and to get in the way as little as possible. It 106 | does not do any link rewriting. The proxy passes nearly everything in both 107 | directions, including POST data, headers (e.g. Content-Type), cookies, etc. 108 | 109 | 110 | Credits 111 | ------- 112 | 113 | This script has taken inspiration from two existing scripts: 114 | 115 | 1. Simple HTTP Reverse Proxy with ASP.NET and IIS, by Vincent Brossier 116 | (http://www.123aspx.com/redir.aspx?res=32037) 117 | 118 | 2. IIS Reverse Proxy, by John Pierce, john@pierce.name 119 | (no longer online) 120 | -------------------------------------------------------------------------------- /code/ReverseProxy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Web; 4 | using System.Net; 5 | using System.Text; 6 | using System.IO; 7 | using System.Diagnostics; 8 | 9 | namespace ReverseProxy 10 | { 11 | public class ReverseProxy: IHttpHandler 12 | { 13 | EventLog event_log; 14 | public ReverseProxy() 15 | { 16 | // Create the event logging stream 17 | if(ConfigurationManager.AppSettings.Get("traceRedirect") != null) 18 | { 19 | string source = "iisproxy"; 20 | if (!EventLog.SourceExists(source)) { 21 | EventLog.CreateEventSource(source, "Application"); 22 | } 23 | event_log = new EventLog(); 24 | event_log.Source = source; 25 | } 26 | } 27 | 28 | public void ProcessRequest(HttpContext context) 29 | { 30 | // Create the web request to communicate with the back-end site 31 | string remoteUrl = ConfigurationManager.AppSettings["ProxyUrl"] + 32 | context.Request.Path; 33 | if (context.Request.QueryString.ToString() != "") 34 | remoteUrl += "?" + context.Request.QueryString; 35 | HttpWebRequest request = (HttpWebRequest) WebRequest.Create(remoteUrl); 36 | request.AllowAutoRedirect = false; 37 | request.Method = context.Request.HttpMethod; 38 | request.ContentType = context.Request.ContentType; 39 | request.UserAgent = context.Request.UserAgent; 40 | string basicPwd = ConfigurationManager.AppSettings.Get("basicPwd"); 41 | request.Credentials = basicPwd == null ? 42 | CredentialCache.DefaultCredentials : 43 | new NetworkCredential(HttpContext.Current.User.Identity.Name, basicPwd); 44 | request.PreAuthenticate = true; 45 | // The Remote-User header is non-ideal; included for compatibility 46 | request.Headers["Remote-User"] = HttpContext.Current.User.Identity.Name; 47 | foreach(String each in context.Request.Headers) 48 | if (!WebHeaderCollection.IsRestricted(each) && each != "Remote-User") 49 | request.Headers.Add(each, context.Request.Headers.Get(each)); 50 | if (context.Request.HttpMethod == "POST") 51 | { 52 | Stream outputStream = request.GetRequestStream(); 53 | CopyStream(context.Request.InputStream, outputStream); 54 | outputStream.Close(); 55 | } 56 | 57 | HttpWebResponse response; 58 | try 59 | { 60 | response = (HttpWebResponse) request.GetResponse(); 61 | } 62 | catch(System.Net.WebException we) 63 | { 64 | response = (HttpWebResponse) we.Response; 65 | if(response == null) 66 | { 67 | context.Response.StatusCode = 13; 68 | context.Response.Write("Could not contact back-end site"); 69 | context.Response.End(); 70 | return; 71 | } 72 | } 73 | 74 | // Copy response from server back to client 75 | context.Response.StatusCode = (int) response.StatusCode; 76 | context.Response.StatusDescription = response.StatusDescription; 77 | context.Response.ContentType = response.ContentType; 78 | if(response.Headers.Get("Location") != null) 79 | { 80 | if(ConfigurationManager.AppSettings.Get("traceRedirect") != null) 81 | event_log.WriteEntry("Back-end redirecting to: " + response.Headers.Get("Location"), EventLogEntryType.Information); 82 | string urlSuffix = response.Headers.Get("Location"); 83 | if(urlSuffix.ToLower().StartsWith(ConfigurationManager.AppSettings["ProxyUrl"].ToLower())) 84 | urlSuffix = urlSuffix.Substring(ConfigurationManager.AppSettings["ProxyUrl"].Length); 85 | context.Response.AddHeader("Location", context.Request.Url.GetLeftPart(UriPartial.Authority) + urlSuffix); 86 | } 87 | foreach(String each in response.Headers) 88 | if(each != "Location" && !WebHeaderCollection.IsRestricted(each)) 89 | context.Response.AddHeader(each, response.Headers.Get(each)); 90 | CopyStream(response.GetResponseStream(), context.Response.OutputStream); 91 | response.Close(); 92 | context.Response.End(); 93 | } 94 | 95 | static public void CopyStream(Stream input, Stream output) { 96 | Byte[] buffer = new byte[1024]; 97 | int bytes = 0; 98 | while( (bytes = input.Read(buffer, 0, 1024) ) > 0 ) 99 | output.Write(buffer, 0, bytes); 100 | } 101 | 102 | public bool IsReusable 103 | { 104 | get 105 | { 106 | return true; 107 | } 108 | } 109 | } 110 | } 111 | --------------------------------------------------------------------------------