-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update Azure Core Shared Codes 2024-05-09_17:02:21 (#4692)
- Loading branch information
Showing
1 changed file
with
149 additions
and
91 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,140 +1,198 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
#nullable enable | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
using System.Linq; | ||
|
||
namespace Azure.Core | ||
namespace Azure.Core; | ||
|
||
internal class HttpMessageSanitizer | ||
{ | ||
internal class HttpMessageSanitizer | ||
{ | ||
private const string LogAllValue = "*"; | ||
private readonly bool _logAllHeaders; | ||
private readonly bool _logFullQueries; | ||
private readonly string[] _allowedQueryParameters; | ||
private readonly string _redactedPlaceholder; | ||
private readonly HashSet<string> _allowedHeaders; | ||
private const string LogAllValue = "*"; | ||
private readonly bool _logAllHeaders; | ||
private readonly bool _logFullQueries; | ||
private readonly string[] _allowedQueryParameters; | ||
private readonly string _redactedPlaceholder; | ||
private readonly HashSet<string> _allowedHeaders; | ||
|
||
internal static HttpMessageSanitizer Default = new HttpMessageSanitizer(Array.Empty<string>(), Array.Empty<string>()); | ||
[ThreadStatic] | ||
private static StringBuilder? s_cachedStringBuilder; | ||
private const int MaxCachedStringBuilderCapacity = 1024; | ||
|
||
public HttpMessageSanitizer(string[] allowedQueryParameters, string[] allowedHeaders, string redactedPlaceholder = "REDACTED") | ||
{ | ||
_logAllHeaders = allowedHeaders.Contains(LogAllValue); | ||
_logFullQueries = allowedQueryParameters.Contains(LogAllValue); | ||
internal static HttpMessageSanitizer Default = new HttpMessageSanitizer(Array.Empty<string>(), Array.Empty<string>()); | ||
|
||
_allowedQueryParameters = allowedQueryParameters; | ||
_redactedPlaceholder = redactedPlaceholder; | ||
_allowedHeaders = new HashSet<string>(allowedHeaders, StringComparer.InvariantCultureIgnoreCase); | ||
} | ||
public HttpMessageSanitizer(string[] allowedQueryParameters, string[] allowedHeaders, string redactedPlaceholder = "REDACTED") | ||
{ | ||
_logAllHeaders = allowedHeaders.Contains(LogAllValue); | ||
_logFullQueries = allowedQueryParameters.Contains(LogAllValue); | ||
|
||
public string SanitizeHeader(string name, string value) | ||
{ | ||
if (_logAllHeaders || _allowedHeaders.Contains(name)) | ||
{ | ||
return value; | ||
} | ||
_allowedQueryParameters = allowedQueryParameters; | ||
_redactedPlaceholder = redactedPlaceholder; | ||
_allowedHeaders = new HashSet<string>(allowedHeaders, StringComparer.InvariantCultureIgnoreCase); | ||
} | ||
|
||
return _redactedPlaceholder; | ||
public string SanitizeHeader(string name, string value) | ||
{ | ||
if (_logAllHeaders || _allowedHeaders.Contains(name)) | ||
{ | ||
return value; | ||
} | ||
|
||
public string SanitizeUrl(string url) | ||
return _redactedPlaceholder; | ||
} | ||
|
||
public string SanitizeUrl(string url) | ||
{ | ||
if (_logFullQueries) | ||
{ | ||
if (_logFullQueries) | ||
{ | ||
return url; | ||
} | ||
return url; | ||
} | ||
|
||
#if NET5_0_OR_GREATER | ||
int indexOfQuerySeparator = url.IndexOf('?', StringComparison.Ordinal); | ||
int indexOfQuerySeparator = url.IndexOf('?', StringComparison.Ordinal); | ||
#else | ||
int indexOfQuerySeparator = url.IndexOf('?'); | ||
int indexOfQuerySeparator = url.IndexOf('?'); | ||
#endif | ||
|
||
if (indexOfQuerySeparator == -1) | ||
if (indexOfQuerySeparator == -1) | ||
{ | ||
return url; | ||
} | ||
|
||
// PERF: Avoid allocations in this heavily-used method: | ||
// 1. Use ReadOnlySpan<char> to avoid creating substrings. | ||
// 2. Defer creating a StringBuilder until absolutely necessary. | ||
// 3. Use a rented StringBuilder to avoid allocating a new one | ||
// each time. | ||
|
||
// Create the StringBuilder only when necessary (when we encounter | ||
// a query parameter that needs to be redacted) | ||
StringBuilder? stringBuilder = null; | ||
|
||
// Keeps track of the number of characters we've processed so far | ||
// so that, if we need to create a StringBuilder, we know how many | ||
// characters to copy over from the original URL. | ||
int lengthSoFar = indexOfQuerySeparator + 1; | ||
|
||
ReadOnlySpan<char> query = url.AsSpan(indexOfQuerySeparator + 1); // +1 to skip the '?' | ||
|
||
while (query.Length > 0) | ||
{ | ||
int endOfParameterValue = query.IndexOf('&'); | ||
int endOfParameterName = query.IndexOf('='); | ||
bool noValue = false; | ||
|
||
// Check if we have parameter without value | ||
if ((endOfParameterValue == -1 && endOfParameterName == -1) || | ||
(endOfParameterValue != -1 && (endOfParameterName == -1 || endOfParameterName > endOfParameterValue))) | ||
{ | ||
return url; | ||
endOfParameterName = endOfParameterValue; | ||
noValue = true; | ||
} | ||
|
||
StringBuilder stringBuilder = new StringBuilder(url.Length); | ||
stringBuilder.Append(url, 0, indexOfQuerySeparator); | ||
if (endOfParameterName == -1) | ||
{ | ||
endOfParameterName = query.Length; | ||
} | ||
|
||
string query = url.Substring(indexOfQuerySeparator); | ||
if (endOfParameterValue == -1) | ||
{ | ||
endOfParameterValue = query.Length; | ||
} | ||
else | ||
{ | ||
// include the separator | ||
endOfParameterValue++; | ||
} | ||
|
||
int queryIndex = 1; | ||
stringBuilder.Append('?'); | ||
ReadOnlySpan<char> parameterName = query.Slice(0, endOfParameterName); | ||
|
||
do | ||
bool isAllowed = false; | ||
foreach (string name in _allowedQueryParameters) | ||
{ | ||
int endOfParameterValue = query.IndexOf('&', queryIndex); | ||
int endOfParameterName = query.IndexOf('=', queryIndex); | ||
bool noValue = false; | ||
|
||
// Check if we have parameter without value | ||
if ((endOfParameterValue == -1 && endOfParameterName == -1) || | ||
(endOfParameterValue != -1 && (endOfParameterName == -1 || endOfParameterName > endOfParameterValue))) | ||
if (parameterName.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
endOfParameterName = endOfParameterValue; | ||
noValue = true; | ||
isAllowed = true; | ||
break; | ||
} | ||
} | ||
|
||
if (endOfParameterName == -1) | ||
{ | ||
endOfParameterName = query.Length; | ||
} | ||
int valueLength = endOfParameterValue; | ||
int nameLength = endOfParameterName; | ||
|
||
if (endOfParameterValue == -1) | ||
if (isAllowed || noValue) | ||
{ | ||
if (stringBuilder is null) | ||
{ | ||
endOfParameterValue = query.Length; | ||
lengthSoFar += valueLength; | ||
} | ||
else | ||
{ | ||
// include the separator | ||
endOfParameterValue++; | ||
AppendReadOnlySpan(stringBuilder, query.Slice(0, valueLength)); | ||
} | ||
} | ||
else | ||
{ | ||
// Encountered a query value that needs to be redacted. | ||
// Create the StringBuilder if we haven't already. | ||
stringBuilder ??= RentStringBuilder(url.Length).Append(url, 0, lengthSoFar); | ||
|
||
ReadOnlySpan<char> parameterName = query.AsSpan(queryIndex, endOfParameterName - queryIndex); | ||
AppendReadOnlySpan(stringBuilder, query.Slice(0, nameLength)) | ||
.Append('=') | ||
.Append(_redactedPlaceholder); | ||
|
||
bool isAllowed = false; | ||
foreach (string name in _allowedQueryParameters) | ||
if (query[endOfParameterValue - 1] == '&') | ||
{ | ||
if (parameterName.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
isAllowed = true; | ||
break; | ||
} | ||
stringBuilder.Append('&'); | ||
} | ||
} | ||
|
||
query = query.Slice(valueLength); | ||
} | ||
|
||
int valueLength = endOfParameterValue - queryIndex; | ||
int nameLength = endOfParameterName - queryIndex; | ||
return stringBuilder is null ? url : ToStringAndReturnStringBuilder(stringBuilder); | ||
|
||
if (isAllowed) | ||
{ | ||
stringBuilder.Append(query, queryIndex, valueLength); | ||
} | ||
else | ||
{ | ||
if (noValue) | ||
{ | ||
stringBuilder.Append(query, queryIndex, valueLength); | ||
} | ||
else | ||
{ | ||
stringBuilder.Append(query, queryIndex, nameLength); | ||
stringBuilder.Append('='); | ||
stringBuilder.Append(_redactedPlaceholder); | ||
if (query[endOfParameterValue - 1] == '&') | ||
{ | ||
stringBuilder.Append('&'); | ||
} | ||
} | ||
} | ||
static StringBuilder AppendReadOnlySpan(StringBuilder builder, ReadOnlySpan<char> span) | ||
{ | ||
#if NET6_0_OR_GREATER | ||
return builder.Append(span); | ||
#else | ||
foreach (char c in span) | ||
{ | ||
builder.Append(c); | ||
} | ||
|
||
return builder; | ||
#endif | ||
} | ||
} | ||
|
||
private static StringBuilder RentStringBuilder(int capacity) | ||
{ | ||
if (capacity <= MaxCachedStringBuilderCapacity) | ||
{ | ||
StringBuilder? builder = s_cachedStringBuilder; | ||
if (builder is not null && builder.Capacity >= capacity) | ||
{ | ||
s_cachedStringBuilder = null; | ||
return builder; | ||
} | ||
} | ||
|
||
queryIndex += valueLength; | ||
} while (queryIndex < query.Length); | ||
return new StringBuilder(capacity); | ||
} | ||
|
||
return stringBuilder.ToString(); | ||
private static string ToStringAndReturnStringBuilder(StringBuilder builder) | ||
{ | ||
string result = builder.ToString(); | ||
if (builder.Capacity <= MaxCachedStringBuilderCapacity) | ||
{ | ||
s_cachedStringBuilder = builder.Clear(); | ||
} | ||
|
||
return result; | ||
} | ||
} |