Skip to content

Commit b107a7c

Browse files
author
Ray Fan
committed
#36 Completed http and www rewrite functionality.
1 parent 6d90c6c commit b107a7c

File tree

9 files changed

+308
-8
lines changed

9 files changed

+308
-8
lines changed

src/Fan.Web/Extensions/IApplicationBuilderExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Fan.Web.MetaWeblog;
2+
using Fan.Web.Middlewares;
23

34
namespace Microsoft.AspNetCore.Builder
45
{
@@ -13,5 +14,16 @@ public static IApplicationBuilder UseMetablog(this IApplicationBuilder builder)
1314
{
1415
return builder.UseMiddleware<MetaWeblogMiddleware>();
1516
}
17+
18+
/// <summary>
19+
/// Adds <see cref="HttpWwwRewriteMiddleware"/> for redirect between http / https and
20+
/// preferred domain www / nonwww.
21+
/// </summary>
22+
/// <param name="builder"></param>
23+
/// <returns></returns>
24+
public static IApplicationBuilder UseHttpWwwRewrite(this IApplicationBuilder builder)
25+
{
26+
return builder.UseMiddleware<HttpWwwRewriteMiddleware>();
27+
}
1628
}
1729
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Fan.Models;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Http.Extensions;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Options;
7+
using Microsoft.Net.Http.Headers;
8+
using System;
9+
using System.Threading.Tasks;
10+
11+
namespace Fan.Web.Middlewares
12+
{
13+
public class HttpWwwRewriteMiddleware
14+
{
15+
private readonly RequestDelegate _next;
16+
private ILogger<HttpWwwRewriteMiddleware> _logger;
17+
18+
public HttpWwwRewriteMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
19+
{
20+
_next = next ?? throw new ArgumentNullException(nameof(next));
21+
_logger = loggerFactory.CreateLogger<HttpWwwRewriteMiddleware>();
22+
}
23+
24+
public Task Invoke(HttpContext context, IHttpWwwRewriter helper)
25+
{
26+
// has to locate service instead of inject in for appsettings update to be picked up in middleware automatically
27+
var settings = context.RequestServices.GetService<IOptionsSnapshot<AppSettings>>().Value;
28+
29+
_logger.LogDebug("PreferredDomain {@PreferredDomain}", settings.PreferredDomain);
30+
_logger.LogDebug("UseHttps {@UseHttps}", settings.UseHttps);
31+
32+
if (helper.ShouldRewrite(settings, context.Request.GetDisplayUrl(), out string url))
33+
{
34+
_logger.LogInformation("RewriteUrl: {@RewriteUrl}", url);
35+
36+
context.Response.Headers[HeaderNames.Location] = url;
37+
context.Response.StatusCode = 301;
38+
context.Response.Redirect(url);
39+
return Task.CompletedTask;
40+
}
41+
42+
return _next(context);
43+
}
44+
}
45+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using Fan.Enums;
2+
using Fan.Models;
3+
using Microsoft.Extensions.Logging;
4+
using System;
5+
using System.Linq;
6+
7+
namespace Fan.Web.Middlewares
8+
{
9+
public class HttpWwwRewriter : IHttpWwwRewriter
10+
{
11+
private ILogger<HttpWwwRewriter> _logger;
12+
private bool _schemeRequireUpdate;
13+
private bool _hostRequireWwwAddition;
14+
private bool _hostRequireWwwRemoval;
15+
16+
public HttpWwwRewriter(ILogger<HttpWwwRewriter> logger)
17+
{
18+
_logger = logger;
19+
}
20+
21+
/// <summary>
22+
/// Returns true if request url requires a url rewrite based on appsettings,
23+
/// the out param url will be the new url to redirect to.
24+
/// </summary>
25+
/// <param name="appSettings"></param>
26+
/// <param name="requestUrl"></param>
27+
/// <param name="url"></param>
28+
/// <returns></returns>
29+
public bool ShouldRewrite(AppSettings appSettings, string requestUrl, out string url)
30+
{
31+
Uri uri = new Uri(requestUrl);
32+
string host = uri.Authority; // host with port
33+
34+
// if useHttps is set to false, but the user is using https, that's ok
35+
_schemeRequireUpdate = appSettings.UseHttps && uri.Scheme != "https";
36+
37+
// add www if domain does not start with www and domain has only 1 dot,
38+
// so yoursite.azurewebsites.net and localhost:1234 would disqualify it
39+
_hostRequireWwwAddition = appSettings.PreferredDomain == EPreferredDomain.Www &&
40+
!host.StartsWith("www.") &&
41+
host.Count(s => s == '.') == 1;
42+
43+
// remove www if domain starts with www
44+
_hostRequireWwwRemoval = appSettings.PreferredDomain == EPreferredDomain.NonWww && host.StartsWith("www.");
45+
46+
url = GetUrl(uri.Scheme, host, uri.PathAndQuery, uri.Fragment);
47+
return _schemeRequireUpdate || _hostRequireWwwAddition || _hostRequireWwwRemoval;
48+
}
49+
50+
private string GetUrl(string scheme, string host, string pathAndQuery, string fragment)
51+
{
52+
scheme = _schemeRequireUpdate ? "https" : scheme;
53+
54+
if (_hostRequireWwwAddition)
55+
{
56+
host = $"www.{host}";
57+
}
58+
else if (_hostRequireWwwRemoval)
59+
{
60+
int index = host.IndexOf("www.");
61+
host = host.Remove(index, 4);
62+
}
63+
64+
return $"{scheme}://{host}{pathAndQuery}{fragment}";
65+
}
66+
}
67+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Fan.Models;
2+
3+
namespace Fan.Web.Middlewares
4+
{
5+
public interface IHttpWwwRewriter
6+
{
7+
/// <summary>
8+
/// Returns true if request url requires a url rewrite based on appsettings,
9+
/// the out param url will be the new url to redirect to.
10+
/// </summary>
11+
/// <param name="appSettings"></param>
12+
/// <param name="requestUrl"></param>
13+
/// <param name="url"></param>
14+
/// <returns></returns>
15+
bool ShouldRewrite(AppSettings appSettings, string requestUrl, out string url);
16+
}
17+
}

src/Fan.Web/Startup.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using AutoMapper;
22
using Fan.Data;
3+
using Fan.Enums;
34
using Fan.Helpers;
45
using Fan.Models;
56
using Fan.Services;
67
using Fan.Web.MetaWeblog;
8+
using Fan.Web.Middlewares;
79
using Microsoft.AspNetCore.Builder;
810
using Microsoft.AspNetCore.Hosting;
911
using Microsoft.AspNetCore.Identity;
@@ -12,6 +14,7 @@
1214
using Microsoft.Extensions.Configuration;
1315
using Microsoft.Extensions.DependencyInjection;
1416
using Microsoft.Extensions.Logging;
17+
using System;
1518
using System.IO;
1619

1720
namespace Fan.Web
@@ -20,11 +23,11 @@ public class Startup
2023
{
2124
private ILogger<Startup> _logger;
2225

23-
public Startup(IConfiguration configuration, IHostingEnvironment env, ILoggerFactory loggerFactory)
26+
public Startup(IConfiguration configuration, IHostingEnvironment env, ILogger<Startup> logger)
2427
{
2528
HostingEnvironment = env;
2629
Configuration = configuration;
27-
_logger = loggerFactory.CreateLogger<Startup>();
30+
_logger = logger;
2831
}
2932

3033
public IConfiguration Configuration { get; }
@@ -35,8 +38,8 @@ public void ConfigureServices(IServiceCollection services)
3538
// Db
3639
services.AddDbContext<FanDbContext>(builder =>
3740
{
38-
bool.TryParse(Configuration["Database:UseSqLite"], out bool useSqLite);
39-
if (useSqLite)
41+
Enum.TryParse(Configuration["AppSettings:Database"], ignoreCase: true, result: out ESupportedDatabase db);
42+
if (db == ESupportedDatabase.Sqlite)
4043
{
4144
builder.UseSqlite("Data Source=" + Path.Combine(HostingEnvironment.ContentRootPath, "Fanray.sqlite"));
4245
_logger.LogInformation("Using SQLite database.");
@@ -76,6 +79,7 @@ public void ConfigureServices(IServiceCollection services)
7679
services.AddScoped<IBlogService, BlogService>();
7780
services.AddScoped<IXmlRpcHelper, XmlRpcHelper>();
7881
services.AddScoped<IMetaWeblogService, MetaWeblogService>();
82+
services.AddScoped<IHttpWwwRewriter, HttpWwwRewriter>();
7983
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
8084

8185
// Mvc
@@ -84,6 +88,9 @@ public void ConfigureServices(IServiceCollection services)
8488

8589
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
8690
{
91+
// https and www rewrite
92+
app.UseHttpWwwRewrite();
93+
8794
// OLW
8895
app.MapWhen(context => context.Request.Path.ToString().Equals("/olw"), appBuilder => appBuilder.UseMetablog());
8996

src/Fan.Web/appsettings.Production.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
2-
"Database": {
3-
"UseSqLite": false
2+
"AppSettings": {
3+
"Database": "sqlserver",
4+
"PreferredDomain": "www",
5+
"UseHttps": true
46
},
57
"Serilog": {
68
"Using": [ "Serilog.Sinks.RollingFile" ],

src/Fan.Web/appsettings.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
2-
"Database": {
3-
"UseSqLite": true
2+
"AppSettings": {
3+
"Version": "1.0.0-alpha",
4+
"Database": "sqlite",
5+
"PreferredDomain": "auto",
6+
"UseHttps": false
47
},
58
"ConnectionStrings": {
69
"DefaultConnection": "Server=.\\sql2016;Database=Fanray;Trusted_Connection=True;MultipleActiveResultSets=true"

src/Fan/Models/AppSettings.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ public class AppSettings
4040
///
4141
/// Note if user sets this value to false but is already using https, I don't downgrade
4242
/// you to http as this is good for SEO, Google strongly recommend all website to use https.
43+
/// Also if you are running locally with console, set this value to false as console may
44+
/// not support https.
4345
/// </remarks>
4446
public bool UseHttps { get; set; }
4547
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using Fan.Enums;
2+
using Fan.Models;
3+
using Fan.Web.Middlewares;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Collections;
8+
using System.Collections.Generic;
9+
using Xunit;
10+
11+
namespace Fan.Web.Tests.UrlRewrite
12+
{
13+
public class AppSettingsTestData : IEnumerable<object[]>
14+
{
15+
private readonly List<object[]> _data = new List<object[]>
16+
{
17+
new object[] {
18+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = true },
19+
"http://test.com/about?name=john&age=10",
20+
true,
21+
"https://www.test.com/about?name=john&age=10"
22+
},
23+
new object[] {
24+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = true },
25+
"https://www.test.com/about?name=john",
26+
false, // when the url is already correct, I don't do redirect
27+
"https://www.test.com/about?name=john"
28+
},
29+
new object[] {
30+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = false },
31+
"http://test.com/about?name=john",
32+
true,
33+
"http://www.test.com/about?name=john"
34+
},
35+
new object[] {
36+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = false }, // when UseHttps is set to false
37+
"https://www.test.com/about?name=john", // but user is using https, the recommended way
38+
false, // I don't do redirect
39+
"https://www.test.com/about?name=john" // I don't downgrade you
40+
},
41+
// --------------------------------------------------- nonwww tests
42+
new object[] {
43+
new AppSettings { PreferredDomain = EPreferredDomain.NonWww, UseHttps = true },
44+
"http://www.test.com/about?name=john",
45+
true,
46+
"https://test.com/about?name=john"
47+
},
48+
new object[] {
49+
new AppSettings { PreferredDomain = EPreferredDomain.NonWww, UseHttps = true },
50+
"https://test.com/about?name=john",
51+
false, // when the url is already correct, we don't do redirect
52+
"https://test.com/about?name=john"
53+
},
54+
new object[] {
55+
new AppSettings { PreferredDomain = EPreferredDomain.NonWww, UseHttps = false },
56+
"http://www.test.com/about?name=john",
57+
true,
58+
"http://test.com/about?name=john"
59+
},
60+
new object[] {
61+
new AppSettings { PreferredDomain = EPreferredDomain.NonWww, UseHttps = false }, // when UseHttps is set to false
62+
"https://test.com/about?name=john", // but user is using https, the recommended way
63+
false, // I don't do redirect
64+
"https://test.com/about?name=john" // I don't downgrade you
65+
},
66+
// -------------------------------------------------- www / nonwww setting makes no effect if subdomain is other than www
67+
new object[] {
68+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = true },
69+
"http://plus.google.com/+John/posts/BPn85MDmCSL",
70+
true,
71+
"https://plus.google.com/+John/posts/BPn85MDmCSL"
72+
},
73+
new object[] {
74+
new AppSettings { PreferredDomain = EPreferredDomain.Www, UseHttps = true },
75+
"https://plus.google.com/+John/posts/BPn85MDmCSL",
76+
false,
77+
"https://plus.google.com/+John/posts/BPn85MDmCSL"
78+
},
79+
new object[] {
80+
new AppSettings { PreferredDomain = EPreferredDomain.NonWww, UseHttps = true },
81+
"http://blog.test.com/#resource/sub/af-6f/res/ft.eb/site",
82+
true,
83+
"https://blog.test.com/#resource/sub/af-6f/res/ft.eb/site",
84+
},
85+
// --------------------------------------------------- auto
86+
new object[] {
87+
new AppSettings { PreferredDomain = EPreferredDomain.Auto, UseHttps = true },
88+
"http://plus.google.com/+John/posts/BPn85MDmCSL",
89+
true,
90+
"https://plus.google.com/+John/posts/BPn85MDmCSL"
91+
},
92+
new object[] {
93+
new AppSettings { PreferredDomain = EPreferredDomain.Auto, UseHttps = false },
94+
"http://plus.google.com/+John/posts/BPn85MDmCSL",
95+
false,
96+
"http://plus.google.com/+John/posts/BPn85MDmCSL"
97+
},
98+
};
99+
100+
public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();
101+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
102+
}
103+
104+
public class HttpWwwRewriterTest
105+
{
106+
private IHttpWwwRewriter _helper;
107+
public HttpWwwRewriterTest()
108+
{
109+
var serviceProvider = new ServiceCollection().AddLogging().BuildServiceProvider();
110+
var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
111+
_helper = new HttpWwwRewriter(loggerFactory.CreateLogger<HttpWwwRewriter>());
112+
}
113+
114+
[Theory]
115+
[ClassData(typeof(AppSettingsTestData))]
116+
public void ShouldRewriteTest(AppSettings appSettings, string requestUrl, bool expectedShould, string expectedUrl)
117+
{
118+
bool should = _helper.ShouldRewrite(appSettings, requestUrl, out string url);
119+
Assert.Equal(expectedShould, should);
120+
Assert.Equal(expectedUrl, url);
121+
}
122+
123+
/// <summary>
124+
/// Test <see cref="Uri"/> class, I use this class as a parser and it's important to
125+
/// understand what it does.
126+
/// </summary>
127+
[Fact]
128+
public void TestUri()
129+
{
130+
Uri uri = new Uri("http://test.com/about?name=john&age=10#abc");
131+
Assert.Equal("/about", uri.AbsolutePath);
132+
Assert.Equal("http://test.com/about?name=john&age=10#abc", uri.AbsoluteUri);
133+
Assert.Equal("test.com", uri.Authority);
134+
Assert.Equal("test.com", uri.DnsSafeHost);
135+
Assert.Equal("#abc", uri.Fragment);
136+
Assert.Equal("test.com", uri.Host);
137+
Assert.Equal("http://test.com/about?name=john&age=10#abc", uri.OriginalString);
138+
Assert.Equal("/about?name=john&age=10", uri.PathAndQuery);
139+
Assert.Equal(80, uri.Port);
140+
Assert.Equal("?name=john&age=10", uri.Query);
141+
Assert.Equal("http", uri.Scheme);
142+
Assert.Equal(2, uri.Segments.Length);
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)