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
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=\"Web\" /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
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.