Skip to content

Commit 3f1140f

Browse files
committed
CanonicalHostRedirect rename and update behavior to align with UseHttpsRedirection in case of not configured. Add logging infrastrcutre for HealthProbe
1 parent 7707fa0 commit 3f1140f

5 files changed

Lines changed: 83 additions & 47 deletions

File tree

src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalRedirect/CanonicalRedirect.cs renamed to src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalHostRedirect/CanonicalHostRedirect.cs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using Microsoft.Extensions.Logging;
88
using Microsoft.Extensions.Options;
99

10-
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalRedirect
10+
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalHostRedirect
1111
{
1212
/// <summary>
1313
/// Middleware that permanently redirects requests to a canonical host and (optionally) enforces HTTPS.
@@ -17,28 +17,28 @@ namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalRedirect
1717
/// If the app is behind a reverse proxy that terminates TLS, ensure forwarded headers are applied before this middleware
1818
/// so <see cref="HttpRequest.IsHttps"/> reflects the external scheme.
1919
/// </remarks>
20-
public sealed class CanonicalRedirect
20+
public sealed class CanonicalHostRedirect
2121
{
2222
private readonly RequestDelegate _next;
23-
private readonly IDeferredLogger<CanonicalRedirect> _logger;
24-
private readonly IOptionsMonitor<CanonicalRedirectOptions> _optionsMonitor;
23+
private readonly IDeferredLogger<CanonicalHostRedirect> _logger;
24+
private readonly IOptionsMonitor<CanonicalHostRedirectOptions> _optionsMonitor;
2525

2626
/// <summary>
27-
/// Initializes a new instance of the <see cref="CanonicalRedirect"/> class.
27+
/// Initializes a new instance of the <see cref="CanonicalHostRedirect"/> class.
2828
/// </summary>
2929
/// <param name="nextMiddleware">The next middleware in the pipeline.</param>
3030
/// <param name="logger">The deferred logger instance.</param>
31-
/// <param name="optionsMonitor">The options monitor for <see cref="CanonicalRedirectOptions"/>.</param>
32-
public CanonicalRedirect(
31+
/// <param name="optionsMonitor">The options monitor for <see cref="CanonicalHostRedirectOptions"/>.</param>
32+
public CanonicalHostRedirect(
3333
RequestDelegate nextMiddleware,
34-
IDeferredLogger<CanonicalRedirect> logger,
35-
IOptionsMonitor<CanonicalRedirectOptions> optionsMonitor)
34+
IDeferredLogger<CanonicalHostRedirect> logger,
35+
IOptionsMonitor<CanonicalHostRedirectOptions> optionsMonitor)
3636
{
3737
_next = nextMiddleware ?? throw new ArgumentNullException(nameof(nextMiddleware));
3838
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3939
_optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor));
4040

41-
_optionsMonitor.OnChange(_ => _logger.LogDebug("Configuration for {MiddlewareName} updated.", () => nameof(CanonicalRedirect)));
41+
_optionsMonitor.OnChange(_ => _logger.LogDebug("Configuration for {MiddlewareName} updated.", () => nameof(CanonicalHostRedirect)));
4242
}
4343

