-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Temporary disable MacOS on GitHub because .NET 9.0 Preview failure * refactor mark static local function * ResponseHeaders support key: value or key=value; multi handlers * Test ResponseHeadersHelper.Parse * Test PrepareResponse * Update tool minver
- Loading branch information
Showing
10 changed files
with
1,249 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,89 +1,84 @@ | ||
using Microsoft.AspNetCore.StaticFiles; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace NetLah.Extensions.SpaServices.Hosting; | ||
|
||
internal class ResponseHeadersHandler | ||
internal class ResponseHeadersHandler(ILogger logger, ResponseHeadersOptions responseHeaders, Action<StaticFileResponseContext> originalResponseHandler) | ||
{ | ||
private readonly ILogger _logger; | ||
private readonly List<KeyValuePair<string, StringValues>> _headers; | ||
private readonly HashSet<string?>? _contentTypeHashSet; | ||
private readonly List<string?>? _contentTypeList; | ||
private readonly Func<int, bool> _matchStatusCode; | ||
private readonly Func<string, bool> _matchContentType; | ||
private readonly HashSet<int>? _statusCodes; | ||
private readonly ILogger _logger = logger; | ||
private readonly WeakReference _originalResponseHandler = new(originalResponseHandler ?? (_ => { })); | ||
private readonly ResponseHandlerEntry[] _handlers = [ | ||
.. (responseHeaders.DefaultHandler is { } handler ? [handler] : Array.Empty<ResponseHandlerEntry>()), | ||
.. responseHeaders.Handlers, | ||
]; | ||
|
||
public ResponseHeadersHandler(ILogger logger, ResponseHeadersOptions responseHeaders) | ||
public void PrepareResponse(StaticFileResponseContext context) | ||
{ | ||
_logger = logger; | ||
_headers = responseHeaders.Headers | ||
.Select(kv => new KeyValuePair<string, StringValues>(kv.Key, new StringValues(kv.Value))) | ||
.ToList(); | ||
var contentType = context.Context.Response.ContentType; | ||
var statusCode = context.Context.Response.StatusCode; | ||
|
||
if (!responseHeaders.IsEnabled || _headers.Count == 0) | ||
if (string.IsNullOrEmpty(contentType) || statusCode > 399) | ||
{ | ||
_matchStatusCode = _ => false; | ||
_matchContentType = _ => false; | ||
logger.LogWarning("ResponseHeadersHandler is disabled"); | ||
GetOriginalResponseHandler()?.Invoke(context); | ||
return; | ||
} | ||
else | ||
{ | ||
if (responseHeaders.FilterHttpStatusCode is { } listHttpStatusCode | ||
&& listHttpStatusCode.Count != 0) | ||
{ | ||
_statusCodes = [.. listHttpStatusCode]; | ||
_matchStatusCode = statusCode => _statusCodes.Contains(statusCode); | ||
logger.LogInformation("ResponseHeadersHandler StatusCode in list"); | ||
} | ||
else | ||
{ | ||
_matchStatusCode = _ => true; | ||
} | ||
|
||
if (responseHeaders.FilterContentType is not { } listContentType | ||
|| listContentType.Count == 0 | ||
|| responseHeaders.IsAnyContentType) | ||
{ | ||
_matchContentType = _ => true; | ||
logger.LogInformation("ResponseHeadersHandler all content-type"); | ||
} | ||
else | ||
foreach (var handler in _handlers) | ||
{ | ||
if ((handler.StatusCode.Count == 0 || handler.StatusCode.Contains(statusCode)) | ||
&& ( | ||
(handler.ContentTypeMatchEq.Count == 0 && handler.ContentTypeMatchContain.Length == 0 && handler.ContentTypeMatchStartWith.Length == 0) | ||
|| (handler.ContentTypeMatchEq.Count > 0 && handler.ContentTypeMatchEq.Contains(contentType)) | ||
|| (handler.ContentTypeMatchContain.Length > 0 && MatchContain(handler.ContentTypeMatchContain)) | ||
|| (handler.ContentTypeMatchStartWith.Length > 0 && MatchStartWith(handler.ContentTypeMatchStartWith)) | ||
)) | ||
{ | ||
_contentTypeHashSet = listContentType.ToHashSet(StringComparer.InvariantCultureIgnoreCase); | ||
_contentTypeList = [.. _contentTypeHashSet]; | ||
|
||
if (responseHeaders.IsContentTypeContainsMatch) | ||
if (handler.StatusCode.Count == 0) | ||
{ | ||
_matchContentType = contentType => _contentTypeList.Any(item => !string.IsNullOrEmpty(item) && item.Contains(contentType)); | ||
logger.LogInformation("ResponseHeadersHandler content-type -contains {contentTypes}", _contentTypeList); | ||
_logger.LogDebug("Add headers for {contentType}", contentType); | ||
} | ||
else | ||
{ | ||
_matchContentType = contentType => _contentTypeHashSet.Contains(contentType); | ||
logger.LogInformation("ResponseHeadersHandler content-type -eq {contentTypes}", _contentTypeList); | ||
_logger.LogDebug("Add headers for {contentType} statusCode={statusCode}", contentType, statusCode); | ||
} | ||
|
||
foreach (var item in handler.Headers) | ||
{ | ||
context.Context.Response.Headers.Add(item); | ||
} | ||
|
||
GetOriginalResponseHandler()?.Invoke(context); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
public void PrepareResponse(StaticFileResponseContext context) | ||
{ | ||
var contentType = context.Context.Response.ContentType; | ||
var statusCode = context.Context.Response.StatusCode; | ||
|
||
if (string.IsNullOrEmpty(contentType) | ||
|| statusCode > 399 | ||
|| !_matchStatusCode(statusCode) | ||
|| !_matchContentType(contentType)) | ||
bool MatchContain(string[] contentTypeMatchContain) | ||
{ | ||
return; | ||
foreach (var item in contentTypeMatchContain) | ||
{ | ||
if (contentType.Contains(item)) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
_logger.LogDebug("Add headers"); | ||
foreach (var item in _headers) | ||
bool MatchStartWith(string[] contentTypeMatchStartWith) | ||
{ | ||
context.Context.Response.Headers.Add(item); | ||
foreach (var item in contentTypeMatchStartWith) | ||
{ | ||
if (contentType.StartsWith(item)) | ||
{ | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
GetOriginalResponseHandler()?.Invoke(context); | ||
|
||
Action<StaticFileResponseContext>? GetOriginalResponseHandler() => _originalResponseHandler.IsAlive ? _originalResponseHandler.Target as Action<StaticFileResponseContext> : null; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
namespace NetLah.Extensions.SpaServices.Hosting; | ||
|
||
internal static class ResponseHeadersHelper | ||
{ | ||
private static readonly StringComparison DefaultStringComparison = StringComparison.OrdinalIgnoreCase; | ||
|
||
private static readonly StringComparer DefaultStringComparer = StringComparer.OrdinalIgnoreCase; | ||
|
||
private static readonly HashSet<string> PropertySet = new(DefaultStringComparer) { | ||
nameof(BaseResponseHeadersConfigurationOptions.ContentType), | ||
nameof(BaseResponseHeadersConfigurationOptions.ContentTypeContain), | ||
nameof(BaseResponseHeadersConfigurationOptions.ContentTypeStartWith), | ||
nameof(BaseResponseHeadersConfigurationOptions.Headers), | ||
nameof(BaseResponseHeadersConfigurationOptions.StatusCode), | ||
nameof(ResponseHeadersConfigurationOptions.IsEnabled), | ||
"IsAnyContentType", // legacy value | ||
"IsContentTypeContainsMatch", // legacy value | ||
}; | ||
|
||
//private static readonly string[] PropertyNames = [.. PropertySet]; | ||
|
||
public static ResponseHeadersOptions Parse(IConfigurationRoot? configurationRoot, string sectionName) | ||
{ | ||
ResponseHandlerEntry? defaultHandlerEntry = null; | ||
var isEnabled = false; | ||
var handlers = new List<ResponseHandlerEntry>(); | ||
|
||
if (configurationRoot != null) | ||
{ | ||
var configuration = configurationRoot.GetSection(sectionName); | ||
var configOptions = new ResponseHeadersConfigurationOptions(); | ||
configuration.Bind(configOptions); | ||
if (configOptions.IsEnabled) | ||
{ | ||
isEnabled = true; | ||
defaultHandlerEntry = ParseHandler(configOptions, configuration); | ||
} | ||
|
||
foreach (var item in configuration.GetChildren()) | ||
{ | ||
var key = item.Key; | ||
if (item.Value == null && int.TryParse(key, out var _)) | ||
{ | ||
var configOptions1 = new BaseResponseHeadersConfigurationOptions(); | ||
item.Bind(configOptions1); | ||
var handlerEntry = ParseHandler(configOptions1, item); | ||
if (handlerEntry.Headers.Length > 0) | ||
{ | ||
handlers.Add(handlerEntry); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return new ResponseHeadersOptions | ||
{ | ||
IsEnabled = isEnabled, | ||
DefaultHandler = defaultHandlerEntry?.Headers.Length > 0 ? defaultHandlerEntry : default, | ||
Handlers = [.. handlers], | ||
}; | ||
} | ||
|
||
private static ResponseHandlerEntry ParseHandler(BaseResponseHeadersConfigurationOptions options, IConfigurationSection configuration) | ||
{ | ||
var headers = new Dictionary<string, string>(); | ||
|
||
foreach (var item in options.Headers ?? []) | ||
{ | ||
if (TryParseKeyValue(item, out var key, out var value) | ||
&& !string.IsNullOrEmpty(key) | ||
&& !string.IsNullOrEmpty(value)) | ||
{ | ||
headers[key] = value; | ||
} | ||
} | ||
|
||
foreach (var item in configuration.GetChildren()) | ||
{ | ||
var key = item.Key; | ||
if (item.Value is { } value && !string.IsNullOrEmpty(value) && !int.TryParse(key, out var _)) | ||
{ | ||
if (nameof(BaseResponseHeadersConfigurationOptions.ContentType).Equals(key, DefaultStringComparison)) | ||
{ | ||
if (options.ContentType == null || options.ContentType.Count == 0) | ||
{ | ||
options.ContentType = [value]; | ||
} | ||
} | ||
else if (nameof(BaseResponseHeadersConfigurationOptions.ContentTypeContain).Equals(key, DefaultStringComparison)) | ||
{ | ||
if (options.ContentTypeContain == null || options.ContentTypeContain.Count == 0) | ||
{ | ||
options.ContentTypeContain = [value]; | ||
} | ||
} | ||
else if (nameof(BaseResponseHeadersConfigurationOptions.ContentTypeStartWith).Equals(key, DefaultStringComparison)) | ||
{ | ||
if (options.ContentTypeStartWith == null || options.ContentTypeStartWith.Count == 0) | ||
{ | ||
options.ContentTypeStartWith = [value]; | ||
} | ||
} | ||
else if (nameof(BaseResponseHeadersConfigurationOptions.StatusCode).Equals(key, DefaultStringComparison) && int.TryParse(value, out var statusCode)) | ||
{ | ||
if (options.StatusCode == null || options.StatusCode.Count == 0) | ||
{ | ||
options.StatusCode = [statusCode]; | ||
} | ||
} | ||
else if (!PropertySet.Contains(key)) | ||
{ | ||
headers[key] = value; | ||
} | ||
} | ||
else if (nameof(BaseResponseHeadersConfigurationOptions.Headers).Equals(key, DefaultStringComparison)) | ||
{ | ||
foreach (var item1 in item.GetChildren()) | ||
{ | ||
if (item1.Value is { } value1 && item1.Key is { } key1 && !string.IsNullOrEmpty(key1) && !string.IsNullOrEmpty(value1) && !int.TryParse(key1, out var _)) | ||
{ | ||
headers[key1] = value1; | ||
} | ||
} | ||
} | ||
} | ||
|
||
var headersStringValues = headers | ||
.Select(kv => new KeyValuePair<string, StringValues>(kv.Key, new StringValues(kv.Value))) | ||
.ToArray(); | ||
|
||
HashSet<string> contentTypesSet = [.. options.ContentType ?? [], .. options.ContentTypeStartWith ?? [], .. options.ContentTypeContain ?? []]; | ||
|
||
var handlerEntry = new ResponseHandlerEntry( | ||
options.ContentType?.Where(v => !string.IsNullOrEmpty(v)).ToHashSet() ?? [], | ||
options.ContentTypeStartWith?.Where(v => !string.IsNullOrEmpty(v)).ToArray() ?? [], | ||
options.ContentTypeContain?.Where(v => !string.IsNullOrEmpty(v)).ToArray() ?? [], | ||
options.StatusCode?.Where(v => v > 0).ToHashSet() ?? [], | ||
headersStringValues, | ||
[.. headers.Keys.OrderBy(s => s, DefaultStringComparer)], | ||
[.. contentTypesSet.OrderBy(s => s, DefaultStringComparer)]); | ||
|
||
return handlerEntry; | ||
|
||
static bool TryParseKeyValue(string keyValue, out string key, out string? value) | ||
{ | ||
var pos = keyValue.IndexOf('='); | ||
if (pos > 0 && pos < keyValue.Length - 1) | ||
{ | ||
key = keyValue[..pos]; | ||
value = keyValue[(pos + 1)..]; | ||
return true; | ||
} | ||
|
||
key = string.Empty; | ||
value = null; | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,42 @@ | ||
namespace NetLah.Extensions.SpaServices.Hosting; | ||
using Microsoft.Extensions.Primitives; | ||
|
||
public class ResponseHeadersOptions | ||
{ | ||
public bool IsEnabled { get; set; } = true; | ||
namespace NetLah.Extensions.SpaServices.Hosting; | ||
|
||
public bool IsContentTypeContainsMatch { get; set; } = true; | ||
internal class BaseResponseHeadersConfigurationOptions | ||
{ | ||
public HashSet<int>? StatusCode { get; set; } | ||
public HashSet<string>? ContentType { get; set; } | ||
public HashSet<string>? ContentTypeContain { get; set; } | ||
public HashSet<string>? ContentTypeStartWith { get; set; } | ||
public List<string>? Headers { get; set; } | ||
} | ||
|
||
public bool IsAnyContentType { get; set; } = false; | ||
internal class ResponseHeadersConfigurationOptions : BaseResponseHeadersConfigurationOptions | ||
{ | ||
public bool IsEnabled { get; set; } = true; | ||
|
||
public Dictionary<string, string?> Headers { get; set; } = []; | ||
} | ||
|
||
public List<string?>? FilterContentType { get; set; } | ||
internal class ResponseHeadersOptions | ||
{ | ||
public bool IsEnabled { get; set; } = true; | ||
public ResponseHandlerEntry[] Handlers { get; set; } = []; | ||
public ResponseHandlerEntry? DefaultHandler { get; set; } | ||
} | ||
|
||
public List<int>? FilterHttpStatusCode { get; set; } | ||
internal class ResponseHandlerEntry(HashSet<string> contentType, | ||
string[] contentTypeMatchStartWith, | ||
string[] contentTypeMatchContain, | ||
HashSet<int> statusCode, | ||
KeyValuePair<string, StringValues>[] headers, | ||
string[] headerNames, | ||
string[] contentTypes) | ||
{ | ||
public HashSet<string> ContentTypeMatchEq { get; set; } = contentType; | ||
public string[] ContentTypeMatchStartWith { get; set; } = contentTypeMatchStartWith; | ||
public string[] ContentTypeMatchContain { get; set; } = contentTypeMatchContain; | ||
public HashSet<int> StatusCode { get; set; } = statusCode; | ||
public KeyValuePair<string, StringValues>[] Headers { get; set; } = headers; | ||
public string[] HeaderNames { get; set; } = headerNames; | ||
public string[] ContentTypes { get; set; } = contentTypes; | ||
} |
Oops, something went wrong.