77using Microsoft . Extensions . Logging ;
88using 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
0 commit comments