Add www to root domain redirects (#12997)

This commit is contained in:
Ken Dale 2019-11-05 16:11:14 -05:00 committed by Justin Kotalik
parent d4f7a199c7
commit a23dd41428
7 changed files with 279 additions and 34 deletions

View File

@ -54,6 +54,12 @@ namespace Microsoft.AspNetCore.Rewrite
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, int? sslPort) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, int? sslPort) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttpsPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttpsPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, params string[] domains) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWwwPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWwwPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, params string[] domains) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; } public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; }

View File

@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Rewrite.Logging
private static readonly Action<ILogger, Exception> _modRewriteMatchedRule; private static readonly Action<ILogger, Exception> _modRewriteMatchedRule;
private static readonly Action<ILogger, Exception> _redirectedToHttps; private static readonly Action<ILogger, Exception> _redirectedToHttps;
private static readonly Action<ILogger, Exception> _redirectedToWww; private static readonly Action<ILogger, Exception> _redirectedToWww;
private static readonly Action<ILogger, Exception> _redirectedToNonWww;
private static readonly Action<ILogger, string, Exception> _redirectedRequest; private static readonly Action<ILogger, string, Exception> _redirectedRequest;
private static readonly Action<ILogger, string, Exception> _rewrittenRequest; private static readonly Action<ILogger, string, Exception> _rewrittenRequest;
private static readonly Action<ILogger, string, Exception> _abortedRequest; private static readonly Action<ILogger, string, Exception> _abortedRequest;
@ -88,6 +89,11 @@ namespace Microsoft.AspNetCore.Rewrite.Logging
LogLevel.Information, LogLevel.Information,
new EventId(13, "RedirectedToWww"), new EventId(13, "RedirectedToWww"),
"Request redirected to www"); "Request redirected to www");
_redirectedToNonWww = LoggerMessage.Define(
LogLevel.Information,
new EventId(14, "RedirectedToNonWww"),
"Request redirected to root domain from www subdomain");
} }
public static void RewriteMiddlewareRequestContinueResults(this ILogger logger, string currentUrl) public static void RewriteMiddlewareRequestContinueResults(this ILogger logger, string currentUrl)
@ -135,6 +141,11 @@ namespace Microsoft.AspNetCore.Rewrite.Logging
_redirectedToWww(logger, null); _redirectedToWww(logger, null);
} }
public static void RedirectedToNonWww(this ILogger logger)
{
_redirectedToNonWww(logger, null);
}
public static void RedirectedRequest(this ILogger logger, string redirectedUrl) public static void RedirectedRequest(this ILogger logger, string redirectedUrl)
{ {
_redirectedRequest(logger, redirectedUrl, null); _redirectedRequest(logger, redirectedUrl, null);

View File

@ -0,0 +1,64 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite.Logging;
namespace Microsoft.AspNetCore.Rewrite
{
internal class RedirectToNonWwwRule : IRule
{
private const string WwwDot = "www.";
private readonly string[] _domains;
private readonly int _statusCode;
public RedirectToNonWwwRule(int statusCode)
{
_statusCode = statusCode;
}
public RedirectToNonWwwRule(int statusCode, params string[] domains)
{
if (domains == null)
{
throw new ArgumentNullException(nameof(domains));
}
if (domains.Length < 1)
{
throw new ArgumentException($"One or more {nameof(domains)} must be provided.");
}
_domains = domains;
_statusCode = statusCode;
}
public void ApplyRule(RewriteContext context)
{
var request = context.HttpContext.Request;
var hostInDomains = RedirectToWwwHelper.IsHostInDomains(request, _domains);
if (!hostInDomains)
{
context.Result = RuleResult.ContinueRules;
return;
}
if (!request.Host.Value.StartsWith(WwwDot, StringComparison.OrdinalIgnoreCase))
{
context.Result = RuleResult.ContinueRules;
return;
}
RedirectToWwwHelper.SetRedirect(
context,
new HostString(request.Host.Value.Substring(4)), // We verified the hostname begins with "www." already.
_statusCode);
context.Logger.RedirectedToNonWww();
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite
{
internal static class RedirectToWwwHelper
{
private const string Localhost = "localhost";
public static bool IsHostInDomains(HttpRequest request, string[] domains)
{
if (request.Host.Host.Equals(Localhost, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (domains != null)
{
var isHostInDomains = false;
foreach (var domain in domains)
{
if (domain.Equals(request.Host.Host, StringComparison.OrdinalIgnoreCase))
{
isHostInDomains = true;
break;
}
}
if (!isHostInDomains)
{
return false;
}
}
return true;
}
public static void SetRedirect(RewriteContext context, HostString newHost, int statusCode)
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var newUrl = UriHelper.BuildAbsolute(
request.Scheme,
newHost,
request.PathBase,
request.Path,
request.QueryString);
response.StatusCode = statusCode;
response.Headers[HeaderNames.Location] = newUrl;
context.Result = RuleResult.EndResponse;
}
}
}

View File

@ -3,16 +3,16 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Rewrite.Logging; using Microsoft.AspNetCore.Rewrite.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Rewrite namespace Microsoft.AspNetCore.Rewrite
{ {
internal class RedirectToWwwRule : IRule internal class RedirectToWwwRule : IRule
{ {
public readonly int _statusCode; private const string WwwDot = "www.";
public readonly string[] _domains;
private readonly string[] _domains;
private readonly int _statusCode;
public RedirectToWwwRule(int statusCode) public RedirectToWwwRule(int statusCode)
{ {
@ -28,55 +28,36 @@ namespace Microsoft.AspNetCore.Rewrite
if (domains.Length < 1) if (domains.Length < 1)
{ {
throw new ArgumentException(nameof(domains)); throw new ArgumentException($"One or more {nameof(domains)} must be provided.");
} }
_domains = domains; _domains = domains;
_statusCode = statusCode; _statusCode = statusCode;
} }
public virtual void ApplyRule(RewriteContext context) public void ApplyRule(RewriteContext context)
{ {
var req = context.HttpContext.Request; var req = context.HttpContext.Request;
if (req.Host.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) var hostInDomains = RedirectToWwwHelper.IsHostInDomains(req, _domains);
if (!hostInDomains)
{ {
context.Result = RuleResult.ContinueRules; context.Result = RuleResult.ContinueRules;
return; return;
} }
if (req.Host.Value.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) if (req.Host.Value.StartsWith(WwwDot, StringComparison.OrdinalIgnoreCase))
{ {
context.Result = RuleResult.ContinueRules; context.Result = RuleResult.ContinueRules;
return; return;
} }
if (_domains != null) RedirectToWwwHelper.SetRedirect(
{ context,
var isHostInDomains = false; new HostString($"www.{context.HttpContext.Request.Host.Value}"),
_statusCode);
foreach (var domain in _domains)
{
if (domain.Equals(req.Host.Host, StringComparison.OrdinalIgnoreCase))
{
isHostInDomains = true;
break;
}
}
if (!isHostInDomains)
{
context.Result = RuleResult.ContinueRules;
return;
}
}
var wwwHost = new HostString($"www.{req.Host.Value}");
var newUrl = UriHelper.BuildAbsolute(req.Scheme, wwwHost, req.PathBase, req.Path, req.QueryString);
var response = context.HttpContext.Response;
response.StatusCode = _statusCode;
response.Headers[HeaderNames.Location] = newUrl;
context.Result = RuleResult.EndResponse;
context.Logger.RedirectedToWww(); context.Logger.RedirectedToWww();
} }
} }

View File

@ -179,5 +179,68 @@ namespace Microsoft.AspNetCore.Rewrite
options.Rules.Add(new RedirectToWwwRule(statusCode, domains)); options.Rules.Add(new RedirectToWwwRule(statusCode, domains));
return options; return options;
} }
/// <summary>
/// Permanently redirects the request to the root domain if the request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
/// <returns></returns>
public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options)
{
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect);
}
/// <summary>
/// Permanently redirects the request to the root domain if the request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
/// <returns></returns>
public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options, params string[] domains)
{
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect, domains);
}
/// <summary>
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options)
{
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect);
}
/// <summary>
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, params string[] domains)
{
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect, domains);
}
/// <summary>
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
/// <param name="statusCode">The status code to add to the response.</param>
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode)
{
options.Rules.Add(new RedirectToNonWwwRule(statusCode));
return options;
}
/// <summary>
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
/// </summary>
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
/// <param name="statusCode">The status code to add to the response.</param>
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode, params string[] domains)
{
options.Rules.Add(new RedirectToNonWwwRule(statusCode, domains));
return options;
}
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -232,6 +232,65 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules
Assert.Null(response.Headers.Location); Assert.Null(response.Headers.Location);
} }
[Theory]
[InlineData(StatusCodes.Status301MovedPermanently)]
[InlineData(StatusCodes.Status302Found)]
[InlineData(StatusCodes.Status307TemporaryRedirect)]
[InlineData(StatusCodes.Status308PermanentRedirect)]
public async Task CheckRedirectToNonWwwWithStatusCode(int statusCode)
{
var options = new RewriteOptions().AddRedirectToNonWww(statusCode: statusCode);
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync(new Uri("https://www.example.com"));
Assert.Equal("https://example.com/", response.Headers.Location.OriginalString);
Assert.Equal(statusCode, (int)response.StatusCode);
}
[Theory]
[InlineData("http://www.example.com", "http://example.com/")]
[InlineData("https://www.example.com", "https://example.com/")]
[InlineData("http://www.example.com:8081", "http://example.com:8081/")]
[InlineData("http://www.example.com:8081/example?q=1", "http://example.com:8081/example?q=1")]
public async Task CheckRedirectToNonWww(string requestUri, string redirectUri)
{
var options = new RewriteOptions().AddRedirectToNonWww();
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync(new Uri(requestUri));
Assert.Equal(redirectUri, response.Headers.Location.OriginalString);
Assert.Equal(StatusCodes.Status307TemporaryRedirect, (int)response.StatusCode);
}
[Fact]
public async Task CheckPermanentRedirectToNonWww()
{
var options = new RewriteOptions().AddRedirectToNonWwwPermanent();
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseRewriter(options);
});
var server = new TestServer(builder);
var response = await server.CreateClient().GetAsync(new Uri("https://www.example.com"));
Assert.Equal("https://example.com/", response.Headers.Location.OriginalString);
Assert.Equal(StatusCodes.Status308PermanentRedirect, (int)response.StatusCode);
}
[Fact] [Fact]
public async Task CheckIfEmptyStringRedirectCorrectly() public async Task CheckIfEmptyStringRedirectCorrectly()
{ {