Skip to content

Add www to root domain redirects #12997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public static partial class RewriteOptionsExtensions
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 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, int statusCode) { throw null; }
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal static class RewriteMiddlewareLoggingExtensions
private static readonly Action<ILogger, Exception> _modRewriteMatchedRule;
private static readonly Action<ILogger, Exception> _redirectedToHttps;
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> _rewrittenRequest;
private static readonly Action<ILogger, string, Exception> _abortedRequest;
Expand Down Expand Up @@ -88,6 +89,11 @@ static RewriteMiddlewareLoggingExtensions()
LogLevel.Information,
new EventId(13, "RedirectedToWww"),
"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)
Expand Down Expand Up @@ -135,6 +141,11 @@ public static void RedirectedToWww(this ILogger logger)
_redirectedToWww(logger, null);
}

public static void RedirectedToNonWww(this ILogger logger)
{
_redirectedToNonWww(logger, null);
}

public static void RedirectedRequest(this ILogger logger, string redirectedUrl)
{
_redirectedRequest(logger, redirectedUrl, null);
Expand Down
64 changes: 64 additions & 0 deletions src/Middleware/Rewrite/src/RedirectToNonWwwRule.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
61 changes: 61 additions & 0 deletions src/Middleware/Rewrite/src/RedirectToWwwHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
47 changes: 14 additions & 33 deletions src/Middleware/Rewrite/src/RedirectToWwwRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Rewrite.Logging;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Rewrite
{
internal class RedirectToWwwRule : IRule
{
public readonly int _statusCode;
public readonly string[] _domains;
private const string WwwDot = "www.";

private readonly string[] _domains;
private readonly int _statusCode;

public RedirectToWwwRule(int statusCode)
{
Expand All @@ -28,55 +28,36 @@ public RedirectToWwwRule(int statusCode, params string[] domains)

if (domains.Length < 1)
{
throw new ArgumentException(nameof(domains));
throw new ArgumentException($"One or more {nameof(domains)} must be provided.");
}

_domains = domains;
_statusCode = statusCode;
}

public virtual void ApplyRule(RewriteContext context)
public void ApplyRule(RewriteContext context)
{
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;
return;
}

if (req.Host.Value.StartsWith("www.", StringComparison.OrdinalIgnoreCase))
if (req.Host.Value.StartsWith(WwwDot, StringComparison.OrdinalIgnoreCase))
{
context.Result = RuleResult.ContinueRules;
return;
}

if (_domains != null)
{
var isHostInDomains = false;

foreach (var domain in _domains)
{
if (domain.Equals(req.Host.Host, StringComparison.OrdinalIgnoreCase))
{
isHostInDomains = true;
break;
}
}

if (!isHostInDomains)
{
context.Result = RuleResult.ContinueRules;
return;
}
}
RedirectToWwwHelper.SetRedirect(
context,
new HostString($"www.{context.HttpContext.Request.Host.Value}"),
_statusCode);

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();
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,68 @@ public static RewriteOptions AddRedirectToWww(this RewriteOptions options, int s
options.Rules.Add(new RedirectToWwwRule(statusCode, domains));
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;
}
}
}
61 changes: 60 additions & 1 deletion src/Middleware/Rewrite/test/MiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -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.

using System;
Expand Down Expand Up @@ -232,6 +232,65 @@ public async Task CheckNoRedirectToWww(string requestUri)
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]
public async Task CheckIfEmptyStringRedirectCorrectly()
{
Expand Down