Skip to content

Commit a23dd41

Browse files
kendaleivjkotalik
authored andcommitted
Add www to root domain redirects (#12997)
1 parent d4f7a19 commit a23dd41

7 files changed

+279
-34
lines changed

src/Middleware/Rewrite/ref/Microsoft.AspNetCore.Rewrite.netcoreapp.cs

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ public static partial class RewriteOptionsExtensions
5454
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
5555
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttps(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, int? sslPort) { throw null; }
5656
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToHttpsPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
57+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
58+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
59+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; }
60+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, params string[] domains) { throw null; }
61+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWwwPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
62+
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToNonWwwPermanent(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, params string[] domains) { throw null; }
5763
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options) { throw null; }
5864
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode) { throw null; }
5965
public static Microsoft.AspNetCore.Rewrite.RewriteOptions AddRedirectToWww(this Microsoft.AspNetCore.Rewrite.RewriteOptions options, int statusCode, params string[] domains) { throw null; }

src/Middleware/Rewrite/src/Extensions/RewriteMiddlewareLoggingExtensions.cs

+11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal static class RewriteMiddlewareLoggingExtensions
1717
private static readonly Action<ILogger, Exception> _modRewriteMatchedRule;
1818
private static readonly Action<ILogger, Exception> _redirectedToHttps;
1919
private static readonly Action<ILogger, Exception> _redirectedToWww;
20+
private static readonly Action<ILogger, Exception> _redirectedToNonWww;
2021
private static readonly Action<ILogger, string, Exception> _redirectedRequest;
2122
private static readonly Action<ILogger, string, Exception> _rewrittenRequest;
2223
private static readonly Action<ILogger, string, Exception> _abortedRequest;
@@ -88,6 +89,11 @@ static RewriteMiddlewareLoggingExtensions()
8889
LogLevel.Information,
8990
new EventId(13, "RedirectedToWww"),
9091
"Request redirected to www");
92+
93+
_redirectedToNonWww = LoggerMessage.Define(
94+
LogLevel.Information,
95+
new EventId(14, "RedirectedToNonWww"),
96+
"Request redirected to root domain from www subdomain");
9197
}
9298

9399
public static void RewriteMiddlewareRequestContinueResults(this ILogger logger, string currentUrl)
@@ -135,6 +141,11 @@ public static void RedirectedToWww(this ILogger logger)
135141
_redirectedToWww(logger, null);
136142
}
137143

