| File: HandshakeHelpers.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\WebSockets\src\Microsoft.AspNetCore.WebSockets.csproj (Microsoft.AspNetCore.WebSockets) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.WebSockets; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.WebSockets; internal static class HandshakeHelpers { // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static private static ReadOnlySpan<byte> EncodedWebSocketKey => "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"u8; public static void GenerateResponseHeaders(bool isHttp1, IHeaderDictionary requestHeaders, string? subProtocol, IHeaderDictionary responseHeaders) { if (isHttp1) { responseHeaders.Connection = HeaderNames.Upgrade; responseHeaders.Upgrade = Constants.Headers.UpgradeWebSocket; responseHeaders.SecWebSocketAccept = CreateResponseKey(requestHeaders.SecWebSocketKey.ToString()); } if (!string.IsNullOrWhiteSpace(subProtocol)) { responseHeaders.SecWebSocketProtocol = subProtocol; } } /// <summary> /// Validates the Sec-WebSocket-Key request header /// </summary> /// <param name="value"></param> /// <returns></returns> public static bool IsRequestKeyValid(string value) { if (string.IsNullOrWhiteSpace(value)) { return false; } Span<byte> temp = stackalloc byte[16]; var success = Convert.TryFromBase64String(value, temp, out var written); return success && written == 16; } public static string CreateResponseKey(string requestKey) { // "The value of this header field is constructed by concatenating /key/, defined above in step 4 // in Section 4.2.2, with the string "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", taking the SHA-1 hash of // this concatenated value to obtain a 20-byte value and base64-encoding" // https://tools.ietf.org/html/rfc6455#section-4.2.2 // requestKey is already verified to be small (24 bytes) by 'IsRequestKeyValid()' and everything is 1:1 mapping to UTF8 bytes // so this can be hardcoded to 60 bytes for the requestKey + static websocket string Span<byte> mergedBytes = stackalloc byte[60]; Encoding.UTF8.GetBytes(requestKey, mergedBytes); EncodedWebSocketKey.CopyTo(mergedBytes[24..]); Span<byte> hashedBytes = stackalloc byte[20]; var written = SHA1.HashData(mergedBytes, hashedBytes); if (written != 20) { throw new InvalidOperationException("Could not compute the hash for the 'Sec-WebSocket-Accept' header."); } return Convert.ToBase64String(hashedBytes); } // https://datatracker.ietf.org/doc/html/rfc7692#section-7.1 public static bool ParseDeflateOptions(ReadOnlySpan<char> extension, bool serverContextTakeover, int serverMaxWindowBits, out WebSocketDeflateOptions parsedOptions, [NotNullWhen(true)] out string? response) { bool hasServerMaxWindowBits = false; bool hasClientMaxWindowBits = false; bool hasClientNoContext = false; bool hasServerNoContext = false; response = null; parsedOptions = new WebSocketDeflateOptions() { ServerContextTakeover = serverContextTakeover, ServerMaxWindowBits = serverMaxWindowBits }; using var builder = new ValueStringBuilder(WebSocketDeflateConstants.MaxExtensionLength); builder.Append(WebSocketDeflateConstants.Extension); while (true) { int end = extension.IndexOf(';'); ReadOnlySpan<char> value = (end >= 0 ? extension[..end] : extension).Trim(); if (value.Length == 0) { break; } if (value.SequenceEqual(WebSocketDeflateConstants.ClientNoContextTakeover)) { // https://datatracker.ietf.org/doc/html/rfc7692#section-7 // MUST decline if: // The negotiation offer contains multiple extension parameters with // the same name. if (hasClientNoContext) { return false; } hasClientNoContext = true; parsedOptions.ClientContextTakeover = false; builder.Append(';'); builder.Append(' '); builder.Append(WebSocketDeflateConstants.ClientNoContextTakeover); } else if (value.SequenceEqual(WebSocketDeflateConstants.ServerNoContextTakeover)) { // https://datatracker.ietf.org/doc/html/rfc7692#section-7 // MUST decline if: // The negotiation offer contains multiple extension parameters with // the same name. if (hasServerNoContext) { return false; } hasServerNoContext = true; parsedOptions.ServerContextTakeover = false; } else if (value.StartsWith(WebSocketDeflateConstants.ClientMaxWindowBits)) { // https://datatracker.ietf.org/doc/html/rfc7692#section-7 // MUST decline if: // The negotiation offer contains multiple extension parameters with // the same name. if (hasClientMaxWindowBits) { return false; } hasClientMaxWindowBits = true; if (!ParseWindowBits(value, out var clientMaxWindowBits)) { return false; } // 8 is a valid value according to the spec, but our zlib implementation does not support it if (clientMaxWindowBits == 8) { return false; } // https://tools.ietf.org/html/rfc7692#section-7.1.2.2 // the server may either ignore this // value or use this value to avoid allocating an unnecessarily big LZ77 // sliding window by including the "client_max_window_bits" extension // parameter in the corresponding extension negotiation response to the // offer with a value equal to or smaller than the received value. parsedOptions.ClientMaxWindowBits = clientMaxWindowBits ?? 15; // If a received extension negotiation offer doesn't have the // "client_max_window_bits" extension parameter, the corresponding // extension negotiation response to the offer MUST NOT include the // "client_max_window_bits" extension parameter. builder.Append(';'); builder.Append(' '); builder.Append(WebSocketDeflateConstants.ClientMaxWindowBits); builder.Append('='); var len = (parsedOptions.ClientMaxWindowBits > 9) ? 2 : 1; var span = builder.AppendSpan(len); var ret = parsedOptions.ClientMaxWindowBits.TryFormat(span, out var written, provider: CultureInfo.InvariantCulture); Debug.Assert(ret); Debug.Assert(written == len); } else if (value.StartsWith(WebSocketDeflateConstants.ServerMaxWindowBits)) { // https://datatracker.ietf.org/doc/html/rfc7692#section-7 // MUST decline if: // The negotiation offer contains multiple extension parameters with // the same name. if (hasServerMaxWindowBits) { return false; } hasServerMaxWindowBits = true; if (!ParseWindowBits(value, out var parsedServerMaxWindowBits)) { return false; } // 8 is a valid value according to the spec, but our zlib implementation does not support it if (parsedServerMaxWindowBits == 8) { return false; } // https://tools.ietf.org/html/rfc7692#section-7.1.2.1 // A server accepts an extension negotiation offer with this parameter // by including the "server_max_window_bits" extension parameter in the // extension negotiation response to send back to the client with the // same or smaller value as the offer. parsedOptions.ServerMaxWindowBits = Math.Min(parsedServerMaxWindowBits ?? 15, serverMaxWindowBits); } static bool ParseWindowBits(ReadOnlySpan<char> value, out int? parsedValue) { var startIndex = value.IndexOf('='); // parameters can be sent without a value by the client, we'll use the values set by the app developer or the default of 15 if (startIndex < 0) { parsedValue = null; return true; } value = value[(startIndex + 1)..].TrimEnd(); if (value.Length == 0) { parsedValue = null; return false; } // https://datatracker.ietf.org/doc/html/rfc7692#section-5.2 // check for value in quotes and pull the value out without the quotes if (value[0] == '"' && value.EndsWith("\"".AsSpan()) && value.Length > 1) { value = value[1..^1]; } if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int windowBits) || windowBits < 8 || windowBits > 15) { parsedValue = null; return false; } parsedValue = windowBits; return true; } if (end < 0) { break; } extension = extension[(end + 1)..]; } if (!parsedOptions.ServerContextTakeover) { builder.Append(';'); builder.Append(' '); builder.Append(WebSocketDeflateConstants.ServerNoContextTakeover); } if (hasServerMaxWindowBits || parsedOptions.ServerMaxWindowBits != 15) { builder.Append(';'); builder.Append(' '); builder.Append(WebSocketDeflateConstants.ServerMaxWindowBits); builder.Append('='); var len = (parsedOptions.ServerMaxWindowBits > 9) ? 2 : 1; var span = builder.AppendSpan(len); var ret = parsedOptions.ServerMaxWindowBits.TryFormat(span, out var written, provider: CultureInfo.InvariantCulture); Debug.Assert(ret); Debug.Assert(written == len); } response = builder.ToString(); return true; } }