From 103190796f04026a9cd7001881646d730f8ba8f1 Mon Sep 17 00:00:00 2001 From: Colin Farr Date: Fri, 9 Feb 2018 22:13:43 +0000 Subject: [PATCH] Adding RedirectToWwwRule and extension (#297) --- .../RewriteMiddlewareLoggingExtensions.cs | 11 +++ .../Internal/RedirectToWwwRule.cs | 45 ++++++++++ .../RewriteOptionsExtensions.cs | 30 +++++++ .../MiddlewareTests.cs | 85 +++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Rewrite/Internal/RedirectToWwwRule.cs diff --git a/src/Microsoft.AspNetCore.Rewrite/Extensions/RewriteMiddlewareLoggingExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/Extensions/RewriteMiddlewareLoggingExtensions.cs index 3ad1e699dd..8b500b4338 100644 --- a/src/Microsoft.AspNetCore.Rewrite/Extensions/RewriteMiddlewareLoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Rewrite/Extensions/RewriteMiddlewareLoggingExtensions.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Rewrite.Logging private static readonly Action _modRewriteDidNotMatchRule; private static readonly Action _modRewriteMatchedRule; private static readonly Action _redirectedToHttps; + private static readonly Action _redirectedToWww; private static readonly Action _redirectSummary; private static readonly Action _rewriteSummary; private static readonly Action _abortedRequest; @@ -82,6 +83,11 @@ namespace Microsoft.AspNetCore.Rewrite.Logging LogLevel.Debug, 12, "Request to {requestedUrl} was ended"); + + _redirectedToWww = LoggerMessage.Define( + LogLevel.Information, + 13, + "Request redirected to www"); } public static void RewriteMiddlewareRequestContinueResults(this ILogger logger, string currentUrl) @@ -124,6 +130,11 @@ namespace Microsoft.AspNetCore.Rewrite.Logging _redirectedToHttps(logger, null); } + public static void RedirectedToWww(this ILogger logger) + { + _redirectedToWww(logger, null); + } + public static void RedirectedSummary(this ILogger logger, string redirectedUrl) { _redirectSummary(logger, redirectedUrl, null); diff --git a/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectToWwwRule.cs b/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectToWwwRule.cs new file mode 100644 index 0000000000..4675476e60 --- /dev/null +++ b/src/Microsoft.AspNetCore.Rewrite/Internal/RedirectToWwwRule.cs @@ -0,0 +1,45 @@ +// 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 +{ + public class RedirectToWwwRule : IRule + { + public readonly int _statusCode; + + public RedirectToWwwRule(int statusCode) + { + _statusCode = statusCode; + } + + public virtual void ApplyRule(RewriteContext context) + { + var req = context.HttpContext.Request; + if (req.Host.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + context.Result = RuleResult.ContinueRules; + return; + } + + if (req.Host.Value.StartsWith("www.", StringComparison.OrdinalIgnoreCase)) + { + 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(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Rewrite/RewriteOptionsExtensions.cs b/src/Microsoft.AspNetCore.Rewrite/RewriteOptionsExtensions.cs index c1aa2b69ed..d1c839899c 100644 --- a/src/Microsoft.AspNetCore.Rewrite/RewriteOptionsExtensions.cs +++ b/src/Microsoft.AspNetCore.Rewrite/RewriteOptionsExtensions.cs @@ -117,5 +117,35 @@ namespace Microsoft.AspNetCore.Rewrite options.Rules.Add(new RedirectToHttpsRule { StatusCode = statusCode, SSLPort = sslPort }); return options; } + + /// + /// Permanently redirects the request to the www subdomain if the request is non-www. + /// + /// The . + /// + public static RewriteOptions AddRedirectToWwwPermanent(this RewriteOptions options) + { + return AddRedirectToWww(options, statusCode: StatusCodes.Status308PermanentRedirect); + } + + /// + /// Redirect the request to the www subdomain if the incoming request is non-www. + /// + /// The . + public static RewriteOptions AddRedirectToWww(this RewriteOptions options) + { + return AddRedirectToWww(options, statusCode: StatusCodes.Status307TemporaryRedirect); + } + + /// + /// Redirect the request to the www subdomain if the incoming request is non-www. + /// + /// The . + /// The status code to add to the response. + public static RewriteOptions AddRedirectToWww(this RewriteOptions options, int statusCode) + { + options.Rules.Add(new RedirectToWwwRule(statusCode)); + return options; + } } } diff --git a/test/Microsoft.AspNetCore.Rewrite.Tests/MiddlewareTests.cs b/test/Microsoft.AspNetCore.Rewrite.Tests/MiddlewareTests.cs index c75ef98c50..5c6fb1e4ec 100644 --- a/test/Microsoft.AspNetCore.Rewrite.Tests/MiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Rewrite.Tests/MiddlewareTests.cs @@ -147,6 +147,91 @@ namespace Microsoft.AspNetCore.Rewrite.Tests.CodeRules Assert.Equal(StatusCodes.Status301MovedPermanently, (int)response.StatusCode); } + [Theory] + [InlineData(StatusCodes.Status301MovedPermanently)] + [InlineData(StatusCodes.Status302Found)] + [InlineData(StatusCodes.Status307TemporaryRedirect)] + [InlineData(StatusCodes.Status308PermanentRedirect)] + public async Task CheckRedirectToWwwWithStatusCode(int statusCode) + { + var options = new RewriteOptions().AddRedirectToWww(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://example.com")); + + Assert.Equal("https://www.example.com/", response.Headers.Location.OriginalString); + Assert.Equal(statusCode, (int)response.StatusCode); + } + + [Theory] + [InlineData("http://example.com", "http://www.example.com/")] + [InlineData("https://example.com", "https://www.example.com/")] + [InlineData("http://example.com:8081", "http://www.example.com:8081/")] + [InlineData("http://example.com:8081/example?q=1", "http://www.example.com:8081/example?q=1")] + public async Task CheckRedirectToWww(string requestUri, string redirectUri) + { + var options = new RewriteOptions().AddRedirectToWww(); + 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 CheckPermanentRedirectToWww() + { + var options = new RewriteOptions().AddRedirectToWwwPermanent(); + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRewriter(options); + }); + var server = new TestServer(builder); + + var response = await server.CreateClient().GetAsync(new Uri("https://example.com")); + + Assert.Equal("https://www.example.com/", response.Headers.Location.OriginalString); + Assert.Equal(StatusCodes.Status308PermanentRedirect, (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")] + [InlineData("http://localhost")] + [InlineData("https://localhost")] + [InlineData("http://localhost:8081")] + [InlineData("https://localhost:8081")] + [InlineData("https://localhost:8081/example?q=1")] + public async Task CheckNoRedirectToWww(string requestUri) + { + var options = new RewriteOptions().AddRedirectToWww(); + 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() {