diff --git a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs index afdad71011f3..3c502d7d4b4f 100644 --- a/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs +++ b/src/Http/WebUtilities/ref/Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs @@ -233,6 +233,7 @@ public static partial class WebEncoders public static string Base64UrlEncode(byte[] input) { throw null; } public static int Base64UrlEncode(byte[] input, int offset, char[] output, int outputOffset, int count) { throw null; } public static string Base64UrlEncode(byte[] input, int offset, int count) { throw null; } + public static string Base64UrlEncode(System.ReadOnlySpan input) { throw null; } public static int GetArraySizeRequiredToDecode(int count) { throw null; } public static int GetArraySizeRequiredToEncode(int count) { throw null; } } diff --git a/src/Shared/WebEncoders/WebEncoders.cs b/src/Shared/WebEncoders/WebEncoders.cs index 17068ae67a55..979ab5d2d7f5 100644 --- a/src/Shared/WebEncoders/WebEncoders.cs +++ b/src/Shared/WebEncoders/WebEncoders.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Buffers; using System.Diagnostics; using System.Globalization; using Microsoft.Extensions.WebEncoders.Sources; @@ -220,6 +221,9 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) ValidateParameters(input.Length, nameof(input), offset, count); +#if NETCOREAPP3_0 + return Base64UrlEncode(input.AsSpan(offset, count)); +#else // Special-case empty input if (count == 0) { @@ -229,7 +233,8 @@ public static string Base64UrlEncode(byte[] input, int offset, int count) var buffer = new char[GetArraySizeRequiredToEncode(count)]; var numBase64Chars = Base64UrlEncode(input, offset, buffer, outputOffset: 0, count: count); - return new String(buffer, startIndex: 0, length: numBase64Chars); + return new string(buffer, startIndex: 0, length: numBase64Chars); +#endif } /// @@ -280,6 +285,9 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o nameof(count)); } +#if NETCOREAPP3_0 + return Base64UrlEncode(input.AsSpan(offset, count), output.AsSpan(outputOffset)); +#else // Special-case empty input. if (count == 0) { @@ -311,6 +319,7 @@ public static int Base64UrlEncode(byte[] input, int offset, char[] output, int o } return numBase64Chars; +#endif } /// @@ -327,6 +336,73 @@ public static int GetArraySizeRequiredToEncode(int count) return checked(numWholeOrPartialInputBlocks * 4); } +#if NETCOREAPP3_0 + /// + /// Encodes using base64url encoding. + /// + /// The binary input to encode. + /// The base64url-encoded form of . + public static string Base64UrlEncode(ReadOnlySpan input) + { + if (input.IsEmpty) + { + return string.Empty; + } + + int bufferSize = GetArraySizeRequiredToEncode(input.Length); + + char[] bufferToReturnToPool = null; + Span buffer = bufferSize <= 128 + ? stackalloc char[bufferSize] + : bufferToReturnToPool = ArrayPool.Shared.Rent(bufferSize); + + var numBase64Chars = Base64UrlEncode(input, buffer); + var base64Url = new string(buffer.Slice(0, numBase64Chars)); + + if (bufferToReturnToPool != null) + { + ArrayPool.Shared.Return(bufferToReturnToPool); + } + + return base64Url; + } + + private static int Base64UrlEncode(ReadOnlySpan input, Span output) + { + Debug.Assert(output.Length >= GetArraySizeRequiredToEncode(input.Length)); + + if (input.IsEmpty) + { + return 0; + } + + // Use base64url encoding with no padding characters. See RFC 4648, Sec. 5. + + Convert.TryToBase64Chars(input, output, out int charsWritten); + + // Fix up '+' -> '-' and '/' -> '_'. Drop padding characters. + for (var i = 0; i < charsWritten; i++) + { + var ch = output[i]; + if (ch == '+') + { + output[i] = '-'; + } + else if (ch == '/') + { + output[i] = '_'; + } + else if (ch == '=') + { + // We've reached a padding character; truncate the remainder. + return i; + } + } + + return charsWritten; + } +#endif + private static int GetNumBase64PaddingCharsInString(string str) { // Assumption: input contains a well-formed base64 string with no whitespace. diff --git a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs index 283bee8d9880..206f352acc1e 100644 --- a/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs +++ b/src/SignalR/common/Http.Connections/src/Internal/HttpConnectionManager.cs @@ -105,11 +105,10 @@ public void RemoveConnection(string id) private static string MakeNewConnectionId() { - // TODO: Use Span when WebEncoders implements Span methods https://github.com/aspnet/Home/issues/2966 // 128 bit buffer / 8 bits per byte = 16 bytes - var buffer = new byte[16]; - _keyGenerator.GetBytes(buffer); + Span buffer = stackalloc byte[16]; // Generate the id with RNGCrypto because we want a cryptographically random id, which GUID is not + _keyGenerator.GetBytes(buffer); return WebEncoders.Base64UrlEncode(buffer); }