Initial development (#1)

Initial development of this middleware
This commit is contained in:
Daniel J. Summers 2023-07-08 07:28:22 -04:00 committed by GitHub
parent f4799fbe5e
commit 9c782fdca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 1 deletions

1
.gitignore vendored
View File

@ -378,7 +378,6 @@ FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>1.0.0</VersionPrefix>
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<Description>ASP.NET Core middleware to enforce canonical domains</Description>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://bitbadger.solutions/open-source/canonical-domain-middleware/</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/bit-badger/BitBadger.AspNetCore.CanonicalDomains</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>aspnetcore middleware canonical</PackageTags>
</PropertyGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace BitBadger.AspNetCore.CanonicalDomains;
/// <summary>
/// Middleware to enforce canonical domains
/// </summary>
public class CanonicalDomainMiddleware
{
/// <summary>
/// The domains which should be redirected
/// </summary>
internal static readonly IDictionary<string, string> CanonicalDomains = new Dictionary<string, string>();
/// <summary>
/// The next middleware in the pipeline to be executed
/// </summary>
private readonly RequestDelegate _next;
/// <summary>
/// Constructor
/// </summary>
/// <param name="next">The next middleware in the pipeline to be exectued</param>
public CanonicalDomainMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext ctx)
{
if (CanonicalDomains.ContainsKey(ctx.Request.Host.Host))
{
UriBuilder uri = new(ctx.Request.GetDisplayUrl());
uri.Host = CanonicalDomains[ctx.Request.Host.Host];
ctx.Response.Redirect(uri.Uri.ToString(), permanent: true);
}
else
{
await _next.Invoke(ctx);
}
}
}

View File

@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace BitBadger.AspNetCore.CanonicalDomains;
/// <summary>
/// Extensions on the <see cref="IApplicationBuilder" /> interface
/// </summary>
public static class IApplicationBuilderExtensions
{
/// <summary>
/// Initialize and use the canonical domain middleware
/// </summary>
public static IApplicationBuilder UseCanonicalDomains(this IApplicationBuilder app)
{
ParseConfiguration(GetService<IConfiguration>(app)!.GetSection("CanonicalDomains"));
if (CanonicalDomainMiddleware.CanonicalDomains.Count > 0)
{
return app.UseMiddleware<CanonicalDomainMiddleware>();
}
WarnForMissingConfig(app);
return app;
}
/// <summary>
/// Shorthand for retrieving typed services from the application's service provider
/// </summary>
/// <param name="app">The application builder</param>
/// <returns>The requested service, or null if it was not able to be found</returns>
private static T? GetService<T>(IApplicationBuilder app) =>
(T?)app.ApplicationServices.GetService(typeof(T));
/// <summary>
/// Extract the from/to domain paris from the configuration
/// </summary>
/// <param name="section">The <tt>CanonicalDomains</tt> configuration section</param>
private static void ParseConfiguration(IConfigurationSection? section)
{
if (section is not null)
{
foreach (var item in section.GetChildren())
{
var nonCanonical = item["From"];
var canonical = item["To"];
if (nonCanonical is not null && canonical is not null)
{
CanonicalDomainMiddleware.CanonicalDomains.Add(nonCanonical, canonical);
}
}
}
}
/// <summary>
/// Generate a warning if no configured domains were found
/// </summary>
/// <param name="app">The application builder</param>
private static void WarnForMissingConfig(IApplicationBuilder app)
{
var logger = GetService<ILogger<CanonicalDomainMiddleware>>(app);
if (logger is not null)
{
logger.LogWarning("No canonical domain configuration was found; no domains will be redirected");
}
}
}

View File

@ -0,0 +1,36 @@
## BitBadger.AspNetCore.CanonicalDomains
This package provides ASP.NET Core middleware to enforce [canonical domains](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Choosing_between_www_and_non-www_URLs).
### What It Does
Having multiple domain names pointing to the same content can lead to inconsistency and diluted search rankings. This middleware intercepts known alternate domains and redirects requests to the canonical domain, ensuring uniformity and unified search rankings.
_If the ASP.NET Core application is running behind a reverse proxy (Nginx, Apache, IIS, etc.), enforcing these domains at that point is the most efficient. This middleware is designed for scenarios where the application is being served directly or in a container, with multiple domains pointed at the running application._
### How to Use
First, install this package.
Second, add the configuration for each domain that needs to be redirected; this middleware will configure itself via the details in the `CanonicalDomains` configuration key. An example:
```json
{
"CanonicalDomains": [
{
"From": "www.example.com",
"To": "example.com"
},
{
"From": "web.example.com",
"To": "example.com"
}
]
}
```
Finally, in your main source file (`Program.cs`, `App.fs`, etc.), import the namespace `BitBadger.AspNetCore.CanonicalDomains`, and call `.UseCanonicalDomains()` on the `IApplicationBuilder` instance. It should be placed after `.UseForwardedHeaders()`, if that is used, but should be ahead of `.UseStaticFiles()`, auth config, endpoints, etc. It should be run as close to the start of the pipeline as possible, as no other processing should take place until the request is made on the canonical domain.
### Troubleshooting
This middleware will not throw errors if it cannot parse its configuration properly _(feel free to do the final step before adding configuration to verify!)_. However, if `.UseCanonicalDomains()` is called, and the setup does not find anything to do, it will emit a warning in the log, and will not add the middleware to the pipeline. If redirection is not occurring as you suspect it should, check the top of the log when the application starts.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB