/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System.Collections.ObjectModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Primitives; namespace OpenIddict.Extensions; /// /// Exposes common helpers used by the OpenIddict assemblies. /// internal static class OpenIddictHelpers { /// /// Determines whether the specified array contains at least one value present in the specified set. /// /// The type of the elements. /// The array. /// The set. /// /// if the specified array contains at least one /// value present in the specified set, otherwise. /// public static bool IncludesAnyFromSet(IReadOnlyList array, ISet set) { ArgumentNullException.ThrowIfNull(set); for (var index = 0; index < array.Count; index++) { var value = array[index]; if (set.Contains(value)) { return true; } } return false; } /// /// Determines whether the specified is considered fatal. /// /// The exception. /// /// if the exception is considered fatal, otherwise. /// public static bool IsFatal(Exception exception) { RuntimeHelpers.EnsureSufficientExecutionStack(); return exception switch { ThreadAbortException => true, OutOfMemoryException and not InsufficientMemoryException => true, AggregateException { InnerExceptions: var exceptions } => IsAnyFatal(exceptions), Exception { InnerException: Exception inner } => IsFatal(inner), _ => false }; static bool IsAnyFatal(ReadOnlyCollection exceptions) { for (var index = 0; index < exceptions.Count; index++) { if (IsFatal(exceptions[index])) { return true; } } return false; } } /// /// Computes an absolute URI from the specified and URIs. /// Note: if the URI is already absolute, it is directly returned. /// /// The left part. /// The right part. /// An absolute URI from the specified and . /// is not an absolute URI. [return: NotNullIfNotNull(nameof(right))] public static Uri? CreateAbsoluteUri(Uri? left, Uri? right) { if (right is null) { return null; } if (right.IsAbsoluteUri) { return right; } if (left is not { IsAbsoluteUri: true }) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); } // Ensure the left part ends with a trailing slash, as it is necessary // for Uri's constructor to include the last path segment in the base URI. left = left.AbsolutePath switch { null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, ['/'] or _ => left }; return new Uri(left, right); } /// /// Determines whether the URI is a base of the URI. /// /// The left part. /// The right part. /// if is base of /// , otherwise. /// or /// is . /// is not an absolute URI. public static bool IsBaseOf(Uri left, Uri right) { ArgumentNullException.ThrowIfNull(left); ArgumentNullException.ThrowIfNull(right); if (left is not { IsAbsoluteUri: true }) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(left)); } // Ensure the left part ends with a trailing slash, as it is necessary // for Uri's constructor to include the last path segment in the base URI. left = left.AbsolutePath switch { null or { Length: 0 } => new UriBuilder(left) { Path = "/" }.Uri, [.., not '/'] => new UriBuilder(left) { Path = left.AbsolutePath + "/" }.Uri, ['/'] or _ => left }; return left.IsBaseOf(right); } /// /// Determines whether the specified represents an implicit file URI. /// /// The URI. /// /// if represents /// an implicit file URI, otherwise. /// /// is . public static bool IsImplicitFileUri(Uri uri) { ArgumentNullException.ThrowIfNull(uri); return uri.IsAbsoluteUri && uri.IsFile && !uri.OriginalString.StartsWith(uri.Scheme, StringComparison.OrdinalIgnoreCase); } /// /// Adds a query string parameter to the specified . /// /// The URI to which the query string parameter will be appended. /// The name of the query string parameter to append. /// The value of the query string parameter to append. /// The final instance, with the specified parameter appended. public static Uri AddQueryStringParameter(Uri uri, string name, string? value) { ArgumentNullException.ThrowIfNull(uri); var builder = new StringBuilder(uri.Query); if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(name)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } return new UriBuilder(uri) { Query = builder.ToString() }.Uri; } /// /// Adds query string parameters to the specified . /// /// The URI to which the query string parameters will be appended. /// The query string parameters to append. /// The final instance, with the specified parameters appended. /// is . /// is . public static Uri AddQueryStringParameters(Uri uri, IReadOnlyDictionary parameters) { ArgumentNullException.ThrowIfNull(uri); ArgumentNullException.ThrowIfNull(parameters); if (parameters.Count is 0) { return uri; } var builder = new StringBuilder(uri.Query); foreach (var parameter in parameters) { // If the parameter doesn't include any string value, // only append the parameter key to the query string. if (parameter.Value.Count is 0) { if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); } // Otherwise, iterate the string values and create // a new "name=value" pair for each iterated value. else { foreach (var value in parameter.Value) { if (builder.Length is > 0) { builder.Append('&'); } builder.Append(Uri.EscapeDataString(parameter.Key)); if (!string.IsNullOrEmpty(value)) { builder.Append('='); builder.Append(Uri.EscapeDataString(value)); } } } } return new UriBuilder(uri) { Query = builder.ToString() }.Uri; } /// /// Extracts the parameters from the specified query string. /// /// The query string, which may start with a '?'. /// The parameters extracted from the specified query string. /// is . public static IReadOnlyDictionary ParseQuery(string query) { ArgumentNullException.ThrowIfNull(query); return query.TrimStart(Separators.QuestionMark[0]) .Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries) .Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries)) .Select(static parts => ( Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) .Where(static pair => !string.IsNullOrEmpty(pair.Key)) .GroupBy(static pair => pair.Key, StringComparer.Ordinal) .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]), StringComparer.Ordinal); } /// /// Extracts the parameters from the specified fragment. /// /// The fragment string, which may start with a '#'. /// The parameters extracted from the specified fragment. /// is . public static IReadOnlyDictionary ParseFragment(string fragment) { ArgumentNullException.ThrowIfNull(fragment); return fragment.TrimStart(Separators.Hash[0]) .Split([Separators.Ampersand[0], Separators.Semicolon[0]], StringSplitOptions.RemoveEmptyEntries) .Select(static parameter => parameter.Split(Separators.EqualsSign, StringSplitOptions.RemoveEmptyEntries)) .Select(static parts => ( Key: parts[0] is string key ? Uri.UnescapeDataString(key) : null, Value: parts.Length is > 1 && parts[1] is string value ? Uri.UnescapeDataString(value) : null)) .Where(static pair => !string.IsNullOrEmpty(pair.Key)) .GroupBy(static pair => pair.Key, StringComparer.Ordinal) .ToDictionary(static pair => pair.Key!, static pair => new StringValues([.. pair.Select(parts => parts.Value)]), StringComparer.Ordinal); } /// /// Extracts the parameters from the specified stream. /// /// The stream containing the formurl-encoded data. /// The encoding used to decode the data. /// The that can be used to abort the operation. /// The parameters extracted from the specified stream. /// is . public static async ValueTask> ParseFormAsync( Stream stream, Encoding encoding, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(encoding); var reader = new FormReader(stream, encoding); return await reader.ReadFormAsync(cancellationToken); } /// /// Removes the characters that are not part of /// from the specified string. /// /// /// Note: if no character is present in , all characters are considered valid. /// /// The original string. /// The list of allowed characters. /// The original string with the disallowed characters removed. /// is . public static string? RemoveDisallowedCharacters(string? value, IReadOnlyCollection charset) { ArgumentNullException.ThrowIfNull(charset); if (charset.Count is 0 || string.IsNullOrEmpty(value)) { return value; } var builder = new StringBuilder(); var enumerator = StringInfo.GetTextElementEnumerator(value); while (enumerator.MoveNext()) { var element = enumerator.GetTextElement(); if (charset.Contains(element, StringComparer.Ordinal)) { builder.Append(element); } } return builder.ToString(); } /// /// Determines whether the specified represents a null, undefined or empty JSON node. /// /// The . /// /// if the JSON node is null, undefined or empty otherwise. /// public static bool IsNullOrEmpty(JsonElement element) => element.ValueKind switch { JsonValueKind.Undefined or JsonValueKind.Null => true, JsonValueKind.String => string.IsNullOrEmpty(element.GetString()), JsonValueKind.Array => element.GetArrayLength() is 0, JsonValueKind.Object => element.GetPropertyCount() is 0, _ => false, }; /// /// Determines whether the specified represents a null or empty JSON node. /// /// The . /// /// if the JSON node is null or empty, otherwise. /// public static bool IsNullOrEmpty([NotNullWhen(false)] JsonNode? node) => node switch { null => true, JsonArray value => value.Count is 0, JsonObject value => value.Count is 0, JsonValue value when value.TryGetValue(out string? result) => string.IsNullOrEmpty(result), JsonValue value when value.TryGetValue(out JsonElement element) => IsNullOrEmpty(element), // If the JSON node cannot be mapped to a primitive type, convert it to // a JsonElement instance and infer the corresponding claim value type. JsonNode value => IsNullOrEmpty(value.Deserialize(OpenIddictSerializer.Default.JsonElement)) }; /// /// Determines whether the specified is a certificate authority. /// /// The . /// /// if the certificate is a certificate authority, otherwise. /// public static bool IsCertificateAuthority(X509Certificate2 certificate) { ArgumentNullException.ThrowIfNull(certificate); return certificate.Extensions.OfType() .Any(static extension => extension.CertificateAuthority); } /// /// Determines whether the specified has the specified extended key usage. /// /// The . /// The extended key usage. /// /// if the certificate has the specified extended key usage, otherwise. /// public static bool HasExtendedKeyUsage(X509Certificate2 certificate, string usage) { for (var index = 0; index < certificate.Extensions.Count; index++) { if (certificate.Extensions[index] is X509EnhancedKeyUsageExtension extension && HasOid(extension.EnhancedKeyUsages, usage)) { return true; } } return false; static bool HasOid(OidCollection collection, string value) { for (var index = 0; index < collection.Count; index++) { if (collection[index] is Oid oid && string.Equals(oid.Value, value, StringComparison.Ordinal)) { return true; } } return false; } } /// /// Determines whether the specified has the specified key usage. /// /// The . /// The . /// /// if the certificate has the specified key usage, otherwise. /// public static bool HasKeyUsage(X509Certificate2 certificate, X509KeyUsageFlags usage) { ArgumentNullException.ThrowIfNull(certificate); for (var index = 0; index < certificate.Extensions.Count; index++) { if (certificate.Extensions[index] is X509KeyUsageExtension extension && extension.KeyUsages.HasFlag(usage)) { return true; } } return false; } /// /// Determines whether the specified is self-issued. /// /// The . /// /// if the certificate is self-issued, otherwise. /// public static bool IsSelfIssuedCertificate(X509Certificate2 certificate) { ArgumentNullException.ThrowIfNull(certificate); return certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData); } /// /// Determines whether the specified is suitable for client authentication. /// /// The . /// /// if the certificate is suitable for client authentication, otherwise. /// public static bool IsClientAuthenticationCertificate(X509Certificate2 certificate) { ArgumentNullException.ThrowIfNull(certificate); return certificate.Version is >= 3 && OpenIddictHelpers.HasKeyUsage(certificate, X509KeyUsageFlags.DigitalSignature) && OpenIddictHelpers.HasExtendedKeyUsage(certificate, ObjectIdentifiers.ExtendedKeyUsages.ClientAuthentication); } /// /// Determines whether the items contained in /// are of the specified . /// /// The . /// The expected . /// /// if the array doesn't contain any value or if all the items /// are of the specified , otherwise. /// public static bool ValidateArrayElements(JsonElement element, JsonValueKind kind) { if (element.ValueKind is not JsonValueKind.Array) { throw new ArgumentOutOfRangeException(nameof(element)); } foreach (var item in element.EnumerateArray()) { if (item.ValueKind != kind) { return false; } } return true; } /// /// Determines whether the items contained in /// are of the specified . /// /// The . /// The expected . /// /// if the object doesn't contain any value or if all the items /// are of the specified , otherwise. /// public static bool ValidateObjectElements(JsonElement element, JsonValueKind kind) { if (element.ValueKind is not JsonValueKind.Object) { throw new ArgumentOutOfRangeException(nameof(element)); } foreach (var property in element.EnumerateObject()) { if (property.Value.ValueKind != kind) { return false; } } return true; } /// /// Note: this implementation was taken from ASP.NET Core. /// private sealed class FormReader { public const int DefaultValueCountLimit = 1024; public const int DefaultKeyLengthLimit = 1024 * 2; public const int DefaultValueLengthLimit = 1024 * 1024 * 4; private readonly TextReader _reader; private readonly char[] _buffer; private readonly StringBuilder _builder = new(); private int _bufferOffset; private int _bufferCount; private string? _currentKey; private string? _currentValue; private bool _endOfStream; public FormReader(Stream stream, Encoding encoding) { _buffer = new char[8192]; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } public int ValueCountLimit { get; set; } = DefaultValueCountLimit; public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; public KeyValuePair? ReadNextPair() { ReadNextPairImpl(); if (ReadSucceeded()) { return KeyValuePair.Create(_currentKey, _currentValue); } return null; } private void ReadNextPairImpl() { StartReadNextPair(); while (!_endOfStream) { // Empty if (_bufferCount is 0) { Buffer(); } if (TryReadNextPair()) { break; } } } public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) { await ReadNextPairAsyncImplAsync(cancellationToken); if (ReadSucceeded()) { return KeyValuePair.Create(_currentKey, _currentValue); } return null; } private async Task ReadNextPairAsyncImplAsync(CancellationToken cancellationToken = new CancellationToken()) { StartReadNextPair(); while (!_endOfStream) { if (_bufferCount is 0) { await BufferAsync(cancellationToken); } if (TryReadNextPair()) { break; } } } private void StartReadNextPair() { _currentKey = null; _currentValue = null; } private bool TryReadNextPair() { if (_currentKey is null) { if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) { return false; } if (_bufferCount is 0) { return false; } } if (_currentValue is null) { if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) { return false; } } return true; } private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value) { do { if (ReadChar(separator, limit, out value)) { return true; } } while (_bufferCount > 0); return false; } private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word) { if (_bufferCount is 0) { word = BuildWord(); return true; } var c = _buffer[_bufferOffset++]; _bufferCount--; if (c == separator) { word = BuildWord(); return true; } if (_builder.Length >= limit) { throw new InvalidDataException(string.Create(CultureInfo.InvariantCulture, $"Form key or value length limit {limit} exceeded.")); } _builder.Append(c); word = null; return false; } private string BuildWord() { _builder.Replace('+', ' '); var result = _builder.ToString(); _builder.Clear(); return Uri.UnescapeDataString(result); } private void Buffer() { _bufferOffset = 0; _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount is 0; } private async Task BufferAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _bufferOffset = 0; _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount is 0; } public Dictionary ReadForm() { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { ReadNextPairImpl(); Append(ref accumulator); } return accumulator.GetResults(); } public async Task> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { await ReadNextPairAsyncImplAsync(cancellationToken); Append(ref accumulator); } return accumulator.GetResults(); } [MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))] private bool ReadSucceeded() { return _currentKey is not null && _currentValue is not null; } private void Append(ref KeyValueAccumulator accumulator) { if (ReadSucceeded()) { accumulator.Append(_currentKey, _currentValue); if (accumulator.ValueCount > ValueCountLimit) { throw new InvalidDataException(string.Create(CultureInfo.InvariantCulture, $"Form value count limit {ValueCountLimit} exceeded.")); } } } } /// /// Note: this implementation was taken from ASP.NET Core. /// private struct KeyValueAccumulator { private Dictionary _accumulator; private Dictionary> _expandingAccumulator; public void Append(string key, string value) { if (_accumulator is null) { _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); } StringValues values; if (_accumulator.TryGetValue(key, out values)) { if (values.Count is 0) { _expandingAccumulator[key].Add(value); } else if (values.Count is 1) { _accumulator[key] = new string[] { values[0]!, value }; } else { _accumulator[key] = default(StringValues); if (_expandingAccumulator is null) { _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); } var list = new List(8); var array = values.ToArray(); list.Add(array[0]!); list.Add(array[1]!); list.Add(value); _expandingAccumulator[key] = list; } } else { _accumulator[key] = new StringValues(value); } ValueCount++; } public bool HasValues => ValueCount > 0; public int KeyCount => _accumulator?.Count ?? 0; public int ValueCount { get; private set; } public Dictionary GetResults() { if (_expandingAccumulator is not null) { foreach (var entry in _expandingAccumulator) { _accumulator[entry.Key] = new StringValues([.. entry.Value]); } } return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } } }