From e39ee0531437c38b4fbec944eb745d2cc14e0216 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 4 Jul 2023 23:33:36 -0400 Subject: [PATCH 1/6] WIP --- .gitignore | 1 - ...tBadger.AspNetCore.CanonicalDomains.csproj | 14 +++++ .../CanonicalDomainMiddleware.cs | 41 +++++++++++++ .../IApplicationBuilderExtensions.cs | 57 +++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj create mode 100644 src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs create mode 100644 src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs diff --git a/.gitignore b/.gitignore index 8a30d25..1b75002 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj new file mode 100644 index 0000000..fd80d9d --- /dev/null +++ b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs b/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs new file mode 100644 index 0000000..ca875be --- /dev/null +++ b/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace BitBadger.AspNetCore.CanonicalDomains; + +public class CanonicalDomainMiddleware +{ + /// + /// The domains which should be redirected + /// + internal static readonly IDictionary CanonicalDomains = new Dictionary(); + + /// + /// The next middleware in the pipeline to be executed + /// + private readonly RequestDelegate _next; + + /// + /// Constructor + /// + /// The next middleware in the pipeline to be exectued + public CanonicalDomainMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext ctx) + { + var host = ctx.Request.Host.Host; + if (CanonicalDomains.ContainsKey(host)) + { + UriBuilder uri = new(ctx.Request.GetDisplayUrl()); + uri.Host = CanonicalDomains[host]; + ctx.Response.Redirect(uri.Uri.ToString ()); + } + else + { + await _next.Invoke(ctx); + } + } +} diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs b/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs new file mode 100644 index 0000000..7610e9d --- /dev/null +++ b/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace BitBadger.AspNetCore.CanonicalDomains; + +/// +/// Extensions on the interface +/// +public static class IApplicationBuilderExtensions +{ + /// + /// Initialize and use the canonical domain middleware + /// + public static IApplicationBuilder UseCanonicalDomains(this IApplicationBuilder app) + { + void warnForMissingConfig() { + var logger = (ILogger?)app.ApplicationServices + .GetService(typeof(ILogger)); + if (logger is not null) + { + logger.LogWarning("No canonical domain configuration was found; no domains will be redirected"); + } + } + + var config = (IConfiguration)app.ApplicationServices.GetService(typeof(IConfiguration))!; + + var section = config.GetSection("CanonicalDomains"); + 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); + } + } + + if (CanonicalDomainMiddleware.CanonicalDomains.Count > 0) + { + app.UseMiddleware (); + } + else + { + warnForMissingConfig(); + } + } + else + { + warnForMissingConfig(); + } + + return app; + } +} \ No newline at end of file -- 2.45.1 From e9a054d743fbe37de538c14451f4b97ba13f9418 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 5 Jul 2023 20:44:41 -0400 Subject: [PATCH 2/6] More work, but still WIP --- .../CanonicalDomainMiddleware.cs | 10 +-- .../IApplicationBuilderExtensions.cs | 63 +++++++++++-------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs b/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs index ca875be..853e8aa 100644 --- a/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs +++ b/src/BitBadger.AspNetCore.CanonicalDomains/CanonicalDomainMiddleware.cs @@ -3,6 +3,9 @@ using Microsoft.AspNetCore.Http.Extensions; namespace BitBadger.AspNetCore.CanonicalDomains; +/// +/// Middleware to enforce canonical domains +/// public class CanonicalDomainMiddleware { /// @@ -26,12 +29,11 @@ public class CanonicalDomainMiddleware public async Task InvokeAsync(HttpContext ctx) { - var host = ctx.Request.Host.Host; - if (CanonicalDomains.ContainsKey(host)) + if (CanonicalDomains.ContainsKey(ctx.Request.Host.Host)) { UriBuilder uri = new(ctx.Request.GetDisplayUrl()); - uri.Host = CanonicalDomains[host]; - ctx.Response.Redirect(uri.Uri.ToString ()); + uri.Host = CanonicalDomains[ctx.Request.Host.Host]; + ctx.Response.Redirect(uri.Uri.ToString(), permanent: true); } else { diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs b/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs index 7610e9d..cf43cc0 100644 --- a/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs +++ b/src/BitBadger.AspNetCore.CanonicalDomains/IApplicationBuilderExtensions.cs @@ -14,18 +14,31 @@ public static class IApplicationBuilderExtensions /// public static IApplicationBuilder UseCanonicalDomains(this IApplicationBuilder app) { - void warnForMissingConfig() { - var logger = (ILogger?)app.ApplicationServices - .GetService(typeof(ILogger)); - if (logger is not null) - { - logger.LogWarning("No canonical domain configuration was found; no domains will be redirected"); - } + ParseConfiguration(GetService(app)!.GetSection("CanonicalDomains")); + + if (CanonicalDomainMiddleware.CanonicalDomains.Count > 0) + { + return app.UseMiddleware(); } + + WarnForMissingConfig(app); + return app; + } - var config = (IConfiguration)app.ApplicationServices.GetService(typeof(IConfiguration))!; - - var section = config.GetSection("CanonicalDomains"); + /// + /// Shorthand for retrieving typed services from the application's service provider + /// + /// The application builder + /// The requested service, or null if it was not able to be found + private static T? GetService(IApplicationBuilder app) => + (T?)app.ApplicationServices.GetService(typeof(T)); + + /// + /// Extract the from/to domain paris from the configuration + /// + /// The CanonicalDomains configuration section + private static void ParseConfiguration(IConfigurationSection? section) + { if (section is not null) { foreach (var item in section.GetChildren()) @@ -37,21 +50,19 @@ public static class IApplicationBuilderExtensions CanonicalDomainMiddleware.CanonicalDomains.Add(nonCanonical, canonical); } } - - if (CanonicalDomainMiddleware.CanonicalDomains.Count > 0) - { - app.UseMiddleware (); - } - else - { - warnForMissingConfig(); - } } - else - { - warnForMissingConfig(); - } - - return app; } -} \ No newline at end of file + + /// + /// Generate a warning if no configured domains were found + /// + /// The application builder + private static void WarnForMissingConfig(IApplicationBuilder app) + { + var logger = GetService>(app); + if (logger is not null) + { + logger.LogWarning("No canonical domain configuration was found; no domains will be redirected"); + } + } +} -- 2.45.1 From 269179e347989e5af2e1d6842d8f12cb55195e97 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 6 Jul 2023 21:22:07 -0400 Subject: [PATCH 3/6] Add pkg README and properties --- ...tBadger.AspNetCore.CanonicalDomains.csproj | 15 +++++++- .../README.md | 36 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/BitBadger.AspNetCore.CanonicalDomains/README.md diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj index fd80d9d..b2358e1 100644 --- a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj +++ b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj @@ -1,9 +1,22 @@ - net7.0 + net6.0;net7.0 enable enable + 1.0.0 + Initial release + danieljsummers + Bit Badger Solutions + ASP.NET Core middleware to enforce canonical domains + README.md + https://github.com/bit-badger/BitBadger.AspNetCore.CanonicalDomains + false + https://github.com/bit-badger/BitBadger.AspNetCore.CanonicalDomains + Git + MIT License + MIT + aspnetcore middleware canonical diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/README.md b/src/BitBadger.AspNetCore.CanonicalDomains/README.md new file mode 100644 index 0000000..483e85c --- /dev/null +++ b/src/BitBadger.AspNetCore.CanonicalDomains/README.md @@ -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. -- 2.45.1 From 22a63209835d91a9192a9939c11233a59cf6e15f Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 7 Jul 2023 16:55:39 -0400 Subject: [PATCH 4/6] Add icon --- ...BitBadger.AspNetCore.CanonicalDomains.csproj | 3 ++- .../icon.png | Bin 0 -> 25176 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 src/BitBadger.AspNetCore.CanonicalDomains/icon.png diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj index b2358e1..876fe19 100644 --- a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj +++ b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj @@ -9,8 +9,9 @@ danieljsummers Bit Badger Solutions ASP.NET Core middleware to enforce canonical domains + icon.png README.md - https://github.com/bit-badger/BitBadger.AspNetCore.CanonicalDomains + https://bitbadger.solutions/open-source/canonical-domain-middleware/ false https://github.com/bit-badger/BitBadger.AspNetCore.CanonicalDomains Git diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/icon.png b/src/BitBadger.AspNetCore.CanonicalDomains/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..da6be2aecde37d3d614f3314a70bcbf0b1b2182f GIT binary patch literal 25176 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 Vtc#mjbM^oL002ovPDHLkV1f;8thWFF literal 0 HcmV?d00001 -- 2.45.1 From f4799fbe5ec5cd81ac10fd9cfd0e57d7c7744c3e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 8 Jul 2023 07:20:56 -0400 Subject: [PATCH 5/6] Add GitHub CI job --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb928dd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-and-test: + + runs-on: ubuntu-latest + + strategy: + matrix: + dotnet-version: [ "6.0", "7.0" ] + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET ${{ matrix.dotnet-version }}.x + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ matrix.dotnet-version }}.x + - name: Restore dependencies + run: dotnet restore src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj + - name: Build + run: dotnet build src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj --no-restore +# TODO: set up tests +# - name: Test (.NET ${{ matrix.dotnet-version }}) +# run: dotnet run --project path/to/project -f net${{ matrix.dotnet-version }} + publish: + runs-on: ubuntu-latest + needs: build-and-test + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: "7.0" + - name: Package library + run: dotnet pack src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj -c Release + - name: Move package + run: cp src/BitBadger.AspNetCore.CanonicalDomains/bin/Release/BitBadger.AspNetCore.CanonicalDomains.*.nupkg . + - name: Save Packages + uses: actions/upload-artifact@v3 + with: + name: packages + path: | + *.nupkg -- 2.45.1 From c17b99fa5b774465ecd5c56bdef65914497b2e86 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sat, 8 Jul 2023 07:26:19 -0400 Subject: [PATCH 6/6] Add package items to project file --- .../BitBadger.AspNetCore.CanonicalDomains.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj index 876fe19..70c76c3 100644 --- a/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj +++ b/src/BitBadger.AspNetCore.CanonicalDomains/BitBadger.AspNetCore.CanonicalDomains.csproj @@ -20,6 +20,11 @@ aspnetcore middleware canonical + + + + + -- 2.45.1