Search code examples
c#asp.neturl-rewritinghttpcontext

Rewrite HOST using ASP.NET C#


I'm trying to redirect the HOST rather than the path of a URL, so external sites can access internal API resources. To keep it simple, for this example, the host will be changed based on a header.

At the moment, I'm just running in Visual Studio. The address will be :

http://externalsite.company.com/testapi/myapi.asmx/GetNewKey

I want it to be changed to:

http://internalsite1.local/testapi/myapi.asmx/GetNewKey

based on the value in header "hostingAuth".

There will be a headers and body coming through to the page which will be consumed by the "internalsiteX.local" server. This will vary from company to company, so I can't account for all possibilities.

At the moment, my security token is header "hostingAuth" and the example below, the only valid tokens are "company1secret" and "company2secret"

What I think I want is the rewrite module, but that requires me to statically code the rewrite/redirects in the web.config (Intelligencia.UrlRewriter.RewriterHttpModule). There will be hundreds of entries, so I don't want a static file, I want to use a database so it can be changed by code. I can't use (maybe?) IIS ARR add-in as I need to keep the companies secured by the security token.

I'm looking for something like this, except "urlRequestContext.Request.Url.Host" is a GET only not a SET

Global.asax.cs:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    HttpContext urlRequestContext = HttpContext.Current;

    if (!(urlRequestContext.Request.Url.AbsolutePath.ToLower().StartsWith("/errorpages/")))
    {
        try
        {
            string hostingAuth = urlRequestContext.Request.Headers.GetValues("hostingAuth").FirstOrDefault();
            if (hostingAuth == "company1secret")
            {
                urlRequestContext.Request.Url.Host = "internalsite1.local";
            }
            if (hostingAuth == "company2secret")
            {
                urlRequestContext.Request.Url.Host = "internalsite2.local";
            }
        }
        catch (Exception ex)
        {
            Response.Redirect("/errorpages/missingtoken.aspx", true);
        }
    }
}

I can't find an example on how to do this. Its either very easy and not worth any examples or not possible. Does anyone have any suggestions? Am I approaching this the wrong way entirely?

Thanks


