Add FormPipeReader (#7964)
This commit is contained in:
parent
9a61275fa6
commit
fc9e48877c
|
|
@ -150,15 +150,13 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
if (HasApplicationFormContentType(contentType))
|
if (HasApplicationFormContentType(contentType))
|
||||||
{
|
{
|
||||||
var encoding = FilterEncoding(contentType.Encoding);
|
var encoding = FilterEncoding(contentType.Encoding);
|
||||||
using (var formReader = new FormReader(_request.Body, encoding)
|
var formReader = new FormPipeReader(_request.BodyPipe, encoding)
|
||||||
{
|
{
|
||||||
ValueCountLimit = _options.ValueCountLimit,
|
ValueCountLimit = _options.ValueCountLimit,
|
||||||
KeyLengthLimit = _options.KeyLengthLimit,
|
KeyLengthLimit = _options.KeyLengthLimit,
|
||||||
ValueLengthLimit = _options.ValueLengthLimit,
|
ValueLengthLimit = _options.ValueLengthLimit,
|
||||||
})
|
};
|
||||||
{
|
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
|
||||||
formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (HasMultipartFormContentType(contentType))
|
else if (HasMultipartFormContentType(contentType))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -82,6 +83,41 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
await responseFeature.CompleteAsync();
|
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 MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T";
|
||||||
|
|
||||||
private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\"";
|
private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\"";
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HttpOv
|
||||||
EndProject
|
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}"
|
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
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -651,6 +665,7 @@ Global
|
||||||
{DC519C5E-CA6E-48CA-BF35-B46305B83013} = {14A7B3DE-46C8-4245-B0BD-9AFF3795C163}
|
{DC519C5E-CA6E-48CA-BF35-B46305B83013} = {14A7B3DE-46C8-4245-B0BD-9AFF3795C163}
|
||||||
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
{611794D2-EF3A-422A-A077-23E61C7ADE49} = {793FFE24-138A-4C3D-81AB-18D625E36230}
|
||||||
{1062FCDE-E145-40EC-B175-FDBCAA0C59A0} = {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
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {85B5E151-2E9D-419C-83DD-0DDCF446C83A}
|
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" />
|
<Compile Include="Microsoft.AspNetCore.WebUtilities.netcoreapp3.0.cs" />
|
||||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
<Reference Include="System.Text.Encodings.Web" />
|
<Reference Include="System.Text.Encodings.Web" />
|
||||||
|
<Reference Include="System.IO.Pipelines" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,16 @@ namespace Microsoft.AspNetCore.WebUtilities
|
||||||
public Microsoft.AspNetCore.WebUtilities.MultipartSection Section { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public Microsoft.AspNetCore.WebUtilities.MultipartSection Section { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
public System.Threading.Tasks.Task<string> GetValueAsync() { 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 partial class FormReader : System.IDisposable
|
||||||
{
|
{
|
||||||
public const int DefaultKeyLengthLimit = 2048;
|
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>
|
<PropertyGroup>
|
||||||
<Description>ASP.NET Core utilities, such as for working with forms, multipart messages, and query strings.</Description>
|
<Description>ASP.NET Core utilities, such as for working with forms, multipart messages, and query strings.</Description>
|
||||||
|
|
@ -12,11 +12,13 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(SharedSourceRoot)WebEncoders\**\*.cs" />
|
<Compile Include="$(SharedSourceRoot)WebEncoders\**\*.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.Net.Http.Headers" />
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
<Reference Include="System.Text.Encodings.Web" />
|
<Reference Include="System.Text.Encodings.Web" />
|
||||||
|
<Reference Include="System.IO.Pipelines" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.WebUtilities.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
[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>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||||
|
|
@ -8,4 +8,8 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
|
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="$(SharedSourceRoot)Buffers.Testing\**\*.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Connections.Abstractions;
|
using Microsoft.AspNetCore.Internal;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
@ -20,8 +20,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
if (pathEncoded)
|
if (pathEncoded)
|
||||||
{
|
{
|
||||||
// URI was encoded, unescape and then parse as UTF-8
|
// URI was encoded, unescape and then parse as UTF-8
|
||||||
// Disabling warning temporary
|
pathLength = UrlDecoder.DecodeInPlace(path, isFormEncoding: false);
|
||||||
pathLength = UrlDecoder.DecodeInPlace(path);
|
|
||||||
|
|
||||||
// Removing dot segments must be done after unescaping. From RFC 3986:
|
// 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>
|
<PropertyGroup>
|
||||||
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>
|
<Description>Core components of ASP.NET Core Kestrel cross-platform web server.</Description>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
|
<Compile Include="$(SharedSourceRoot)CertificateGeneration\**\*.cs" />
|
||||||
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
|
||||||
|
<Compile Include="$(SharedSourceRoot)UrlDecoder\**\*.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Connections.Abstractions
|
namespace Microsoft.AspNetCore.Internal
|
||||||
{
|
{
|
||||||
internal class UrlDecoder
|
internal class UrlDecoder
|
||||||
{
|
{
|
||||||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">The byte span represents a UTF8 encoding url path.</param>
|
/// <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="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>
|
/// <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)
|
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
|
// This requires the destination span to be larger or equal to source span
|
||||||
source.CopyTo(destination);
|
source.CopyTo(destination);
|
||||||
return DecodeInPlace(destination);
|
return DecodeInPlace(destination, isFormEncoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unescape a URL path in place.
|
/// Unescape a URL path in place.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="buffer">The byte span represents a UTF8 encoding url path.</param>
|
/// <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>
|
/// <returns>The number of the bytes representing the result.</returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The unescape is done in place, which means after decoding the result is the subset of
|
/// The unescape is done in place, which means after decoding the result is the subset of
|
||||||
/// the input span.
|
/// the input span.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static int DecodeInPlace(Span<byte> buffer)
|
public static int DecodeInPlace(Span<byte> buffer, bool isFormEncoding)
|
||||||
{
|
{
|
||||||
// the slot to read the input
|
// the slot to read the input
|
||||||
var sourceIndex = 0;
|
var sourceIndex = 0;
|
||||||
|
|
@ -52,7 +54,12 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
break;
|
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;
|
var decodeIndex = sourceIndex;
|
||||||
|
|
||||||
|
|
@ -63,7 +70,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
// The decodeReader iterator is always moved to the first byte not yet
|
// The decodeReader iterator is always moved to the first byte not yet
|
||||||
// be scanned after the process. A failed decoding means the chars
|
// be scanned after the process. A failed decoding means the chars
|
||||||
// between the reader and decodeReader can be copied to output untouched.
|
// 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);
|
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="sourceIndex">The iterator point to the first % char</param>
|
||||||
/// <param name="destinationIndex">The place to write to</param>
|
/// <param name="destinationIndex">The place to write to</param>
|
||||||
/// <param name="buffer">The byte array</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,
|
// 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.
|
// 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)
|
if (byte1 == -1)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -150,7 +158,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextSourceIndex = sourceIndex;
|
var nextSourceIndex = sourceIndex;
|
||||||
var nextByte = UnescapePercentEncoding(ref nextSourceIndex, buffer);
|
var nextByte = UnescapePercentEncoding(ref nextSourceIndex, buffer, isFormEncoding);
|
||||||
if (nextByte == -1)
|
if (nextByte == -1)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -249,8 +257,9 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scan">The value to read</param>
|
/// <param name="scan">The value to read</param>
|
||||||
/// <param name="buffer">The byte array</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>
|
/// <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++] != '%')
|
if (buffer[scan++] != '%')
|
||||||
{
|
{
|
||||||
|
|
@ -271,7 +280,7 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SkipUnescape(value1, value2))
|
if (SkipUnescape(value1, value2, isFormEncoding))
|
||||||
{
|
{
|
||||||
return -1;
|
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 - '/'
|
// skip %2F - '/'
|
||||||
if (value1 == 2 && value2 == 15)
|
if (value1 == 2 && value2 == 15)
|
||||||
{
|
{
|
||||||
|
|
@ -331,6 +345,5 @@ namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue