| File: Internal\Http\ChunkedExtensionParser.cs | Web Access |
| Project: src\aspnetcore\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Diagnostics; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; // https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1 // chunk-ext = *( BWS ";" BWS chunk-ext-name // [BWS "=" BWS chunk-ext-val] ) // // chunk-ext-name = token // // chunk-ext-val = token / quoted-string // // https://www.rfc-editor.org/info/rfc9110#section-5.6.4 // quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE // // qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text // // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) // // obs-text = %x80-FF // // https://www.rfc-editor.org/info/rfc9110/#section-5.6.2 // token = 1*tchar // // https://www.rfc-editor.org/info/rfc9110#section-5.6.3 // BWS = OWS // ; "bad" whitespace // // OWS = *(SP / HTAB) // ; optional whitespace // // https://www.rfc-editor.org/info/rfc9110/#section-5.6.2 // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" // / DIGIT / ALPHA // ; any VCHAR, except delimiters // // https://www.rfc-editor.org/info/rfc5234/#appendix-B.1 // HTAB = %x09 // SP = %x20 // VCHAR = %x21-7E internal struct ChunkedExtensionParser { private const byte ByteCR = (byte)'\r'; private const byte ByteLF = (byte)'\n'; private const byte ByteSemicolon = (byte)';'; private const byte ByteEqual = (byte)'='; private const byte ByteDQuote = (byte)'"'; private const byte ByteBackslash = (byte)'\\'; private State _state; public ChunkedExtensionParser() => _state = State.StartOfExtension; public bool Consume(ref SequenceReader<byte> reader, out SequencePosition consumed, out SequencePosition examined) { while (reader.TryRead(out var b)) { switch (_state) { case State.StartOfExtension: if (IsBadWhitespace(b)) { continue; } if (b == ByteSemicolon) { _state = State.BeforeExtensionName; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.BeforeExtensionName: if (IsBadWhitespace(b)) { continue; } if (HttpCharacters.IsValidTokenByte(b)) { _state = State.InExtensionName; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.InExtensionName: if (HttpCharacters.IsValidTokenByte(b)) { _state = State.InExtensionName; continue; } if (IsBadWhitespace(b)) { _state = State.BadWhitespaceAfterExtensionName; continue; } if (b == ByteEqual) { _state = State.BeforeExtensionValue; continue; } if (b == ByteSemicolon) { _state = State.BeforeExtensionName; continue; } if (b == ByteCR) { _state = State.WaitTerminatingLF; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.BadWhitespaceAfterExtensionName: if (IsBadWhitespace(b)) { continue; } if (b == ByteEqual) { _state = State.BeforeExtensionValue; continue; } if (b == ByteSemicolon) { _state = State.BeforeExtensionName; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.BeforeExtensionValue: if (IsBadWhitespace(b)) { continue; } if (HttpCharacters.IsValidTokenByte(b)) { _state = State.ExtensionValueToken; continue; } if (b == ByteDQuote) { _state = State.ExtensionValueQuotedString; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.ExtensionValueToken: if (HttpCharacters.IsValidTokenByte(b)) { _state = State.ExtensionValueToken; continue; } if (b == ByteCR) { _state = State.WaitTerminatingLF; continue; } _state = State.StartOfExtension; goto case State.StartOfExtension; case State.ExtensionValueQuotedString: if (b == ByteBackslash) { _state = State.ExtensionValueQuotedPair; continue; } // qdtext if (b == 0x09 || b == 0x20 || b == 0x21 || (b >= 0x23 && b <= 0x5B) || (b >= 0x5D && b <= 0x7E) || (b >= 0x80 && b <= 0xFF)) { continue; } if (b == ByteDQuote) { _state = State.ExtensionValueQuotedStringEnd; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.ExtensionValueQuotedPair: // quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) if (b == 0x09 || b == 0x20 || (b >= 0x21 && b <= 0x7E) || (b >= 0x80 && b <= 0xFF)) { _state = State.ExtensionValueQuotedString; continue; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; case State.ExtensionValueQuotedStringEnd: if (b == ByteCR) { _state = State.WaitTerminatingLF; continue; } _state = State.StartOfExtension; goto case State.StartOfExtension; case State.WaitTerminatingLF: if (b == ByteLF) { _state = State.Completed; consumed = reader.Position; examined = reader.Position; return true; } KestrelBadHttpRequestException.Throw(RequestRejectionReason.BadChunkExtension); break; default: throw new UnreachableException(); } } consumed = reader.Position; examined = reader.Position; return false; } private static bool IsBadWhitespace(byte b) { return b is 0x20 or 0x09; } private enum State { /// <summary> /// In this state, we wait for BWS followed by a semicolon. /// </summary> StartOfExtension, /// <summary> /// In this state, we received the semicolon and are waiting for BWS followed by the extension name. /// </summary> BeforeExtensionName, /// <summary> /// In this state, we started processing the extension name. /// We consume token (one or more tchar) until we reach a BWS, equal sign, or semicolon. /// </summary> InExtensionName, /// <summary> /// In this state, we started receiving BWS after the extension name. /// In this case, we can either receive an equal sign or a semicolon after consuming BWS. /// </summary> BadWhitespaceAfterExtensionName, /// <summary> /// In this state, we received the equal sign and are waiting for BWS followed by the extension value. /// </summary> BeforeExtensionValue, /// <summary> /// In this state, we are processing the extension value as a token (one or more tchar). /// </summary> ExtensionValueToken, /// <summary> /// In this state, we are processing the extension value as a quoted string. /// We have already consumed the opening double quote, and are processing zero /// or more of either qdtext or quoted-pair /// </summary> ExtensionValueQuotedString, /// <summary> /// In this state, we received the "\" of a quoted pair and are waiting for the next character. /// </summary> ExtensionValueQuotedPair, /// <summary> /// In this state, we received the closing double quote of a quoted string value. /// We are waiting for either CRLF or start of new extension. /// </summary> ExtensionValueQuotedStringEnd, /// <summary> /// In this state, we received CR, and are waiting for LF. /// </summary> WaitTerminatingLF, /// <summary> /// In this state, we completed processing and we should never be called again. /// </summary> Completed, } }