diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs index c6b9d0c59..4d234322a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/FrameHeaders.cs @@ -233,16 +233,41 @@ public static void ValidateHeaderCharacters(string headerCharacters) } } - public static long ParseContentLength(StringValues value) + public static unsafe long ParseContentLength(StringValues value) { - try - { - return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture); - } - catch (FormatException ex) + var input = value.ToString(); + var parsed = 0L; + + fixed (char* ptr = input) { - throw new InvalidOperationException("Content-Length value must be an integral number.", ex); + var ch = (ushort*)ptr; + var end = ch + input.Length; + + if (ch == end) + { + ThrowInvalidContentLengthException(value); + } + + ushort digit = 0; + while (ch < end && (digit = (ushort)(*ch - 0x30)) <= 9) + { + parsed *= 10; + parsed += digit; + ch++; + } + + if (ch != end) + { + ThrowInvalidContentLengthException(value); + } } + + return parsed; + } + + private static void ThrowInvalidContentLengthException(string value) + { + throw new InvalidOperationException($"Invalid Content-Length: \"{value}\". Value must be a positive integral number."); } private static void ThrowInvalidHeaderCharacter(char ch) diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs index c21d6b242..bc76f2c09 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameResponseHeadersTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel; @@ -149,75 +150,88 @@ public void ThrowsWhenClearingHeadersAfterReadOnlyIsSet() Assert.Throws(() => dictionary.Clear()); } - [Fact] - public void ThrowsWhenAddingContentLengthWithNonNumericValue() + [Theory] + [MemberData(nameof(BadContentLengths))] + public void ThrowsWhenAddingContentLengthWithNonNumericValue(string contentLength) { var headers = new FrameResponseHeaders(); var dictionary = (IDictionary)headers; - Assert.Throws(() => dictionary.Add("Content-Length", new[] { "bad" })); + var exception = Assert.Throws(() => dictionary.Add("Content-Length", new[] { contentLength })); + Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message); } - [Fact] - public void ThrowsWhenSettingContentLengthToNonNumericValue() + [Theory] + [MemberData(nameof(BadContentLengths))] + public void ThrowsWhenSettingContentLengthToNonNumericValue(string contentLength) { var headers = new FrameResponseHeaders(); var dictionary = (IDictionary)headers; - Assert.Throws(() => ((IHeaderDictionary)headers)["Content-Length"] = "bad"); + var exception = Assert.Throws(() => ((IHeaderDictionary)headers)["Content-Length"] = contentLength); + Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message); } - [Fact] - public void ThrowsWhenSettingRawContentLengthToNonNumericValue() + [Theory] + [MemberData(nameof(BadContentLengths))] + public void ThrowsWhenSettingRawContentLengthToNonNumericValue(string contentLength) { var headers = new FrameResponseHeaders(); - Assert.Throws(() => headers.SetRawContentLength("bad", Encoding.ASCII.GetBytes("bad"))); + var exception = Assert.Throws(() => headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength))); + Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message); } - [Fact] - public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue() + [Theory] + [MemberData(nameof(BadContentLengths))] + public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue(string contentLength) { var headers = new FrameResponseHeaders(); - Assert.Throws(() => headers.HeaderContentLength = "bad"); + + var exception = Assert.Throws(() => headers.HeaderContentLength = contentLength); + Assert.Equal($"Invalid Content-Length: \"{contentLength}\". Value must be a positive integral number.", exception.Message); } - [Fact] - public void ContentLengthValueCanBeReadAsLongAfterAddingHeader() + [Theory] + [MemberData(nameof(GoodContentLengths))] + public void ContentLengthValueCanBeReadAsLongAfterAddingHeader(string contentLength) { var headers = new FrameResponseHeaders(); var dictionary = (IDictionary)headers; - dictionary.Add("Content-Length", "42"); + dictionary.Add("Content-Length", contentLength); - Assert.Equal(42, headers.HeaderContentLengthValue); + Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue); } - [Fact] - public void ContentLengthValueCanBeReadAsLongAfterSettingHeader() + [Theory] + [MemberData(nameof(GoodContentLengths))] + public void ContentLengthValueCanBeReadAsLongAfterSettingHeader(string contentLength) { var headers = new FrameResponseHeaders(); var dictionary = (IDictionary)headers; - dictionary["Content-Length"] = "42"; + dictionary["Content-Length"] = contentLength; - Assert.Equal(42, headers.HeaderContentLengthValue); + Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue); } - [Fact] - public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader() + [Theory] + [MemberData(nameof(GoodContentLengths))] + public void ContentLengthValueCanBeReadAsLongAfterSettingRawHeader(string contentLength) { var headers = new FrameResponseHeaders(); - headers.SetRawContentLength("42", Encoding.ASCII.GetBytes("42")); + headers.SetRawContentLength(contentLength, Encoding.ASCII.GetBytes(contentLength)); - Assert.Equal(42, headers.HeaderContentLengthValue); + Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue); } - [Fact] - public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader() + [Theory] + [MemberData(nameof(GoodContentLengths))] + public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader(string contentLength) { var headers = new FrameResponseHeaders(); - headers.HeaderContentLength = "42"; + headers.HeaderContentLength = contentLength; - Assert.Equal(42, headers.HeaderContentLengthValue); + Assert.Equal(ParseLong(contentLength), headers.HeaderContentLengthValue); } [Fact] @@ -243,5 +257,33 @@ public void ContentLengthValueClearedWhenHeadersCleared() Assert.Equal(null, headers.HeaderContentLengthValue); } + + private static long ParseLong(string value) + { + return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture); + } + + public static TheoryData GoodContentLengths => new TheoryData + { + "0", + "00", + "042", + "42", + long.MaxValue.ToString(CultureInfo.InvariantCulture) + }; + + public static TheoryData BadContentLengths => new TheoryData + { + "", + " ", + " 42", + "42 ", + "bad", + "!", + "!42", + "42!", + "42,000", + "42.000", + }; } -} +} \ No newline at end of file