Solution

  • Turns out its not an easy solution and you don't change the host.

    ARR almost worked with a bit of ingenuity, by writing the host header as the server name and domain, except the URL-rewrite worked first time, but was ignored second time around when the app send the info back to the internal server.

    Only one reverse proxy solution came close which was: https://gist.github.com/anth-3/6169292

    which is the basis for the code. The basic idea is:

    For completeness the code is included. This is a few drafts in, but not refined or QA'd, so expect bugs. It appears to work with all the verbs *1 , but mostly tested with GET and this also works with https *2

    *1 if you want PUT, DELETE and other verbs to work, you need to enable this in IIS and it looks like you need to do it in the web.config too

    *2 for https to work, the external website website needs the valid cert for it to work. This was tested with a wildcard cert on the external and internal website both over https://443

    This got the job done for me for the initial POC.

    The only pages in the blank project are:

    global.asax

    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpContext urlRequestContext = HttpContext.Current;
    
        if (!(urlRequestContext.Request.Url.AbsolutePath.ToLower().StartsWith("/errorpages/")))
        {
            try
            {
                string hostingAuth = urlRequestContext.Request.Headers.GetValues("hostingauth").FirstOrDefault();
    
                if (hostingAuth == "company2secret")
                {
                    string newHost = "company2internal.validdomain.com";
                    reverseProxy.ProcessRequest(urlRequestContext, newHost);
                }
            }
            catch (System.Threading.ThreadAbortException)
            {
                // ignore it
            }
    
            catch (Exception ex)
            {
                Response.Redirect("/errorpages/missingtoken.aspx", true);
            }
        }
    }
    

    .

    class1.cs

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    
    namespace redirect
    {
        public class reverseProxy
        {
            //public static bool AcceptAllCertifications(object sender, System.Security.Cryptography.X509Certificates.X509Certificate certification, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors)
            //{
            //    return true;
            //}
    
            public static void ProcessRequest(HttpContext Context, string newHost)
            {
    
                //ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(AcceptAllCertifications);
                //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
    
                /* Create variables to hold the request and response. */
                HttpRequest Request = Context.Request;
                HttpResponse Response = Context.Response;
    
                string URI = null;
                URI = Request.Url.Scheme.ToString() + "://" + newHost + Request.Url.PathAndQuery;
    
                /* Create an HttpWebRequest to send the URI on and process results. */
                System.Net.HttpWebRequest ProxyRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(URI);
    
    
                /* Set the same requests to our request as in the incoming request */
                ProxyRequest.Method = Request.HttpMethod.ToUpper();
                ProxyRequest.ServicePoint.Expect100Continue = false;
                ProxyRequest.Accept = Request.Headers["Accept"];
                //ProxyRequest.TransferEncoding = Request.Headers["Accept-encoding"];
                ProxyRequest.SendChunked = false;
                //ProxyRequest.Date = Request.Headers["Date"];
                ProxyRequest.Expect = Request.Headers["Expect"];
                //ProxyRequest.IfModifiedSince = Request.Headers["If-Modified-Since"];
                //ProxyRequest.Range = Request.Headers["Range"];
                ProxyRequest.Referer = Request.Headers["Referer"];
                ProxyRequest.TransferEncoding = Request.Headers["Transfer-Encoding"];
                ProxyRequest.UserAgent = Request.Headers["User-Agent"];
    
                //set the same headers except for certain ones as they need to be set not in this way
                foreach (string strKey in Request.Headers.AllKeys)
                {
                    if ((strKey != "Accept") && (strKey != "Connection") && (strKey != "Content-Length") && (strKey != "Content-Type") && (strKey != "Date") && (strKey != "Expect") && (strKey != "Host") && (strKey != "If-Modified-Since") && (strKey != "Range") && (strKey != "Referer") && (strKey != "Transfer-Encoding") && (strKey != "User-Agent") && (strKey != "Proxy-Connection") && (strKey != "hostingauth"))
                    ProxyRequest.Headers.Add(strKey, Request.Headers[strKey]);
                }
    
                if (Request.InputStream.Length > 0)
                {
                    /* 
                     * Since we are using the same request method as the original request, and that is 
                     * a POST, the values to send on in the new request must be grabbed from the 
                     * original POSTed request.
                     */
                    byte[] Bytes = new byte[Request.InputStream.Length];
                    Request.InputStream.Read(Bytes, 0, (int)Request.InputStream.Length);
                    ProxyRequest.ContentLength = Bytes.Length;
                    string ContentType = Request.ContentType;
    
                    if (String.IsNullOrEmpty(ContentType))
                    {
                        ProxyRequest.ContentType = "application/x-www-form-urlencoded";
                    }
                    else
                    {
                        ProxyRequest.ContentType = ContentType;
                    }
    
                    using (Stream OutputStream = ProxyRequest.GetRequestStream())
                    {
                        OutputStream.Write(Bytes, 0, Bytes.Length);
                    }
                }
                //else
                //{
                //    /*
                //     * When the original request is a GET, things are much easier, as we need only to 
                //     * pass the URI we collected earlier which will still have any parameters 
                //     * associated with the request attached to it.
                //     */
                //    //ProxyRequest.Method = "GET";
                //}
    
                System.Net.WebResponse ServerResponse = null;
    
                /* Send the proxy request to the remote server or fail. */
                try
                {
                    //even if it isn't gzipped it tries but ignores if it fails
                    ProxyRequest.AutomaticDecompression = DecompressionMethods.GZip;
                    ServerResponse = ProxyRequest.GetResponse();
                }
                catch (System.Net.WebException WebEx)
                {
                    #region exceptionError
                    Response.StatusCode = 500;
                    Response.StatusDescription = WebEx.Status.ToString();
                    Response.Write(WebEx.Message);
                    Response.Write("\r\n");
                    Response.Write(((System.Net.HttpWebResponse)WebEx.Response).ResponseUri);
                    Response.Write("\r\n");
                    Response.Write(((System.Net.HttpWebResponse)WebEx.Response).Method);
                    Response.Write("\r\n");
                    Response.Write("Headers\r\n");
                    foreach (string strKey in Request.Headers.AllKeys)
                    {
                        Response.Write(strKey + ": " +Request.Headers[strKey]);
                        Response.Write("\r\n");
                    }
                    Response.End();
                    #endregion
                    return;
                }
    
                /* Set up the response to the client if there is one to set up. */
                if (ServerResponse != null)
                {
                    Response.ContentType = ServerResponse.ContentType;
                    using (Stream ByteStream = ServerResponse.GetResponseStream())
                    {
                        /* What is the response type? */
                        if (ServerResponse.ContentType.Contains("text") ||
                                ServerResponse.ContentType.Contains("json") ||
                                ServerResponse.ContentType.Contains("xml"))
                        {
                            /* These "text" types are easy to handle. */
                            using (StreamReader Reader = new StreamReader(ByteStream))
                            {
                                string ResponseString = Reader.ReadToEnd();
    
                                /* 
                                 * Tell the client not to cache the response since it 
                                 * could easily be dynamic, and we do not want to mess
                                 * that up!
                                 */
                                Response.CacheControl = "no-cache";
    
                                //If the request came with a gzip request, send it back gzipped
                                if (Request.Headers["Accept-encoding"].Contains("gzip"))
                                {
                                    Response.Filter = new System.IO.Compression.GZipStream(Response.Filter,
                                          System.IO.Compression.CompressionMode.Compress);
                                    Response.AppendHeader("Content-Encoding", "gzip");
                                }
    
                                //write webpage/results back
                                Response.Write(ResponseString);
    
                            }
                        }
                        else
                        {
    
                            //This is completely untested
    
                            /* 
                             * Handle binary responses (image, layer file, other binary 
                             * files) differently than text.
                             */
                            BinaryReader BinReader = new BinaryReader(ByteStream);
    
                            byte[] BinaryOutputs = BinReader.ReadBytes((int)ServerResponse.ContentLength);
    
                            BinReader.Close();
    
                            /* 
                             * Tell the client not to cache the response since it could 
                             * easily be dynamic, and we do not want to mess that up!
                             */
                            Response.CacheControl = "no-cache";
    
                            //could this make it more efficient - untested
                            if (Request.Headers["Accept-encoding"].Contains("gzip"))
                            {
                                Response.Filter = new System.IO.Compression.GZipStream(Response.Filter,
                                      System.IO.Compression.CompressionMode.Compress);
                                Response.AppendHeader("Content-Encoding", "gzip");
                            }
    
    
                            /*
                             * Send the binary response to the client.
                             * (Note: if large images/files are sent, we could modify this to 
                             * send back in chunks instead...something to think about for 
                             * future.)
                             */
                            Response.OutputStream.Write(BinaryOutputs, 0, BinaryOutputs.Length);
                        }
                        ServerResponse.Close();
                    }
                }
    
                //done
                Response.End();
            }
    
    
        }
    }
    

    .

    In the web.config this shows the verb override for PUT and DELETE to work. I know i'd be reverse proxying aspx and an api without an extension. This is probably overkill and inefficient, but it works

    web.config

    <!--
      For more information on how to configure your ASP.NET application, please visit
      https://go.microsoft.com/fwlink/?LinkId=169433
      -->
    <configuration>
      <system.web>
        <compilation debug="true" targetFramework="4.6.1"/>
        <httpRuntime targetFramework="4.6.1"/>
      </system.web>
      <system.codedom>
        <compilers>
          <compiler language="c#;cs;csharp" extension=".cs"
            type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
            warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701"/>
          <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
            type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
            warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+"/>
        </compilers>
      </system.codedom>
      <system.webServer>
        <handlers>
          <remove name="PageHandlerFactory-ISAPI-2.0-64" />
          <remove name="PageHandlerFactory-ISAPI-2.0" />
          <remove name="PageHandlerFactory-Integrated-4.0" />
          <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
          <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
          <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
          <remove name="PageHandlerFactory-ISAPI-4.0_32bit" />
          <remove name="PageHandlerFactory-ISAPI-4.0_64bit" />
          <remove name="PageHandlerFactory-Integrated" />
          <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv2.0" />
          <add name="PageHandlerFactory-ISAPI-4.0_64bit" path="*.aspx" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
          <add name="PageHandlerFactory-ISAPI-4.0_32bit" path="*.aspx" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
          <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0" />
          <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*." verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
          <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
          <add name="PageHandlerFactory-Integrated-4.0" path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" />
          <add name="PageHandlerFactory-ISAPI-2.0" path="*.aspx" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" />
          <add name="PageHandlerFactory-ISAPI-2.0-64" path="*.aspx" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" resourceType="Unspecified" requireAccess="Script" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" />
        </handlers>
      </system.webServer>
    
    </configuration>
    

    /errorpages/missingtoken.aspx

    <body>
    the token is missing
    </body>
    

    If you were wondering about speed, Postman was giving me times of

    • 350-600ms direct over the internet to render the .aspx page.
    • 600-900ms via the reverse proxy (2 different locations. RP at home, the internal server (presented externally) on site). I suspect that is due to the multiple unGzipping and reGzipping.
    • 800ms-1s if it went via the RP, but no GZIPping was done.

    If I didn't have to unzip to rezip, I suspect the time would be closer to the direct time.

    If I could have deployed the RP on site and requested it from the internal server unzipped and presented it back zipped, this may have been quicker too. YMMV

    .

    If you were wondering why bother - this is to put authentication into a request to an API which is usually behind an IP whitelist. The RP won't be whitelisted and I have no access to the API code to change it.