diff --git a/src/Middleware/Rewrite/ref/Microsoft.AspNetCore.Rewrite.netcoreapp.cs b/src/Middleware/Rewrite/ref/Microsoft.AspNetCore.Rewrite.netcoreapp.cs index 4a2f96518607..49ff64cecb0b 100644 --- a/src/Middleware/Rewrite/ref/Microsoft.AspNetCore.Rewrite.netcoreapp.cs +++ b/src/Middleware/Rewrite/ref/Microsoft.AspNetCore.Rewrite.netcoreapp.cs @@ -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; } diff --git a/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs b/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs index 4e35fc478c62..177469464f4f 100644 --- a/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs +++ b/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs @@ -17,6 +17,7 @@ internal static class RewriteMiddlewareLoggingExtensions private static readonly Action _modRewriteMatchedRule; private static readonly Action _redirectedToHttps; private static readonly Action _redirectedToWww; + private static readonly Action _redirectedToNonWww; private static readonly Action _redirectedRequest; private static readonly Action _rewrittenRequest; private static readonly Action _abortedRequest; @@ -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) @@ -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); diff --git a/src/Middleware/Rewrite/src/RedirectToNonWwwRule.cs b/src/Middleware/Rewrite/src/RedirectToNonWwwRule.cs new file mode 100644 index 000000000000..1c6c0a7fbc6f --- /dev/null +++ b/src/Middleware/Rewrite/src/RedirectToNonWwwRule.cs @@ -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(); + } + } +} diff --git a/src/Middleware/Rewrite/src/RedirectToWwwHelper.cs b/src/Middleware/Rewrite/src/RedirectToWwwHelper.cs new file mode 100644 index 000000000000..4550e7ad9d49 --- /dev/null +++ b/src/Middleware/Rewrite/src/RedirectToWwwHelper.cs @@ -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; + } + } +} diff --git a/src/Middleware/Rewrite/src/RedirectToWwwRule.cs b/src/Middleware/Rewrite/src/RedirectToWwwRule.cs index fcf9735aef6b..f5b715a6488f 100644 --- a/src/Middleware/Rewrite/src/RedirectToWwwRule.cs +++ b/src/Middleware/Rewrite/src/RedirectToWwwRule.cs @@ -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) { @@ -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(); } } diff --git a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs index 88b6d783b4db..28d2660faec8 100644 --- a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs +++ b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs @@ -179,5 +179,68 @@ public static RewriteOptions AddRedirectToWww(this RewriteOptions options, int s options.Rules.Add(new RedirectToWwwRule(statusCode, domains)); return options; } + + /// + /// Permanently redirects the request to the root domain if the request is from the www subdomain. + /// + /// The . + /// + public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options) + { + return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect); + } + + /// + /// Permanently redirects the request to the root domain if the request is from the www subdomain. + /// + /// The . + /// Limit the rule to apply only on the specified domain(s). + /// + public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options, params string[] domains) + { + return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect, domains); + } + + /// + /// Redirect the request to the root domain if the incoming request is from the www subdomain. + /// + /// The . + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options) + { + return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect); + } + + /// + /// Redirect the request to the root domain if the incoming request is from the www subdomain. + /// + /// The . + /// Limit the rule to apply only on the specified domain(s). + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, params string[] domains) + { + return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect, domains); + } + + /// + /// Redirect the request to the root domain if the incoming request is from the www subdomain. + /// + /// The . + /// The status code to add to the response. + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode) + { + options.Rules.Add(new RedirectToNonWwwRule(statusCode)); + return options; + } + + /// + /// Redirect the request to the root domain if the incoming request is from the www subdomain. + /// + /// The . + /// The status code to add to the response. + /// Limit the rule to apply only on the specified domain(s). + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode, params string[] domains) + { + options.Rules.Add(new RedirectToNonWwwRule(statusCode, domains)); + return options; + } } } diff --git a/src/Middleware/Rewrite/test/MiddlewareTests.cs b/src/Middleware/Rewrite/test/MiddlewareTests.cs index 0e7eb2c0479e..e8a4a4aec893 100644 --- a/src/Middleware/Rewrite/test/MiddlewareTests.cs +++ b/src/Middleware/Rewrite/test/MiddlewareTests.cs @@ -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; @@ -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() {