144+
public static void RedirectedToNonWww(this ILogger logger)
145+
{
146+
_redirectedToNonWww(logger, null);
147+
}
148+
138149
public static void RedirectedRequest(this ILogger logger, string redirectedUrl)
139150
{
140151
_redirectedRequest(logger, redirectedUrl, null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Rewrite.Logging;
7+
8+
namespace Microsoft.AspNetCore.Rewrite
9+
{
10+
internal class RedirectToNonWwwRule : IRule
11+
{
12+
private const string WwwDot = "www.";
13+
14+
private readonly string[] _domains;
15+
private readonly int _statusCode;
16+
17+
public RedirectToNonWwwRule(int statusCode)
18+
{
19+
_statusCode = statusCode;
20+
}
21+
22+
public RedirectToNonWwwRule(int statusCode, params string[] domains)
23+
{
24+
if (domains == null)
25+
{
26+
throw new ArgumentNullException(nameof(domains));
27+
}
28+
29+
if (domains.Length < 1)
30+
{
31+
throw new ArgumentException($"One or more {nameof(domains)} must be provided.");
32+
}
33+
34+
_domains = domains;
35+
_statusCode = statusCode;
36+
}
37+
38+
public void ApplyRule(RewriteContext context)
39+
{
40+
var request = context.HttpContext.Request;
41+
42+
var hostInDomains = RedirectToWwwHelper.IsHostInDomains(request, _domains);
43+
44+
if (!hostInDomains)
45+
{
46+
context.Result = RuleResult.ContinueRules;
47+
return;
48+
}
49+
50+
if (!request.Host.Value.StartsWith(WwwDot, StringComparison.OrdinalIgnoreCase))
51+
{
52+
context.Result = RuleResult.ContinueRules;
53+
return;
54+
}
55+
56+
RedirectToWwwHelper.SetRedirect(
57+
context,
58+
new HostString(request.Host.Value.Substring(4)), // We verified the hostname begins with "www." already.
59+
_statusCode);
60+
61+
context.Logger.RedirectedToNonWww();
62+
}
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Http.Extensions;
7+
using Microsoft.Net.Http.Headers;
8+
9+
namespace Microsoft.AspNetCore.Rewrite
10+
{
11+
internal static class RedirectToWwwHelper
12+
{
13+
private const string Localhost = "localhost";
14+
15+
public static bool IsHostInDomains(HttpRequest request, string[] domains)
16+
{
17+
if (request.Host.Host.Equals(Localhost, StringComparison.OrdinalIgnoreCase))
18+
{
19+
return false;
20+
}
21+
22+
if (domains != null)
23+
{
24+
var isHostInDomains = false;
25+
26+
foreach (var domain in domains)
27+
{
28+
if (domain.Equals(request.Host.Host, StringComparison.OrdinalIgnoreCase))
29+
{
30+
isHostInDomains = true;
31+
break;
32+
}
33+
}
34+
35+
if (!isHostInDomains)
36+
{
37+
return false;
38+
}
39+
}
40+
41+
return true;
42+
}
43+
44+
public static void SetRedirect(RewriteContext context, HostString newHost, int statusCode)
45+
{
46+
var request = context.HttpContext.Request;
47+
var response = context.HttpContext.Response;
48+
49+
var newUrl = UriHelper.BuildAbsolute(
50+
request.Scheme,
51+
newHost,
52+
request.PathBase,
53+
request.Path,
54+
request.QueryString);
55+
56+
response.StatusCode = statusCode;
57+
response.Headers[HeaderNames.Location] = newUrl;
58+
context.Result = RuleResult.EndResponse;
59+
}
60+
}
61+
}

src/Middleware/Rewrite/src/RedirectToWwwRule.cs

+14-33
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33

44
using System;
55
using Microsoft.AspNetCore.Http;
6-
using Microsoft.AspNetCore.Http.Extensions;
76
using Microsoft.AspNetCore.Rewrite.Logging;
8-
using Microsoft.Net.Http.Headers;
97

108
namespace Microsoft.AspNetCore.Rewrite
119
{
1210
internal class RedirectToWwwRule : IRule
1311
{
14-
public readonly int _statusCode;
15-
public readonly string[] _domains;
12+
private const string WwwDot = "www.";
13+
14+
private readonly string[] _domains;
15+
private readonly int _statusCode;
1616

1717
public RedirectToWwwRule(int statusCode)
1818
{
@@ -28,55 +28,36 @@ public RedirectToWwwRule(int statusCode, params string[] domains)
2828

2929
if (domains.Length < 1)
3030
{
31-
throw new ArgumentException(nameof(domains));
31+
throw new ArgumentException($"One or more {nameof(domains)} must be provided.");
3232
}
3333

3434
_domains = domains;
3535
_statusCode = statusCode;
3636
}
3737

38-
public virtual void ApplyRule(RewriteContext context)
38+
public void ApplyRule(RewriteContext context)
3939
{
4040
var req = context.HttpContext.Request;
4141

42-
if (req.Host.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase))
42+
var hostInDomains = RedirectToWwwHelper.IsHostInDomains(req, _domains);
43+
44+
if (!hostInDomains)
4345
{
4446
context.Result = RuleResult.ContinueRules;
4547
return;
4648
}
4749

48-
if (req.Host.Value.StartsWith("www.", StringComparison.OrdinalIgnoreCase))
50+
if (req.Host.Value.StartsWith(WwwDot, StringComparison.OrdinalIgnoreCase))
4951
{
5052
context.Result = RuleResult.ContinueRules;
5153
return;
5254
}
5355

54-
if (_domains != null)
55-
{
56-
var isHostInDomains = false;
57-
58-
foreach (var domain in _domains)
59-
{
60-
if (domain.Equals(req.Host.Host, StringComparison.OrdinalIgnoreCase))
61-
{
62-
isHostInDomains = true;
63-
break;
64-
}
65-
}
66-
67-
if (!isHostInDomains)
68-
{
69-
context.Result = RuleResult.ContinueRules;
70-
return;
71-
}
72-
}
56+
RedirectToWwwHelper.SetRedirect(
57+
context,
58+
new HostString($"www.{context.HttpContext.Request.Host.Value}"),
59+
_statusCode);
7360

74-
var wwwHost = new HostString($"www.{req.Host.Value}");
75-
var newUrl = UriHelper.BuildAbsolute(req.Scheme, wwwHost, req.PathBase, req.Path, req.QueryString);
76-
var response = context.HttpContext.Response;
77-
response.StatusCode = _statusCode;
78-
response.Headers[HeaderNames.Location] = newUrl;
79-
context.Result = RuleResult.EndResponse;
8061
context.Logger.RedirectedToWww();
8162
}
8263
}

src/Middleware/Rewrite/src/RewriteOptionsExtensions.cs

+63
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,68 @@ public static RewriteOptions AddRedirectToWww(this RewriteOptions options, int s
179179
options.Rules.Add(new RedirectToWwwRule(statusCode, domains));
180180
return options;
181181
}
182+
183+
/// <summary>
184+
/// Permanently redirects the request to the root domain if the request is from the www subdomain.
185+
/// </summary>
186+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
187+
/// <returns></returns>
188+
public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options)
189+
{
190+
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect);
191+
}
192+
193+
/// <summary>
194+
/// Permanently redirects the request to the root domain if the request is from the www subdomain.
195+
/// </summary>
196+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
197+
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
198+
/// <returns></returns>
199+
public static RewriteOptions AddRedirectToNonWwwPermanent(this RewriteOptions options, params string[] domains)
200+
{
201+
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status308PermanentRedirect, domains);
202+
}
203+
204+
/// <summary>
205+
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
206+
/// </summary>
207+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
208+
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options)
209+
{
210+
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect);
211+
}
212+
213+
/// <summary>
214+
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
215+
/// </summary>
216+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
217+
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
218+
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, params string[] domains)
219+
{
220+
return AddRedirectToNonWww(options, statusCode: StatusCodes.Status307TemporaryRedirect, domains);
221+
}
222+
223+
/// <summary>
224+
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
225+
/// </summary>
226+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
227+
/// <param name="statusCode">The status code to add to the response.</param>
228+
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode)
229+
{
230+
options.Rules.Add(new RedirectToNonWwwRule(statusCode));
231+
return options;
232+
}
233+
234+
/// <summary>
235+
/// Redirect the request to the root domain if the incoming request is from the www subdomain.
236+
/// </summary>
237+
/// <param name="options">The <see cref="RewriteOptions"/>.</param>
238+
/// <param name="statusCode">The status code to add to the response.</param>
239+
/// <param name="domains">Limit the rule to apply only on the specified domain(s).</param>
240+
public static RewriteOptions AddRedirectToNonWww(this RewriteOptions options, int statusCode, params string[] domains)
241+
{
242+
options.Rules.Add(new RedirectToNonWwwRule(statusCode, domains));
243+
return options;
244+
}
182245
}
183246
}