4444
/// <summary>
@@ -50,47 +50,65 @@ public async Task InvokeAsync(HttpContext context)
5050
{
5151
if (context == null) throw new ArgumentNullException(nameof(context));
5252

53-
CanonicalRedirectOptions options = _optionsMonitor.CurrentValue;
53+
CanonicalHostRedirectOptions options = _optionsMonitor.CurrentValue;
5454

5555
if (!options.Enabled)
5656
{
5757
await _next(context);
5858
return;
5959
}
6060

61-
string requestHost = context.Request.Host.Host ?? string.Empty;
62-
if (requestHost.Length == 0)
61+
if (!context.Request.Host.HasValue)
6362
{
6463
await _next(context);
6564
return;
6665
}
6766

68-
if (!TryResolveCanonicalHost(requestHost, options, out string canonicalHost))
67+
var requestHost = context.Request.Host; // includes port when present
68+
var requestHostOnly = requestHost.Host ?? string.Empty;
69+
70+
if (requestHostOnly.Length == 0)
6971
{
7072
await _next(context);
7173
return;
7274
}
7375

74-
bool needsHostRedirect = !string.Equals(requestHost, canonicalHost, StringComparison.OrdinalIgnoreCase);
76+
// 1) HTTPS enforcement must be independent of canonical-host logic.
7577
bool needsHttpsRedirect = options.EnforceHttps && !context.Request.IsHttps;
7678

79+
// 2) Canonical host resolution is optional. If not configured / not matching, keep current host.
80+
string targetHostOnly = requestHostOnly;
81+
bool needsHostRedirect = false;
82+
83+
if (!string.IsNullOrWhiteSpace(options.PrimaryApexHost) &&
84+
TryResolveCanonicalHost(requestHostOnly, options, out string canonicalHostOnly))
85+
{
86+
targetHostOnly = canonicalHostOnly;
87+
needsHostRedirect = !string.Equals(requestHostOnly, targetHostOnly, StringComparison.OrdinalIgnoreCase);
88+
}
89+
7790
if (!needsHostRedirect && !needsHttpsRedirect)
7891
{
7992
await _next(context);
8093
return;
8194
}
8295

83-
string targetScheme = options.EnforceHttps ? "https" : (context.Request.Scheme ?? "http");
96+
// Preserve port from incoming Host header (important for dev / non-443 setups).
97+
HostString targetHost = requestHost.Port.HasValue
98+
? new HostString(targetHostOnly, requestHost.Port.Value)
99+
: new HostString(targetHostOnly);
100+
101+
string targetScheme = needsHttpsRedirect ? "https" : (context.Request.Scheme ?? "http");
84102
string pathAndQuery = $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
85-
string location = $"{targetScheme}://{canonicalHost}{pathAndQuery}";
103+
string location = $"{targetScheme}://{targetHost}{pathAndQuery}";
86104

87105
if (options.LogLevelRedirect != LogLevel.None && _logger.IsEnabled(options.LogLevelRedirect))
88106
{
89107
_logger.Log(
90108
options.LogLevelRedirect,
91109
"Permanent canonical redirect. From {FromScheme}://{FromHost}{FromPath} to {ToLocation}.",
92110
() => context.Request.Scheme ?? string.Empty,
93-
() => requestHost,
111+
() => requestHost.Value ?? string.Empty,
94112
() => pathAndQuery,
95113
() => location);
96114
}
@@ -99,7 +117,8 @@ public async Task InvokeAsync(HttpContext context)
99117
context.Response.Headers.Location = location;
100118
}
101119

