diff --git a/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs b/src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs index 4e35fc478c62..81f1230c1995 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 Non-www"); } 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..ada03346cc03 --- /dev/null +++ b/src/Middleware/Rewrite/src/RedirectToNonWwwRule.cs @@ -0,0 +1,85 @@ +// 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.AspNetCore.Rewrite.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Rewrite +{ + internal class RedirectToNonWwwRule : IRule + { + public readonly int _statusCode; + public readonly string[] _domains; + + 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("Atleast provide one value.", nameof(domains)); + } + + foreach(var domain in domains) + { + if (!domain.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + throw new NotSupportedException($"Domain: {domain}. Not supported for this redirection rule. Domain should start with www."); + } + } + + _domains = domains; + _statusCode = statusCode; + } + + public virtual void ApplyRule(RewriteContext context) + { + var req = context.HttpContext.Request; + + if (!req.Host.Value.StartsWith("www.", 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; + } + } + + var wwwHost = new HostString(req.Host.Value.Remove(0, 4)); + 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.RedirectedToNonWww(); + } + } +} diff --git a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs index 88b6d783b4db..cd8012c7176d 100644 --- a/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs +++ b/src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs @@ -179,5 +179,72 @@ public static RewriteOptions AddRedirectToWww(this RewriteOptions options, int s options.Rules.Add(new RedirectToWwwRule(statusCode, domains)); return options; } + + /// + /// Permanently redirects the request to the domain if the request is www. + /// + /// The . + /// + public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options) + { + return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect); + } + + /// + /// Permanently redirects the request to the domain if the request is www. + /// + /// 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 domain if the incoming request is www. + /// + /// The . + /// + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options) + { + return AddRedirectToNonWww(options, StatusCodes.Status307TemporaryRedirect); + } + + /// + /// Redirect the request to the domain if the incoming request is www. + /// + /// The . + /// The status code to add the response. + /// + public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode) + { + options.Rules.Add(new RedirectToNonWwwRule(statusCode)); + return options; + } + + /// + /// Redirect the request to the domain if the incoming request is www. + /// + /// 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, StatusCodes.Status307TemporaryRedirect, domains); + } + + /// + /// Redirect the request to the domain if the incoming request is www. + /// + /// The . + /// The status code to add 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..4457d267c18e 100644 --- a/src/Middleware/Rewrite/test/MiddlewareTests.cs +++ b/src/Middleware/Rewrite/test/MiddlewareTests.cs @@ -232,6 +232,32 @@ public async Task CheckNoRedirectToWww(string requestUri) Assert.Null(response.Headers.Location); } + [Theory] + [InlineData("http://example.com")] + [InlineData("https://example.com")] + [InlineData("http://example.com:8081")] + [InlineData("https://example.com:8081")] + [InlineData("https://example.com:8081/example?q=1")] + [InlineData("http://localhost")] + [InlineData("https://localhost")] + [InlineData("http://localhost:8081")] + [InlineData("https://localhost:8081")] + [InlineData("https://localhost:8081/example?q=1")] + public async Task CheckNoRedirectToDomain(string requestUri) + { + 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.Null(response.Headers.Location); + } + [Fact] public async Task CheckIfEmptyStringRedirectCorrectly() { @@ -365,5 +391,84 @@ public async Task CheckRedirectToWwwWithStatusCodeInWhitelistedDomains(int statu Assert.Equal(statusCode, (int)response.StatusCode); } + [Theory] + [InlineData("http://www.example.com")] + [InlineData("https://www.example.com")] + [InlineData("http://www.example.com:8081")] + [InlineData("https://www.example.com:8081")] + [InlineData("https://www.example.com:8081/example?q=1")] + public async Task CheckNoRedirectToNonWwwInNonWhitelistedWwwSubdomain(string requestUri) + { + var options = new RewriteOptions().AddRedirectToNonWww("www.example2.com"); + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRewriter(options); + }); + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(new Uri(requestUri)); + + Assert.Null(response.Headers.Location); + } + + [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 CheckRedirectToNonWwwInWhitelistedWwwSubdomain(string requestUri, string redirectUri) + { + var options = new RewriteOptions().AddRedirectToNonWww("www.example.com"); + 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 CheckPermanentRedirectToNonWwwInWhitelistedWwwSubDomains() + { + var options = new RewriteOptions().AddRedirectToNonWwwPermanent("www.example.com"); + 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); + } + + [Theory] + [InlineData(StatusCodes.Status301MovedPermanently)] + [InlineData(StatusCodes.Status302Found)] + [InlineData(StatusCodes.Status307TemporaryRedirect)] + [InlineData(StatusCodes.Status308PermanentRedirect)] + public async Task CheckRedirectToNonWwwWithStatusCodeInWhitelistedWwwSubDomain(int statusCode) + { + var options = new RewriteOptions().AddRedirectToNonWww(statusCode: statusCode, "www.example.com"); + 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); + } } }