Add FormPipeReader (#7964)
This commit is contained in:
parent
9a61275fa6
commit
fc9e48877c
|
|
@ -150,15 +150,13 @@ namespace Microsoft.AspNetCore.Http.Features
|
|||
if (HasApplicationFormContentType(contentType))
|
||||
{
|
||||
var encoding = FilterEncoding(contentType.Encoding);
|
||||
using (var formReader = new FormReader(_request.Body, encoding)
|
||||
var formReader = new FormPipeReader(_request.BodyPipe, encoding)
|
||||
{
|
||||
ValueCountLimit = _options.ValueCountLimit,
|
||||
KeyLengthLimit = _options.KeyLengthLimit,
|
||||
ValueLengthLimit = _options.ValueLengthLimit,
|
||||
})
|
||||
{
|
||||
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
|
||||
}
|
||||
};
|
||||
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
|
||||
}
|
||||
else if (HasMultipartFormContentType(contentType))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
|
@ -82,6 +83,41 @@ namespace Microsoft.AspNetCore.Http.Features
|
|||
await responseFeature.CompleteAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task ReadFormAsync_SimpleData_ReplacePipeReader_ReturnsParsedFormCollection(bool bufferRequest)
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2");
|
||||
var context = new DefaultHttpContext();
|
||||
var responseFeature = new FakeResponseFeature();
|
||||
context.Features.Set<IHttpResponseFeature>(responseFeature);
|
||||
context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8";
|
||||
|
||||
var pipe = new Pipe();
|
||||
await pipe.Writer.WriteAsync(formContent);
|
||||
pipe.Writer.Complete();
|
||||
|
||||
context.Request.BodyPipe = pipe.Reader;
|
||||
|
||||
IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest });
|
||||
context.Features.Set<IFormFeature>(formFeature);
|
||||
|
||||
var formCollection = await context.Request.ReadFormAsync();
|
||||
|
||||
Assert.Equal("bar", formCollection["foo"]);
|
||||
Assert.Equal("2", formCollection["baz"]);
|
||||
|
||||
// Cached
|
||||
formFeature = context.Features.Get<IFormFeature>();
|
||||
Assert.NotNull(formFeature);
|
||||
Assert.NotNull(formFeature.Form);
|
||||
Assert.Same(formFeature.Form, formCollection);
|
||||
|
||||
// Cleanup
|
||||
await responseFeature.CompleteAsync();
|
||||
}
|
||||
|
||||
private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T";
|
||||
|
||||
private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\"";
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOv
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "..\Servers\IIS\IISIntegration\src\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities.Performance", "WebUtilities\perf\Microsoft.AspNetCore.WebUtilities.Performance\Microsoft.AspNetCore.WebUtilities.Performance.csproj", "{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -603,6 +605,18 @@ Global
|
|||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -651,6 +665,7 @@ Global
|
|||
{DC519C5E-CA6E-48CA-BF35-B46305B83013} = {14A7B3DE-46C8-4245-B0BD-9AFF3795C163}
|
||||
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||
{21AC56E7-4E77-4B0E-B63E-C8E836E4D14E} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Text;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.WebUtilities.Performance
|
||||
{
|
||||
/// <summary>
|
||||
/// Test internal parsing speed of FormPipeReader without pipe
|
||||
/// </summary>
|
||||
public class FormPipeReaderInternalsBenchmark
|
||||
{
|
||||
private byte[] _singleUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=boo&haha=hehe&lol=temp");
|
||||
private byte[] _firstUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=boo");
|
||||
private byte[] _secondUtf8 = Encoding.UTF8.GetBytes("&haha=hehe&lol=temp");
|
||||
private FormPipeReader _formPipeReader;
|
||||
|
||||
[IterationSetup]
|
||||
public void Setup()
|
||||
{
|
||||
_formPipeReader = new FormPipeReader(null);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void ReadUtf8Data()
|
||||
{
|
||||
var buffer = new ReadOnlySequence<byte>(_singleUtf8);
|
||||
KeyValueAccumulator accum = default;
|
||||
|
||||
_formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void ReadUtf8MultipleBlockData()
|
||||
{
|
||||
var buffer = ReadOnlySequenceFactory.CreateSegments(_firstUtf8, _secondUtf8);
|
||||
KeyValueAccumulator accum = default;
|
||||
|
||||
_formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.WebUtilities
|
||||
{
|
||||
public class FormReaderBenchmark
|
||||
{
|
||||
[Benchmark]
|
||||
public async Task ReadSmallFormAsyncStream()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo");
|
||||
var stream = new MemoryStream(bytes);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
var formReader = new FormReader(stream);
|
||||
await formReader.ReadFormAsync();
|
||||
stream.Position = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public async Task ReadSmallFormAsyncPipe()
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo");
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
pipe.Writer.Write(bytes);
|
||||
pipe.Writer.Complete();
|
||||
var formReader = new FormPipeReader(pipe.Reader);
|
||||
await formReader.ReadFormAsync();
|
||||
pipe.Reader.Complete();
|
||||
pipe.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Microsoft.AspNetCore.WebUtilities</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="BenchmarkDotNet" />
|
||||
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
|
||||
<Reference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -7,5 +7,6 @@
|
|||
<Compile Include="Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs" />
|
||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||
<Reference Include="System.Text.Encodings.Web" />
|
||||
<Reference Include="System.IO.Pipelines" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,16 @@ namespace Microsoft.AspNetCore.WebUtilities
|
|||
public Microsoft.AspNetCore.WebUtilities.MultipartSection Section { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||
public System.Threading.Tasks.Task<string> GetValueAsync() { throw null; }
|
||||
}
|
||||
public partial class FormPipeReader
|
||||
{
|
||||
public FormPipeReader(System.IO.Pipelines.PipeReader pipeReader) { }
|
||||
public FormPipeReader(System.IO.Pipelines.PipeReader pipeReader, System.Text.Encoding encoding) { }
|
||||
public int KeyLengthLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public int ValueCountLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
public int ValueLengthLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
[System.Diagnostics.DebuggerStepThroughAttribute]
|
||||
public System.Threading.Tasks.Task<System.Collections.Generic.Dictionary<string, Microsoft.Extensions.Primitives.StringValues>> ReadFormAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
||||
}
|
||||
public partial class FormReader : System.IDisposable
|
||||
{
|
||||
public const int DefaultKeyLengthLimit = 2048;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,381 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.WebUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to read an 'application/x-www-form-urlencoded' form.
|
||||
/// Internally reads from a PipeReader.
|
||||
/// </summary>
|
||||
public class FormPipeReader
|
||||
{
|
||||
private const int StackAllocThreshold = 128;
|
||||
private const int DefaultValueCountLimit = 1024;
|
||||
private const int DefaultKeyLengthLimit = 1024 * 2;
|
||||
private const int DefaultValueLengthLimit = 1024 * 1024 * 4;
|
||||
|
||||
// Used for UTF8/ASCII (precalculated for fast path)
|
||||
private static ReadOnlySpan<byte> UTF8EqualEncoded => new byte[] { (byte)'=' };
|
||||
private static ReadOnlySpan<byte> UTF8AndEncoded => new byte[] { (byte)'&' };
|
||||
|
||||
// Used for other encodings
|
||||
private byte[] _otherEqualEncoding;
|
||||
private byte[] _otherAndEncoding;
|
||||
|
||||
private readonly PipeReader _pipeReader;
|
||||
private readonly Encoding _encoding;
|
||||
|
||||
public FormPipeReader(PipeReader pipeReader)
|
||||
: this(pipeReader, Encoding.UTF8)
|
||||
{
|
||||
}
|
||||
|
||||
public FormPipeReader(PipeReader pipeReader, Encoding encoding)
|
||||
{
|
||||
if (encoding == Encoding.UTF7)
|
||||
{
|
||||
throw new ArgumentException("UTF7 is unsupported and insecure. Please select a different encoding.");
|
||||
}
|
||||
|
||||
_pipeReader = pipeReader;
|
||||
_encoding = encoding;
|
||||
|
||||
if (_encoding != Encoding.UTF8 && _encoding != Encoding.ASCII)
|
||||
{
|
||||
_otherEqualEncoding = _encoding.GetBytes("=");
|
||||
_otherAndEncoding = _encoding.GetBytes("&");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The limit on the number of form values to allow in ReadForm or ReadFormAsync.
|
||||
/// </summary>
|
||||
public int ValueCountLimit { get; set; } = DefaultValueCountLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The limit on the length of form keys.
|
||||
/// </summary>
|
||||
public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The limit on the length of form values.
|
||||
/// </summary>
|
||||
public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an HTTP form body.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
|
||||
/// <returns>The collection containing the parsed HTTP form body.</returns>
|
||||
public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
KeyValueAccumulator accumulator = default;
|
||||
while (true)
|
||||
{
|
||||
var readResult = await _pipeReader.ReadAsync(cancellationToken);
|
||||
|
||||
var buffer = readResult.Buffer;
|
||||
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
ParseFormValues(ref buffer, ref accumulator, readResult.IsCompleted);
|
||||
}
|
||||
|
||||
if (readResult.IsCompleted)
|
||||
{
|
||||
_pipeReader.AdvanceTo(buffer.End);
|
||||
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
throw new InvalidOperationException("End of body before form was fully parsed.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_pipeReader.AdvanceTo(buffer.Start, buffer.End);
|
||||
}
|
||||
|
||||
return accumulator.GetResults();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal void ParseFormValues(
|
||||
ref ReadOnlySequence<byte> buffer,
|
||||
ref KeyValueAccumulator accumulator,
|
||||
bool isFinalBlock)
|
||||
{
|
||||
if (buffer.IsSingleSegment)
|
||||
{
|
||||
ParseFormValuesFast(buffer.First.Span,
|
||||
ref accumulator,
|
||||
isFinalBlock,
|
||||
out var consumed);
|
||||
|
||||
buffer = buffer.Slice(consumed);
|
||||
return;
|
||||
}
|
||||
|
||||
ParseValuesSlow(ref buffer,
|
||||
ref accumulator,
|
||||
isFinalBlock);
|
||||
}
|
||||
|
||||
// Fast parsing for single span in ReadOnlySequence
|
||||
private void ParseFormValuesFast(ReadOnlySpan<byte> span,
|
||||
ref KeyValueAccumulator accumulator,
|
||||
bool isFinalBlock,
|
||||
out int consumed)
|
||||
{
|
||||
ReadOnlySpan<byte> key = default;
|
||||
ReadOnlySpan<byte> value = default;
|
||||
consumed = 0;
|
||||
var equalsDelimiter = GetEqualsForEncoding();
|
||||
var andDelimiter = GetAndForEncoding();
|
||||
|
||||
while (span.Length > 0)
|
||||
{
|
||||
var equals = span.IndexOf(equalsDelimiter);
|
||||
|
||||
if (equals == -1)
|
||||
{
|
||||
if (span.Length > KeyLengthLimit)
|
||||
{
|
||||
ThrowKeyTooLargeException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (equals > KeyLengthLimit)
|
||||
{
|
||||
ThrowKeyTooLargeException();
|
||||
}
|
||||
|
||||
key = span.Slice(0, equals);
|
||||
|
||||
span = span.Slice(key.Length + equalsDelimiter.Length);
|
||||
value = span;
|
||||
|
||||
var ampersand = span.IndexOf(andDelimiter);
|
||||
|
||||
if (ampersand == -1)
|
||||
{
|
||||
if (span.Length > ValueLengthLimit)
|
||||
{
|
||||
ThrowValueTooLargeException();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFinalBlock)
|
||||
{
|
||||
// We can't know that what is currently read is the end of the form value, that's only the case if this is the final block
|
||||
// If we're not in the final block, then consume nothing
|
||||
break;
|
||||
}
|
||||
|
||||
// If we are on the final block, the remaining content in value is what we want to add to the KVAccumulator.
|
||||
// Clear out the remaining span such that the loop will exit.
|
||||
span = Span<byte>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ampersand > ValueLengthLimit)
|
||||
{
|
||||
ThrowValueTooLargeException();
|
||||
}
|
||||
|
||||
value = span.Slice(0, ampersand);
|
||||
span = span.Slice(ampersand + andDelimiter.Length);
|
||||
}
|
||||
|
||||
var decodedKey = GetDecodedString(key);
|
||||
var decodedValue = GetDecodedString(value);
|
||||
|
||||
AppendAndVerify(ref accumulator, decodedKey, decodedValue);
|
||||
|
||||
// Cover case where we don't have an ampersand at the end.
|
||||
consumed += key.Length + value.Length + (ampersand == -1 ? equalsDelimiter.Length : equalsDelimiter.Length + andDelimiter.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// For multi-segment parsing of a read only sequence
|
||||
private void ParseValuesSlow(
|
||||
ref ReadOnlySequence<byte> buffer,
|
||||
ref KeyValueAccumulator accumulator,
|
||||
bool isFinalBlock)
|
||||
{
|
||||
var sequenceReader = new SequenceReader<byte>(buffer);
|
||||
var consumed = sequenceReader.Position;
|
||||
var equalsDelimiter = GetEqualsForEncoding();
|
||||
var andDelimiter = GetAndForEncoding();
|
||||
|
||||
while (!sequenceReader.End)
|
||||
{
|
||||
// TODO seems there is a bug with TryReadTo (advancePastDelimiter: true). It isn't advancing past the delimiter on second read.
|
||||
if (!sequenceReader.TryReadTo(out ReadOnlySequence<byte> key, equalsDelimiter, advancePastDelimiter: false) ||
|
||||
!sequenceReader.IsNext(equalsDelimiter, true))
|
||||
{
|
||||
if (sequenceReader.Consumed > KeyLengthLimit)
|
||||
{
|
||||
ThrowKeyTooLargeException();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (key.Length > KeyLengthLimit)
|
||||
{
|
||||
ThrowKeyTooLargeException();
|
||||
}
|
||||
|
||||
if (!sequenceReader.TryReadTo(out ReadOnlySequence<byte> value, andDelimiter, false) ||
|
||||
!sequenceReader.IsNext(andDelimiter, true))
|
||||
{
|
||||
if (!isFinalBlock)
|
||||
{
|
||||
if (sequenceReader.Consumed - key.Length > ValueLengthLimit)
|
||||
{
|
||||
ThrowValueTooLargeException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
value = buffer.Slice(sequenceReader.Position);
|
||||
|
||||
sequenceReader.Advance(value.Length);
|
||||
}
|
||||
|
||||
if (value.Length > ValueLengthLimit)
|
||||
{
|
||||
ThrowValueTooLargeException();
|
||||
}
|
||||
|
||||
// Need to call ToArray if the key/value spans multiple segments
|
||||
var decodedKey = GetDecodedStringFromReadOnlySequence(key);
|
||||
var decodedValue = GetDecodedStringFromReadOnlySequence(value);
|
||||
|
||||
AppendAndVerify(ref accumulator, decodedKey, decodedValue);
|
||||
|
||||
consumed = sequenceReader.Position;
|
||||
}
|
||||
|
||||
buffer = buffer.Slice(consumed);
|
||||
}
|
||||
|
||||
private void ThrowKeyTooLargeException()
|
||||
{
|
||||
throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded.");
|
||||
}
|
||||
|
||||
private void ThrowValueTooLargeException()
|
||||
{
|
||||
throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded.");
|
||||
}
|
||||
|
||||
private string GetDecodedStringFromReadOnlySequence(ReadOnlySequence<byte> ros)
|
||||
{
|
||||
if (ros.IsSingleSegment)
|
||||
{
|
||||
return GetDecodedString(ros.First.Span);
|
||||
}
|
||||
|
||||
if (ros.Length < StackAllocThreshold)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[(int)ros.Length];
|
||||
ros.CopyTo(buffer);
|
||||
return GetDecodedString(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var byteArray = ArrayPool<byte>.Shared.Rent((int)ros.Length);
|
||||
|
||||
try
|
||||
{
|
||||
Span<byte> buffer = byteArray.AsSpan(0, (int)ros.Length);
|
||||
ros.CopyTo(buffer);
|
||||
return GetDecodedString(buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(byteArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that key/value constraints are met and appends value to accumulator.
|
||||
private void AppendAndVerify(ref KeyValueAccumulator accumulator, string decodedKey, string decodedValue)
|
||||
{
|
||||
accumulator.Append(decodedKey, decodedValue);
|
||||
|
||||
if (accumulator.ValueCount > ValueCountLimit)
|
||||
{
|
||||
throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded.");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDecodedString(ReadOnlySpan<byte> readOnlySpan)
|
||||
{
|
||||
if (readOnlySpan.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII)
|
||||
{
|
||||
// UrlDecoder only works on UTF8 (and implicitly ASCII)
|
||||
|
||||
// We need to create a Span from a ReadOnlySpan. This cast is safe because the memory is still held by the pipe
|
||||
// We will also create a string from it by the end of the function.
|
||||
var span = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(readOnlySpan[0]), readOnlySpan.Length);
|
||||
|
||||
var bytes = UrlDecoder.DecodeInPlace(span, isFormEncoding: true);
|
||||
span = span.Slice(0, bytes);
|
||||
|
||||
return _encoding.GetString(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Slow path for Unicode and other encodings.
|
||||
// Just do raw string replacement.
|
||||
var decodedString = _encoding.GetString(readOnlySpan);
|
||||
decodedString = decodedString.Replace('+', ' ');
|
||||
return Uri.UnescapeDataString(decodedString);
|
||||
}
|
||||
}
|
||||
|
||||
private ReadOnlySpan<byte> GetEqualsForEncoding()
|
||||
{
|
||||
if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII)
|
||||
{
|
||||
return UTF8EqualEncoded;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _otherEqualEncoding;
|
||||
}
|
||||
}
|
||||
|
||||
private ReadOnlySpan<byte> GetAndForEncoding()
|
||||
{
|
||||
if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII)
|
||||
{
|
||||
return UTF8AndEncoded;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _otherAndEncoding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -309,4 +309,4 @@ namespace Microsoft.AspNetCore.WebUtilities
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>ASP.NET Core utilities, such as for working with forms, multipart messages, and query strings.</Description>
|
||||
|
|
@ -12,11 +12,13 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)WebEncoders\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||
<Reference Include="System.Text.Encodings.Web" />
|
||||
<Reference Include="System.IO.Pipelines" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.WebUtilities.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.WebUtilities.Performance, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,406 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.WebUtilities.Test
|
||||
{
|
||||
public class FormPipeReaderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyKeyAtEndAllowed()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("=bar");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe));
|
||||
|
||||
Assert.Equal("bar", formCollection[""].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("=bar&baz=2");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe));
|
||||
|
||||
Assert.Equal("bar", formCollection[""].ToString());
|
||||
Assert.Equal("2", formCollection["baz"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyValuedAtEndAllowed()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe));
|
||||
|
||||
Assert.Equal("", formCollection["foo"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=&baz=2");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe));
|
||||
|
||||
Assert.Equal("", formCollection["foo"].ToString());
|
||||
Assert.Equal("2", formCollection["baz"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_ValueCountLimitMet_Success()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=1&bar=2&baz=3");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 });
|
||||
|
||||
Assert.Equal("1", formCollection["foo"].ToString());
|
||||
Assert.Equal("2", formCollection["bar"].ToString());
|
||||
Assert.Equal("3", formCollection["baz"].ToString());
|
||||
Assert.Equal(3, formCollection.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_ValueCountLimitExceeded_Throw()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=1&baz=2&bar=3&baz=4&baf=5");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 }));
|
||||
Assert.Equal("Form value count limit 3 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("baz=1&baz=2&baz=3&baz=4");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 }));
|
||||
Assert.Equal("Form value count limit 3 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_KeyLengthLimitMet_Success()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("fooooooooo=1&bar=2&baz=3&baz=4");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 });
|
||||
|
||||
Assert.Equal("1", formCollection["fooooooooo"].ToString());
|
||||
Assert.Equal("2", formCollection["bar"].ToString());
|
||||
Assert.Equal("3,4", formCollection["baz"].ToString());
|
||||
Assert.Equal(3, formCollection.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=1&baz12345678=2");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 }));
|
||||
Assert.Equal("Form key length limit 10 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_ValueLengthLimitMet_Success()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=1&bar=1234567890&baz=3&baz=4");
|
||||
|
||||
var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 });
|
||||
|
||||
Assert.Equal("1", formCollection["foo"].ToString());
|
||||
Assert.Equal("1234567890", formCollection["bar"].ToString());
|
||||
Assert.Equal("3,4", formCollection["baz"].ToString());
|
||||
Assert.Equal(3, formCollection.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw()
|
||||
{
|
||||
var bodyPipe = await MakePipeReader("foo=1&baz=12345678901");
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidDataException>(
|
||||
() => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 }));
|
||||
Assert.Equal("Form value length limit 10 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Percent-encoding
|
||||
[Theory]
|
||||
[InlineData("++=hello", " ", "hello")]
|
||||
[InlineData("a=1+1", "a", "1 1")]
|
||||
[InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")]
|
||||
[InlineData("a=%41", "a", "A")] // ascii encoded hex
|
||||
[InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points
|
||||
[InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported
|
||||
public async Task ReadForm_Decoding(string formData, string key, string expectedValue)
|
||||
{
|
||||
var bodyPipe = await MakePipeReader(text: formData);
|
||||
|
||||
var form = await ReadFormAsync(new FormPipeReader(bodyPipe));
|
||||
|
||||
Assert.Equal(expectedValue, form[key]);
|
||||
}
|
||||
|
||||
public static TheoryData<Encoding> Encodings =>
|
||||
new TheoryData<Encoding>
|
||||
{
|
||||
{ Encoding.UTF8 },
|
||||
{ Encoding.UTF32 },
|
||||
{ Encoding.ASCII },
|
||||
{ Encoding.Unicode }
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_SingleSegmentWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = new ReadOnlySequence<byte>(encoding.GetBytes("foo=bar&baz=boo"));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(2, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal("bar", dict["foo"]);
|
||||
Assert.Equal("boo", dict["baz"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_MultiSegmentWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(3, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal("bar", dict["foo"]);
|
||||
Assert.Equal("boo", dict["baz"]);
|
||||
Assert.Equal("", dict["t"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_MultiSegmentSplitAcrossSegmentsWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(3, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal("bar", dict["foo"]);
|
||||
Assert.Equal("boo", dict["baz"]);
|
||||
Assert.Equal("", dict["t"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_MultiSegmentWithArrayPoolAcrossSegmentsWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=bo" + new string('a', 128)));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(2, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal("bar", dict["foo"]);
|
||||
Assert.Equal("bo" + new string('a', 128), dict["baz"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_MultiSegmentSplitAcrossSegmentsWithPlusesWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("+++=+++&++++=++++&+="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(3, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal(" ", dict[" "]);
|
||||
Assert.Equal(" ", dict[" "]);
|
||||
Assert.Equal("", dict[" "]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_DecodedPlusesWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("++%2B=+++%2B&++++=++++&+="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(3, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal(" ", dict[" "]);
|
||||
Assert.Equal(" +", dict[" +"]);
|
||||
Assert.Equal("", dict[" "]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Encodings))]
|
||||
public void TryParseFormValues_MultiSegmentSplitAcrossSegmentsThatNeedDecodingWorks(Encoding encoding)
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("\"%-.<>\\^_`{|}~=\"%-.<>\\^_`{|}~&\"%-.<>\\^_`{|}=wow"));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null, encoding);
|
||||
formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true);
|
||||
|
||||
Assert.Equal(2, accumulator.KeyCount);
|
||||
var dict = accumulator.GetResults();
|
||||
Assert.Equal("\"%-.<>\\^_`{|}~", dict["\"%-.<>\\^_`{|}~"]);
|
||||
Assert.Equal("wow", dict["\"%-.<>\\^_`{|}"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedKeyLengthThrows()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(Encoding.UTF8.GetBytes("foo=bar&baz=boo&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.KeyLengthLimit = 2;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form key length limit 2 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedKeyLengthThrowsInSplitSegment()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("fo=bar&ba"), Encoding.UTF8.GetBytes("z=boo&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.KeyLengthLimit = 2;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form key length limit 2 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedValueLengthThrows()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=bar&baz=bo"), Encoding.UTF8.GetBytes("o&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.ValueLengthLimit = 2;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form value length limit 2 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedValueLengthThrowsInSplitSegment()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.ValueLengthLimit = 2;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form value length limit 2 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedKeLengthThrowsInSplitSegmentEnd()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&asdfasdfasd="));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.KeyLengthLimit = 10;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form key length limit 10 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParseFormValues_MultiSegmentExceedValueLengthThrowsInSplitSegmentEnd()
|
||||
{
|
||||
var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t=asdfasdfasd"));
|
||||
|
||||
KeyValueAccumulator accumulator = default;
|
||||
|
||||
var formReader = new FormPipeReader(null);
|
||||
formReader.ValueLengthLimit = 10;
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true));
|
||||
Assert.Equal("Form value length limit 10 exceeded.", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResetPipeWorks()
|
||||
{
|
||||
// Same test that is in the benchmark
|
||||
var pipe = new Pipe();
|
||||
var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo");
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
pipe.Writer.Write(bytes);
|
||||
pipe.Writer.Complete();
|
||||
var formReader = new FormPipeReader(pipe.Reader);
|
||||
await formReader.ReadFormAsync();
|
||||
pipe.Reader.Complete();
|
||||
pipe.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual Task<Dictionary<string, StringValues>> ReadFormAsync(FormPipeReader reader)
|
||||
{
|
||||
return reader.ReadFormAsync();
|
||||
}
|
||||
|
||||
private static async Task<PipeReader> MakePipeReader(string text)
|
||||
{
|
||||
var formContent = Encoding.UTF8.GetBytes(text);
|
||||
Pipe bodyPipe = new Pipe();
|
||||
|
||||
await bodyPipe.Writer.WriteAsync(formContent);
|
||||
|
||||
// Complete the writer so the reader will complete after processing all data.
|
||||
bodyPipe.Writer.Complete();
|
||||
return bodyPipe.Reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
@ -8,4 +8,8 @@
|
|||
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Connections.Abstractions;
|
||||
using Microsoft.AspNetCore.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
|
|
@ -20,8 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
|||
if (pathEncoded)
|
||||
{
|
||||
// URI was encoded, unescape and then parse as UTF-8
|
||||
// Disabling warning temporary
|
||||
pathLength = UrlDecoder.DecodeInPlace(path);
|
||||
pathLength = UrlDecoder.DecodeInPlace(path, isFormEncoding: false);
|
||||
|
||||
// Removing dot segments must be done after unescaping. From RFC 3986:
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
||||
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||
namespace Microsoft.AspNetCore.Internal
|
||||
{
|
||||
internal class UrlDecoder
|
||||
{
|
||||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
/// </summary>
|
||||
/// <param name="source">The byte span represents a UTF8 encoding url path.</param>
|
||||
/// <param name="destination">The byte span where unescaped url path is copied to.</param>
|
||||
/// <param name="isFormEncoding">Whether we are doing form encoding or not.</param>
|
||||
/// <returns>The length of the byte sequence of the unescaped url path.</returns>
|
||||
public static int Decode(ReadOnlySpan<byte> source, Span<byte> destination)
|
||||
public static int DecodeRequestLine(ReadOnlySpan<byte> source, Span<byte> destination, bool isFormEncoding)
|
||||
{
|
||||
if (destination.Length < source.Length)
|
||||
{
|
||||
|
|
@ -25,19 +26,20 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
|
||||
// This requires the destination span to be larger or equal to source span
|
||||
source.CopyTo(destination);
|
||||
return DecodeInPlace(destination);
|
||||
return DecodeInPlace(destination, isFormEncoding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unescape a URL path in place.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The byte span represents a UTF8 encoding url path.</param>
|
||||
/// <param name="isFormEncoding">Whether we are doing form encoding or not.</param>
|
||||
/// <returns>The number of the bytes representing the result.</returns>
|
||||
/// <remarks>
|
||||
/// The unescape is done in place, which means after decoding the result is the subset of
|
||||
/// the input span.
|
||||
/// </remarks>
|
||||
public static int DecodeInPlace(Span<byte> buffer)
|
||||
public static int DecodeInPlace(Span<byte> buffer, bool isFormEncoding)
|
||||
{
|
||||
// the slot to read the input
|
||||
var sourceIndex = 0;
|
||||
|
|
@ -52,7 +54,12 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
break;
|
||||
}
|
||||
|
||||
if (buffer[sourceIndex] == '%')
|
||||
if (buffer[sourceIndex] == '+' && isFormEncoding)
|
||||
{
|
||||
// Set it to ' ' when we are doing form encoding.
|
||||
buffer[sourceIndex] = 0x20;
|
||||
}
|
||||
else if (buffer[sourceIndex] == '%')
|
||||
{
|
||||
var decodeIndex = sourceIndex;
|
||||
|
||||
|
|
@ -63,7 +70,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
// The decodeReader iterator is always moved to the first byte not yet
|
||||
// be scanned after the process. A failed decoding means the chars
|
||||
// between the reader and decodeReader can be copied to output untouched.
|
||||
if (!DecodeCore(ref decodeIndex, ref destinationIndex, buffer))
|
||||
if (!DecodeCore(ref decodeIndex, ref destinationIndex, buffer, isFormEncoding))
|
||||
{
|
||||
Copy(sourceIndex, decodeIndex, ref destinationIndex, buffer);
|
||||
}
|
||||
|
|
@ -85,11 +92,12 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
/// <param name="sourceIndex">The iterator point to the first % char</param>
|
||||
/// <param name="destinationIndex">The place to write to</param>
|
||||
/// <param name="buffer">The byte array</param>
|
||||
private static bool DecodeCore(ref int sourceIndex, ref int destinationIndex, Span<byte> buffer)
|
||||
/// <param name="isFormEncoding">Whether we are doing form encodoing</param>
|
||||
private static bool DecodeCore(ref int sourceIndex, ref int destinationIndex, Span<byte> buffer, bool isFormEncoding)
|
||||
{
|
||||
// preserves the original head. if the percent-encodings cannot be interpreted as sequence of UTF-8 octets,
|
||||
// bytes from this till the last scanned one will be copied to the memory pointed by writer.
|
||||
var byte1 = UnescapePercentEncoding(ref sourceIndex, buffer);
|
||||
var byte1 = UnescapePercentEncoding(ref sourceIndex, buffer, isFormEncoding);
|
||||
if (byte1 == -1)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -150,7 +158,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
}
|
||||
|
||||
var nextSourceIndex = sourceIndex;
|
||||
var nextByte = UnescapePercentEncoding(ref nextSourceIndex, buffer);
|
||||
var nextByte = UnescapePercentEncoding(ref nextSourceIndex, buffer, isFormEncoding);
|
||||
if (nextByte == -1)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -249,8 +257,9 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
/// </summary>
|
||||
/// <param name="scan">The value to read</param>
|
||||
/// <param name="buffer">The byte array</param>
|
||||
/// <param name="isFormEncoding">Whether we are decoding a form or not. Will escape '/' if we are doing form encoding</param>
|
||||
/// <returns>The unescaped byte if success. Otherwise return -1.</returns>
|
||||
private static int UnescapePercentEncoding(ref int scan, Span<byte> buffer)
|
||||
private static int UnescapePercentEncoding(ref int scan, Span<byte> buffer, bool isFormEncoding)
|
||||
{
|
||||
if (buffer[scan++] != '%')
|
||||
{
|
||||
|
|
@ -271,7 +280,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
return -1;
|
||||
}
|
||||
|
||||
if (SkipUnescape(value1, value2))
|
||||
if (SkipUnescape(value1, value2, isFormEncoding))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -321,8 +330,13 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
}
|
||||
}
|
||||
|
||||
private static bool SkipUnescape(int value1, int value2)
|
||||
private static bool SkipUnescape(int value1, int value2, bool isFormEncoding)
|
||||
{
|
||||
if (isFormEncoding)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// skip %2F - '/'
|
||||
if (value1 == 2 && value2 == 15)
|
||||
{
|
||||
|
|
@ -331,6 +345,5 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue