In my ASP.NET MVC application there are occasions where I need to generate an absolute URI, such as when sending an email confirmation link to a user. The most straightforward approach is to use UrlHelper.Action()
with the protocol
parameter, and this has worked well for me.
However, now my server is behind a reverse proxy, so the absolute URIs that I'm generating are pointing to backendserver.myorg.local
instead of publicsite.example.com
, and that's not good for business.
So, I wrote this method to inspect the Origin header. Do you see any security problems or other reasons this might not be a good solution?
static class UrlHelperExtensions
{
private const string OriginHeader = "Origin";
/// <summary>
/// Generates a full absolute URI, with HTTP(S), host name, and everything. Is reverse-proxy-aware,
/// so will generate public URIs in the event that our server is behind a reverse proxy.
/// </summary>
/// <param name="urlHelper"></param>
/// <param name="actionName"></param>
/// <param name="controllerName"></param>
/// <param name="routeValues"></param>
/// <returns></returns>
public static string AbsoluteAction(this UrlHelper urlHelper, string actionName, string controllerName, object routeValues = null)
{
if (urlHelper == null)
throw new ArgumentNullException(nameof(urlHelper), $"{nameof(urlHelper)} is null.");
var protocol = urlHelper.RequestContext.HttpContext.Request.Url.Scheme; // Should be "http" or "https"
var localUriString = urlHelper.Action(actionName, controllerName, routeValues, protocol);
var originValues = urlHelper.RequestContext.HttpContext.Request.Headers.GetValues(OriginHeader)
.Where(x => !String.IsNullOrWhiteSpace(x))
.ToList();
if (!originValues.Any())
return localUriString; // We are NOT behind a reverse proxy. Easy peasy, return local URI.
var originUriString = originValues.First(); // There should only be only 1 Origin header, but theoretically it's possible to have more than 1.
var originUri = parseOriginUri(originUriString);
var uriBuilder = new UriBuilder(localUriString);
uriBuilder.Scheme = originUri.Scheme; // AKA "protocol" (http or https)
uriBuilder.Host = originUri.Host;
uriBuilder.Port = originUri.Port;
return uriBuilder.ToString();
}
private static Uri parseOriginUri(string originText)
{
try
{
return new Uri(originText);
}
catch (UriFormatException ex)
{
throw new UriFormatException($"{OriginHeader} header URI is in an invalid format: \"{originText}\"", ex);
}
}
}
Note: Right now I don't have any requirements to actually manipulate the URL path.