src/Middleware/Rewrite/test/MiddlewareTests.cs

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -232,6 +232,65 @@ public async Task CheckNoRedirectToWww(string requestUri)
232232
Assert.Null(response.Headers.Location);
233233
}
234234

235+
[Theory]
236+
[InlineData(StatusCodes.Status301MovedPermanently)]
237+
[InlineData(StatusCodes.Status302Found)]
238+
[InlineData(StatusCodes.Status307TemporaryRedirect)]
239+
[InlineData(StatusCodes.Status308PermanentRedirect)]
240+
public async Task CheckRedirectToNonWwwWithStatusCode(int statusCode)
241+
{
242+
var options = new RewriteOptions().AddRedirectToNonWww(statusCode: statusCode);
243+
var builder = new WebHostBuilder()
244+
.Configure(app =>
245+
{
246+
app.UseRewriter(options);
247+
});
248+
var server = new TestServer(builder);
249+
250+
var response = await server.CreateClient().GetAsync(new Uri("https://www.example.com"));
251+
252+
Assert.Equal("https://example.com/", response.Headers.Location.OriginalString);
253+
Assert.Equal(statusCode, (int)response.StatusCode);
254+
}
255+
256+
[Theory]
257+
[InlineData("http://www.example.com", "http://example.com/")]
258+
[InlineData("https://www.example.com", "https://example.com/")]
259+
[InlineData("http://www.example.com:8081", "http://example.com:8081/")]
260+
[InlineData("http://www.example.com:8081/example?q=1", "http://example.com:8081/example?q=1")]
261+
public async Task CheckRedirectToNonWww(string requestUri, string redirectUri)
262+
{
263+
var options = new RewriteOptions().AddRedirectToNonWww();
264+
var builder = new WebHostBuilder()
265+
.Configure(app =>
266+
{
267+
app.UseRewriter(options);
268+
});
269+
var server = new TestServer(builder);
270+
271+
var response = await server.CreateClient().GetAsync(new Uri(requestUri));
272+
273+
Assert.Equal(redirectUri, response.Headers.Location.OriginalString);
274+
Assert.Equal(StatusCodes.Status307TemporaryRedirect, (int)response.StatusCode);
275+
}
276+
277+
[Fact]
278+
public async Task CheckPermanentRedirectToNonWww()
279+
{
280+
var options = new RewriteOptions().AddRedirectToNonWwwPermanent();
281+
var builder = new WebHostBuilder()
282+
.Configure(app =>
283+
{
284+
app.UseRewriter(options);
285+
});
286+
var server = new TestServer(builder);
287+
288+
var response = await server.CreateClient().GetAsync(new Uri("https://www.example.com"));
289+
290+
Assert.Equal("https://example.com/", response.Headers.Location.OriginalString);
291+
Assert.Equal(StatusCodes.Status308PermanentRedirect, (int)response.StatusCode);
292+
}
293+
235294
[Fact]
236295
public async Task CheckIfEmptyStringRedirectCorrectly()
237296
{

0 commit comments

Comments
 (0)