102-
private static bool TryResolveCanonicalHost(string requestHost, CanonicalRedirectOptions options, out string canonicalHost)
120+
121+
private static bool TryResolveCanonicalHost(string requestHost, CanonicalHostRedirectOptions options, out string canonicalHost)
103122
{
104123
canonicalHost = string.Empty;
105124

src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalRedirect/CanonicalRedirectOptions.cs renamed to src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalHostRedirect/CanonicalHostRedirectOptions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Microsoft.AspNetCore.Http;
22
using Microsoft.Extensions.Logging;
33

4-
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalRedirect
4+
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalHostRedirect
55
{
66
/// <summary>
77
/// Determines how host canonicalization is applied.
@@ -25,13 +25,13 @@ public enum CanonicalHostMode
2525
}
2626

2727
/// <summary>
28-
/// Provides configuration options for <see cref="CanonicalRedirect"/>.
28+
/// Provides configuration options for <see cref="CanonicalHostRedirect"/>.
2929
/// </summary>
3030
/// <remarks>
3131
/// This options type is designed to be bound from configuration, for example from a section named
32-
/// <c>CanonicalRedirectOptions</c>. If the section is missing, property initializers act as defaults.
32+
/// <c>CanonicalHostRedirectOptions</c>. If the section is missing, property initializers act as defaults.
3333
/// </remarks>
34-
public sealed class CanonicalRedirectOptions
34+
public sealed class CanonicalHostRedirectOptions
3535
{
3636
/// <summary>
3737
/// Gets or sets a value indicating whether the middleware is active.

src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalRedirect/IApplicationBuilderExtensions.cs renamed to src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalHostRedirect/IApplicationBuilderExtensions.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,51 +8,51 @@
88
using Microsoft.Extensions.DependencyInjection;
99
using Microsoft.Extensions.Options;
1010

11-
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalRedirect
11+
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalHostRedirect
1212
{
1313
/// <summary>
14-
/// Provides extension methods for registering <see cref="CanonicalRedirect"/> in the application's request pipeline.
14+
/// Provides extension methods for registering <see cref="CanonicalHostRedirect"/> in the application's request pipeline.
1515
/// </summary>
1616
public static partial class IApplicationBuilderExtensions
1717
{
1818
/// <summary>
19-
/// Adds <see cref="CanonicalRedirect"/> to the application's request pipeline.
19+
/// Adds <see cref="CanonicalHostRedirect"/> to the application's request pipeline.
2020
/// </summary>
2121
/// <param name="app">The application builder.</param>
2222
/// <returns>The updated application builder.</returns>
2323
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> is null.</exception>
24-
public static IApplicationBuilder UseCanonicalRedirect(this IApplicationBuilder app)
24+
public static IApplicationBuilder UseCanonicalHostRedirect(this IApplicationBuilder app)
2525
{
2626
ArgumentNullException.ThrowIfNull(app);
2727

2828
app.ApplicationServices.EnsureServicesRegistered(
29-
$"Make sure to register deferred logging via services.{nameof(IServiceCollectionExtensions.AddCanonicalRedirect)}().",
29+
$"Make sure to register deferred logging via services.{nameof(IServiceCollectionExtensions.AddCanonicalHostRedirect)}().",
3030
typeof(IDeferredLogger<>));
3131

32-
return app.UseMiddleware<CanonicalRedirect>();
32+
return app.UseMiddleware<CanonicalHostRedirect>();
3333
}
3434

3535
/// <summary>
36-
/// Adds <see cref="CanonicalRedirect"/> to the request pipeline while applying an additional configuration.
36+
/// Adds <see cref="CanonicalHostRedirect"/> to the request pipeline while applying an additional configuration.
3737
/// The extra configuration is applied on top of the DI-registered options (which are auto-refreshed if appsettings change).
3838
/// </summary>
3939
/// <param name="app">The application builder.</param>
40-
/// <param name="additionalConfigure">Delegate to apply extra configuration to <see cref="CanonicalRedirectOptions"/>.</param>
40+
/// <param name="additionalConfigure">Delegate to apply extra configuration to <see cref="CanonicalHostRedirectOptions"/>.</param>
4141
/// <returns>The updated application builder.</returns>
4242
/// <exception cref="ArgumentNullException">Thrown if <paramref name="app"/> or <paramref name="additionalConfigure"/> is null.</exception>
43-
public static IApplicationBuilder UseCanonicalRedirect(this IApplicationBuilder app, Action<CanonicalRedirectOptions> additionalConfigure)
43+
public static IApplicationBuilder UseCanonicalHostRedirect(this IApplicationBuilder app, Action<CanonicalHostRedirectOptions> additionalConfigure)
4444
{
4545
ArgumentNullException.ThrowIfNull(app);
4646
ArgumentNullException.ThrowIfNull(additionalConfigure);
4747

4848
app.ApplicationServices.EnsureServicesRegistered(
49-
$"Make sure to register deferred logging via services.{nameof(IServiceCollectionExtensions.AddCanonicalRedirect)}().",
49+
$"Make sure to register deferred logging via services.{nameof(IServiceCollectionExtensions.AddCanonicalHostRedirect)}().",
5050
typeof(IDeferredLogger<>));
5151

52-
IOptionsMonitor<CanonicalRedirectOptions> innerOptionsMonitor = app.ApplicationServices.GetRequiredService<IOptionsMonitor<CanonicalRedirectOptions>>();
53-
var decoratedOptionsMonitor = new ConfiguredOptionsMonitor<CanonicalRedirectOptions>(innerOptionsMonitor, additionalConfigure);
52+
IOptionsMonitor<CanonicalHostRedirectOptions> innerOptionsMonitor = app.ApplicationServices.GetRequiredService<IOptionsMonitor<CanonicalHostRedirectOptions>>();
53+
var decoratedOptionsMonitor = new ConfiguredOptionsMonitor<CanonicalHostRedirectOptions>(innerOptionsMonitor, additionalConfigure);
5454

55-
return app.UseMiddleware<CanonicalRedirect>(decoratedOptionsMonitor);
55+
return app.UseMiddleware<CanonicalHostRedirect>(decoratedOptionsMonitor);
5656
}
5757
}
5858
}

src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalRedirect/IServiceCollectionExtensions.cs renamed to src/prj/Eigenverft.Routed.RequestFilters/Middleware/CanonicalHostRedirect/IServiceCollectionExtensions.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using Microsoft.Extensions.DependencyInjection;
77
using Microsoft.Extensions.DependencyInjection.Extensions;
88

9-
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalRedirect
9+
namespace Eigenverft.Routed.RequestFilters.Middleware.CanonicalHostRedirect
1010
{
1111
/// <summary>
1212
/// Provides extension methods for configuring canonical host redirects.
@@ -15,23 +15,23 @@ public static partial class IServiceCollectionExtensions
1515
{
1616
/// <summary>
1717
/// Registers canonical redirect with the standard behavior:
18-
/// binds from configuration section <c>CanonicalRedirectOptions</c> if present,
19-
/// otherwise uses defaults defined on <see cref="CanonicalRedirectOptions"/>.
18+
/// binds from configuration section <c>CanonicalHostRedirectOptions</c> if present,
19+
/// otherwise uses defaults defined on <see cref="CanonicalHostRedirectOptions"/>.
2020
/// </summary>
2121
/// <param name="services">The service collection.</param>
2222
/// <returns>The updated service collection.</returns>
2323
/// <example>
2424
/// <code>
25-
/// builder.Services.AddCanonicalRedirect();
25+
/// builder.Services.AddCanonicalHostRedirect();
2626
/// </code>
2727
/// </example>
28-
public static IServiceCollection AddCanonicalRedirect(this IServiceCollection services)
28+
public static IServiceCollection AddCanonicalHostRedirect(this IServiceCollection services)
2929
{
3030
ArgumentNullException.ThrowIfNull(services);
3131

3232
AddInfrastructure(services);
3333

34-
services.AddOptions<CanonicalRedirectOptions>().BindConfiguration(nameof(CanonicalRedirectOptions));
34+
services.AddOptions<CanonicalHostRedirectOptions>().BindConfiguration(nameof(CanonicalHostRedirectOptions));
3535

3636
return services;
3737
}
@@ -43,12 +43,12 @@ public static IServiceCollection AddCanonicalRedirect(this IServiceCollection se
4343
/// <param name="manualConfigure">Delegate to modify or augment the bound configuration.</param>
4444
/// <returns>The updated service collection.</returns>
4545
/// <exception cref="ArgumentNullException">Thrown if <paramref name="services"/> or <paramref name="manualConfigure"/> is null.</exception>
46-
public static IServiceCollection AddCanonicalRedirect(this IServiceCollection services, Action<CanonicalRedirectOptions> manualConfigure)
46+
public static IServiceCollection AddCanonicalHostRedirect(this IServiceCollection services, Action<CanonicalHostRedirectOptions> manualConfigure)
4747
{
4848
ArgumentNullException.ThrowIfNull(services);
4949
ArgumentNullException.ThrowIfNull(manualConfigure);
5050

51-
services.AddCanonicalRedirect();
51+
services.AddCanonicalHostRedirect();
5252
services.Configure(manualConfigure);
5353

5454
return services;
@@ -58,18 +58,18 @@ public static IServiceCollection AddCanonicalRedirect(this IServiceCollection se
5858
/// Registers canonical redirect options explicitly from a provided configuration and optionally applies extra code-based configuration.
5959
/// </summary>
6060
/// <param name="services">The service collection.</param>
61-
/// <param name="configuration">Configuration root that contains a section named <c>CanonicalRedirectOptions</c>.</param>
61+
/// <param name="configuration">Configuration root that contains a section named <c>CanonicalHostRedirectOptions</c>.</param>
6262
/// <param name="manualConfigure">Optional delegate to modify or augment the bound configuration.</param>
6363
/// <returns>The updated service collection.</returns>
6464
/// <exception cref="ArgumentNullException">Thrown if <paramref name="services"/> or <paramref name="configuration"/> is null.</exception>
65-
public static IServiceCollection AddCanonicalRedirect(this IServiceCollection services, IConfiguration configuration, Action<CanonicalRedirectOptions>? manualConfigure = null)
65+
public static IServiceCollection AddCanonicalHostRedirect(this IServiceCollection services, IConfiguration configuration, Action<CanonicalHostRedirectOptions>? manualConfigure = null)
6666
{
6767
ArgumentNullException.ThrowIfNull(services);
6868
ArgumentNullException.ThrowIfNull(configuration);
6969

7070
AddInfrastructure(services);
7171

72-
services.AddOptions<CanonicalRedirectOptions>().Bind(configuration.GetSection(nameof(CanonicalRedirectOptions)));
72+
services.AddOptions<CanonicalHostRedirectOptions>().Bind(configuration.GetSection(nameof(CanonicalHostRedirectOptions)));
7373

7474
if (manualConfigure != null)
7575
{

src/prj/Eigenverft.Routed.RequestFilters/Middleware/HealthProbeFaviconAware/IServiceCollection.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
22

3+
using Eigenverft.Routed.RequestFilters.Services.DeferredLogger;
4+
35
using Microsoft.Extensions.Configuration;
46
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
58

69
namespace Eigenverft.Routed.RequestFilters.Middleware.HealthProbeFaviconAware
710
{
@@ -26,6 +29,8 @@ public static IServiceCollection AddHealthProbeFaviconAware(this IServiceCollect
2629
{
2730
ArgumentNullException.ThrowIfNull(services);
2831

32+
AddInfrastructure(services);
33+
2934
services.AddOptions<HealthProbeFaviconAwareOptions>().BindConfiguration(nameof(HealthProbeFaviconAwareOptions));
3035

3136
return services;
@@ -63,10 +68,22 @@ public static IServiceCollection AddHealthProbeFaviconAware(this IServiceCollect
6368
ArgumentNullException.ThrowIfNull(services);
6469
ArgumentNullException.ThrowIfNull(configuration);
6570

71+
AddInfrastructure(services);
72+
6673
services.AddOptions<HealthProbeFaviconAwareOptions>().Bind(configuration.GetSection(nameof(HealthProbeFaviconAwareOptions)));
6774
if (manualConfigure != null) services.Configure(manualConfigure);
6875

6976
return services;
7077
}
78+
79+
/// <summary>
80+
/// Adds shared registrations required by canonical redirect.
81+
/// </summary>
82+
/// <param name="services">The service collection.</param>
83+
private static void AddInfrastructure(IServiceCollection services)
84+
{
85+
services.TryAddSingleton(typeof(IDeferredLogger<>), typeof(DeferredLogger<>));
86+
services.AddOptions();
87+
}
7188
}
7289
}

0 commit comments

Comments
 (0)