Merge source code from aspnet/KestrelHttpServer into this repo
This commit is contained in:
commit
02536ff991
|
|
@ -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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class ConnectionBuilder : IConnectionBuilder
|
||||
{
|
||||
private readonly IList<Func<ConnectionDelegate, ConnectionDelegate>> _components = new List<Func<ConnectionDelegate, ConnectionDelegate>>();
|
||||
|
||||
public IServiceProvider ApplicationServices { get; }
|
||||
|
||||
public ConnectionBuilder(IServiceProvider applicationServices)
|
||||
{
|
||||
ApplicationServices = applicationServices;
|
||||
}
|
||||
|
||||
public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware)
|
||||
{
|
||||
_components.Add(middleware);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConnectionDelegate Build()
|
||||
{
|
||||
ConnectionDelegate app = features =>
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
foreach (var component in _components.Reverse())
|
||||
{
|
||||
app = component(app);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.Extensions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public static class ConnectionBuilderExtensions
|
||||
{
|
||||
public static IConnectionBuilder UseConnectionHandler<TConnectionHandler>(this IConnectionBuilder connectionBuilder) where TConnectionHandler : ConnectionHandler
|
||||
{
|
||||
var handler = ActivatorUtilities.GetServiceOrCreateInstance<TConnectionHandler>(connectionBuilder.ApplicationServices);
|
||||
|
||||
// This is a terminal middleware, so there's no need to use the 'next' parameter
|
||||
return connectionBuilder.Run(connection => handler.OnConnectedAsync(connection));
|
||||
}
|
||||
|
||||
public static IConnectionBuilder Use(this IConnectionBuilder connectionBuilder, Func<ConnectionContext, Func<Task>, Task> middleware)
|
||||
{
|
||||
return connectionBuilder.Use(next =>
|
||||
{
|
||||
return context =>
|
||||
{
|
||||
Func<Task> simpleNext = () => next(context);
|
||||
return middleware(context, simpleNext);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public static IConnectionBuilder Run(this IConnectionBuilder connectionBuilder, Func<ConnectionContext, Task> middleware)
|
||||
{
|
||||
return connectionBuilder.Use(next =>
|
||||
{
|
||||
return context =>
|
||||
{
|
||||
return middleware(context);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public abstract class ConnectionContext
|
||||
{
|
||||
public abstract string ConnectionId { get; set; }
|
||||
|
||||
public abstract IFeatureCollection Features { get; }
|
||||
|
||||
public abstract IDictionary<object, object> Items { get; set; }
|
||||
|
||||
public abstract IDuplexPipe Transport { get; set; }
|
||||
|
||||
public virtual void Abort(ConnectionAbortedException abortReason)
|
||||
{
|
||||
// We expect this to be overridden, but this helps maintain back compat
|
||||
// with implementations of ConnectionContext that predate the addition of
|
||||
// ConnectioContext.Abort()
|
||||
Features.Get<IConnectionLifetimeFeature>()?.Abort();
|
||||
}
|
||||
|
||||
public virtual void Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application."));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public delegate Task ConnectionDelegate(ConnectionContext connection);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an end point that multiple connections connect to. For HTTP, endpoints are URLs, for non HTTP it can be a TCP listener (or similar)
|
||||
/// </summary>
|
||||
public abstract class ConnectionHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a new connection is accepted to the endpoint
|
||||
/// </summary>
|
||||
/// <param name="connection">The new <see cref="ConnectionContext"/></param>
|
||||
/// <returns>A <see cref="Task"/> that represents the connection lifetime. When the task completes, the connection is complete.</returns>
|
||||
public abstract Task OnConnectedAsync(ConnectionContext connection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class ConnectionItems : IDictionary<object, object>
|
||||
{
|
||||
public ConnectionItems()
|
||||
: this(new Dictionary<object, object>())
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionItems(IDictionary<object, object> items)
|
||||
{
|
||||
Items = items;
|
||||
}
|
||||
|
||||
public IDictionary<object, object> Items { get; }
|
||||
|
||||
// Replace the indexer with one that returns null for missing values
|
||||
object IDictionary<object, object>.this[object key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Items.TryGetValue(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
set { Items[key] = value; }
|
||||
}
|
||||
|
||||
void IDictionary<object, object>.Add(object key, object value)
|
||||
{
|
||||
Items.Add(key, value);
|
||||
}
|
||||
|
||||
bool IDictionary<object, object>.ContainsKey(object key)
|
||||
{
|
||||
return Items.ContainsKey(key);
|
||||
}
|
||||
|
||||
ICollection<object> IDictionary<object, object>.Keys
|
||||
{
|
||||
get { return Items.Keys; }
|
||||
}
|
||||
|
||||
bool IDictionary<object, object>.Remove(object key)
|
||||
{
|
||||
return Items.Remove(key);
|
||||
}
|
||||
|
||||
bool IDictionary<object, object>.TryGetValue(object key, out object value)
|
||||
{
|
||||
return Items.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
ICollection<object> IDictionary<object, object>.Values
|
||||
{
|
||||
get { return Items.Values; }
|
||||
}
|
||||
|
||||
void ICollection<KeyValuePair<object, object>>.Add(KeyValuePair<object, object> item)
|
||||
{
|
||||
Items.Add(item);
|
||||
}
|
||||
|
||||
void ICollection<KeyValuePair<object, object>>.Clear()
|
||||
{
|
||||
Items.Clear();
|
||||
}
|
||||
|
||||
bool ICollection<KeyValuePair<object, object>>.Contains(KeyValuePair<object, object> item)
|
||||
{
|
||||
return Items.Contains(item);
|
||||
}
|
||||
|
||||
void ICollection<KeyValuePair<object, object>>.CopyTo(KeyValuePair<object, object>[] array, int arrayIndex)
|
||||
{
|
||||
Items.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
int ICollection<KeyValuePair<object, object>>.Count
|
||||
{
|
||||
get { return Items.Count; }
|
||||
}
|
||||
|
||||
bool ICollection<KeyValuePair<object, object>>.IsReadOnly
|
||||
{
|
||||
get { return Items.IsReadOnly; }
|
||||
}
|
||||
|
||||
bool ICollection<KeyValuePair<object, object>>.Remove(KeyValuePair<object, object> item)
|
||||
{
|
||||
object value;
|
||||
if (Items.TryGetValue(item.Key, out value) && Equals(item.Value, value))
|
||||
{
|
||||
return Items.Remove(item.Key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<object, object>> IEnumerable<KeyValuePair<object, object>>.GetEnumerator()
|
||||
{
|
||||
return Items.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Items.GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class DefaultConnectionContext : ConnectionContext,
|
||||
IDisposable,
|
||||
IConnectionIdFeature,
|
||||
IConnectionItemsFeature,
|
||||
IConnectionTransportFeature,
|
||||
IConnectionUserFeature,
|
||||
IConnectionLifetimeFeature
|
||||
{
|
||||
private CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource();
|
||||
|
||||
public DefaultConnectionContext() :
|
||||
this(Guid.NewGuid().ToString())
|
||||
{
|
||||
ConnectionClosed = _connectionClosedTokenSource.Token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
|
||||
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
public DefaultConnectionContext(string id)
|
||||
{
|
||||
ConnectionId = id;
|
||||
|
||||
Features = new FeatureCollection();
|
||||
Features.Set<IConnectionUserFeature>(this);
|
||||
Features.Set<IConnectionItemsFeature>(this);
|
||||
Features.Set<IConnectionIdFeature>(this);
|
||||
Features.Set<IConnectionTransportFeature>(this);
|
||||
Features.Set<IConnectionLifetimeFeature>(this);
|
||||
}
|
||||
|
||||
public DefaultConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application)
|
||||
: this(id)
|
||||
{
|
||||
Transport = transport;
|
||||
Application = application;
|
||||
}
|
||||
|
||||
public override string ConnectionId { get; set; }
|
||||
|
||||
public override IFeatureCollection Features { get; }
|
||||
|
||||
public ClaimsPrincipal User { get; set; }
|
||||
|
||||
public override IDictionary<object, object> Items { get; set; } = new ConnectionItems();
|
||||
|
||||
public IDuplexPipe Application { get; set; }
|
||||
|
||||
public override IDuplexPipe Transport { get; set; }
|
||||
|
||||
public CancellationToken ConnectionClosed { get; set; }
|
||||
|
||||
public override void Abort(ConnectionAbortedException abortReason)
|
||||
{
|
||||
ThreadPool.QueueUserWorkItem(cts => ((CancellationTokenSource)cts).Cancel(), _connectionClosedTokenSource);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connectionClosedTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class AddressInUseException : InvalidOperationException
|
||||
{
|
||||
public AddressInUseException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public AddressInUseException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class ConnectionAbortedException : OperationCanceledException
|
||||
{
|
||||
public ConnectionAbortedException() :
|
||||
this("The connection was aborted")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ConnectionAbortedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionAbortedException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public class ConnectionResetException : IOException
|
||||
{
|
||||
public ConnectionResetException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ConnectionResetException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionHeartbeatFeature
|
||||
{
|
||||
void OnHeartbeat(Action<object> action, object state);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionIdFeature
|
||||
{
|
||||
string ConnectionId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the connection transport has an "inherent keep-alive", which means that the transport will automatically
|
||||
/// inform the client that it is still present.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The most common example of this feature is the Long Polling HTTP transport, which must (due to HTTP limitations) terminate
|
||||
/// each poll within a particular interval and return a signal indicating "the server is still here, but there is no data yet".
|
||||
/// This feature allows applications to add keep-alive functionality, but limit it only to transports that don't have some kind
|
||||
/// of inherent keep-alive.
|
||||
/// </remarks>
|
||||
public interface IConnectionInherentKeepAliveFeature
|
||||
{
|
||||
bool HasInherentKeepAlive { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionItemsFeature
|
||||
{
|
||||
IDictionary<object, object> Items { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionLifetimeFeature
|
||||
{
|
||||
CancellationToken ConnectionClosed { get; set; }
|
||||
void Abort();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionTransportFeature
|
||||
{
|
||||
IDuplexPipe Transport { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System.Security.Claims;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IConnectionUserFeature
|
||||
{
|
||||
ClaimsPrincipal User { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface IMemoryPoolFeature
|
||||
{
|
||||
MemoryPool<byte> MemoryPool { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Features
|
||||
{
|
||||
public interface ITransferFormatFeature
|
||||
{
|
||||
TransferFormat SupportedFormats { get; }
|
||||
TransferFormat ActiveFormat { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
public interface IConnectionBuilder
|
||||
{
|
||||
IServiceProvider ApplicationServices { get; }
|
||||
|
||||
IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware);
|
||||
|
||||
ConnectionDelegate Build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Core components of ASP.NET Core networking protocol stack.</Description>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageTags>aspnetcore</PackageTags>
|
||||
<NoWarn>CS1591;$(NoWarn)</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.AspNetCore.Http.Features" />
|
||||
<Reference Include="Microsoft.Extensions.ActivatorUtilities.Sources" PrivateAssets="All" />
|
||||
<Reference Include="System.IO.Pipelines" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections
|
||||
{
|
||||
[Flags]
|
||||
public enum TransferFormat
|
||||
{
|
||||
Binary = 0x01,
|
||||
Text = 0x02
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<Project>
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<BaseIntermediateOutputPath>$(RepositoryRoot)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>$(RepositoryRoot)bin\$(MSBuildProjectName)\</BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// 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.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
public class AdaptedPipeline : IDuplexPipe
|
||||
{
|
||||
private static readonly int MinAllocBufferSize = KestrelMemoryPool.MinimumSegmentSize / 2;
|
||||
|
||||
private readonly IDuplexPipe _transport;
|
||||
|
||||
public AdaptedPipeline(IDuplexPipe transport,
|
||||
Pipe inputPipe,
|
||||
Pipe outputPipe,
|
||||
IKestrelTrace log)
|
||||
{
|
||||
_transport = transport;
|
||||
Input = inputPipe;
|
||||
Output = outputPipe;
|
||||
Log = log;
|
||||
}
|
||||
|
||||
public Pipe Input { get; }
|
||||
|
||||
public Pipe Output { get; }
|
||||
|
||||
public IKestrelTrace Log { get; }
|
||||
|
||||
PipeReader IDuplexPipe.Input => Input.Reader;
|
||||
|
||||
PipeWriter IDuplexPipe.Output => Output.Writer;
|
||||
|
||||
public async Task RunAsync(Stream stream)
|
||||
{
|
||||
var inputTask = ReadInputAsync(stream);
|
||||
var outputTask = WriteOutputAsync(stream);
|
||||
|
||||
await inputTask;
|
||||
await outputTask;
|
||||
}
|
||||
|
||||
private async Task WriteOutputAsync(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await Output.Reader.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
|
||||
try
|
||||
{
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
await stream.FlushAsync();
|
||||
}
|
||||
else if (buffer.IsSingleSegment)
|
||||
{
|
||||
#if NETCOREAPP2_1
|
||||
await stream.WriteAsync(buffer.First);
|
||||
#else
|
||||
var array = buffer.First.GetArray();
|
||||
await stream.WriteAsync(array.Array, array.Offset, array.Count);
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var memory in buffer)
|
||||
{
|
||||
#if NETCOREAPP2_1
|
||||
await stream.WriteAsync(memory);
|
||||
#else
|
||||
var array = memory.GetArray();
|
||||
await stream.WriteAsync(array.Array, array.Offset, array.Count);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Output.Reader.AdvanceTo(buffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.LogError(0, ex, $"{nameof(AdaptedPipeline)}.{nameof(WriteOutputAsync)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Output.Reader.Complete();
|
||||
_transport.Output.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReadInputAsync(Stream stream)
|
||||
{
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
// REVIEW: Do we need an exception here?
|
||||
return;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
||||
var outputBuffer = Input.Writer.GetMemory(MinAllocBufferSize);
|
||||
#if NETCOREAPP2_1
|
||||
var bytesRead = await stream.ReadAsync(outputBuffer);
|
||||
#else
|
||||
var array = outputBuffer.GetArray();
|
||||
var bytesRead = await stream.ReadAsync(array.Array, array.Offset, array.Count);
|
||||
#endif
|
||||
Input.Writer.Advance(bytesRead);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
// FIN
|
||||
break;
|
||||
}
|
||||
|
||||
var result = await Input.Writer.FlushAsync();
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't rethrow the exception. It should be handled by the Pipeline consumer.
|
||||
error = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Input.Writer.Complete(error);
|
||||
// The application could have ended the input pipe so complete
|
||||
// the transport pipe as well
|
||||
_transport.Input.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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.IO;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
// Even though this only includes the non-adapted ConnectionStream currently, this is a context in case
|
||||
// we want to add more connection metadata later.
|
||||
public class ConnectionAdapterContext
|
||||
{
|
||||
internal ConnectionAdapterContext(ConnectionContext connectionContext, Stream connectionStream)
|
||||
{
|
||||
ConnectionContext = connectionContext;
|
||||
ConnectionStream = connectionStream;
|
||||
}
|
||||
|
||||
internal ConnectionContext ConnectionContext { get; }
|
||||
|
||||
public IFeatureCollection Features => ConnectionContext.Features;
|
||||
|
||||
public Stream ConnectionStream { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
public interface IAdaptedConnection : IDisposable
|
||||
{
|
||||
Stream ConnectionStream { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
public interface IConnectionAdapter
|
||||
{
|
||||
bool IsHttps { get; }
|
||||
Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
public class LoggingConnectionAdapter : IConnectionAdapter
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LoggingConnectionAdapter(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IsHttps => false;
|
||||
|
||||
public Task<IAdaptedConnection> OnConnectionAsync(ConnectionAdapterContext context)
|
||||
{
|
||||
return Task.FromResult<IAdaptedConnection>(
|
||||
new LoggingAdaptedConnection(context.ConnectionStream, _logger));
|
||||
}
|
||||
|
||||
private class LoggingAdaptedConnection : IAdaptedConnection
|
||||
{
|
||||
public LoggingAdaptedConnection(Stream rawStream, ILogger logger)
|
||||
{
|
||||
ConnectionStream = new LoggingStream(rawStream, logger);
|
||||
}
|
||||
|
||||
public Stream ConnectionStream { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
internal class LoggingStream : Stream
|
||||
{
|
||||
private readonly Stream _inner;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LoggingStream(Stream inner, ILogger logger)
|
||||
{
|
||||
_inner = inner;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanRead;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanSeek;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.CanWrite;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return _inner.Position;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_inner.Position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_inner.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _inner.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
int read = _inner.Read(buffer, offset, count);
|
||||
Log("Read", new ReadOnlySpan<byte>(buffer, offset, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override int Read(Span<byte> destination)
|
||||
{
|
||||
int read = _inner.Read(destination);
|
||||
Log("Read", destination.Slice(0, read));
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public async override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int read = await _inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
Log("ReadAsync", new ReadOnlySpan<byte>(buffer, offset, read));
|
||||
return read;
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override async ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
int read = await _inner.ReadAsync(destination, cancellationToken);
|
||||
Log("ReadAsync", destination.Span.Slice(0, read));
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return _inner.Seek(offset, origin);
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
_inner.SetLength(value);
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Log("Write", new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
_inner.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
Log("Write", source);
|
||||
_inner.Write(source);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
Log("WriteAsync", new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
return _inner.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Log("WriteAsync", source.Span);
|
||||
return _inner.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
private void Log(string method, ReadOnlySpan<byte> buffer)
|
||||
{
|
||||
var builder = new StringBuilder($"{method}[{buffer.Length}] ");
|
||||
|
||||
// Write the hex
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
builder.Append(buffer[i].ToString("X2"));
|
||||
builder.Append(" ");
|
||||
}
|
||||
builder.AppendLine();
|
||||
// Write the bytes as if they were ASCII
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
builder.Append((char)buffer[i]);
|
||||
}
|
||||
|
||||
_logger.LogDebug(builder.ToString());
|
||||
}
|
||||
|
||||
// The below APM methods call the underlying Read/WriteAsync methods which will still be logged.
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = ReadAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return ((Task<int>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
var task = ReadAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<int>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(task2.Result);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = WriteAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
((Task<object>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(state);
|
||||
var task = WriteAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<object>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(null);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal
|
||||
{
|
||||
public class RawStream : Stream
|
||||
{
|
||||
private readonly PipeReader _input;
|
||||
private readonly PipeWriter _output;
|
||||
|
||||
public RawStream(PipeReader input, PipeWriter output)
|
||||
{
|
||||
_input = input;
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
// ValueTask uses .GetAwaiter().GetResult() if necessary
|
||||
// https://github.com/dotnet/corefx/blob/f9da3b4af08214764a51b2331f3595ffaf162abe/src/System.Threading.Tasks.Extensions/src/System/Threading/Tasks/ValueTask.cs#L156
|
||||
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count)).Result;
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count)).AsTask();
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return ReadAsyncInternal(destination);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
WriteAsync(buffer, offset, count).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
if (buffer != null)
|
||||
{
|
||||
_output.Write(new ReadOnlySpan<byte>(buffer, offset, count));
|
||||
}
|
||||
|
||||
await _output.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_output.Write(source.Span);
|
||||
await _output.FlushAsync(cancellationToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WriteAsync(null, 0, 0, cancellationToken);
|
||||
}
|
||||
|
||||
private async ValueTask<int> ReadAsyncInternal(Memory<byte> destination)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var result = await _input.ReadAsync();
|
||||
var readableBuffer = result.Buffer;
|
||||
try
|
||||
{
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
// buffer.Count is int
|
||||
var count = (int) Math.Min(readableBuffer.Length, destination.Length);
|
||||
readableBuffer = readableBuffer.Slice(0, count);
|
||||
readableBuffer.CopyTo(destination.Span);
|
||||
return count;
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_input.AdvanceTo(readableBuffer.End, readableBuffer.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = ReadAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return ((Task<int>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
var task = ReadAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<int>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(task2.Result);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = WriteAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
((Task<object>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(state);
|
||||
var task = WriteAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<object>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(null);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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 Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Hosting
|
||||
{
|
||||
public static class ListenOptionsConnectionLoggingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Emits verbose logs for bytes read from and written to the connection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The <see cref="ListenOptions"/>.
|
||||
/// </returns>
|
||||
public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions)
|
||||
{
|
||||
return listenOptions.UseConnectionLogging(nameof(LoggingConnectionAdapter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits verbose logs for bytes read from and written to the connection.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The <see cref="ListenOptions"/>.
|
||||
/// </returns>
|
||||
public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions, string loggerName)
|
||||
{
|
||||
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger(loggerName ?? nameof(LoggingConnectionAdapter));
|
||||
listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(logger));
|
||||
return listenOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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.IO;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||
{
|
||||
internal class AnyIPListenOptions : ListenOptions
|
||||
{
|
||||
internal AnyIPListenOptions(int port)
|
||||
: base(new IPEndPoint(IPAddress.IPv6Any, port))
|
||||
{
|
||||
}
|
||||
|
||||
internal override async Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
|
||||
try
|
||||
{
|
||||
await base.BindAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is IOException))
|
||||
{
|
||||
context.Logger.LogDebug(CoreStrings.FormatFallbackToIPv4Any(IPEndPoint.Port));
|
||||
|
||||
// for machines that do not support IPv6
|
||||
IPEndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.Port);
|
||||
await base.BindAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||
{
|
||||
public sealed class BadHttpRequestException : IOException
|
||||
{
|
||||
private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason)
|
||||
: this(message, statusCode, reason, null)
|
||||
{ }
|
||||
|
||||
private BadHttpRequestException(string message, int statusCode, RequestRejectionReason reason, HttpMethod? requiredMethod)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Reason = reason;
|
||||
|
||||
if (requiredMethod.HasValue)
|
||||
{
|
||||
AllowedHeader = HttpUtilities.MethodToString(requiredMethod.Value);
|
||||
}
|
||||
}
|
||||
|
||||
internal int StatusCode { get; }
|
||||
|
||||
internal StringValues AllowedHeader { get; }
|
||||
|
||||
internal RequestRejectionReason Reason { get; }
|
||||
|
||||
[StackTraceHidden]
|
||||
internal static void Throw(RequestRejectionReason reason)
|
||||
{
|
||||
throw GetException(reason);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
public static void Throw(RequestRejectionReason reason, HttpMethod method)
|
||||
=> throw GetException(reason, method.ToString().ToUpperInvariant());
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static BadHttpRequestException GetException(RequestRejectionReason reason)
|
||||
{
|
||||
BadHttpRequestException ex;
|
||||
switch (reason)
|
||||
{
|
||||
case RequestRejectionReason.InvalidRequestHeadersNoCRLF:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestHeadersNoCRLF, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidRequestLine:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidRequestLine, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.MalformedRequestInvalidHeaders:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.MultipleContentLengths:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleContentLengths, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.UnexpectedEndOfRequestContent:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_UnexpectedEndOfRequestContent, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.BadChunkSuffix:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSuffix, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.BadChunkSizeData:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_BadChunkSizeData, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.ChunkedRequestIncomplete:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_ChunkedRequestIncomplete, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidCharactersInHeaderName:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidCharactersInHeaderName, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.RequestLineTooLong:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestLineTooLong, StatusCodes.Status414UriTooLong, reason);
|
||||
break;
|
||||
case RequestRejectionReason.HeadersExceedMaxTotalSize:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason);
|
||||
break;
|
||||
case RequestRejectionReason.TooManyHeaders:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_TooManyHeaders, StatusCodes.Status431RequestHeaderFieldsTooLarge, reason);
|
||||
break;
|
||||
case RequestRejectionReason.RequestBodyTooLarge:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTooLarge, StatusCodes.Status413PayloadTooLarge, reason);
|
||||
break;
|
||||
case RequestRejectionReason.RequestHeadersTimeout:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestHeadersTimeout, StatusCodes.Status408RequestTimeout, reason);
|
||||
break;
|
||||
case RequestRejectionReason.RequestBodyTimeout:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_RequestBodyTimeout, StatusCodes.Status408RequestTimeout, reason);
|
||||
break;
|
||||
case RequestRejectionReason.OptionsMethodRequired:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Options);
|
||||
break;
|
||||
case RequestRejectionReason.ConnectMethodRequired:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MethodNotAllowed, StatusCodes.Status405MethodNotAllowed, reason, HttpMethod.Connect);
|
||||
break;
|
||||
case RequestRejectionReason.MissingHostHeader:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MissingHostHeader, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.MultipleHostHeaders:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_MultipleHostHeaders, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidHostHeader:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_InvalidHostHeader, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.UpgradeRequestCannotHavePayload:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest_UpgradeRequestCannotHavePayload, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
default:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
internal static void Throw(RequestRejectionReason reason, string detail)
|
||||
{
|
||||
throw GetException(reason, detail);
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
internal static void Throw(RequestRejectionReason reason, in StringValues detail)
|
||||
{
|
||||
throw GetException(reason, detail.ToString());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
internal static BadHttpRequestException GetException(RequestRejectionReason reason, string detail)
|
||||
{
|
||||
BadHttpRequestException ex;
|
||||
switch (reason)
|
||||
{
|
||||
case RequestRejectionReason.InvalidRequestLine:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestLine_Detail(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidRequestTarget:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestTarget_Detail(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidRequestHeader:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidRequestHeader_Detail(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidContentLength:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidContentLength_Detail(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.UnrecognizedHTTPVersion:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_UnrecognizedHTTPVersion(detail), StatusCodes.Status505HttpVersionNotsupported, reason);
|
||||
break;
|
||||
case RequestRejectionReason.FinalTransferCodingNotChunked:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_FinalTransferCodingNotChunked(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.LengthRequired:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequired(detail), StatusCodes.Status411LengthRequired, reason);
|
||||
break;
|
||||
case RequestRejectionReason.LengthRequiredHttp10:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_LengthRequiredHttp10(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
case RequestRejectionReason.InvalidHostHeader:
|
||||
ex = new BadHttpRequestException(CoreStrings.FormatBadRequest_InvalidHostHeader_Detail(detail), StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
default:
|
||||
ex = new BadHttpRequestException(CoreStrings.BadRequest, StatusCodes.Status400BadRequest, reason);
|
||||
break;
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the client certificate requirements for a HTTPS connection.
|
||||
/// </summary>
|
||||
public enum ClientCertificateMode
|
||||
{
|
||||
/// <summary>
|
||||
/// A client certificate is not required and will not be requested from clients.
|
||||
/// </summary>
|
||||
NoCertificate,
|
||||
|
||||
/// <summary>
|
||||
/// A client certificate will be requested; however, authentication will not fail if a certificate is not provided by the client.
|
||||
/// </summary>
|
||||
AllowCertificate,
|
||||
|
||||
/// <summary>
|
||||
/// A client certificate will be requested, and the client must provide a valid certificate for authentication to succeed.
|
||||
/// </summary>
|
||||
RequireCertificate
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="BadRequest" xml:space="preserve">
|
||||
<value>Bad request.</value>
|
||||
</data>
|
||||
<data name="BadRequest_BadChunkSizeData" xml:space="preserve">
|
||||
<value>Bad chunk size data.</value>
|
||||
</data>
|
||||
<data name="BadRequest_BadChunkSuffix" xml:space="preserve">
|
||||
<value>Bad chunk suffix.</value>
|
||||
</data>
|
||||
<data name="BadRequest_ChunkedRequestIncomplete" xml:space="preserve">
|
||||
<value>Chunked request incomplete.</value>
|
||||
</data>
|
||||
<data name="BadRequest_FinalTransferCodingNotChunked" xml:space="preserve">
|
||||
<value>The message body length cannot be determined because the final transfer coding was set to '{detail}' instead of 'chunked'.</value>
|
||||
</data>
|
||||
<data name="BadRequest_HeadersExceedMaxTotalSize" xml:space="preserve">
|
||||
<value>Request headers too long.</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidCharactersInHeaderName" xml:space="preserve">
|
||||
<value>Invalid characters in header name.</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidContentLength_Detail" xml:space="preserve">
|
||||
<value>Invalid content length: {detail}</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidHostHeader" xml:space="preserve">
|
||||
<value>Invalid Host header.</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidHostHeader_Detail" xml:space="preserve">
|
||||
<value>Invalid Host header: '{detail}'</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidRequestHeadersNoCRLF" xml:space="preserve">
|
||||
<value>Invalid request headers: missing final CRLF in header fields.</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidRequestHeader_Detail" xml:space="preserve">
|
||||
<value>Invalid request header: '{detail}'</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidRequestLine" xml:space="preserve">
|
||||
<value>Invalid request line.</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidRequestLine_Detail" xml:space="preserve">
|
||||
<value>Invalid request line: '{detail}'</value>
|
||||
</data>
|
||||
<data name="BadRequest_InvalidRequestTarget_Detail" xml:space="preserve">
|
||||
<value>Invalid request target: '{detail}'</value>
|
||||
</data>
|
||||
<data name="BadRequest_LengthRequired" xml:space="preserve">
|
||||
<value>{detail} request contains no Content-Length or Transfer-Encoding header.</value>
|
||||
</data>
|
||||
<data name="BadRequest_LengthRequiredHttp10" xml:space="preserve">
|
||||
<value>{detail} request contains no Content-Length header.</value>
|
||||
</data>
|
||||
<data name="BadRequest_MalformedRequestInvalidHeaders" xml:space="preserve">
|
||||
<value>Malformed request: invalid headers.</value>
|
||||
</data>
|
||||
<data name="BadRequest_MethodNotAllowed" xml:space="preserve">
|
||||
<value>Method not allowed.</value>
|
||||
</data>
|
||||
<data name="BadRequest_MissingHostHeader" xml:space="preserve">
|
||||
<value>Request is missing Host header.</value>
|
||||
</data>
|
||||
<data name="BadRequest_MultipleContentLengths" xml:space="preserve">
|
||||
<value>Multiple Content-Length headers.</value>
|
||||
</data>
|
||||
<data name="BadRequest_MultipleHostHeaders" xml:space="preserve">
|
||||
<value>Multiple Host headers.</value>
|
||||
</data>
|
||||
<data name="BadRequest_RequestLineTooLong" xml:space="preserve">
|
||||
<value>Request line too long.</value>
|
||||
</data>
|
||||
<data name="BadRequest_RequestHeadersTimeout" xml:space="preserve">
|
||||
<value>Reading the request headers timed out.</value>
|
||||
</data>
|
||||
<data name="BadRequest_TooManyHeaders" xml:space="preserve">
|
||||
<value>Request contains too many headers.</value>
|
||||
</data>
|
||||
<data name="BadRequest_UnexpectedEndOfRequestContent" xml:space="preserve">
|
||||
<value>Unexpected end of request content.</value>
|
||||
</data>
|
||||
<data name="BadRequest_UnrecognizedHTTPVersion" xml:space="preserve">
|
||||
<value>Unrecognized HTTP version: '{detail}'</value>
|
||||
</data>
|
||||
<data name="BadRequest_UpgradeRequestCannotHavePayload" xml:space="preserve">
|
||||
<value>Requests with 'Connection: Upgrade' cannot have content in the request body.</value>
|
||||
</data>
|
||||
<data name="FallbackToIPv4Any" xml:space="preserve">
|
||||
<value>Failed to bind to http://[::]:{port} (IPv6Any). Attempting to bind to http://0.0.0.0:{port} instead.</value>
|
||||
</data>
|
||||
<data name="ResponseStreamWasUpgraded" xml:space="preserve">
|
||||
<value>Cannot write to response body after connection has been upgraded.</value>
|
||||
</data>
|
||||
<data name="BigEndianNotSupported" xml:space="preserve">
|
||||
<value>Kestrel does not support big-endian architectures.</value>
|
||||
</data>
|
||||
<data name="MaxRequestBufferSmallerThanRequestHeaderBuffer" xml:space="preserve">
|
||||
<value>Maximum request buffer size ({requestBufferSize}) must be greater than or equal to maximum request header size ({requestHeaderSize}).</value>
|
||||
</data>
|
||||
<data name="MaxRequestBufferSmallerThanRequestLineBuffer" xml:space="preserve">
|
||||
<value>Maximum request buffer size ({requestBufferSize}) must be greater than or equal to maximum request line size ({requestLineSize}).</value>
|
||||
</data>
|
||||
<data name="ServerAlreadyStarted" xml:space="preserve">
|
||||
<value>Server has already started.</value>
|
||||
</data>
|
||||
<data name="UnknownTransportMode" xml:space="preserve">
|
||||
<value>Unknown transport mode: '{mode}'.</value>
|
||||
</data>
|
||||
<data name="InvalidAsciiOrControlChar" xml:space="preserve">
|
||||
<value>Invalid non-ASCII or control character in header: {character}</value>
|
||||
</data>
|
||||
<data name="InvalidContentLength_InvalidNumber" xml:space="preserve">
|
||||
<value>Invalid Content-Length: "{value}". Value must be a positive integral number.</value>
|
||||
</data>
|
||||
<data name="NonNegativeNumberOrNullRequired" xml:space="preserve">
|
||||
<value>Value must be null or a non-negative number.</value>
|
||||
</data>
|
||||
<data name="NonNegativeNumberRequired" xml:space="preserve">
|
||||
<value>Value must be a non-negative number.</value>
|
||||
</data>
|
||||
<data name="PositiveNumberRequired" xml:space="preserve">
|
||||
<value>Value must be a positive number.</value>
|
||||
</data>
|
||||
<data name="PositiveNumberOrNullRequired" xml:space="preserve">
|
||||
<value>Value must be null or a positive number.</value>
|
||||
</data>
|
||||
<data name="UnixSocketPathMustBeAbsolute" xml:space="preserve">
|
||||
<value>Unix socket path must be absolute.</value>
|
||||
</data>
|
||||
<data name="AddressBindingFailed" xml:space="preserve">
|
||||
<value>Failed to bind to address {address}.</value>
|
||||
</data>
|
||||
<data name="BindingToDefaultAddress" xml:space="preserve">
|
||||
<value>No listening endpoints were configured. Binding to {address} by default.</value>
|
||||
</data>
|
||||
<data name="ConfigureHttpsFromMethodCall" xml:space="preserve">
|
||||
<value>HTTPS endpoints can only be configured using {methodName}.</value>
|
||||
</data>
|
||||
<data name="ConfigurePathBaseFromMethodCall" xml:space="preserve">
|
||||
<value>A path base can only be configured using {methodName}.</value>
|
||||
</data>
|
||||
<data name="DynamicPortOnLocalhostNotSupported" xml:space="preserve">
|
||||
<value>Dynamic port binding is not supported when binding to localhost. You must either bind to 127.0.0.1:0 or [::1]:0, or both.</value>
|
||||
</data>
|
||||
<data name="EndpointAlreadyInUse" xml:space="preserve">
|
||||
<value>Failed to bind to address {endpoint}: address already in use.</value>
|
||||
</data>
|
||||
<data name="InvalidUrl" xml:space="preserve">
|
||||
<value>Invalid URL: '{url}'.</value>
|
||||
</data>
|
||||
<data name="NetworkInterfaceBindingFailed" xml:space="preserve">
|
||||
<value>Unable to bind to {address} on the {interfaceName} interface: '{error}'.</value>
|
||||
</data>
|
||||
<data name="OverridingWithKestrelOptions" xml:space="preserve">
|
||||
<value>Overriding address(es) '{addresses}'. Binding to endpoints defined in {methodName} instead.</value>
|
||||
</data>
|
||||
<data name="OverridingWithPreferHostingUrls" xml:space="preserve">
|
||||
<value>Overriding endpoints defined in UseKestrel() because {settingName} is set to true. Binding to address(es) '{addresses}' instead.</value>
|
||||
</data>
|
||||
<data name="UnsupportedAddressScheme" xml:space="preserve">
|
||||
<value>Unrecognized scheme in server address '{address}'. Only 'http://' is supported.</value>
|
||||
</data>
|
||||
<data name="HeadersAreReadOnly" xml:space="preserve">
|
||||
<value>Headers are read-only, response has already started.</value>
|
||||
</data>
|
||||
<data name="KeyAlreadyExists" xml:space="preserve">
|
||||
<value>An item with the same key has already been added.</value>
|
||||
</data>
|
||||
<data name="HeaderNotAllowedOnResponse" xml:space="preserve">
|
||||
<value>Setting the header {name} is not allowed on responses with status code {statusCode}.</value>
|
||||
</data>
|
||||
<data name="ParameterReadOnlyAfterResponseStarted" xml:space="preserve">
|
||||
<value>{name} cannot be set because the response has already started.</value>
|
||||
</data>
|
||||
<data name="RequestProcessingAborted" xml:space="preserve">
|
||||
<value>Request processing didn't complete within the shutdown timeout.</value>
|
||||
</data>
|
||||
<data name="TooFewBytesWritten" xml:space="preserve">
|
||||
<value>Response Content-Length mismatch: too few bytes written ({written} of {expected}).</value>
|
||||
</data>
|
||||
<data name="TooManyBytesWritten" xml:space="preserve">
|
||||
<value>Response Content-Length mismatch: too many bytes written ({written} of {expected}).</value>
|
||||
</data>
|
||||
<data name="UnhandledApplicationException" xml:space="preserve">
|
||||
<value>The response has been aborted due to an unhandled application exception.</value>
|
||||
</data>
|
||||
<data name="WritingToResponseBodyNotSupported" xml:space="preserve">
|
||||
<value>Writing to the response body is invalid for responses with status code {statusCode}.</value>
|
||||
</data>
|
||||
<data name="ConnectionShutdownError" xml:space="preserve">
|
||||
<value>Connection shutdown abnormally.</value>
|
||||
</data>
|
||||
<data name="RequestProcessingEndError" xml:space="preserve">
|
||||
<value>Connection processing ended abnormally.</value>
|
||||
</data>
|
||||
<data name="CannotUpgradeNonUpgradableRequest" xml:space="preserve">
|
||||
<value>Cannot upgrade a non-upgradable request. Check IHttpUpgradeFeature.IsUpgradableRequest to determine if a request can be upgraded.</value>
|
||||
</data>
|
||||
<data name="UpgradedConnectionLimitReached" xml:space="preserve">
|
||||
<value>Request cannot be upgraded because the server has already opened the maximum number of upgraded connections.</value>
|
||||
</data>
|
||||
<data name="UpgradeCannotBeCalledMultipleTimes" xml:space="preserve">
|
||||
<value>IHttpUpgradeFeature.UpgradeAsync was already called and can only be called once per connection.</value>
|
||||
</data>
|
||||
<data name="BadRequest_RequestBodyTooLarge" xml:space="preserve">
|
||||
<value>Request body too large.</value>
|
||||
</data>
|
||||
<data name="MaxRequestBodySizeCannotBeModifiedAfterRead" xml:space="preserve">
|
||||
<value>The maximum request body size cannot be modified after the app has already started reading from the request body.</value>
|
||||
</data>
|
||||
<data name="MaxRequestBodySizeCannotBeModifiedForUpgradedRequests" xml:space="preserve">
|
||||
<value>The maximum request body size cannot be modified after the request has been upgraded.</value>
|
||||
</data>
|
||||
<data name="PositiveTimeSpanRequired" xml:space="preserve">
|
||||
<value>Value must be a positive TimeSpan.</value>
|
||||
</data>
|
||||
<data name="NonNegativeTimeSpanRequired" xml:space="preserve">
|
||||
<value>Value must be a non-negative TimeSpan.</value>
|
||||
</data>
|
||||
<data name="MinimumGracePeriodRequired" xml:space="preserve">
|
||||
<value>The request body rate enforcement grace period must be greater than {heartbeatInterval} second.</value>
|
||||
</data>
|
||||
<data name="SynchronousReadsDisallowed" xml:space="preserve">
|
||||
<value>Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead.</value>
|
||||
</data>
|
||||
<data name="SynchronousWritesDisallowed" xml:space="preserve">
|
||||
<value>Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.</value>
|
||||
</data>
|
||||
<data name="PositiveNumberOrNullMinDataRateRequired" xml:space="preserve">
|
||||
<value>Value must be a positive number. To disable a minimum data rate, use null where a MinDataRate instance is expected.</value>
|
||||
</data>
|
||||
<data name="ConcurrentTimeoutsNotSupported" xml:space="preserve">
|
||||
<value>Concurrent timeouts are not supported.</value>
|
||||
</data>
|
||||
<data name="PositiveFiniteTimeSpanRequired" xml:space="preserve">
|
||||
<value>Timespan must be positive and finite.</value>
|
||||
</data>
|
||||
<data name="EndPointRequiresAtLeastOneProtocol" xml:space="preserve">
|
||||
<value>An endpoint must be configured to serve at least one protocol.</value>
|
||||
</data>
|
||||
<data name="EndPointRequiresTlsForHttp1AndHttp2" xml:space="preserve">
|
||||
<value>Using both HTTP/1.x and HTTP/2 on the same endpoint requires the use of TLS.</value>
|
||||
</data>
|
||||
<data name="EndPointHttp2NotNegotiated" xml:space="preserve">
|
||||
<value>HTTP/2 over TLS was not negotiated on an HTTP/2-only endpoint.</value>
|
||||
</data>
|
||||
<data name="HPackErrorDynamicTableSizeUpdateTooLarge" xml:space="preserve">
|
||||
<value>A dynamic table size of {size} octets is greater than the configured maximum size of {maxSize} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIndexOutOfRange" xml:space="preserve">
|
||||
<value>Index {index} is outside the bounds of the header field table.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorIncomplete" xml:space="preserve">
|
||||
<value>Input data could not be fully decoded.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorEOS" xml:space="preserve">
|
||||
<value>Input data contains the EOS symbol.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanErrorDestinationTooSmall" xml:space="preserve">
|
||||
<value>The destination buffer is not large enough to store the decoded data.</value>
|
||||
</data>
|
||||
<data name="HPackHuffmanError" xml:space="preserve">
|
||||
<value>Huffman decoding error.</value>
|
||||
</data>
|
||||
<data name="HPackStringLengthTooLarge" xml:space="preserve">
|
||||
<value>Decoded string length of {length} octets is greater than the configured maximum length of {maxStringLength} octets.</value>
|
||||
</data>
|
||||
<data name="HPackErrorIncompleteHeaderBlock" xml:space="preserve">
|
||||
<value>The header block was incomplete and could not be fully decoded.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamIdEven" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with even stream ID {streamId}.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorPushPromiseReceived" xml:space="preserve">
|
||||
<value>The client sent a A PUSH_PROMISE frame.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorHeadersInterleaved" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame to stream ID {streamId} before signaling of the header block for stream ID {headersStreamId}.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamIdZero" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with stream ID 0.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamIdNotZero" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with stream ID different than 0.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorPaddingTooLong" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with padding longer than or with the same length as the sent data.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamClosed" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame to closed stream ID {streamId}.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamHalfClosedRemote" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame to stream ID {streamId} which is in the "half-closed (remote) state".</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamSelfDependency" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with dependency information that would cause stream ID {streamId} to depend on itself.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorUnexpectedFrameLength" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame with length different than {expectedLength}.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorSettingsLengthNotMultipleOfSix" xml:space="preserve">
|
||||
<value>The client sent a SETTINGS frame with a length that is not a multiple of 6.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorSettingsAckLengthNotZero" xml:space="preserve">
|
||||
<value>The client sent a SETTINGS frame with ACK set and length different than 0.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorSettingsParameterOutOfRange" xml:space="preserve">
|
||||
<value>The client sent a SETTINGS frame with a value for parameter {parameter} that is out of range.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorWindowUpdateIncrementZero" xml:space="preserve">
|
||||
<value>The client sent a WINDOW_UPDATE frame with a window size increment of 0.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorContinuationWithNoHeaders" xml:space="preserve">
|
||||
<value>The client sent a CONTINUATION frame not preceded by a HEADERS frame.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorStreamIdle" xml:space="preserve">
|
||||
<value>The client sent a {frameType} frame to idle stream ID {streamId}.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorTrailersContainPseudoHeaderField" xml:space="preserve">
|
||||
<value>The client sent trailers containing one or more pseudo-header fields.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorHeaderNameUppercase" xml:space="preserve">
|
||||
<value>The client sent a header with uppercase characters in its name.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorTrailerNameUppercase" xml:space="preserve">
|
||||
<value>The client sent a trailer with uppercase characters in its name.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorHeadersWithTrailersNoEndStream" xml:space="preserve">
|
||||
<value>The client sent a HEADERS frame containing trailers without setting the END_STREAM flag.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorMissingMandatoryPseudoHeaderFields" xml:space="preserve">
|
||||
<value>Request headers missing one or more mandatory pseudo-header fields.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorPseudoHeaderFieldAfterRegularHeaders" xml:space="preserve">
|
||||
<value>Pseudo-header field found in request headers after regular header fields.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorUnknownPseudoHeaderField" xml:space="preserve">
|
||||
<value>Request headers contain unknown pseudo-header field.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorResponsePseudoHeaderField" xml:space="preserve">
|
||||
<value>Request headers contain response-specific pseudo-header field.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorDuplicatePseudoHeaderField" xml:space="preserve">
|
||||
<value>Request headers contain duplicate pseudo-header field.</value>
|
||||
</data>
|
||||
<data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve">
|
||||
<value>Request headers contain connection-specific header field.</value>
|
||||
</data>
|
||||
<data name="UnableToConfigureHttpsBindings" xml:space="preserve">
|
||||
<value>Unable to configure default https bindings because no IDefaultHttpsProvider service was provided.</value>
|
||||
</data>
|
||||
<data name="AuthenticationFailed" xml:space="preserve">
|
||||
<value>Failed to authenticate HTTPS connection.</value>
|
||||
</data>
|
||||
<data name="AuthenticationTimedOut" xml:space="preserve">
|
||||
<value>Authentication of the HTTPS connection timed out.</value>
|
||||
</data>
|
||||
<data name="InvalidServerCertificateEku" xml:space="preserve">
|
||||
<value>Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).</value>
|
||||
</data>
|
||||
<data name="PositiveTimeSpanRequired1" xml:space="preserve">
|
||||
<value>Value must be a positive TimeSpan.</value>
|
||||
</data>
|
||||
<data name="ServerCertificateRequired" xml:space="preserve">
|
||||
<value>The server certificate parameter is required.</value>
|
||||
</data>
|
||||
<data name="BindingToDefaultAddresses" xml:space="preserve">
|
||||
<value>No listening endpoints were configured. Binding to {address0} and {address1} by default.</value>
|
||||
</data>
|
||||
<data name="CertNotFoundInStore" xml:space="preserve">
|
||||
<value>The requested certificate {subject} could not be found in {storeLocation}/{storeName} with AllowInvalid setting: {allowInvalid}.</value>
|
||||
</data>
|
||||
<data name="EndpointMissingUrl" xml:space="preserve">
|
||||
<value>The endpoint {endpointName} is missing the required 'Url' parameter.</value>
|
||||
</data>
|
||||
<data name="NoCertSpecifiedNoDevelopmentCertificateFound" xml:space="preserve">
|
||||
<value>Unable to configure HTTPS endpoint. No server certificate was specified, and the default developer certificate could not be found.
|
||||
To generate a developer certificate run 'dotnet dev-certs https'. To trust the certificate (Windows and macOS only) run 'dotnet dev-certs https --trust'.
|
||||
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.</value>
|
||||
</data>
|
||||
<data name="MultipleCertificateSources" xml:space="preserve">
|
||||
<value>The endpoint {endpointName} specified multiple certificate sources.</value>
|
||||
</data>
|
||||
<data name="Http2NotSupported" xml:space="preserve">
|
||||
<value>HTTP/2 support is experimental, see https://go.microsoft.com/fwlink/?linkid=866785 to enable it.</value>
|
||||
</data>
|
||||
<data name="WritingToResponseBodyAfterResponseCompleted" xml:space="preserve">
|
||||
<value>Cannot write to the response body, the response has completed.</value>
|
||||
</data>
|
||||
<data name="BadRequest_RequestBodyTimeout" xml:space="preserve">
|
||||
<value>Reading the request body timed out due to data arriving too slowly. See MinRequestBodyDataRate.</value>
|
||||
</data>
|
||||
<data name="ConnectionAbortedByApplication" xml:space="preserve">
|
||||
<value>The connection was aborted by the application.</value>
|
||||
</data>
|
||||
<data name="ConnectionAbortedDuringServerShutdown" xml:space="preserve">
|
||||
<value>The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout.</value>
|
||||
</data>
|
||||
<data name="ConnectionTimedBecauseResponseMininumDataRateNotSatisfied" xml:space="preserve">
|
||||
<value>The connection was timed out by the server because the response was not read by the client at the specified minimum data rate.</value>
|
||||
</data>
|
||||
<data name="ConnectionTimedOutByServer" xml:space="preserve">
|
||||
<value>The connection was timed out by the server.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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 Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel
|
||||
{
|
||||
public class EndpointConfiguration
|
||||
{
|
||||
internal EndpointConfiguration(bool isHttps, ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, IConfigurationSection configSection)
|
||||
{
|
||||
IsHttps = isHttps;
|
||||
ListenOptions = listenOptions ?? throw new ArgumentNullException(nameof(listenOptions));
|
||||
HttpsOptions = httpsOptions ?? throw new ArgumentNullException(nameof(httpsOptions));
|
||||
ConfigSection = configSection ?? throw new ArgumentNullException(nameof(configSection));
|
||||
}
|
||||
|
||||
public bool IsHttps { get; }
|
||||
public ListenOptions ListenOptions { get; }
|
||||
public HttpsConnectionAdapterOptions HttpsOptions { get; }
|
||||
public IConfigurationSection ConfigSection { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Feature for efficiently handling connection timeouts.
|
||||
/// </summary>
|
||||
public interface IConnectionTimeoutFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Close the connection after the specified positive finite <see cref="TimeSpan"/>
|
||||
/// unless the timeout is canceled or reset. This will fail if there is an ongoing timeout.
|
||||
/// </summary>
|
||||
void SetTimeout(TimeSpan timeSpan);
|
||||
|
||||
/// <summary>
|
||||
/// Close the connection after the specified positive finite <see cref="TimeSpan"/>
|
||||
/// unless the timeout is canceled or reset. This will cancel any ongoing timeouts.
|
||||
/// </summary>
|
||||
void ResetTimeout(TimeSpan timeSpan);
|
||||
|
||||
/// <summary>
|
||||
/// Prevent the connection from closing after a timeout specified by <see cref="SetTimeout(TimeSpan)"/>
|
||||
/// or <see cref="ResetTimeout(TimeSpan)"/>.
|
||||
/// </summary>
|
||||
void CancelTimeout();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// A connection feature allowing middleware to stop counting connections towards <see cref="KestrelServerLimits.MaxConcurrentConnections"/>.
|
||||
/// This is used by Kestrel internally to stop counting upgraded connections towards this limit.
|
||||
/// </summary>
|
||||
public interface IDecrementConcurrentConnectionCountFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// Idempotent method to stop counting a connection towards <see cref="KestrelServerLimits.MaxConcurrentConnections"/>.
|
||||
/// </summary>
|
||||
void ReleaseConnection();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
public interface IHttp2StreamIdFeature
|
||||
{
|
||||
int StreamId { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Feature to set the minimum data rate at which the the request body must be sent by the client.
|
||||
/// </summary>
|
||||
public interface IHttpMinRequestBodyDataRateFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum data rate in bytes/second at which the request body must be sent by the client.
|
||||
/// Setting this property to null indicates no minimum data rate should be enforced.
|
||||
/// This limit has no effect on upgraded connections which are always unlimited.
|
||||
/// </summary>
|
||||
MinDataRate MinDataRate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Feature to set the minimum data rate at which the response must be received by the client.
|
||||
/// </summary>
|
||||
public interface IHttpMinResponseDataRateFeature
|
||||
{
|
||||
/// <summary>
|
||||
/// The minimum data rate in bytes/second at which the response must be received by the client.
|
||||
/// Setting this property to null indicates no minimum data rate should be enforced.
|
||||
/// This limit has no effect on upgraded connections which are always unlimited.
|
||||
/// </summary>
|
||||
MinDataRate MinDataRate { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Features
|
||||
{
|
||||
public interface ITlsApplicationProtocolFeature
|
||||
{
|
||||
ReadOnlyMemory<byte> ApplicationProtocol { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||
{
|
||||
[Flags]
|
||||
public enum HttpProtocols
|
||||
{
|
||||
None = 0x0,
|
||||
Http1 = 0x1,
|
||||
Http2 = 0x2,
|
||||
Http1AndHttp2 = Http1 | Http2,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// 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.Net.Security;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings for how Kestrel should handle HTTPS connections.
|
||||
/// </summary>
|
||||
public class HttpsConnectionAdapterOptions
|
||||
{
|
||||
private TimeSpan _handshakeTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="HttpsConnectionAdapterOptions"/>.
|
||||
/// </summary>
|
||||
public HttpsConnectionAdapterOptions()
|
||||
{
|
||||
ClientCertificateMode = ClientCertificateMode.NoCertificate;
|
||||
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
|
||||
HandshakeTimeout = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Specifies the server certificate used to authenticate HTTPS connections. This is ignored if ServerCertificateSelector is set.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public X509Certificate2 ServerCertificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// A callback that will be invoked to dynamically select a server certificate. This is higher priority than ServerCertificate.
|
||||
/// If SNI is not avialable then the name parameter will be null.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public Func<ConnectionContext, string, X509Certificate2> ServerCertificateSelector { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the client certificate requirements for a HTTPS connection. Defaults to <see cref="ClientCertificateMode.NoCertificate"/>.
|
||||
/// </summary>
|
||||
public ClientCertificateMode ClientCertificateMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a callback for additional client certificate validation that will be invoked during authentication.
|
||||
/// </summary>
|
||||
public Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> ClientCertificateValidation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies allowable SSL protocols. Defaults to <see cref="SslProtocols.Tls12" /> and <see cref="SslProtocols.Tls11"/>.
|
||||
/// </summary>
|
||||
public SslProtocols SslProtocols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The protocols enabled on this endpoint.
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to HTTP/1.x only.</remarks>
|
||||
internal HttpProtocols HttpProtocols { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the certificate revocation list is checked during authentication.
|
||||
/// </summary>
|
||||
public bool CheckCertificateRevocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
|
||||
/// </summary>
|
||||
public TimeSpan HandshakeTimeout
|
||||
{
|
||||
get => _handshakeTimeout;
|
||||
set
|
||||
{
|
||||
if (value <= TimeSpan.Zero && value != Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.PositiveTimeSpanRequired);
|
||||
}
|
||||
_handshakeTimeout = value != Timeout.InfiniteTimeSpan ? value : TimeSpan.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
internal class AddressBindContext
|
||||
{
|
||||
public ICollection<string> Addresses { get; set; }
|
||||
public List<ListenOptions> ListenOptions { get; set; }
|
||||
public KestrelServerOptions ServerOptions { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public Func<ListenOptions, Task> CreateBinding { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
internal class AddressBinder
|
||||
{
|
||||
public static async Task BindAsync(IServerAddressesFeature addresses,
|
||||
KestrelServerOptions serverOptions,
|
||||
ILogger logger,
|
||||
Func<ListenOptions, Task> createBinding)
|
||||
{
|
||||
var listenOptions = serverOptions.ListenOptions;
|
||||
var strategy = CreateStrategy(
|
||||
listenOptions.ToArray(),
|
||||
addresses.Addresses.ToArray(),
|
||||
addresses.PreferHostingUrls);
|
||||
|
||||
var context = new AddressBindContext
|
||||
{
|
||||
Addresses = addresses.Addresses,
|
||||
ListenOptions = listenOptions,
|
||||
ServerOptions = serverOptions,
|
||||
Logger = logger,
|
||||
CreateBinding = createBinding
|
||||
};
|
||||
|
||||
// reset options. The actual used options and addresses will be populated
|
||||
// by the address binding feature
|
||||
listenOptions.Clear();
|
||||
addresses.Addresses.Clear();
|
||||
|
||||
await strategy.BindAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static IStrategy CreateStrategy(ListenOptions[] listenOptions, string[] addresses, bool preferAddresses)
|
||||
{
|
||||
var hasListenOptions = listenOptions.Length > 0;
|
||||
var hasAddresses = addresses.Length > 0;
|
||||
|
||||
if (preferAddresses && hasAddresses)
|
||||
{
|
||||
if (hasListenOptions)
|
||||
{
|
||||
return new OverrideWithAddressesStrategy(addresses);
|
||||
}
|
||||
|
||||
return new AddressesStrategy(addresses);
|
||||
}
|
||||
else if (hasListenOptions)
|
||||
{
|
||||
if (hasAddresses)
|
||||
{
|
||||
return new OverrideWithEndpointsStrategy(listenOptions, addresses);
|
||||
}
|
||||
|
||||
return new EndpointsStrategy(listenOptions);
|
||||
}
|
||||
else if (hasAddresses)
|
||||
{
|
||||
// If no endpoints are configured directly using KestrelServerOptions, use those configured via the IServerAddressesFeature.
|
||||
return new AddressesStrategy(addresses);
|
||||
}
|
||||
else
|
||||
{
|
||||
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
|
||||
return new DefaultAddressStrategy();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an <see cref="IPEndPoint"/> for the given host an port.
|
||||
/// If the host parameter isn't "localhost" or an IP address, use IPAddress.Any.
|
||||
/// </summary>
|
||||
protected internal static bool TryCreateIPEndPoint(ServerAddress address, out IPEndPoint endpoint)
|
||||
{
|
||||
if (!IPAddress.TryParse(address.Host, out var ip))
|
||||
{
|
||||
endpoint = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
endpoint = new IPEndPoint(ip, address.Port);
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static async Task BindEndpointAsync(ListenOptions endpoint, AddressBindContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await context.CreateBinding(endpoint).ConfigureAwait(false);
|
||||
}
|
||||
catch (AddressInUseException ex)
|
||||
{
|
||||
throw new IOException(CoreStrings.FormatEndpointAlreadyInUse(endpoint), ex);
|
||||
}
|
||||
|
||||
context.ListenOptions.Add(endpoint);
|
||||
}
|
||||
|
||||
internal static ListenOptions ParseAddress(string address, out bool https)
|
||||
{
|
||||
var parsedAddress = ServerAddress.FromUrl(address);
|
||||
https = false;
|
||||
|
||||
if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
https = true;
|
||||
}
|
||||
else if (!parsedAddress.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatUnsupportedAddressScheme(address));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(parsedAddress.PathBase))
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatConfigurePathBaseFromMethodCall($"{nameof(IApplicationBuilder)}.UsePathBase()"));
|
||||
}
|
||||
|
||||
ListenOptions options = null;
|
||||
if (parsedAddress.IsUnixPipe)
|
||||
{
|
||||
options = new ListenOptions(parsedAddress.UnixPipePath);
|
||||
}
|
||||
else if (string.Equals(parsedAddress.Host, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// "localhost" for both IPv4 and IPv6 can't be represented as an IPEndPoint.
|
||||
options = new LocalhostListenOptions(parsedAddress.Port);
|
||||
}
|
||||
else if (TryCreateIPEndPoint(parsedAddress, out var endpoint))
|
||||
{
|
||||
options = new ListenOptions(endpoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
// when address is 'http://hostname:port', 'http://*:port', or 'http://+:port'
|
||||
options = new AnyIPListenOptions(parsedAddress.Port);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private interface IStrategy
|
||||
{
|
||||
Task BindAsync(AddressBindContext context);
|
||||
}
|
||||
|
||||
private class DefaultAddressStrategy : IStrategy
|
||||
{
|
||||
public async Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
var httpDefault = ParseAddress(Constants.DefaultServerAddress, out var https);
|
||||
context.ServerOptions.ApplyEndpointDefaults(httpDefault);
|
||||
await httpDefault.BindAsync(context).ConfigureAwait(false);
|
||||
|
||||
// Conditional https default, only if a cert is available
|
||||
var httpsDefault = ParseAddress(Constants.DefaultServerHttpsAddress, out https);
|
||||
context.ServerOptions.ApplyEndpointDefaults(httpsDefault);
|
||||
|
||||
if (httpsDefault.ConnectionAdapters.Any(f => f.IsHttps)
|
||||
|| httpsDefault.TryUseHttps())
|
||||
{
|
||||
await httpsDefault.BindAsync(context).ConfigureAwait(false);
|
||||
context.Logger.LogDebug(CoreStrings.BindingToDefaultAddresses,
|
||||
Constants.DefaultServerAddress, Constants.DefaultServerHttpsAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
// No default cert is available, do not bind to the https endpoint.
|
||||
context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OverrideWithAddressesStrategy : AddressesStrategy
|
||||
{
|
||||
public OverrideWithAddressesStrategy(IReadOnlyCollection<string> addresses)
|
||||
: base(addresses)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
var joined = string.Join(", ", _addresses);
|
||||
context.Logger.LogInformation(CoreStrings.OverridingWithPreferHostingUrls, nameof(IServerAddressesFeature.PreferHostingUrls), joined);
|
||||
|
||||
return base.BindAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private class OverrideWithEndpointsStrategy : EndpointsStrategy
|
||||
{
|
||||
private readonly string[] _originalAddresses;
|
||||
|
||||
public OverrideWithEndpointsStrategy(IReadOnlyCollection<ListenOptions> endpoints, string[] originalAddresses)
|
||||
: base(endpoints)
|
||||
{
|
||||
_originalAddresses = originalAddresses;
|
||||
}
|
||||
|
||||
public override Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
var joined = string.Join(", ", _originalAddresses);
|
||||
context.Logger.LogWarning(CoreStrings.OverridingWithKestrelOptions, joined, "UseKestrel()");
|
||||
|
||||
return base.BindAsync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private class EndpointsStrategy : IStrategy
|
||||
{
|
||||
private readonly IReadOnlyCollection<ListenOptions> _endpoints;
|
||||
|
||||
public EndpointsStrategy(IReadOnlyCollection<ListenOptions> endpoints)
|
||||
{
|
||||
_endpoints = endpoints;
|
||||
}
|
||||
|
||||
public virtual async Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
foreach (var endpoint in _endpoints)
|
||||
{
|
||||
await endpoint.BindAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class AddressesStrategy : IStrategy
|
||||
{
|
||||
protected readonly IReadOnlyCollection<string> _addresses;
|
||||
|
||||
public AddressesStrategy(IReadOnlyCollection<string> addresses)
|
||||
{
|
||||
_addresses = addresses;
|
||||
}
|
||||
|
||||
public virtual async Task BindAsync(AddressBindContext context)
|
||||
{
|
||||
foreach (var address in _addresses)
|
||||
{
|
||||
var options = ParseAddress(address, out var https);
|
||||
context.ServerOptions.ApplyEndpointDefaults(options);
|
||||
|
||||
if (https && !options.ConnectionAdapters.Any(f => f.IsHttps))
|
||||
{
|
||||
options.UseHttps();
|
||||
}
|
||||
|
||||
await options.BindAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace System.Buffers
|
||||
{
|
||||
internal ref struct BufferReader
|
||||
{
|
||||
private ReadOnlySpan<byte> _currentSpan;
|
||||
private int _index;
|
||||
|
||||
private ReadOnlySequence<byte> _sequence;
|
||||
private SequencePosition _currentSequencePosition;
|
||||
private SequencePosition _nextSequencePosition;
|
||||
|
||||
private int _consumedBytes;
|
||||
private bool _end;
|
||||
|
||||
public BufferReader(ReadOnlySequence<byte> buffer)
|
||||
{
|
||||
_end = false;
|
||||
_index = 0;
|
||||
_consumedBytes = 0;
|
||||
_sequence = buffer;
|
||||
_currentSequencePosition = _sequence.Start;
|
||||
_nextSequencePosition = _currentSequencePosition;
|
||||
_currentSpan = ReadOnlySpan<byte>.Empty;
|
||||
MoveNext();
|
||||
}
|
||||
|
||||
public bool End => _end;
|
||||
|
||||
public int CurrentSegmentIndex => _index;
|
||||
|
||||
public SequencePosition Position => _sequence.GetPosition(_index, _currentSequencePosition);
|
||||
|
||||
public ReadOnlySpan<byte> CurrentSegment => _currentSpan;
|
||||
|
||||
public ReadOnlySpan<byte> UnreadSegment => _currentSpan.Slice(_index);
|
||||
|
||||
public int ConsumedBytes => _consumedBytes;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Peek()
|
||||
{
|
||||
if (_end)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return _currentSpan[_index];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int Read()
|
||||
{
|
||||
if (_end)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var value = _currentSpan[_index];
|
||||
_index++;
|
||||
_consumedBytes++;
|
||||
|
||||
if (_index >= _currentSpan.Length)
|
||||
{
|
||||
MoveNext();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void MoveNext()
|
||||
{
|
||||
var previous = _nextSequencePosition;
|
||||
while (_sequence.TryGet(ref _nextSequencePosition, out var memory, true))
|
||||
{
|
||||
_currentSequencePosition = previous;
|
||||
_currentSpan = memory.Span;
|
||||
_index = 0;
|
||||
if (_currentSpan.Length > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
_end = true;
|
||||
}
|
||||
|
||||
public void Advance(int byteCount)
|
||||
{
|
||||
if (byteCount < 0)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
|
||||
}
|
||||
|
||||
_consumedBytes += byteCount;
|
||||
|
||||
while (!_end && byteCount > 0)
|
||||
{
|
||||
if ((_index + byteCount) < _currentSpan.Length)
|
||||
{
|
||||
_index += byteCount;
|
||||
byteCount = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
var remaining = (_currentSpan.Length - _index);
|
||||
|
||||
_index += remaining;
|
||||
byteCount -= remaining;
|
||||
|
||||
MoveNext();
|
||||
}
|
||||
|
||||
if (byteCount > 0)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
|
||||
namespace System.Buffers
|
||||
{
|
||||
internal ref struct BufferWriter<T> where T : IBufferWriter<byte>
|
||||
{
|
||||
private T _output;
|
||||
private Span<byte> _span;
|
||||
private int _buffered;
|
||||
private long _bytesCommitted;
|
||||
|
||||
public BufferWriter(T output)
|
||||
{
|
||||
_buffered = 0;
|
||||
_bytesCommitted = 0;
|
||||
_output = output;
|
||||
_span = output.GetSpan();
|
||||
}
|
||||
|
||||
public Span<byte> Span => _span;
|
||||
public long BytesCommitted => _bytesCommitted;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Commit()
|
||||
{
|
||||
var buffered = _buffered;
|
||||
if (buffered > 0)
|
||||
{
|
||||
_bytesCommitted += buffered;
|
||||
_buffered = 0;
|
||||
_output.Advance(buffered);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Advance(int count)
|
||||
{
|
||||
_buffered += count;
|
||||
_span = _span.Slice(count);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (_span.Length >= source.Length)
|
||||
{
|
||||
source.CopyTo(_span);
|
||||
Advance(source.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteMultiBuffer(source);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Ensure(int count = 1)
|
||||
{
|
||||
if (_span.Length < count)
|
||||
{
|
||||
EnsureMore(count);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void EnsureMore(int count = 0)
|
||||
{
|
||||
if (_buffered > 0)
|
||||
{
|
||||
Commit();
|
||||
}
|
||||
|
||||
_output.GetMemory(count);
|
||||
_span = _output.GetSpan();
|
||||
}
|
||||
|
||||
private void WriteMultiBuffer(ReadOnlySpan<byte> source)
|
||||
{
|
||||
while (source.Length > 0)
|
||||
{
|
||||
if (_span.Length == 0)
|
||||
{
|
||||
EnsureMore();
|
||||
}
|
||||
|
||||
var writable = Math.Min(source.Length, _span.Length);
|
||||
source.Slice(0, writable).CopyTo(_span);
|
||||
source = source.Slice(writable);
|
||||
Advance(writable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// 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.Linq;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
||||
{
|
||||
public static class CertificateLoader
|
||||
{
|
||||
// See http://oid-info.com/get/1.3.6.1.5.5.7.3.1
|
||||
// Indicates that a certificate can be used as a SSL server certificate
|
||||
private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1";
|
||||
|
||||
public static X509Certificate2 LoadFromStoreCert(string subject, string storeName, StoreLocation storeLocation, bool allowInvalid)
|
||||
{
|
||||
using (var store = new X509Store(storeName, storeLocation))
|
||||
{
|
||||
X509Certificate2Collection storeCertificates = null;
|
||||
X509Certificate2 foundCertificate = null;
|
||||
|
||||
try
|
||||
{
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
storeCertificates = store.Certificates;
|
||||
var foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectName, subject, !allowInvalid);
|
||||
foundCertificate = foundCertificates
|
||||
.OfType<X509Certificate2>()
|
||||
.Where(IsCertificateAllowedForServerAuth)
|
||||
.OrderByDescending(certificate => certificate.NotAfter)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (foundCertificate == null)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatCertNotFoundInStore(subject, storeLocation, storeName, allowInvalid));
|
||||
}
|
||||
|
||||
return foundCertificate;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeCertificates(storeCertificates, except: foundCertificate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsCertificateAllowedForServerAuth(X509Certificate2 certificate)
|
||||
{
|
||||
/* If the Extended Key Usage extension is included, then we check that the serverAuth usage is included. (http://oid-info.com/get/1.3.6.1.5.5.7.3.1)
|
||||
* If the Extended Key Usage extension is not included, then we assume the certificate is allowed for all usages.
|
||||
*
|
||||
* See also https://blogs.msdn.microsoft.com/kaushal/2012/02/17/client-certificates-vs-server-certificates/
|
||||
*
|
||||
* From https://tools.ietf.org/html/rfc3280#section-4.2.1.13 "Certificate Extensions: Extended Key Usage"
|
||||
*
|
||||
* If the (Extended Key Usage) extension is present, then the certificate MUST only be used
|
||||
* for one of the purposes indicated. If multiple purposes are
|
||||
* indicated the application need not recognize all purposes indicated,
|
||||
* as long as the intended purpose is present. Certificate using
|
||||
* applications MAY require that a particular purpose be indicated in
|
||||
* order for the certificate to be acceptable to that application.
|
||||
*/
|
||||
|
||||
var hasEkuExtension = false;
|
||||
|
||||
foreach (var extension in certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>())
|
||||
{
|
||||
hasEkuExtension = true;
|
||||
foreach (var oid in extension.EnhancedKeyUsages)
|
||||
{
|
||||
if (oid.Value.Equals(ServerAuthenticationOid, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !hasEkuExtension;
|
||||
}
|
||||
|
||||
private static void DisposeCertificates(X509Certificate2Collection certificates, X509Certificate2 except)
|
||||
{
|
||||
if (certificates != null)
|
||||
{
|
||||
foreach (var certificate in certificates)
|
||||
{
|
||||
if (!certificate.Equals(except))
|
||||
{
|
||||
certificate.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal
|
||||
{
|
||||
internal class ClosedStream : Stream
|
||||
{
|
||||
private static readonly Task<int> ZeroResultTask = Task.FromResult(result: 0);
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
set
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return ZeroResultTask;
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
// 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.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
internal class ConfigurationReader
|
||||
{
|
||||
private IConfiguration _configuration;
|
||||
private IDictionary<string, CertificateConfig> _certificates;
|
||||
private IList<EndpointConfig> _endpoints;
|
||||
|
||||
public ConfigurationReader(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
}
|
||||
|
||||
public IDictionary<string, CertificateConfig> Certificates
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_certificates == null)
|
||||
{
|
||||
ReadCertificates();
|
||||
}
|
||||
|
||||
return _certificates;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<EndpointConfig> Endpoints
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_endpoints == null)
|
||||
{
|
||||
ReadEndpoints();
|
||||
}
|
||||
|
||||
return _endpoints;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadCertificates()
|
||||
{
|
||||
_certificates = new Dictionary<string, CertificateConfig>(0);
|
||||
|
||||
var certificatesConfig = _configuration.GetSection("Certificates").GetChildren();
|
||||
foreach (var certificateConfig in certificatesConfig)
|
||||
{
|
||||
_certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadEndpoints()
|
||||
{
|
||||
_endpoints = new List<EndpointConfig>();
|
||||
|
||||
var endpointsConfig = _configuration.GetSection("Endpoints").GetChildren();
|
||||
foreach (var endpointConfig in endpointsConfig)
|
||||
{
|
||||
// "EndpointName": {
|
||||
// "Url": "https://*:5463",
|
||||
// "Certificate": {
|
||||
// "Path": "testCert.pfx",
|
||||
// "Password": "testPassword"
|
||||
// }
|
||||
// }
|
||||
|
||||
var url = endpointConfig["Url"];
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatEndpointMissingUrl(endpointConfig.Key));
|
||||
}
|
||||
|
||||
var endpoint = new EndpointConfig()
|
||||
{
|
||||
Name = endpointConfig.Key,
|
||||
Url = url,
|
||||
ConfigSection = endpointConfig,
|
||||
Certificate = new CertificateConfig(endpointConfig.GetSection("Certificate")),
|
||||
};
|
||||
_endpoints.Add(endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "EndpointName": {
|
||||
// "Url": "https://*:5463",
|
||||
// "Certificate": {
|
||||
// "Path": "testCert.pfx",
|
||||
// "Password": "testPassword"
|
||||
// }
|
||||
// }
|
||||
internal class EndpointConfig
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
public IConfigurationSection ConfigSection { get; set; }
|
||||
public CertificateConfig Certificate { get; set; }
|
||||
}
|
||||
|
||||
// "CertificateName": {
|
||||
// "Path": "testCert.pfx",
|
||||
// "Password": "testPassword"
|
||||
// }
|
||||
internal class CertificateConfig
|
||||
{
|
||||
public CertificateConfig(IConfigurationSection configSection)
|
||||
{
|
||||
ConfigSection = configSection;
|
||||
ConfigSection.Bind(this);
|
||||
}
|
||||
|
||||
public IConfigurationSection ConfigSection { get; }
|
||||
|
||||
// File
|
||||
public bool IsFileCert => !string.IsNullOrEmpty(Path);
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
|
||||
// Cert store
|
||||
|
||||
public bool IsStoreCert => !string.IsNullOrEmpty(Subject);
|
||||
|
||||
public string Subject { get; set; }
|
||||
|
||||
public string Store { get; set; }
|
||||
|
||||
public string Location { get; set; }
|
||||
|
||||
public bool? AllowInvalid { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
public class ConnectionDispatcher : IConnectionDispatcher
|
||||
{
|
||||
private readonly ServiceContext _serviceContext;
|
||||
private readonly ConnectionDelegate _connectionDelegate;
|
||||
|
||||
public ConnectionDispatcher(ServiceContext serviceContext, ConnectionDelegate connectionDelegate)
|
||||
{
|
||||
_serviceContext = serviceContext;
|
||||
_connectionDelegate = connectionDelegate;
|
||||
}
|
||||
|
||||
private IKestrelTrace Log => _serviceContext.Log;
|
||||
|
||||
public void OnConnection(TransportConnection connection)
|
||||
{
|
||||
// REVIEW: Unfortunately, we still need to use the service context to create the pipes since the settings
|
||||
// for the scheduler and limits are specified here
|
||||
var inputOptions = GetInputPipeOptions(_serviceContext, connection.MemoryPool, connection.InputWriterScheduler);
|
||||
var outputOptions = GetOutputPipeOptions(_serviceContext, connection.MemoryPool, connection.OutputReaderScheduler);
|
||||
|
||||
var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions);
|
||||
|
||||
// Set the transport and connection id
|
||||
connection.ConnectionId = CorrelationIdGenerator.GetNextId();
|
||||
connection.Transport = pair.Transport;
|
||||
|
||||
// This *must* be set before returning from OnConnection
|
||||
connection.Application = pair.Application;
|
||||
|
||||
// REVIEW: This task should be tracked by the server for graceful shutdown
|
||||
// Today it's handled specifically for http but not for aribitrary middleware
|
||||
_ = Execute(connection);
|
||||
}
|
||||
|
||||
private async Task Execute(ConnectionContext connectionContext)
|
||||
{
|
||||
using (BeginConnectionScope(connectionContext))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _connectionDelegate(connectionContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.LogCritical(0, ex, $"{nameof(ConnectionDispatcher)}.{nameof(Execute)}() {connectionContext.ConnectionId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IDisposable BeginConnectionScope(ConnectionContext connectionContext)
|
||||
{
|
||||
if (Log.IsEnabled(LogLevel.Critical))
|
||||
{
|
||||
return Log.BeginScope(new ConnectionLogScope(connectionContext.ConnectionId));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal static PipeOptions GetInputPipeOptions(ServiceContext serviceContext, MemoryPool<byte> memoryPool, PipeScheduler writerScheduler) => new PipeOptions
|
||||
(
|
||||
pool: memoryPool,
|
||||
readerScheduler: serviceContext.Scheduler,
|
||||
writerScheduler: writerScheduler,
|
||||
pauseWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0,
|
||||
resumeWriterThreshold: serviceContext.ServerOptions.Limits.MaxRequestBufferSize ?? 0,
|
||||
useSynchronizationContext: false,
|
||||
minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize
|
||||
);
|
||||
|
||||
internal static PipeOptions GetOutputPipeOptions(ServiceContext serviceContext, MemoryPool<byte> memoryPool, PipeScheduler readerScheduler) => new PipeOptions
|
||||
(
|
||||
pool: memoryPool,
|
||||
readerScheduler: readerScheduler,
|
||||
writerScheduler: serviceContext.Scheduler,
|
||||
pauseWriterThreshold: GetOutputResponseBufferSize(serviceContext),
|
||||
resumeWriterThreshold: GetOutputResponseBufferSize(serviceContext),
|
||||
useSynchronizationContext: false,
|
||||
minimumSegmentSize: KestrelMemoryPool.MinimumSegmentSize
|
||||
);
|
||||
|
||||
private static long GetOutputResponseBufferSize(ServiceContext serviceContext)
|
||||
{
|
||||
var bufferSize = serviceContext.ServerOptions.Limits.MaxResponseBufferSize;
|
||||
if (bufferSize == 0)
|
||||
{
|
||||
// 0 = no buffering so we need to configure the pipe so the the writer waits on the reader directly
|
||||
return 1;
|
||||
}
|
||||
|
||||
// null means that we have no back pressure
|
||||
return bufferSize ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// 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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
public class ConnectionLimitMiddleware
|
||||
{
|
||||
private readonly ConnectionDelegate _next;
|
||||
private readonly ResourceCounter _concurrentConnectionCounter;
|
||||
private readonly IKestrelTrace _trace;
|
||||
|
||||
public ConnectionLimitMiddleware(ConnectionDelegate next, long connectionLimit, IKestrelTrace trace)
|
||||
: this(next, ResourceCounter.Quota(connectionLimit), trace)
|
||||
{
|
||||
}
|
||||
|
||||
// For Testing
|
||||
internal ConnectionLimitMiddleware(ConnectionDelegate next, ResourceCounter concurrentConnectionCounter, IKestrelTrace trace)
|
||||
{
|
||||
_next = next;
|
||||
_concurrentConnectionCounter = concurrentConnectionCounter;
|
||||
_trace = trace;
|
||||
}
|
||||
|
||||
public async Task OnConnectionAsync(ConnectionContext connection)
|
||||
{
|
||||
if (!_concurrentConnectionCounter.TryLockOne())
|
||||
{
|
||||
KestrelEventSource.Log.ConnectionRejected(connection.ConnectionId);
|
||||
_trace.ConnectionRejected(connection.ConnectionId);
|
||||
connection.Transport.Input.Complete();
|
||||
connection.Transport.Output.Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
var releasor = new ConnectionReleasor(_concurrentConnectionCounter);
|
||||
|
||||
try
|
||||
{
|
||||
connection.Features.Set<IDecrementConcurrentConnectionCountFeature>(releasor);
|
||||
await _next(connection);
|
||||
}
|
||||
finally
|
||||
{
|
||||
releasor.ReleaseConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private class ConnectionReleasor : IDecrementConcurrentConnectionCountFeature
|
||||
{
|
||||
private readonly ResourceCounter _concurrentConnectionCounter;
|
||||
private bool _connectionReleased;
|
||||
|
||||
public ConnectionReleasor(ResourceCounter normalConnectionCounter)
|
||||
{
|
||||
_concurrentConnectionCounter = normalConnectionCounter;
|
||||
}
|
||||
|
||||
public void ReleaseConnection()
|
||||
{
|
||||
if (!_connectionReleased)
|
||||
{
|
||||
_connectionReleased = true;
|
||||
_concurrentConnectionCounter.ReleaseOne();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
|
||||
{
|
||||
public class ConnectionLogScope : IReadOnlyList<KeyValuePair<string, object>>
|
||||
{
|
||||
private readonly string _connectionId;
|
||||
|
||||
private string _cachedToString;
|
||||
|
||||
public ConnectionLogScope(string connectionId)
|
||||
{
|
||||
_connectionId = connectionId;
|
||||
}
|
||||
|
||||
public KeyValuePair<string, object> this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index == 0)
|
||||
{
|
||||
return new KeyValuePair<string, object>("ConnectionId", _connectionId);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => 1;
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < Count; ++i)
|
||||
{
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (_cachedToString == null)
|
||||
{
|
||||
_cachedToString = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"ConnectionId:{0}",
|
||||
_connectionId);
|
||||
}
|
||||
|
||||
return _cachedToString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System.Buffers;
|
||||
|
||||
namespace System.IO.Pipelines
|
||||
{
|
||||
internal class DuplexPipe : IDuplexPipe
|
||||
{
|
||||
public DuplexPipe(PipeReader reader, PipeWriter writer)
|
||||
{
|
||||
Input = reader;
|
||||
Output = writer;
|
||||
}
|
||||
|
||||
public PipeReader Input { get; }
|
||||
|
||||
public PipeWriter Output { get; }
|
||||
|
||||
public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions)
|
||||
{
|
||||
var input = new Pipe(inputOptions);
|
||||
var output = new Pipe(outputOptions);
|
||||
|
||||
var transportToApplication = new DuplexPipe(output.Reader, input.Writer);
|
||||
var applicationToTransport = new DuplexPipe(input.Reader, output.Writer);
|
||||
|
||||
return new DuplexPipePair(applicationToTransport, transportToApplication);
|
||||
}
|
||||
|
||||
// This class exists to work around issues with value tuple on .NET Framework
|
||||
public readonly struct DuplexPipePair
|
||||
{
|
||||
public IDuplexPipe Transport { get; }
|
||||
public IDuplexPipe Application { get; }
|
||||
|
||||
public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application)
|
||||
{
|
||||
Transport = transport;
|
||||
Application = application;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
internal static class ChunkWriter
|
||||
{
|
||||
private static readonly ArraySegment<byte> _endChunkBytes = CreateAsciiByteArraySegment("\r\n");
|
||||
private static readonly byte[] _hex = Encoding.ASCII.GetBytes("0123456789abcdef");
|
||||
|
||||
private static ArraySegment<byte> CreateAsciiByteArraySegment(string text)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(text);
|
||||
return new ArraySegment<byte>(bytes);
|
||||
}
|
||||
|
||||
public static ArraySegment<byte> BeginChunkBytes(int dataCount)
|
||||
{
|
||||
var bytes = new byte[10]
|
||||
{
|
||||
_hex[((dataCount >> 0x1c) & 0x0f)],
|
||||
_hex[((dataCount >> 0x18) & 0x0f)],
|
||||
_hex[((dataCount >> 0x14) & 0x0f)],
|
||||
_hex[((dataCount >> 0x10) & 0x0f)],
|
||||
_hex[((dataCount >> 0x0c) & 0x0f)],
|
||||
_hex[((dataCount >> 0x08) & 0x0f)],
|
||||
_hex[((dataCount >> 0x04) & 0x0f)],
|
||||
_hex[((dataCount >> 0x00) & 0x0f)],
|
||||
(byte)'\r',
|
||||
(byte)'\n',
|
||||
};
|
||||
|
||||
// Determine the most-significant non-zero nibble
|
||||
int total, shift;
|
||||
total = (dataCount > 0xffff) ? 0x10 : 0x00;
|
||||
dataCount >>= total;
|
||||
shift = (dataCount > 0x00ff) ? 0x08 : 0x00;
|
||||
dataCount >>= shift;
|
||||
total |= shift;
|
||||
total |= (dataCount > 0x000f) ? 0x04 : 0x00;
|
||||
|
||||
var offset = 7 - (total >> 2);
|
||||
return new ArraySegment<byte>(bytes, offset, 10 - offset);
|
||||
}
|
||||
|
||||
internal static int WriteBeginChunkBytes(ref BufferWriter<PipeWriter> start, int dataCount)
|
||||
{
|
||||
var chunkSegment = BeginChunkBytes(dataCount);
|
||||
start.Write(new ReadOnlySpan<byte>(chunkSegment.Array, chunkSegment.Offset, chunkSegment.Count));
|
||||
return chunkSegment.Count;
|
||||
}
|
||||
|
||||
internal static void WriteEndChunkBytes(ref BufferWriter<PipeWriter> start)
|
||||
{
|
||||
start.Write(new ReadOnlySpan<byte>(_endChunkBytes.Array, _endChunkBytes.Offset, _endChunkBytes.Count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
[Flags]
|
||||
public enum ConnectionOptions
|
||||
{
|
||||
None = 0,
|
||||
Close = 1,
|
||||
KeepAlive = 2,
|
||||
Upgrade = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// 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.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the generation of the date header value.
|
||||
/// </summary>
|
||||
public class DateHeaderValueManager : IHeartbeatHandler
|
||||
{
|
||||
private static readonly byte[] _datePreambleBytes = Encoding.ASCII.GetBytes("\r\nDate: ");
|
||||
|
||||
private DateHeaderValues _dateValues;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DateHeaderValueManager"/> class.
|
||||
/// </summary>
|
||||
public DateHeaderValueManager()
|
||||
: this(systemClock: new SystemClock())
|
||||
{
|
||||
}
|
||||
|
||||
// Internal for testing
|
||||
internal DateHeaderValueManager(ISystemClock systemClock)
|
||||
{
|
||||
SetDateValues(systemClock.UtcNow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value representing the current server date/time for use in the HTTP "Date" response header
|
||||
/// in accordance with http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
|
||||
/// </summary>
|
||||
/// <returns>The value in string and byte[] format.</returns>
|
||||
public DateHeaderValues GetDateHeaderValues() => _dateValues;
|
||||
|
||||
// Called by the Timer (background) thread
|
||||
public void OnHeartbeat(DateTimeOffset now)
|
||||
{
|
||||
SetDateValues(now);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets date values from a provided ticks value
|
||||
/// </summary>
|
||||
/// <param name="value">A DateTimeOffset value</param>
|
||||
private void SetDateValues(DateTimeOffset value)
|
||||
{
|
||||
var dateValue = HeaderUtilities.FormatDate(value);
|
||||
var dateBytes = new byte[_datePreambleBytes.Length + dateValue.Length];
|
||||
Buffer.BlockCopy(_datePreambleBytes, 0, dateBytes, 0, _datePreambleBytes.Length);
|
||||
Encoding.ASCII.GetBytes(dateValue, 0, dateValue.Length, dateBytes, _datePreambleBytes.Length);
|
||||
|
||||
var dateValues = new DateHeaderValues()
|
||||
{
|
||||
Bytes = dateBytes,
|
||||
String = dateValue
|
||||
};
|
||||
Volatile.Write(ref _dateValues, dateValues);
|
||||
}
|
||||
|
||||
public class DateHeaderValues
|
||||
{
|
||||
public byte[] Bytes;
|
||||
public string String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class Http1Connection : IHttpUpgradeFeature
|
||||
{
|
||||
bool IHttpUpgradeFeature.IsUpgradableRequest => IsUpgradableRequest;
|
||||
|
||||
async Task<Stream> IHttpUpgradeFeature.UpgradeAsync()
|
||||
{
|
||||
if (!((IHttpUpgradeFeature)this).IsUpgradableRequest)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.CannotUpgradeNonUpgradableRequest);
|
||||
}
|
||||
|
||||
if (IsUpgraded)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.UpgradeCannotBeCalledMultipleTimes);
|
||||
}
|
||||
|
||||
if (!ServiceContext.ConnectionManager.UpgradedConnectionCount.TryLockOne())
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.UpgradedConnectionLimitReached);
|
||||
}
|
||||
|
||||
IsUpgraded = true;
|
||||
|
||||
ConnectionFeatures.Get<IDecrementConcurrentConnectionCountFeature>()?.ReleaseConnection();
|
||||
|
||||
StatusCode = StatusCodes.Status101SwitchingProtocols;
|
||||
ReasonPhrase = "Switching Protocols";
|
||||
ResponseHeaders["Connection"] = "Upgrade";
|
||||
|
||||
await FlushAsync(default(CancellationToken));
|
||||
|
||||
return _streams.Upgrade();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,527 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO.Pipelines;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections.Abstractions;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class Http1Connection : HttpProtocol, IRequestProcessor
|
||||
{
|
||||
private const byte ByteAsterisk = (byte)'*';
|
||||
private const byte ByteForwardSlash = (byte)'/';
|
||||
private const string Asterisk = "*";
|
||||
|
||||
private readonly Http1ConnectionContext _context;
|
||||
private readonly IHttpParser<Http1ParsingHandler> _parser;
|
||||
protected readonly long _keepAliveTicks;
|
||||
private readonly long _requestHeadersTimeoutTicks;
|
||||
|
||||
private volatile bool _requestTimedOut;
|
||||
private uint _requestCount;
|
||||
|
||||
private HttpRequestTarget _requestTargetForm = HttpRequestTarget.Unknown;
|
||||
private Uri _absoluteRequestTarget;
|
||||
|
||||
private int _remainingRequestHeadersBytesAllowed;
|
||||
|
||||
public Http1Connection(Http1ConnectionContext context)
|
||||
: base(context)
|
||||
{
|
||||
_context = context;
|
||||
_parser = ServiceContext.HttpParser;
|
||||
_keepAliveTicks = ServerOptions.Limits.KeepAliveTimeout.Ticks;
|
||||
_requestHeadersTimeoutTicks = ServerOptions.Limits.RequestHeadersTimeout.Ticks;
|
||||
|
||||
Output = new Http1OutputProducer(
|
||||
_context.Transport.Output,
|
||||
_context.ConnectionId,
|
||||
_context.ConnectionContext,
|
||||
_context.ServiceContext.Log,
|
||||
_context.TimeoutControl,
|
||||
_context.ConnectionFeatures.Get<IBytesWrittenFeature>());
|
||||
}
|
||||
|
||||
public PipeReader Input => _context.Transport.Input;
|
||||
|
||||
public ITimeoutControl TimeoutControl => _context.TimeoutControl;
|
||||
public bool RequestTimedOut => _requestTimedOut;
|
||||
|
||||
public override bool IsUpgradableRequest => _upgradeAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Stops the request processing loop between requests.
|
||||
/// Called on all active connections when the server wants to initiate a shutdown
|
||||
/// and after a keep-alive timeout.
|
||||
/// </summary>
|
||||
public void StopProcessingNextRequest()
|
||||
{
|
||||
_keepAlive = false;
|
||||
Input.CancelPendingRead();
|
||||
}
|
||||
|
||||
public void SendTimeoutResponse()
|
||||
{
|
||||
_requestTimedOut = true;
|
||||
Input.CancelPendingRead();
|
||||
}
|
||||
|
||||
public void ParseRequest(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
||||
switch (_requestProcessingStatus)
|
||||
{
|
||||
case RequestProcessingStatus.RequestPending:
|
||||
if (buffer.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TimeoutControl.ResetTimeout(_requestHeadersTimeoutTicks, TimeoutAction.SendTimeoutResponse);
|
||||
|
||||
_requestProcessingStatus = RequestProcessingStatus.ParsingRequestLine;
|
||||
goto case RequestProcessingStatus.ParsingRequestLine;
|
||||
case RequestProcessingStatus.ParsingRequestLine:
|
||||
if (TakeStartLine(buffer, out consumed, out examined))
|
||||
{
|
||||
buffer = buffer.Slice(consumed, buffer.End);
|
||||
|
||||
_requestProcessingStatus = RequestProcessingStatus.ParsingHeaders;
|
||||
goto case RequestProcessingStatus.ParsingHeaders;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
case RequestProcessingStatus.ParsingHeaders:
|
||||
if (TakeMessageHeaders(buffer, out consumed, out examined))
|
||||
{
|
||||
_requestProcessingStatus = RequestProcessingStatus.AppStarted;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TakeStartLine(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
var overLength = false;
|
||||
if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize)
|
||||
{
|
||||
buffer = buffer.Slice(buffer.Start, ServerOptions.Limits.MaxRequestLineSize);
|
||||
overLength = true;
|
||||
}
|
||||
|
||||
var result = _parser.ParseRequestLine(new Http1ParsingHandler(this), buffer, out consumed, out examined);
|
||||
if (!result && overLength)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.RequestLineTooLong);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TakeMessageHeaders(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
// Make sure the buffer is limited
|
||||
bool overLength = false;
|
||||
if (buffer.Length >= _remainingRequestHeadersBytesAllowed)
|
||||
{
|
||||
buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);
|
||||
|
||||
// If we sliced it means the current buffer bigger than what we're
|
||||
// allowed to look at
|
||||
overLength = true;
|
||||
}
|
||||
|
||||
var result = _parser.ParseHeaders(new Http1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes);
|
||||
_remainingRequestHeadersBytesAllowed -= consumedBytes;
|
||||
|
||||
if (!result && overLength)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
|
||||
}
|
||||
if (result)
|
||||
{
|
||||
TimeoutControl.CancelTimeout();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
{
|
||||
Debug.Assert(target.Length != 0, "Request target must be non-zero length");
|
||||
|
||||
var ch = target[0];
|
||||
if (ch == ByteForwardSlash)
|
||||
{
|
||||
// origin-form.
|
||||
// The most common form of request-target.
|
||||
// https://tools.ietf.org/html/rfc7230#section-5.3.1
|
||||
OnOriginFormTarget(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
else if (ch == ByteAsterisk && target.Length == 1)
|
||||
{
|
||||
OnAsteriskFormTarget(method);
|
||||
}
|
||||
else if (target.GetKnownHttpScheme(out var scheme))
|
||||
{
|
||||
OnAbsoluteFormTarget(target, query);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume anything else is considered authority form.
|
||||
// FYI: this should be an edge case. This should only happen when
|
||||
// a client mistakenly thinks this server is a proxy server.
|
||||
OnAuthorityFormTarget(method, target);
|
||||
}
|
||||
|
||||
Method = method;
|
||||
if (method == HttpMethod.Custom)
|
||||
{
|
||||
_methodText = customMethod.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
_httpVersion = version;
|
||||
|
||||
Debug.Assert(RawTarget != null, "RawTarget was not set");
|
||||
Debug.Assert(((IHttpRequestFeature)this).Method != null, "Method was not set");
|
||||
Debug.Assert(Path != null, "Path was not set");
|
||||
Debug.Assert(QueryString != null, "QueryString was not set");
|
||||
Debug.Assert(HttpVersion != null, "HttpVersion was not set");
|
||||
}
|
||||
|
||||
private void OnOriginFormTarget(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
{
|
||||
Debug.Assert(target[0] == ByteForwardSlash, "Should only be called when path starts with /");
|
||||
|
||||
_requestTargetForm = HttpRequestTarget.OriginForm;
|
||||
|
||||
// URIs are always encoded/escaped to ASCII https://tools.ietf.org/html/rfc3986#page-11
|
||||
// Multibyte Internationalized Resource Identifiers (IRIs) are first converted to utf8;
|
||||
// then encoded/escaped to ASCII https://www.ietf.org/rfc/rfc3987.txt "Mapping of IRIs to URIs"
|
||||
string requestUrlPath = null;
|
||||
string rawTarget = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Read raw target before mutating memory.
|
||||
rawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
if (pathEncoded)
|
||||
{
|
||||
// URI was encoded, unescape and then parse as UTF-8
|
||||
// Disabling warning temporary
|
||||
var pathLength = UrlDecoder.Decode(path, path);
|
||||
|
||||
// Removing dot segments must be done after unescaping. From RFC 3986:
|
||||
//
|
||||
// URI producing applications should percent-encode data octets that
|
||||
// correspond to characters in the reserved set unless these characters
|
||||
// are specifically allowed by the URI scheme to represent data in that
|
||||
// component. If a reserved character is found in a URI component and
|
||||
// no delimiting role is known for that character, then it must be
|
||||
// interpreted as representing the data octet corresponding to that
|
||||
// character's encoding in US-ASCII.
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc3986#section-2.2
|
||||
pathLength = PathNormalizer.RemoveDotSegments(path.Slice(0, pathLength));
|
||||
|
||||
requestUrlPath = GetUtf8String(path.Slice(0, pathLength));
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathLength = PathNormalizer.RemoveDotSegments(path);
|
||||
|
||||
if (path.Length == pathLength && query.Length == 0)
|
||||
{
|
||||
// If no decoding was required, no dot segments were removed and
|
||||
// there is no query, the request path is the same as the raw target
|
||||
requestUrlPath = rawTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
requestUrlPath = path.Slice(0, pathLength).GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
ThrowRequestTargetRejected(target);
|
||||
}
|
||||
|
||||
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||
RawTarget = rawTarget;
|
||||
Path = requestUrlPath;
|
||||
}
|
||||
|
||||
private void OnAuthorityFormTarget(HttpMethod method, Span<byte> target)
|
||||
{
|
||||
_requestTargetForm = HttpRequestTarget.AuthorityForm;
|
||||
|
||||
// This is not complete validation. It is just a quick scan for invalid characters
|
||||
// but doesn't check that the target fully matches the URI spec.
|
||||
for (var i = 0; i < target.Length; i++)
|
||||
{
|
||||
var ch = target[i];
|
||||
if (!UriUtilities.IsValidAuthorityCharacter(ch))
|
||||
{
|
||||
ThrowRequestTargetRejected(target);
|
||||
}
|
||||
}
|
||||
|
||||
// The authority-form of request-target is only used for CONNECT
|
||||
// requests (https://tools.ietf.org/html/rfc7231#section-4.3.6).
|
||||
if (method != HttpMethod.Connect)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.ConnectMethodRequired);
|
||||
}
|
||||
|
||||
// When making a CONNECT request to establish a tunnel through one or
|
||||
// more proxies, a client MUST send only the target URI's authority
|
||||
// component (excluding any userinfo and its "@" delimiter) as the
|
||||
// request-target.For example,
|
||||
//
|
||||
// CONNECT www.example.com:80 HTTP/1.1
|
||||
//
|
||||
// Allowed characters in the 'host + port' section of authority.
|
||||
// See https://tools.ietf.org/html/rfc3986#section-3.2
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
Path = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
}
|
||||
|
||||
private void OnAsteriskFormTarget(HttpMethod method)
|
||||
{
|
||||
_requestTargetForm = HttpRequestTarget.AsteriskForm;
|
||||
|
||||
// The asterisk-form of request-target is only used for a server-wide
|
||||
// OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7).
|
||||
if (method != HttpMethod.Options)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.OptionsMethodRequired);
|
||||
}
|
||||
|
||||
RawTarget = Asterisk;
|
||||
Path = string.Empty;
|
||||
QueryString = string.Empty;
|
||||
}
|
||||
|
||||
private void OnAbsoluteFormTarget(Span<byte> target, Span<byte> query)
|
||||
{
|
||||
_requestTargetForm = HttpRequestTarget.AbsoluteForm;
|
||||
|
||||
// absolute-form
|
||||
// https://tools.ietf.org/html/rfc7230#section-5.3.2
|
||||
|
||||
// This code should be the edge-case.
|
||||
|
||||
// From the spec:
|
||||
// a server MUST accept the absolute-form in requests, even though
|
||||
// HTTP/1.1 clients will only send them in requests to proxies.
|
||||
|
||||
RawTarget = target.GetAsciiStringNonNullCharacters();
|
||||
|
||||
// Validation of absolute URIs is slow, but clients
|
||||
// should not be sending this form anyways, so perf optimization
|
||||
// not high priority
|
||||
|
||||
if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
|
||||
{
|
||||
ThrowRequestTargetRejected(target);
|
||||
}
|
||||
|
||||
_absoluteRequestTarget = uri;
|
||||
Path = uri.LocalPath;
|
||||
// don't use uri.Query because we need the unescaped version
|
||||
QueryString = query.GetAsciiStringNonNullCharacters();
|
||||
}
|
||||
|
||||
private static unsafe string GetUtf8String(Span<byte> path)
|
||||
{
|
||||
// .NET 451 doesn't have pointer overloads for Encoding.GetString so we
|
||||
// copy to an array
|
||||
fixed (byte* pointer = &MemoryMarshal.GetReference(path))
|
||||
{
|
||||
return Encoding.UTF8.GetString(pointer, path.Length);
|
||||
}
|
||||
}
|
||||
|
||||
internal void EnsureHostHeaderExists()
|
||||
{
|
||||
// https://tools.ietf.org/html/rfc7230#section-5.4
|
||||
// A server MUST respond with a 400 (Bad Request) status code to any
|
||||
// HTTP/1.1 request message that lacks a Host header field and to any
|
||||
// request message that contains more than one Host header field or a
|
||||
// Host header field with an invalid field-value.
|
||||
|
||||
var hostCount = HttpRequestHeaders.HostCount;
|
||||
var hostText = HttpRequestHeaders.HeaderHost.ToString();
|
||||
if (hostCount <= 0)
|
||||
{
|
||||
if (_httpVersion == Http.HttpVersion.Http10)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MissingHostHeader);
|
||||
}
|
||||
else if (hostCount > 1)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MultipleHostHeaders);
|
||||
}
|
||||
else if (_requestTargetForm != HttpRequestTarget.OriginForm)
|
||||
{
|
||||
// Tail call
|
||||
ValidateNonOrginHostHeader(hostText);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Tail call
|
||||
HttpUtilities.ValidateHostHeader(hostText);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateNonOrginHostHeader(string hostText)
|
||||
{
|
||||
if (_requestTargetForm == HttpRequestTarget.AuthorityForm)
|
||||
{
|
||||
if (hostText != RawTarget)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||
}
|
||||
}
|
||||
else if (_requestTargetForm == HttpRequestTarget.AbsoluteForm)
|
||||
{
|
||||
// If the target URI includes an authority component, then a
|
||||
// client MUST send a field - value for Host that is identical to that
|
||||
// authority component, excluding any userinfo subcomponent and its "@"
|
||||
// delimiter.
|
||||
|
||||
// System.Uri doesn't not tell us if the port was in the original string or not.
|
||||
// When IsDefaultPort = true, we will allow Host: with or without the default port
|
||||
if (hostText != _absoluteRequestTarget.Authority)
|
||||
{
|
||||
if (!_absoluteRequestTarget.IsDefaultPort
|
||||
|| hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture))
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tail call
|
||||
HttpUtilities.ValidateHostHeader(hostText);
|
||||
}
|
||||
|
||||
protected override void OnReset()
|
||||
{
|
||||
ResetIHttpUpgradeFeature();
|
||||
|
||||
_requestTimedOut = false;
|
||||
_requestTargetForm = HttpRequestTarget.Unknown;
|
||||
_absoluteRequestTarget = null;
|
||||
_remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize + 2;
|
||||
_requestCount++;
|
||||
}
|
||||
|
||||
protected override void OnRequestProcessingEnding()
|
||||
{
|
||||
Input.Complete();
|
||||
}
|
||||
|
||||
protected override string CreateRequestId()
|
||||
=> StringUtilities.ConcatAsHexSuffix(ConnectionId, ':', _requestCount);
|
||||
|
||||
protected override MessageBody CreateMessageBody()
|
||||
=> Http1MessageBody.For(_httpVersion, HttpRequestHeaders, this);
|
||||
|
||||
protected override void BeginRequestProcessing()
|
||||
{
|
||||
// Reset the features and timeout.
|
||||
Reset();
|
||||
TimeoutControl.SetTimeout(_keepAliveTicks, TimeoutAction.StopProcessingNextRequest);
|
||||
}
|
||||
|
||||
protected override bool BeginRead(out ValueTask<ReadResult> awaitable)
|
||||
{
|
||||
awaitable = Input.ReadAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool TryParseRequest(ReadResult result, out bool endConnection)
|
||||
{
|
||||
var examined = result.Buffer.End;
|
||||
var consumed = result.Buffer.End;
|
||||
|
||||
try
|
||||
{
|
||||
ParseRequest(result.Buffer, out consumed, out examined);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
if (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Input.AdvanceTo(consumed, examined);
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
switch (_requestProcessingStatus)
|
||||
{
|
||||
case RequestProcessingStatus.RequestPending:
|
||||
endConnection = true;
|
||||
return true;
|
||||
case RequestProcessingStatus.ParsingRequestLine:
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
|
||||
break;
|
||||
case RequestProcessingStatus.ParsingHeaders:
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
|
||||
{
|
||||
// Stop the request processing loop if the server is shutting down or there was a keep-alive timeout
|
||||
// and there is no ongoing request.
|
||||
endConnection = true;
|
||||
return true;
|
||||
}
|
||||
else if (RequestTimedOut)
|
||||
{
|
||||
// In this case, there is an ongoing request but the start line/header parsing has timed out, so send
|
||||
// a 408 response.
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout);
|
||||
}
|
||||
|
||||
endConnection = false;
|
||||
if (_requestProcessingStatus == RequestProcessingStatus.AppStarted)
|
||||
{
|
||||
EnsureHostHeaderExists();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public class Http1ConnectionContext : IHttpProtocolContext
|
||||
{
|
||||
public string ConnectionId { get; set; }
|
||||
public ServiceContext ServiceContext { get; set; }
|
||||
public ConnectionContext ConnectionContext { get; set; }
|
||||
public IFeatureCollection ConnectionFeatures { get; set; }
|
||||
public MemoryPool<byte> MemoryPool { get; set; }
|
||||
public IPEndPoint RemoteEndPoint { get; set; }
|
||||
public IPEndPoint LocalEndPoint { get; set; }
|
||||
public ITimeoutControl TimeoutControl { get; set; }
|
||||
public IDuplexPipe Transport { get; set; }
|
||||
public IDuplexPipe Application { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,716 @@
|
|||
// 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.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public abstract class Http1MessageBody : MessageBody
|
||||
{
|
||||
private readonly Http1Connection _context;
|
||||
|
||||
private volatile bool _canceled;
|
||||
private Task _pumpTask;
|
||||
|
||||
protected Http1MessageBody(Http1Connection context)
|
||||
: base(context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private async Task PumpAsync()
|
||||
{
|
||||
Exception error = null;
|
||||
|
||||
try
|
||||
{
|
||||
var awaitable = _context.Input.ReadAsync();
|
||||
|
||||
if (!awaitable.IsCompleted)
|
||||
{
|
||||
TryProduceContinue();
|
||||
}
|
||||
|
||||
TryStartTimingReads();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await awaitable;
|
||||
|
||||
if (_context.RequestTimedOut)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
|
||||
}
|
||||
|
||||
var readableBuffer = result.Buffer;
|
||||
var consumed = readableBuffer.Start;
|
||||
var examined = readableBuffer.End;
|
||||
|
||||
try
|
||||
{
|
||||
if (_canceled)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
bool done;
|
||||
done = Read(readableBuffer, _context.RequestBodyPipe.Writer, out consumed, out examined);
|
||||
|
||||
var writeAwaitable = _context.RequestBodyPipe.Writer.FlushAsync();
|
||||
var backpressure = false;
|
||||
|
||||
if (!writeAwaitable.IsCompleted)
|
||||
{
|
||||
// Backpressure, stop controlling incoming data rate until data is read.
|
||||
backpressure = true;
|
||||
TryPauseTimingReads();
|
||||
}
|
||||
|
||||
await writeAwaitable;
|
||||
|
||||
if (backpressure)
|
||||
{
|
||||
TryResumeTimingReads();
|
||||
}
|
||||
|
||||
if (done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read() will have already have greedily consumed the entire request body if able.
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
// Treat any FIN from an upgraded request as expected.
|
||||
// It's up to higher-level consumer (i.e. WebSocket middleware) to determine
|
||||
// if the end is actually expected based on higher-level framing.
|
||||
if (RequestUpgrade)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context.Input.AdvanceTo(consumed, examined);
|
||||
}
|
||||
|
||||
awaitable = _context.Input.ReadAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context.RequestBodyPipe.Writer.Complete(error);
|
||||
TryStopTimingReads();
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync()
|
||||
{
|
||||
if (!_context.HasStartedConsumingRequestBody)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_canceled = true;
|
||||
_context.Input.CancelPendingRead();
|
||||
return _pumpTask;
|
||||
}
|
||||
|
||||
protected override Task OnConsumeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_context.RequestBodyPipe.Reader.TryRead(out var readResult))
|
||||
{
|
||||
_context.RequestBodyPipe.Reader.AdvanceTo(readResult.Buffer.End);
|
||||
|
||||
if (readResult.IsCompleted)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (BadHttpRequestException ex)
|
||||
{
|
||||
// At this point, the response has already been written, so this won't result in a 4XX response;
|
||||
// however, we still need to stop the request processing loop and log.
|
||||
_context.SetBadRequestState(ex);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return OnConsumeAsyncAwaited();
|
||||
}
|
||||
|
||||
private async Task OnConsumeAsyncAwaited()
|
||||
{
|
||||
Log.RequestBodyNotEntirelyRead(_context.ConnectionIdFeature, _context.TraceIdentifier);
|
||||
|
||||
_context.TimeoutControl.SetTimeout(Constants.RequestBodyDrainTimeout.Ticks, TimeoutAction.AbortConnection);
|
||||
|
||||
try
|
||||
{
|
||||
ReadResult result;
|
||||
do
|
||||
{
|
||||
result = await _context.RequestBodyPipe.Reader.ReadAsync();
|
||||
_context.RequestBodyPipe.Reader.AdvanceTo(result.Buffer.End);
|
||||
} while (!result.IsCompleted);
|
||||
}
|
||||
catch (BadHttpRequestException ex)
|
||||
{
|
||||
_context.SetBadRequestState(ex);
|
||||
}
|
||||
catch (ConnectionAbortedException)
|
||||
{
|
||||
Log.RequestBodyDrainTimedOut(_context.ConnectionIdFeature, _context.TraceIdentifier);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context.TimeoutControl.CancelTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
protected void Copy(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer)
|
||||
{
|
||||
_context.TimeoutControl.BytesRead(readableBuffer.Length);
|
||||
|
||||
if (readableBuffer.IsSingleSegment)
|
||||
{
|
||||
writableBuffer.Write(readableBuffer.First.Span);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var memory in readableBuffer)
|
||||
{
|
||||
writableBuffer.Write(memory.Span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnReadStarted()
|
||||
{
|
||||
_pumpTask = PumpAsync();
|
||||
}
|
||||
|
||||
protected virtual bool Read(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void TryStartTimingReads()
|
||||
{
|
||||
if (!RequestUpgrade)
|
||||
{
|
||||
Log.RequestBodyStart(_context.ConnectionIdFeature, _context.TraceIdentifier);
|
||||
_context.TimeoutControl.StartTimingReads();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryPauseTimingReads()
|
||||
{
|
||||
if (!RequestUpgrade)
|
||||
{
|
||||
_context.TimeoutControl.PauseTimingReads();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryResumeTimingReads()
|
||||
{
|
||||
if (!RequestUpgrade)
|
||||
{
|
||||
_context.TimeoutControl.ResumeTimingReads();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryStopTimingReads()
|
||||
{
|
||||
if (!RequestUpgrade)
|
||||
{
|
||||
Log.RequestBodyDone(_context.ConnectionIdFeature, _context.TraceIdentifier);
|
||||
_context.TimeoutControl.StopTimingReads();
|
||||
}
|
||||
}
|
||||
|
||||
public static MessageBody For(
|
||||
HttpVersion httpVersion,
|
||||
HttpRequestHeaders headers,
|
||||
Http1Connection context)
|
||||
{
|
||||
// see also http://tools.ietf.org/html/rfc2616#section-4.4
|
||||
var keepAlive = httpVersion != HttpVersion.Http10;
|
||||
|
||||
var upgrade = false;
|
||||
if (headers.HasConnection)
|
||||
{
|
||||
var connectionOptions = HttpHeaders.ParseConnection(headers.HeaderConnection);
|
||||
|
||||
upgrade = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade;
|
||||
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
|
||||
}
|
||||
|
||||
if (upgrade)
|
||||
{
|
||||
if (headers.HeaderTransferEncoding.Count > 0 || (headers.ContentLength.HasValue && headers.ContentLength.Value != 0))
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload);
|
||||
}
|
||||
|
||||
return new ForUpgrade(context);
|
||||
}
|
||||
|
||||
if (headers.HasTransferEncoding)
|
||||
{
|
||||
var transferEncoding = headers.HeaderTransferEncoding;
|
||||
var transferCoding = HttpHeaders.GetFinalTransferCoding(transferEncoding);
|
||||
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
// If a Transfer-Encoding header field
|
||||
// is present in a request and the chunked transfer coding is not
|
||||
// the final encoding, the message body length cannot be determined
|
||||
// reliably; the server MUST respond with the 400 (Bad Request)
|
||||
// status code and then close the connection.
|
||||
if (transferCoding != TransferCoding.Chunked)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.FinalTransferCodingNotChunked, in transferEncoding);
|
||||
}
|
||||
|
||||
return new ForChunkedEncoding(keepAlive, context);
|
||||
}
|
||||
|
||||
if (headers.ContentLength.HasValue)
|
||||
{
|
||||
var contentLength = headers.ContentLength.Value;
|
||||
|
||||
if (contentLength == 0)
|
||||
{
|
||||
return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose;
|
||||
}
|
||||
|
||||
return new ForContentLength(keepAlive, contentLength, context);
|
||||
}
|
||||
|
||||
// Avoid slowing down most common case
|
||||
if (!object.ReferenceEquals(context.Method, HttpMethods.Get))
|
||||
{
|
||||
// If we got here, request contains no Content-Length or Transfer-Encoding header.
|
||||
// Reject with 411 Length Required.
|
||||
if (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put)
|
||||
{
|
||||
var requestRejectionReason = httpVersion == HttpVersion.Http11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredHttp10;
|
||||
BadHttpRequestException.Throw(requestRejectionReason, context.Method);
|
||||
}
|
||||
}
|
||||
|
||||
return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose;
|
||||
}
|
||||
|
||||
private class ForUpgrade : Http1MessageBody
|
||||
{
|
||||
public ForUpgrade(Http1Connection context)
|
||||
: base(context)
|
||||
{
|
||||
RequestUpgrade = true;
|
||||
}
|
||||
|
||||
public override bool IsEmpty => true;
|
||||
|
||||
protected override bool Read(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
Copy(readableBuffer, writableBuffer);
|
||||
consumed = readableBuffer.End;
|
||||
examined = readableBuffer.End;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class ForContentLength : Http1MessageBody
|
||||
{
|
||||
private readonly long _contentLength;
|
||||
private long _inputLength;
|
||||
|
||||
public ForContentLength(bool keepAlive, long contentLength, Http1Connection context)
|
||||
: base(context)
|
||||
{
|
||||
RequestKeepAlive = keepAlive;
|
||||
_contentLength = contentLength;
|
||||
_inputLength = _contentLength;
|
||||
}
|
||||
|
||||
protected override bool Read(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
if (_inputLength == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Attempted to read from completed Content-Length request body.");
|
||||
}
|
||||
|
||||
var actual = (int)Math.Min(readableBuffer.Length, _inputLength);
|
||||
_inputLength -= actual;
|
||||
|
||||
consumed = readableBuffer.GetPosition(actual);
|
||||
examined = consumed;
|
||||
|
||||
Copy(readableBuffer.Slice(0, actual), writableBuffer);
|
||||
|
||||
return _inputLength == 0;
|
||||
}
|
||||
|
||||
protected override void OnReadStarting()
|
||||
{
|
||||
if (_contentLength > _context.MaxRequestBodySize)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// http://tools.ietf.org/html/rfc2616#section-3.6.1
|
||||
/// </summary>
|
||||
private class ForChunkedEncoding : Http1MessageBody
|
||||
{
|
||||
// byte consts don't have a data type annotation so we pre-cast it
|
||||
private const byte ByteCR = (byte)'\r';
|
||||
// "7FFFFFFF\r\n" is the largest chunk size that could be returned as an int.
|
||||
private const int MaxChunkPrefixBytes = 10;
|
||||
|
||||
private long _inputLength;
|
||||
private long _consumedBytes;
|
||||
|
||||
private Mode _mode = Mode.Prefix;
|
||||
|
||||
public ForChunkedEncoding(bool keepAlive, Http1Connection context)
|
||||
: base(context)
|
||||
{
|
||||
RequestKeepAlive = keepAlive;
|
||||
}
|
||||
|
||||
protected override bool Read(ReadOnlySequence<byte> readableBuffer, PipeWriter writableBuffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = default(SequencePosition);
|
||||
examined = default(SequencePosition);
|
||||
|
||||
while (_mode < Mode.Trailer)
|
||||
{
|
||||
if (_mode == Mode.Prefix)
|
||||
{
|
||||
ParseChunkedPrefix(readableBuffer, out consumed, out examined);
|
||||
|
||||
if (_mode == Mode.Prefix)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
readableBuffer = readableBuffer.Slice(consumed);
|
||||
}
|
||||
|
||||
if (_mode == Mode.Extension)
|
||||
{
|
||||
ParseExtension(readableBuffer, out consumed, out examined);
|
||||
|
||||
if (_mode == Mode.Extension)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
readableBuffer = readableBuffer.Slice(consumed);
|
||||
}
|
||||
|
||||
if (_mode == Mode.Data)
|
||||
{
|
||||
ReadChunkedData(readableBuffer, writableBuffer, out consumed, out examined);
|
||||
|
||||
if (_mode == Mode.Data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
readableBuffer = readableBuffer.Slice(consumed);
|
||||
}
|
||||
|
||||
if (_mode == Mode.Suffix)
|
||||
{
|
||||
ParseChunkedSuffix(readableBuffer, out consumed, out examined);
|
||||
|
||||
if (_mode == Mode.Suffix)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
readableBuffer = readableBuffer.Slice(consumed);
|
||||
}
|
||||
}
|
||||
|
||||
// Chunks finished, parse trailers
|
||||
if (_mode == Mode.Trailer)
|
||||
{
|
||||
ParseChunkedTrailer(readableBuffer, out consumed, out examined);
|
||||
|
||||
if (_mode == Mode.Trailer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
readableBuffer = readableBuffer.Slice(consumed);
|
||||
}
|
||||
|
||||
// _consumedBytes aren't tracked for trailer headers, since headers have separate limits.
|
||||
if (_mode == Mode.TrailerHeaders)
|
||||
{
|
||||
if (_context.TakeMessageHeaders(readableBuffer, out consumed, out examined))
|
||||
{
|
||||
_mode = Mode.Complete;
|
||||
}
|
||||
}
|
||||
|
||||
return _mode == Mode.Complete;
|
||||
}
|
||||
|
||||
private void AddAndCheckConsumedBytes(long consumedBytes)
|
||||
{
|
||||
_consumedBytes += consumedBytes;
|
||||
|
||||
if (_consumedBytes > _context.MaxRequestBodySize)
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseChunkedPrefix(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.Start;
|
||||
var reader = new BufferReader(buffer);
|
||||
var ch1 = reader.Read();
|
||||
var ch2 = reader.Read();
|
||||
|
||||
if (ch1 == -1 || ch2 == -1)
|
||||
{
|
||||
examined = reader.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
var chunkSize = CalculateChunkSize(ch1, 0);
|
||||
ch1 = ch2;
|
||||
|
||||
while (reader.ConsumedBytes < MaxChunkPrefixBytes)
|
||||
{
|
||||
if (ch1 == ';')
|
||||
{
|
||||
consumed = reader.Position;
|
||||
examined = reader.Position;
|
||||
|
||||
AddAndCheckConsumedBytes(reader.ConsumedBytes);
|
||||
_inputLength = chunkSize;
|
||||
_mode = Mode.Extension;
|
||||
return;
|
||||
}
|
||||
|
||||
ch2 = reader.Read();
|
||||
if (ch2 == -1)
|
||||
{
|
||||
examined = reader.Position;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch1 == '\r' && ch2 == '\n')
|
||||
{
|
||||
consumed = reader.Position;
|
||||
examined = reader.Position;
|
||||
|
||||
AddAndCheckConsumedBytes(reader.ConsumedBytes);
|
||||
_inputLength = chunkSize;
|
||||
_mode = chunkSize > 0 ? Mode.Data : Mode.Trailer;
|
||||
return;
|
||||
}
|
||||
|
||||
chunkSize = CalculateChunkSize(ch1, chunkSize);
|
||||
ch1 = ch2;
|
||||
}
|
||||
|
||||
// At this point, 10 bytes have been consumed which is enough to parse the max value "7FFFFFFF\r\n".
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData);
|
||||
}
|
||||
|
||||
private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
// Chunk-extensions not currently parsed
|
||||
// Just drain the data
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.Start;
|
||||
|
||||
do
|
||||
{
|
||||
SequencePosition? extensionCursorPosition = buffer.PositionOf(ByteCR);
|
||||
if (extensionCursorPosition == null)
|
||||
{
|
||||
// End marker not found yet
|
||||
consumed = buffer.End;
|
||||
examined = buffer.End;
|
||||
AddAndCheckConsumedBytes(buffer.Length);
|
||||
return;
|
||||
};
|
||||
|
||||
var extensionCursor = extensionCursorPosition.Value;
|
||||
var charsToByteCRExclusive = buffer.Slice(0, extensionCursor).Length;
|
||||
|
||||
var sufixBuffer = buffer.Slice(extensionCursor);
|
||||
if (sufixBuffer.Length < 2)
|
||||
{
|
||||
consumed = extensionCursor;
|
||||
examined = buffer.End;
|
||||
AddAndCheckConsumedBytes(charsToByteCRExclusive);
|
||||
return;
|
||||
}
|
||||
|
||||
sufixBuffer = sufixBuffer.Slice(0, 2);
|
||||
var sufixSpan = sufixBuffer.ToSpan();
|
||||
|
||||
if (sufixSpan[1] == '\n')
|
||||
{
|
||||
// We consumed the \r\n at the end of the extension, so switch modes.
|
||||
_mode = _inputLength > 0 ? Mode.Data : Mode.Trailer;
|
||||
|
||||
consumed = sufixBuffer.End;
|
||||
examined = sufixBuffer.End;
|
||||
AddAndCheckConsumedBytes(charsToByteCRExclusive + 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't consume suffixSpan[1] in case it is also a \r.
|
||||
buffer = buffer.Slice(charsToByteCRExclusive + 1);
|
||||
consumed = extensionCursor;
|
||||
AddAndCheckConsumedBytes(charsToByteCRExclusive + 1);
|
||||
}
|
||||
} while (_mode == Mode.Extension);
|
||||
}
|
||||
|
||||
private void ReadChunkedData(ReadOnlySequence<byte> buffer, PipeWriter writableBuffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
var actual = Math.Min(buffer.Length, _inputLength);
|
||||
consumed = buffer.GetPosition(actual);
|
||||
examined = consumed;
|
||||
|
||||
Copy(buffer.Slice(0, actual), writableBuffer);
|
||||
|
||||
_inputLength -= actual;
|
||||
AddAndCheckConsumedBytes(actual);
|
||||
|
||||
if (_inputLength == 0)
|
||||
{
|
||||
_mode = Mode.Suffix;
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseChunkedSuffix(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.Start;
|
||||
|
||||
if (buffer.Length < 2)
|
||||
{
|
||||
examined = buffer.End;
|
||||
return;
|
||||
}
|
||||
|
||||
var suffixBuffer = buffer.Slice(0, 2);
|
||||
var suffixSpan = suffixBuffer.ToSpan();
|
||||
if (suffixSpan[0] == '\r' && suffixSpan[1] == '\n')
|
||||
{
|
||||
consumed = suffixBuffer.End;
|
||||
examined = suffixBuffer.End;
|
||||
AddAndCheckConsumedBytes(2);
|
||||
_mode = Mode.Prefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseChunkedTrailer(ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.Start;
|
||||
|
||||
if (buffer.Length < 2)
|
||||
{
|
||||
examined = buffer.End;
|
||||
return;
|
||||
}
|
||||
|
||||
var trailerBuffer = buffer.Slice(0, 2);
|
||||
var trailerSpan = trailerBuffer.ToSpan();
|
||||
|
||||
if (trailerSpan[0] == '\r' && trailerSpan[1] == '\n')
|
||||
{
|
||||
consumed = trailerBuffer.End;
|
||||
examined = trailerBuffer.End;
|
||||
AddAndCheckConsumedBytes(2);
|
||||
_mode = Mode.Complete;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mode = Mode.TrailerHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculateChunkSize(int extraHexDigit, int currentParsedSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
checked
|
||||
{
|
||||
if (extraHexDigit >= '0' && extraHexDigit <= '9')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - '0');
|
||||
}
|
||||
else if (extraHexDigit >= 'A' && extraHexDigit <= 'F')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - ('A' - 10));
|
||||
}
|
||||
else if (extraHexDigit >= 'a' && extraHexDigit <= 'f')
|
||||
{
|
||||
return currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OverflowException ex)
|
||||
{
|
||||
throw new IOException(CoreStrings.BadRequest_BadChunkSizeData, ex);
|
||||
}
|
||||
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.BadChunkSizeData);
|
||||
return -1; // can't happen, but compiler complains
|
||||
}
|
||||
|
||||
private enum Mode
|
||||
{
|
||||
Prefix,
|
||||
Extension,
|
||||
Data,
|
||||
Suffix,
|
||||
Trailer,
|
||||
TrailerHeaders,
|
||||
Complete
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Connections.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public class Http1OutputProducer : IHttpOutputProducer
|
||||
{
|
||||
private static readonly ReadOnlyMemory<byte> _continueBytes = new ReadOnlyMemory<byte>(Encoding.ASCII.GetBytes("HTTP/1.1 100 Continue\r\n\r\n"));
|
||||
private static readonly byte[] _bytesHttpVersion11 = Encoding.ASCII.GetBytes("HTTP/1.1 ");
|
||||
private static readonly byte[] _bytesEndHeaders = Encoding.ASCII.GetBytes("\r\n\r\n");
|
||||
private static readonly ReadOnlyMemory<byte> _endChunkedResponseBytes = new ReadOnlyMemory<byte>(Encoding.ASCII.GetBytes("0\r\n\r\n"));
|
||||
|
||||
private readonly string _connectionId;
|
||||
private readonly ConnectionContext _connectionContext;
|
||||
private readonly ITimeoutControl _timeoutControl;
|
||||
private readonly IKestrelTrace _log;
|
||||
private readonly IBytesWrittenFeature _transportBytesWrittenFeature;
|
||||
|
||||
// This locks access to to all of the below fields
|
||||
private readonly object _contextLock = new object();
|
||||
|
||||
private bool _completed = false;
|
||||
private bool _aborted;
|
||||
private long _unflushedBytes;
|
||||
private long _totalBytesCommitted;
|
||||
|
||||
private readonly PipeWriter _pipeWriter;
|
||||
|
||||
// https://github.com/dotnet/corefxlab/issues/1334
|
||||
// Pipelines don't support multiple awaiters on flush
|
||||
// this is temporary until it does
|
||||
private TaskCompletionSource<object> _flushTcs;
|
||||
private readonly object _flushLock = new object();
|
||||
private Action _flushCompleted;
|
||||
|
||||
private ValueTask<FlushResult> _flushTask;
|
||||
|
||||
public Http1OutputProducer(
|
||||
PipeWriter pipeWriter,
|
||||
string connectionId,
|
||||
ConnectionContext connectionContext,
|
||||
IKestrelTrace log,
|
||||
ITimeoutControl timeoutControl,
|
||||
IBytesWrittenFeature transportBytesWrittenFeature)
|
||||
{
|
||||
_pipeWriter = pipeWriter;
|
||||
_connectionId = connectionId;
|
||||
_connectionContext = connectionContext;
|
||||
_timeoutControl = timeoutControl;
|
||||
_log = log;
|
||||
_flushCompleted = OnFlushCompleted;
|
||||
_transportBytesWrittenFeature = transportBytesWrittenFeature;
|
||||
}
|
||||
|
||||
public Task WriteDataAsync(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled(cancellationToken);
|
||||
}
|
||||
|
||||
return WriteAsync(buffer, cancellationToken);
|
||||
}
|
||||
|
||||
public Task WriteStreamSuffixAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WriteAsync(_endChunkedResponseBytes.Span, cancellationToken);
|
||||
}
|
||||
|
||||
public Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return WriteAsync(Constants.EmptyData, cancellationToken);
|
||||
}
|
||||
|
||||
public void Write<T>(Func<PipeWriter, T, long> callback, T state)
|
||||
{
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = _pipeWriter;
|
||||
var bytesCommitted = callback(buffer, state);
|
||||
_unflushedBytes += bytesCommitted;
|
||||
_totalBytesCommitted += bytesCommitted;
|
||||
}
|
||||
}
|
||||
|
||||
public Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state)
|
||||
{
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var buffer = _pipeWriter;
|
||||
var bytesCommitted = callback(buffer, state);
|
||||
_unflushedBytes += bytesCommitted;
|
||||
_totalBytesCommitted += bytesCommitted;
|
||||
}
|
||||
|
||||
return FlushAsync();
|
||||
}
|
||||
|
||||
public void WriteResponseHeaders(int statusCode, string reasonPhrase, HttpResponseHeaders responseHeaders)
|
||||
{
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = _pipeWriter;
|
||||
var writer = new BufferWriter<PipeWriter>(buffer);
|
||||
|
||||
writer.Write(_bytesHttpVersion11);
|
||||
var statusBytes = ReasonPhrases.ToStatusBytes(statusCode, reasonPhrase);
|
||||
writer.Write(statusBytes);
|
||||
responseHeaders.CopyTo(ref writer);
|
||||
writer.Write(_bytesEndHeaders);
|
||||
|
||||
writer.Commit();
|
||||
|
||||
_unflushedBytes += writer.BytesCommitted;
|
||||
_totalBytesCommitted += writer.BytesCommitted;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_log.ConnectionDisconnect(_connectionId);
|
||||
_completed = true;
|
||||
_pipeWriter.Complete();
|
||||
|
||||
var unsentBytes = _totalBytesCommitted - _transportBytesWrittenFeature.TotalBytesWritten;
|
||||
|
||||
if (unsentBytes > 0)
|
||||
{
|
||||
// unsentBytes should never be over 64KB in the default configuration.
|
||||
_timeoutControl.StartTimingWrite((int)Math.Min(unsentBytes, int.MaxValue));
|
||||
_pipeWriter.OnReaderCompleted((ex, state) => ((ITimeoutControl)state).StopTimingWrite(), _timeoutControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Abort(ConnectionAbortedException error)
|
||||
{
|
||||
// Abort can be called after Dispose if there's a flush timeout.
|
||||
// It's important to still call _lifetimeFeature.Abort() in this case.
|
||||
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_aborted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_aborted = true;
|
||||
_connectionContext.Abort(error);
|
||||
|
||||
if (!_completed)
|
||||
{
|
||||
_log.ConnectionDisconnect(_connectionId);
|
||||
_completed = true;
|
||||
_pipeWriter.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task Write100ContinueAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return WriteAsync(_continueBytes.Span, default(CancellationToken));
|
||||
}
|
||||
|
||||
private Task WriteAsync(
|
||||
ReadOnlySpan<byte> buffer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var writableBuffer = default(PipeWriter);
|
||||
long bytesWritten = 0;
|
||||
lock (_contextLock)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
writableBuffer = _pipeWriter;
|
||||
var writer = new BufferWriter<PipeWriter>(writableBuffer);
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
writer.Write(buffer);
|
||||
|
||||
_unflushedBytes += buffer.Length;
|
||||
_totalBytesCommitted += buffer.Length;
|
||||
}
|
||||
writer.Commit();
|
||||
|
||||
bytesWritten = _unflushedBytes;
|
||||
_unflushedBytes = 0;
|
||||
}
|
||||
|
||||
return FlushAsync(writableBuffer, bytesWritten, cancellationToken);
|
||||
}
|
||||
|
||||
// Single caller, at end of method - so inline
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private Task FlushAsync(PipeWriter writableBuffer, long bytesWritten, CancellationToken cancellationToken)
|
||||
{
|
||||
var awaitable = writableBuffer.FlushAsync(cancellationToken);
|
||||
if (awaitable.IsCompleted)
|
||||
{
|
||||
// The flush task can't fail today
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return FlushAsyncAwaited(awaitable, bytesWritten, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task FlushAsyncAwaited(ValueTask<FlushResult> awaitable, long count, CancellationToken cancellationToken)
|
||||
{
|
||||
// https://github.com/dotnet/corefxlab/issues/1334
|
||||
// Since the flush awaitable doesn't currently support multiple awaiters
|
||||
// we need to use a task to track the callbacks.
|
||||
// All awaiters get the same task
|
||||
lock (_flushLock)
|
||||
{
|
||||
_flushTask = awaitable;
|
||||
if (_flushTcs == null || _flushTcs.Task.IsCompleted)
|
||||
{
|
||||
_flushTcs = new TaskCompletionSource<object>();
|
||||
|
||||
_flushTask.GetAwaiter().OnCompleted(_flushCompleted);
|
||||
}
|
||||
}
|
||||
|
||||
_timeoutControl.StartTimingWrite(count);
|
||||
try
|
||||
{
|
||||
await _flushTcs.Task;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_completed = true;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_timeoutControl.StopTimingWrite();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFlushCompleted()
|
||||
{
|
||||
try
|
||||
{
|
||||
_flushTask.GetAwaiter().GetResult();
|
||||
_flushTcs.TrySetResult(null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_flushTcs.TrySetResult(exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_flushTask = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public struct Http1ParsingHandler : IHttpRequestLineHandler, IHttpHeadersHandler
|
||||
{
|
||||
public Http1Connection Connection;
|
||||
|
||||
public Http1ParsingHandler(Http1Connection connection)
|
||||
{
|
||||
Connection = connection;
|
||||
}
|
||||
|
||||
public void OnHeader(Span<byte> name, Span<byte> value)
|
||||
=> Connection.OnHeader(name, value);
|
||||
|
||||
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
|
||||
=> Connection.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,444 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public abstract class HttpHeaders : IHeaderDictionary
|
||||
{
|
||||
protected long? _contentLength;
|
||||
protected bool _isReadOnly;
|
||||
protected Dictionary<string, StringValues> MaybeUnknown;
|
||||
protected Dictionary<string, StringValues> Unknown => MaybeUnknown ?? (MaybeUnknown = new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
public long? ContentLength
|
||||
{
|
||||
get { return _contentLength; }
|
||||
set
|
||||
{
|
||||
if (value.HasValue && value.Value < 0)
|
||||
{
|
||||
ThrowInvalidContentLengthException(value.Value);
|
||||
}
|
||||
_contentLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
StringValues IHeaderDictionary.this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
StringValues value;
|
||||
TryGetValueFast(key, out value);
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_isReadOnly)
|
||||
{
|
||||
ThrowHeadersReadOnlyException();
|
||||
}
|
||||
if (value.Count == 0)
|
||||
{
|
||||
RemoveFast(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValueFast(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StringValues IDictionary<string, StringValues>.this[string key]
|
||||
{
|
||||
get
|
||||
{
|
||||
// Unlike the IHeaderDictionary version, this getter will throw a KeyNotFoundException.
|
||||
StringValues value;
|
||||
if (!TryGetValueFast(key, out value))
|
||||
{
|
||||
ThrowKeyNotFoundException();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
set
|
||||
{
|
||||
((IHeaderDictionary)this)[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected void ThrowHeadersReadOnlyException()
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.HeadersAreReadOnly);
|
||||
}
|
||||
|
||||
protected void ThrowArgumentException()
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
protected void ThrowKeyNotFoundException()
|
||||
{
|
||||
throw new KeyNotFoundException();
|
||||
}
|
||||
|
||||
protected void ThrowDuplicateKeyException()
|
||||
{
|
||||
throw new ArgumentException(CoreStrings.KeyAlreadyExists);
|
||||
}
|
||||
|
||||
int ICollection<KeyValuePair<string, StringValues>>.Count => GetCountFast();
|
||||
|
||||
bool ICollection<KeyValuePair<string, StringValues>>.IsReadOnly => _isReadOnly;
|
||||
|
||||
ICollection<string> IDictionary<string, StringValues>.Keys => ((IDictionary<string, StringValues>)this).Select(pair => pair.Key).ToList();
|
||||
|
||||
ICollection<StringValues> IDictionary<string, StringValues>.Values => ((IDictionary<string, StringValues>)this).Select(pair => pair.Value).ToList();
|
||||
|
||||
public void SetReadOnly()
|
||||
{
|
||||
_isReadOnly = true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_isReadOnly = false;
|
||||
ClearFast();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
protected static StringValues AppendValue(in StringValues existing, string append)
|
||||
{
|
||||
return StringValues.Concat(existing, append);
|
||||
}
|
||||
|
||||
protected static int BitCount(long value)
|
||||
{
|
||||
// see https://github.com/dotnet/corefx/blob/5965fd3756bc9dd9c89a27621eb10c6931126de2/src/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BitArithmetic.cs
|
||||
|
||||
const ulong Mask01010101 = 0x5555555555555555UL;
|
||||
const ulong Mask00110011 = 0x3333333333333333UL;
|
||||
const ulong Mask00001111 = 0x0F0F0F0F0F0F0F0FUL;
|
||||
const ulong Mask00000001 = 0x0101010101010101UL;
|
||||
|
||||
var v = (ulong)value;
|
||||
|
||||
v = v - ((v >> 1) & Mask01010101);
|
||||
v = (v & Mask00110011) + ((v >> 2) & Mask00110011);
|
||||
return (int)(unchecked(((v + (v >> 4)) & Mask00001111) * Mask00000001) >> 56);
|
||||
}
|
||||
|
||||
protected virtual int GetCountFast()
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual bool TryGetValueFast(string key, out StringValues value)
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual void SetValueFast(string key, in StringValues value)
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual bool AddValueFast(string key, in StringValues value)
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual bool RemoveFast(string key)
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual void ClearFast()
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual bool CopyToFast(KeyValuePair<string, StringValues>[] array, int arrayIndex)
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
protected virtual IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
|
||||
{ throw new NotImplementedException(); }
|
||||
|
||||
void ICollection<KeyValuePair<string, StringValues>>.Add(KeyValuePair<string, StringValues> item)
|
||||
{
|
||||
((IDictionary<string, StringValues>)this).Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
void IDictionary<string, StringValues>.Add(string key, StringValues value)
|
||||
{
|
||||
if (_isReadOnly)
|
||||
{
|
||||
ThrowHeadersReadOnlyException();
|
||||
}
|
||||
|
||||
if (value.Count > 0 && !AddValueFast(key, value))
|
||||
{
|
||||
ThrowDuplicateKeyException();
|
||||
}
|
||||
}
|
||||
|
||||
void ICollection<KeyValuePair<string, StringValues>>.Clear()
|
||||
{
|
||||
if (_isReadOnly)
|
||||
{
|
||||
ThrowHeadersReadOnlyException();
|
||||
}
|
||||
ClearFast();
|
||||
}
|
||||
|
||||
bool ICollection<KeyValuePair<string, StringValues>>.Contains(KeyValuePair<string, StringValues> item)
|
||||
{
|
||||
StringValues value;
|
||||
return
|
||||
TryGetValueFast(item.Key, out value) &&
|
||||
value.Equals(item.Value);
|
||||
}
|
||||
|
||||
bool IDictionary<string, StringValues>.ContainsKey(string key)
|
||||
{
|
||||
StringValues value;
|
||||
return TryGetValueFast(key, out value);
|
||||
}
|
||||
|
||||
void ICollection<KeyValuePair<string, StringValues>>.CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex)
|
||||
{
|
||||
if (!CopyToFast(array, arrayIndex))
|
||||
{
|
||||
ThrowArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumeratorFast();
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<string, StringValues>> IEnumerable<KeyValuePair<string, StringValues>>.GetEnumerator()
|
||||
{
|
||||
return GetEnumeratorFast();
|
||||
}
|
||||
|
||||
bool ICollection<KeyValuePair<string, StringValues>>.Remove(KeyValuePair<string, StringValues> item)
|
||||
{
|
||||
StringValues value;
|
||||
return
|
||||
TryGetValueFast(item.Key, out value) &&
|
||||
value.Equals(item.Value) &&
|
||||
RemoveFast(item.Key);
|
||||
}
|
||||
|
||||
bool IDictionary<string, StringValues>.Remove(string key)
|
||||
{
|
||||
if (_isReadOnly)
|
||||
{
|
||||
ThrowHeadersReadOnlyException();
|
||||
}
|
||||
return RemoveFast(key);
|
||||
}
|
||||
|
||||
bool IDictionary<string, StringValues>.TryGetValue(string key, out StringValues value)
|
||||
{
|
||||
return TryGetValueFast(key, out value);
|
||||
}
|
||||
|
||||
public static void ValidateHeaderCharacters(in StringValues headerValues)
|
||||
{
|
||||
var count = headerValues.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
|
||||
{
|
||||
ValidateHeaderCharacters(headerValues[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ValidateHeaderCharacters(string headerCharacters)
|
||||
{
|
||||
if (headerCharacters != null)
|
||||
{
|
||||
foreach (var ch in headerCharacters)
|
||||
{
|
||||
if (ch < 0x20 || ch > 0x7E)
|
||||
{
|
||||
ThrowInvalidHeaderCharacter(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe ConnectionOptions ParseConnection(in StringValues connection)
|
||||
{
|
||||
var connectionOptions = ConnectionOptions.None;
|
||||
|
||||
var connectionCount = connection.Count;
|
||||
for (var i = 0; i < connectionCount; i++)
|
||||
{
|
||||
var value = connection[i];
|
||||
fixed (char* ptr = value)
|
||||
{
|
||||
var ch = ptr;
|
||||
var tokenEnd = ch;
|
||||
var end = ch + value.Length;
|
||||
|
||||
while (ch < end)
|
||||
{
|
||||
while (tokenEnd < end && *tokenEnd != ',')
|
||||
{
|
||||
tokenEnd++;
|
||||
}
|
||||
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
var tokenLength = tokenEnd - ch;
|
||||
|
||||
if (tokenLength >= 9 && (*ch | 0x20) == 'k')
|
||||
{
|
||||
if ((*++ch | 0x20) == 'e' &&
|
||||
(*++ch | 0x20) == 'e' &&
|
||||
(*++ch | 0x20) == 'p' &&
|
||||
*++ch == '-' &&
|
||||
(*++ch | 0x20) == 'a' &&
|
||||
(*++ch | 0x20) == 'l' &&
|
||||
(*++ch | 0x20) == 'i' &&
|
||||
(*++ch | 0x20) == 'v' &&
|
||||
(*++ch | 0x20) == 'e')
|
||||
{
|
||||
ch++;
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
if (ch == tokenEnd)
|
||||
{
|
||||
connectionOptions |= ConnectionOptions.KeepAlive;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tokenLength >= 7 && (*ch | 0x20) == 'u')
|
||||
{
|
||||
if ((*++ch | 0x20) == 'p' &&
|
||||
(*++ch | 0x20) == 'g' &&
|
||||
(*++ch | 0x20) == 'r' &&
|
||||
(*++ch | 0x20) == 'a' &&
|
||||
(*++ch | 0x20) == 'd' &&
|
||||
(*++ch | 0x20) == 'e')
|
||||
{
|
||||
ch++;
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
if (ch == tokenEnd)
|
||||
{
|
||||
connectionOptions |= ConnectionOptions.Upgrade;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (tokenLength >= 5 && (*ch | 0x20) == 'c')
|
||||
{
|
||||
if ((*++ch | 0x20) == 'l' &&
|
||||
(*++ch | 0x20) == 'o' &&
|
||||
(*++ch | 0x20) == 's' &&
|
||||
(*++ch | 0x20) == 'e')
|
||||
{
|
||||
ch++;
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
if (ch == tokenEnd)
|
||||
{
|
||||
connectionOptions |= ConnectionOptions.Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenEnd++;
|
||||
ch = tokenEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return connectionOptions;
|
||||
}
|
||||
|
||||
public static unsafe TransferCoding GetFinalTransferCoding(in StringValues transferEncoding)
|
||||
{
|
||||
var transferEncodingOptions = TransferCoding.None;
|
||||
|
||||
var transferEncodingCount = transferEncoding.Count;
|
||||
for (var i = 0; i < transferEncodingCount; i++)
|
||||
{
|
||||
var value = transferEncoding[i];
|
||||
fixed (char* ptr = value)
|
||||
{
|
||||
var ch = ptr;
|
||||
var tokenEnd = ch;
|
||||
var end = ch + value.Length;
|
||||
|
||||
while (ch < end)
|
||||
{
|
||||
while (tokenEnd < end && *tokenEnd != ',')
|
||||
{
|
||||
tokenEnd++;
|
||||
}
|
||||
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
var tokenLength = tokenEnd - ch;
|
||||
|
||||
if (tokenLength >= 7 && (*ch | 0x20) == 'c')
|
||||
{
|
||||
if ((*++ch | 0x20) == 'h' &&
|
||||
(*++ch | 0x20) == 'u' &&
|
||||
(*++ch | 0x20) == 'n' &&
|
||||
(*++ch | 0x20) == 'k' &&
|
||||
(*++ch | 0x20) == 'e' &&
|
||||
(*++ch | 0x20) == 'd')
|
||||
{
|
||||
ch++;
|
||||
while (ch < tokenEnd && *ch == ' ')
|
||||
{
|
||||
ch++;
|
||||
}
|
||||
|
||||
if (ch == tokenEnd)
|
||||
{
|
||||
transferEncodingOptions = TransferCoding.Chunked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenLength > 0 && ch != tokenEnd)
|
||||
{
|
||||
transferEncodingOptions = TransferCoding.Other;
|
||||
}
|
||||
|
||||
tokenEnd++;
|
||||
ch = tokenEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transferEncodingOptions;
|
||||
}
|
||||
|
||||
private static void ThrowInvalidContentLengthException(long value)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(CoreStrings.FormatInvalidContentLength_InvalidNumber(value));
|
||||
}
|
||||
|
||||
private static void ThrowInvalidHeaderCharacter(char ch)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatInvalidAsciiOrControlChar(string.Format("0x{0:X4}", (ushort)ch)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum HttpMethod: byte
|
||||
{
|
||||
Get,
|
||||
Put,
|
||||
Delete,
|
||||
Post,
|
||||
Head,
|
||||
Trace,
|
||||
Patch,
|
||||
Connect,
|
||||
Options,
|
||||
|
||||
Custom,
|
||||
|
||||
None = byte.MaxValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
private bool _showErrorDetails;
|
||||
|
||||
public HttpParser() : this(showErrorDetails: true)
|
||||
{
|
||||
}
|
||||
|
||||
public HttpParser(bool showErrorDetails)
|
||||
{
|
||||
_showErrorDetails = showErrorDetails;
|
||||
}
|
||||
|
||||
// byte types don't have a data type annotation so we pre-cast them; to avoid in-place casts
|
||||
private const byte ByteCR = (byte)'\r';
|
||||
private const byte ByteLF = (byte)'\n';
|
||||
private const byte ByteColon = (byte)':';
|
||||
private const byte ByteSpace = (byte)' ';
|
||||
private const byte ByteTab = (byte)'\t';
|
||||
private const byte ByteQuestionMark = (byte)'?';
|
||||
private const byte BytePercentage = (byte)'%';
|
||||
|
||||
public unsafe bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
|
||||
// Prepare the first span
|
||||
var span = buffer.First.Span;
|
||||
var lineIndex = span.IndexOf(ByteLF);
|
||||
if (lineIndex >= 0)
|
||||
{
|
||||
consumed = buffer.GetPosition(lineIndex + 1, consumed);
|
||||
span = span.Slice(0, lineIndex + 1);
|
||||
}
|
||||
else if (buffer.IsSingleSegment)
|
||||
{
|
||||
// No request line end
|
||||
return false;
|
||||
}
|
||||
else if (TryGetNewLine(buffer, out var found))
|
||||
{
|
||||
span = buffer.Slice(consumed, found).ToSpan();
|
||||
consumed = found;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No request line end
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fix and parse the span
|
||||
fixed (byte* data = &MemoryMarshal.GetReference(span))
|
||||
{
|
||||
ParseRequestLine(handler, data, span.Length);
|
||||
}
|
||||
|
||||
examined = consumed;
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe void ParseRequestLine(TRequestHandler handler, byte* data, int length)
|
||||
{
|
||||
int offset;
|
||||
// Get Method and set the offset
|
||||
var method = HttpUtilities.GetKnownMethod(data, length, out offset);
|
||||
|
||||
Span<byte> customMethod = method == HttpMethod.Custom ?
|
||||
GetUnknownMethod(data, length, out offset) :
|
||||
default;
|
||||
|
||||
// Skip space
|
||||
offset++;
|
||||
|
||||
byte ch = 0;
|
||||
// Target = Path and Query
|
||||
var pathEncoded = false;
|
||||
var pathStart = -1;
|
||||
for (; offset < length; offset++)
|
||||
{
|
||||
ch = data[offset];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (ch == ByteQuestionMark)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Empty path is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (ch == BytePercentage)
|
||||
{
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Path starting with % is illegal
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
pathEncoded = true;
|
||||
}
|
||||
else if (pathStart == -1)
|
||||
{
|
||||
pathStart = offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathStart == -1)
|
||||
{
|
||||
// Start of path not found
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
var pathBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||
|
||||
// Query string
|
||||
var queryStart = offset;
|
||||
if (ch == ByteQuestionMark)
|
||||
{
|
||||
// We have a query string
|
||||
for (; offset < length; offset++)
|
||||
{
|
||||
ch = data[offset];
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End of query string not found
|
||||
if (offset == length)
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
var targetBuffer = new Span<byte>(data + pathStart, offset - pathStart);
|
||||
var query = new Span<byte>(data + queryStart, offset - queryStart);
|
||||
|
||||
// Consume space
|
||||
offset++;
|
||||
|
||||
// Version
|
||||
var httpVersion = HttpUtilities.GetKnownVersion(data + offset, length - offset);
|
||||
if (httpVersion == HttpVersion.Unknown)
|
||||
{
|
||||
if (data[offset] == ByteCR || data[length - 2] != ByteCR)
|
||||
{
|
||||
// If missing delimiter or CR before LF, reject and log entire line
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// else inform HTTP version is unsupported.
|
||||
RejectUnknownVersion(data + offset, length - offset - 2);
|
||||
}
|
||||
}
|
||||
|
||||
// After version's 8 bytes and CR, expect LF
|
||||
if (data[offset + 8 + 1] != ByteLF)
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded);
|
||||
}
|
||||
|
||||
public unsafe bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes)
|
||||
{
|
||||
consumed = buffer.Start;
|
||||
examined = buffer.End;
|
||||
consumedBytes = 0;
|
||||
|
||||
var bufferEnd = buffer.End;
|
||||
|
||||
var reader = new BufferReader(buffer);
|
||||
var start = default(BufferReader);
|
||||
var done = false;
|
||||
|
||||
try
|
||||
{
|
||||
while (!reader.End)
|
||||
{
|
||||
var span = reader.CurrentSegment;
|
||||
var remaining = span.Length - reader.CurrentSegmentIndex;
|
||||
|
||||
fixed (byte* pBuffer = &MemoryMarshal.GetReference(span))
|
||||
{
|
||||
while (remaining > 0)
|
||||
{
|
||||
var index = reader.CurrentSegmentIndex;
|
||||
int ch1;
|
||||
int ch2;
|
||||
var readAhead = false;
|
||||
|
||||
// Fast path, we're still looking at the same span
|
||||
if (remaining >= 2)
|
||||
{
|
||||
ch1 = pBuffer[index];
|
||||
ch2 = pBuffer[index + 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Store the reader before we look ahead 2 bytes (probably straddling
|
||||
// spans)
|
||||
start = reader;
|
||||
|
||||
// Possibly split across spans
|
||||
ch1 = reader.Read();
|
||||
ch2 = reader.Read();
|
||||
|
||||
readAhead = true;
|
||||
}
|
||||
|
||||
if (ch1 == ByteCR)
|
||||
{
|
||||
// Check for final CRLF.
|
||||
if (ch2 == -1)
|
||||
{
|
||||
// Reset the reader so we don't consume anything
|
||||
reader = start;
|
||||
return false;
|
||||
}
|
||||
else if (ch2 == ByteLF)
|
||||
{
|
||||
// If we got 2 bytes from the span directly so skip ahead 2 so that
|
||||
// the reader's state matches what we expect
|
||||
if (!readAhead)
|
||||
{
|
||||
reader.Advance(2);
|
||||
}
|
||||
|
||||
done = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Headers don't end in CRLF line.
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidRequestHeadersNoCRLF);
|
||||
}
|
||||
|
||||
// We moved the reader so look ahead 2 bytes so reset both the reader
|
||||
// and the index
|
||||
if (readAhead)
|
||||
{
|
||||
reader = start;
|
||||
index = reader.CurrentSegmentIndex;
|
||||
}
|
||||
|
||||
var endIndex = new Span<byte>(pBuffer + index, remaining).IndexOf(ByteLF);
|
||||
var length = 0;
|
||||
|
||||
if (endIndex != -1)
|
||||
{
|
||||
length = endIndex + 1;
|
||||
var pHeader = pBuffer + index;
|
||||
|
||||
TakeSingleHeader(pHeader, length, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
var current = reader.Position;
|
||||
var currentSlice = buffer.Slice(current, bufferEnd);
|
||||
|
||||
var lineEndPosition = currentSlice.PositionOf(ByteLF);
|
||||
// Split buffers
|
||||
if (lineEndPosition == null)
|
||||
{
|
||||
// Not there
|
||||
return false;
|
||||
}
|
||||
|
||||
var lineEnd = lineEndPosition.Value;
|
||||
|
||||
// Make sure LF is included in lineEnd
|
||||
lineEnd = buffer.GetPosition(1, lineEnd);
|
||||
var headerSpan = buffer.Slice(current, lineEnd).ToSpan();
|
||||
length = headerSpan.Length;
|
||||
|
||||
fixed (byte* pHeader = &MemoryMarshal.GetReference(headerSpan))
|
||||
{
|
||||
TakeSingleHeader(pHeader, length, handler);
|
||||
}
|
||||
|
||||
// We're going to the next span after this since we know we crossed spans here
|
||||
// so mark the remaining as equal to the headerSpan so that we end up at 0
|
||||
// on the next iteration
|
||||
remaining = length;
|
||||
}
|
||||
|
||||
// Skip the reader forward past the header line
|
||||
reader.Advance(length);
|
||||
remaining -= length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
consumed = reader.Position;
|
||||
consumedBytes = reader.ConsumedBytes;
|
||||
|
||||
if (done)
|
||||
{
|
||||
examined = consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe int FindEndOfName(byte* headerLine, int length)
|
||||
{
|
||||
var index = 0;
|
||||
var sawWhitespace = false;
|
||||
for (; index < length; index++)
|
||||
{
|
||||
var ch = headerLine[index];
|
||||
if (ch == ByteColon)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (ch == ByteTab || ch == ByteSpace || ch == ByteCR)
|
||||
{
|
||||
sawWhitespace = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (index == length || sawWhitespace)
|
||||
{
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe void TakeSingleHeader(byte* headerLine, int length, TRequestHandler handler)
|
||||
{
|
||||
// Skip CR, LF from end position
|
||||
var valueEnd = length - 3;
|
||||
var nameEnd = FindEndOfName(headerLine, length);
|
||||
|
||||
if (headerLine[valueEnd + 2] != ByteLF)
|
||||
{
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
if (headerLine[valueEnd + 1] != ByteCR)
|
||||
{
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
// Skip colon from value start
|
||||
var valueStart = nameEnd + 1;
|
||||
// Ignore start whitespace
|
||||
for (; valueStart < valueEnd; valueStart++)
|
||||
{
|
||||
var ch = headerLine[valueStart];
|
||||
if (ch != ByteTab && ch != ByteSpace && ch != ByteCR)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (ch == ByteCR)
|
||||
{
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for CR in value
|
||||
var valueBuffer = new Span<byte>(headerLine + valueStart, valueEnd - valueStart + 1);
|
||||
if (valueBuffer.IndexOf(ByteCR) >= 0)
|
||||
{
|
||||
RejectRequestHeader(headerLine, length);
|
||||
}
|
||||
|
||||
// Ignore end whitespace
|
||||
var lengthChanged = false;
|
||||
for (; valueEnd >= valueStart; valueEnd--)
|
||||
{
|
||||
var ch = headerLine[valueEnd];
|
||||
if (ch != ByteTab && ch != ByteSpace)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
lengthChanged = true;
|
||||
}
|
||||
|
||||
if (lengthChanged)
|
||||
{
|
||||
// Length changed
|
||||
valueBuffer = new Span<byte>(headerLine + valueStart, valueEnd - valueStart + 1);
|
||||
}
|
||||
|
||||
var nameBuffer = new Span<byte>(headerLine, nameEnd);
|
||||
|
||||
handler.OnHeader(nameBuffer, valueBuffer);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static bool TryGetNewLine(in ReadOnlySequence<byte> buffer, out SequencePosition found)
|
||||
{
|
||||
var byteLfPosition = buffer.PositionOf(ByteLF);
|
||||
if (byteLfPosition != null)
|
||||
{
|
||||
// Move 1 byte past the \n
|
||||
found = buffer.GetPosition(1, byteLfPosition.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
found = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe Span<byte> GetUnknownMethod(byte* data, int length, out int methodLength)
|
||||
{
|
||||
methodLength = 0;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var ch = data[i];
|
||||
|
||||
if (ch == ByteSpace)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
|
||||
methodLength = i;
|
||||
break;
|
||||
}
|
||||
else if (!IsValidTokenChar((char)ch))
|
||||
{
|
||||
RejectRequestLine(data, length);
|
||||
}
|
||||
}
|
||||
|
||||
return new Span<byte>(data, methodLength);
|
||||
}
|
||||
|
||||
private static bool IsValidTokenChar(char c)
|
||||
{
|
||||
// Determines if a character is valid as a 'token' as defined in the
|
||||
// HTTP spec: https://tools.ietf.org/html/rfc7230#section-3.2.6
|
||||
return
|
||||
(c >= '0' && c <= '9') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
c == '!' ||
|
||||
c == '#' ||
|
||||
c == '$' ||
|
||||
c == '%' ||
|
||||
c == '&' ||
|
||||
c == '\'' ||
|
||||
c == '*' ||
|
||||
c == '+' ||
|
||||
c == '-' ||
|
||||
c == '.' ||
|
||||
c == '^' ||
|
||||
c == '_' ||
|
||||
c == '`' ||
|
||||
c == '|' ||
|
||||
c == '~';
|
||||
}
|
||||
|
||||
[StackTraceHidden]
|
||||
private unsafe void RejectRequestLine(byte* requestLine, int length)
|
||||
=> throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestLine, requestLine, length);
|
||||
|
||||
[StackTraceHidden]
|
||||
private unsafe void RejectRequestHeader(byte* headerLine, int length)
|
||||
=> throw GetInvalidRequestException(RequestRejectionReason.InvalidRequestHeader, headerLine, length);
|
||||
|
||||
[StackTraceHidden]
|
||||
private unsafe void RejectUnknownVersion(byte* version, int length)
|
||||
=> throw GetInvalidRequestException(RequestRejectionReason.UnrecognizedHTTPVersion, version, length);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe BadHttpRequestException GetInvalidRequestException(RequestRejectionReason reason, byte* detail, int length)
|
||||
=> BadHttpRequestException.GetException(
|
||||
reason,
|
||||
_showErrorDetails
|
||||
? new Span<byte>(detail, length).GetAsciiStringEscaped(Constants.MaxExceptionDetailSize)
|
||||
: string.Empty);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class HttpProtocol : IFeatureCollection,
|
||||
IHttpRequestFeature,
|
||||
IHttpResponseFeature,
|
||||
IHttpConnectionFeature,
|
||||
IHttpRequestLifetimeFeature,
|
||||
IHttpRequestIdentifierFeature,
|
||||
IHttpBodyControlFeature,
|
||||
IHttpMaxRequestBodySizeFeature,
|
||||
IHttpMinRequestBodyDataRateFeature,
|
||||
IHttpMinResponseDataRateFeature
|
||||
{
|
||||
// NOTE: When feature interfaces are added to or removed from this HttpProtocol class implementation,
|
||||
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
||||
// See also: tools/Microsoft.AspNetCore.Server.Kestrel.GeneratedCode/HttpProtocolFeatureCollection.cs
|
||||
|
||||
private int _featureRevision;
|
||||
|
||||
private List<KeyValuePair<Type, object>> MaybeExtra;
|
||||
|
||||
public void ResetFeatureCollection()
|
||||
{
|
||||
FastReset();
|
||||
MaybeExtra?.Clear();
|
||||
_featureRevision++;
|
||||
}
|
||||
|
||||
private object ExtraFeatureGet(Type key)
|
||||
{
|
||||
if (MaybeExtra == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (var i = 0; i < MaybeExtra.Count; i++)
|
||||
{
|
||||
var kv = MaybeExtra[i];
|
||||
if (kv.Key == key)
|
||||
{
|
||||
return kv.Value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExtraFeatureSet(Type key, object value)
|
||||
{
|
||||
if (MaybeExtra == null)
|
||||
{
|
||||
MaybeExtra = new List<KeyValuePair<Type, object>>(2);
|
||||
}
|
||||
|
||||
for (var i = 0; i < MaybeExtra.Count; i++)
|
||||
{
|
||||
if (MaybeExtra[i].Key == key)
|
||||
{
|
||||
MaybeExtra[i] = new KeyValuePair<Type, object>(key, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
MaybeExtra.Add(new KeyValuePair<Type, object>(key, value));
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.Protocol
|
||||
{
|
||||
get => HttpVersion;
|
||||
set => HttpVersion = value;
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.Scheme
|
||||
{
|
||||
get => Scheme ?? "http";
|
||||
set => Scheme = value;
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.Method
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_methodText != null)
|
||||
{
|
||||
return _methodText;
|
||||
}
|
||||
|
||||
_methodText = HttpUtilities.MethodToString(Method) ?? string.Empty;
|
||||
return _methodText;
|
||||
}
|
||||
set
|
||||
{
|
||||
_methodText = value;
|
||||
}
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.PathBase
|
||||
{
|
||||
get => PathBase ?? "";
|
||||
set => PathBase = value;
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.Path
|
||||
{
|
||||
get => Path;
|
||||
set => Path = value;
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.QueryString
|
||||
{
|
||||
get => QueryString;
|
||||
set => QueryString = value;
|
||||
}
|
||||
|
||||
string IHttpRequestFeature.RawTarget
|
||||
{
|
||||
get => RawTarget;
|
||||
set => RawTarget = value;
|
||||
}
|
||||
|
||||
IHeaderDictionary IHttpRequestFeature.Headers
|
||||
{
|
||||
get => RequestHeaders;
|
||||
set => RequestHeaders = value;
|
||||
}
|
||||
|
||||
Stream IHttpRequestFeature.Body
|
||||
{
|
||||
get => RequestBody;
|
||||
set => RequestBody = value;
|
||||
}
|
||||
|
||||
int IHttpResponseFeature.StatusCode
|
||||
{
|
||||
get => StatusCode;
|
||||
set => StatusCode = value;
|
||||
}
|
||||
|
||||
string IHttpResponseFeature.ReasonPhrase
|
||||
{
|
||||
get => ReasonPhrase;
|
||||
set => ReasonPhrase = value;
|
||||
}
|
||||
|
||||
IHeaderDictionary IHttpResponseFeature.Headers
|
||||
{
|
||||
get => ResponseHeaders;
|
||||
set => ResponseHeaders = value;
|
||||
}
|
||||
|
||||
Stream IHttpResponseFeature.Body
|
||||
{
|
||||
get => ResponseBody;
|
||||
set => ResponseBody = value;
|
||||
}
|
||||
|
||||
CancellationToken IHttpRequestLifetimeFeature.RequestAborted
|
||||
{
|
||||
get => RequestAborted;
|
||||
set => RequestAborted = value;
|
||||
}
|
||||
|
||||
bool IHttpResponseFeature.HasStarted => HasResponseStarted;
|
||||
|
||||
bool IFeatureCollection.IsReadOnly => false;
|
||||
|
||||
int IFeatureCollection.Revision => _featureRevision;
|
||||
|
||||
IPAddress IHttpConnectionFeature.RemoteIpAddress
|
||||
{
|
||||
get => RemoteIpAddress;
|
||||
set => RemoteIpAddress = value;
|
||||
}
|
||||
|
||||
IPAddress IHttpConnectionFeature.LocalIpAddress
|
||||
{
|
||||
get => LocalIpAddress;
|
||||
set => LocalIpAddress = value;
|
||||
}
|
||||
|
||||
int IHttpConnectionFeature.RemotePort
|
||||
{
|
||||
get => RemotePort;
|
||||
set => RemotePort = value;
|
||||
}
|
||||
|
||||
int IHttpConnectionFeature.LocalPort
|
||||
{
|
||||
get => LocalPort;
|
||||
set => LocalPort = value;
|
||||
}
|
||||
|
||||
string IHttpConnectionFeature.ConnectionId
|
||||
{
|
||||
get => ConnectionIdFeature;
|
||||
set => ConnectionIdFeature = value;
|
||||
}
|
||||
|
||||
string IHttpRequestIdentifierFeature.TraceIdentifier
|
||||
{
|
||||
get => TraceIdentifier;
|
||||
set => TraceIdentifier = value;
|
||||
}
|
||||
|
||||
bool IHttpBodyControlFeature.AllowSynchronousIO
|
||||
{
|
||||
get => AllowSynchronousIO;
|
||||
set => AllowSynchronousIO = value;
|
||||
}
|
||||
|
||||
bool IHttpMaxRequestBodySizeFeature.IsReadOnly => HasStartedConsumingRequestBody || IsUpgraded;
|
||||
|
||||
long? IHttpMaxRequestBodySizeFeature.MaxRequestBodySize
|
||||
{
|
||||
get => MaxRequestBodySize;
|
||||
set
|
||||
{
|
||||
if (HasStartedConsumingRequestBody)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedAfterRead);
|
||||
}
|
||||
if (IsUpgraded)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.MaxRequestBodySizeCannotBeModifiedForUpgradedRequests);
|
||||
}
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), CoreStrings.NonNegativeNumberOrNullRequired);
|
||||
}
|
||||
|
||||
MaxRequestBodySize = value;
|
||||
}
|
||||
}
|
||||
|
||||
MinDataRate IHttpMinRequestBodyDataRateFeature.MinDataRate
|
||||
{
|
||||
get => MinRequestBodyDataRate;
|
||||
set => MinRequestBodyDataRate = value;
|
||||
}
|
||||
|
||||
MinDataRate IHttpMinResponseDataRateFeature.MinDataRate
|
||||
{
|
||||
get => MinResponseDataRate;
|
||||
set => MinResponseDataRate = value;
|
||||
}
|
||||
|
||||
protected void ResetIHttpUpgradeFeature()
|
||||
{
|
||||
_currentIHttpUpgradeFeature = this;
|
||||
}
|
||||
|
||||
protected void ResetIHttp2StreamIdFeature()
|
||||
{
|
||||
_currentIHttp2StreamIdFeature = this;
|
||||
}
|
||||
|
||||
void IHttpResponseFeature.OnStarting(Func<object, Task> callback, object state)
|
||||
{
|
||||
OnStarting(callback, state);
|
||||
}
|
||||
|
||||
void IHttpResponseFeature.OnCompleted(Func<object, Task> callback, object state)
|
||||
{
|
||||
OnCompleted(callback, state);
|
||||
}
|
||||
|
||||
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
|
||||
|
||||
void IHttpRequestLifetimeFeature.Abort()
|
||||
{
|
||||
Log.ApplicationAbortedConnection(ConnectionId, TraceIdentifier);
|
||||
Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByApplication));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Http.Features.Authentication;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class HttpProtocol
|
||||
{
|
||||
private static readonly Type IHttpRequestFeatureType = typeof(IHttpRequestFeature);
|
||||
private static readonly Type IHttpResponseFeatureType = typeof(IHttpResponseFeature);
|
||||
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(IHttpRequestIdentifierFeature);
|
||||
private static readonly Type IServiceProvidersFeatureType = typeof(IServiceProvidersFeature);
|
||||
private static readonly Type IHttpRequestLifetimeFeatureType = typeof(IHttpRequestLifetimeFeature);
|
||||
private static readonly Type IHttpConnectionFeatureType = typeof(IHttpConnectionFeature);
|
||||
private static readonly Type IHttpAuthenticationFeatureType = typeof(IHttpAuthenticationFeature);
|
||||
private static readonly Type IQueryFeatureType = typeof(IQueryFeature);
|
||||
private static readonly Type IFormFeatureType = typeof(IFormFeature);
|
||||
private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
|
||||
private static readonly Type IHttp2StreamIdFeatureType = typeof(IHttp2StreamIdFeature);
|
||||
private static readonly Type IResponseCookiesFeatureType = typeof(IResponseCookiesFeature);
|
||||
private static readonly Type IItemsFeatureType = typeof(IItemsFeature);
|
||||
private static readonly Type ITlsConnectionFeatureType = typeof(ITlsConnectionFeature);
|
||||
private static readonly Type IHttpWebSocketFeatureType = typeof(IHttpWebSocketFeature);
|
||||
private static readonly Type ISessionFeatureType = typeof(ISessionFeature);
|
||||
private static readonly Type IHttpMaxRequestBodySizeFeatureType = typeof(IHttpMaxRequestBodySizeFeature);
|
||||
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(IHttpMinRequestBodyDataRateFeature);
|
||||
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
|
||||
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
|
||||
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
|
||||
|
||||
private object _currentIHttpRequestFeature;
|
||||
private object _currentIHttpResponseFeature;
|
||||
private object _currentIHttpRequestIdentifierFeature;
|
||||
private object _currentIServiceProvidersFeature;
|
||||
private object _currentIHttpRequestLifetimeFeature;
|
||||
private object _currentIHttpConnectionFeature;
|
||||
private object _currentIHttpAuthenticationFeature;
|
||||
private object _currentIQueryFeature;
|
||||
private object _currentIFormFeature;
|
||||
private object _currentIHttpUpgradeFeature;
|
||||
private object _currentIHttp2StreamIdFeature;
|
||||
private object _currentIResponseCookiesFeature;
|
||||
private object _currentIItemsFeature;
|
||||
private object _currentITlsConnectionFeature;
|
||||
private object _currentIHttpWebSocketFeature;
|
||||
private object _currentISessionFeature;
|
||||
private object _currentIHttpMaxRequestBodySizeFeature;
|
||||
private object _currentIHttpMinRequestBodyDataRateFeature;
|
||||
private object _currentIHttpMinResponseDataRateFeature;
|
||||
private object _currentIHttpBodyControlFeature;
|
||||
private object _currentIHttpSendFileFeature;
|
||||
|
||||
private void FastReset()
|
||||
{
|
||||
_currentIHttpRequestFeature = this;
|
||||
_currentIHttpResponseFeature = this;
|
||||
_currentIHttpRequestIdentifierFeature = this;
|
||||
_currentIHttpRequestLifetimeFeature = this;
|
||||
_currentIHttpConnectionFeature = this;
|
||||
_currentIHttpMaxRequestBodySizeFeature = this;
|
||||
_currentIHttpMinRequestBodyDataRateFeature = this;
|
||||
_currentIHttpMinResponseDataRateFeature = this;
|
||||
_currentIHttpBodyControlFeature = this;
|
||||
|
||||
_currentIServiceProvidersFeature = null;
|
||||
_currentIHttpAuthenticationFeature = null;
|
||||
_currentIQueryFeature = null;
|
||||
_currentIFormFeature = null;
|
||||
_currentIHttpUpgradeFeature = null;
|
||||
_currentIHttp2StreamIdFeature = null;
|
||||
_currentIResponseCookiesFeature = null;
|
||||
_currentIItemsFeature = null;
|
||||
_currentITlsConnectionFeature = null;
|
||||
_currentIHttpWebSocketFeature = null;
|
||||
_currentISessionFeature = null;
|
||||
_currentIHttpSendFileFeature = null;
|
||||
}
|
||||
|
||||
object IFeatureCollection.this[Type key]
|
||||
{
|
||||
get
|
||||
{
|
||||
object feature = null;
|
||||
if (key == IHttpRequestFeatureType)
|
||||
{
|
||||
feature = _currentIHttpRequestFeature;
|
||||
}
|
||||
else if (key == IHttpResponseFeatureType)
|
||||
{
|
||||
feature = _currentIHttpResponseFeature;
|
||||
}
|
||||
else if (key == IHttpRequestIdentifierFeatureType)
|
||||
{
|
||||
feature = _currentIHttpRequestIdentifierFeature;
|
||||
}
|
||||
else if (key == IServiceProvidersFeatureType)
|
||||
{
|
||||
feature = _currentIServiceProvidersFeature;
|
||||
}
|
||||
else if (key == IHttpRequestLifetimeFeatureType)
|
||||
{
|
||||
feature = _currentIHttpRequestLifetimeFeature;
|
||||
}
|
||||
else if (key == IHttpConnectionFeatureType)
|
||||
{
|
||||
feature = _currentIHttpConnectionFeature;
|
||||
}
|
||||
else if (key == IHttpAuthenticationFeatureType)
|
||||
{
|
||||
feature = _currentIHttpAuthenticationFeature;
|
||||
}
|
||||
else if (key == IQueryFeatureType)
|
||||
{
|
||||
feature = _currentIQueryFeature;
|
||||
}
|
||||
else if (key == IFormFeatureType)
|
||||
{
|
||||
feature = _currentIFormFeature;
|
||||
}
|
||||
else if (key == IHttpUpgradeFeatureType)
|
||||
{
|
||||
feature = _currentIHttpUpgradeFeature;
|
||||
}
|
||||
else if (key == IHttp2StreamIdFeatureType)
|
||||
{
|
||||
feature = _currentIHttp2StreamIdFeature;
|
||||
}
|
||||
else if (key == IResponseCookiesFeatureType)
|
||||
{
|
||||
feature = _currentIResponseCookiesFeature;
|
||||
}
|
||||
else if (key == IItemsFeatureType)
|
||||
{
|
||||
feature = _currentIItemsFeature;
|
||||
}
|
||||
else if (key == ITlsConnectionFeatureType)
|
||||
{
|
||||
feature = _currentITlsConnectionFeature;
|
||||
}
|
||||
else if (key == IHttpWebSocketFeatureType)
|
||||
{
|
||||
feature = _currentIHttpWebSocketFeature;
|
||||
}
|
||||
else if (key == ISessionFeatureType)
|
||||
{
|
||||
feature = _currentISessionFeature;
|
||||
}
|
||||
else if (key == IHttpMaxRequestBodySizeFeatureType)
|
||||
{
|
||||
feature = _currentIHttpMaxRequestBodySizeFeature;
|
||||
}
|
||||
else if (key == IHttpMinRequestBodyDataRateFeatureType)
|
||||
{
|
||||
feature = _currentIHttpMinRequestBodyDataRateFeature;
|
||||
}
|
||||
else if (key == IHttpMinResponseDataRateFeatureType)
|
||||
{
|
||||
feature = _currentIHttpMinResponseDataRateFeature;
|
||||
}
|
||||
else if (key == IHttpBodyControlFeatureType)
|
||||
{
|
||||
feature = _currentIHttpBodyControlFeature;
|
||||
}
|
||||
else if (key == IHttpSendFileFeatureType)
|
||||
{
|
||||
feature = _currentIHttpSendFileFeature;
|
||||
}
|
||||
else if (MaybeExtra != null)
|
||||
{
|
||||
feature = ExtraFeatureGet(key);
|
||||
}
|
||||
|
||||
return feature ?? ConnectionFeatures[key];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_featureRevision++;
|
||||
|
||||
if (key == IHttpRequestFeatureType)
|
||||
{
|
||||
_currentIHttpRequestFeature = value;
|
||||
}
|
||||
else if (key == IHttpResponseFeatureType)
|
||||
{
|
||||
_currentIHttpResponseFeature = value;
|
||||
}
|
||||
else if (key == IHttpRequestIdentifierFeatureType)
|
||||
{
|
||||
_currentIHttpRequestIdentifierFeature = value;
|
||||
}
|
||||
else if (key == IServiceProvidersFeatureType)
|
||||
{
|
||||
_currentIServiceProvidersFeature = value;
|
||||
}
|
||||
else if (key == IHttpRequestLifetimeFeatureType)
|
||||
{
|
||||
_currentIHttpRequestLifetimeFeature = value;
|
||||
}
|
||||
else if (key == IHttpConnectionFeatureType)
|
||||
{
|
||||
_currentIHttpConnectionFeature = value;
|
||||
}
|
||||
else if (key == IHttpAuthenticationFeatureType)
|
||||
{
|
||||
_currentIHttpAuthenticationFeature = value;
|
||||
}
|
||||
else if (key == IQueryFeatureType)
|
||||
{
|
||||
_currentIQueryFeature = value;
|
||||
}
|
||||
else if (key == IFormFeatureType)
|
||||
{
|
||||
_currentIFormFeature = value;
|
||||
}
|
||||
else if (key == IHttpUpgradeFeatureType)
|
||||
{
|
||||
_currentIHttpUpgradeFeature = value;
|
||||
}
|
||||
else if (key == IHttp2StreamIdFeatureType)
|
||||
{
|
||||
_currentIHttp2StreamIdFeature = value;
|
||||
}
|
||||
else if (key == IResponseCookiesFeatureType)
|
||||
{
|
||||
_currentIResponseCookiesFeature = value;
|
||||
}
|
||||
else if (key == IItemsFeatureType)
|
||||
{
|
||||
_currentIItemsFeature = value;
|
||||
}
|
||||
else if (key == ITlsConnectionFeatureType)
|
||||
{
|
||||
_currentITlsConnectionFeature = value;
|
||||
}
|
||||
else if (key == IHttpWebSocketFeatureType)
|
||||
{
|
||||
_currentIHttpWebSocketFeature = value;
|
||||
}
|
||||
else if (key == ISessionFeatureType)
|
||||
{
|
||||
_currentISessionFeature = value;
|
||||
}
|
||||
else if (key == IHttpMaxRequestBodySizeFeatureType)
|
||||
{
|
||||
_currentIHttpMaxRequestBodySizeFeature = value;
|
||||
}
|
||||
else if (key == IHttpMinRequestBodyDataRateFeatureType)
|
||||
{
|
||||
_currentIHttpMinRequestBodyDataRateFeature = value;
|
||||
}
|
||||
else if (key == IHttpMinResponseDataRateFeatureType)
|
||||
{
|
||||
_currentIHttpMinResponseDataRateFeature = value;
|
||||
}
|
||||
else if (key == IHttpBodyControlFeatureType)
|
||||
{
|
||||
_currentIHttpBodyControlFeature = value;
|
||||
}
|
||||
else if (key == IHttpSendFileFeatureType)
|
||||
{
|
||||
_currentIHttpSendFileFeature = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExtraFeatureSet(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IFeatureCollection.Set<TFeature>(TFeature feature)
|
||||
{
|
||||
_featureRevision++;
|
||||
if (typeof(TFeature) == typeof(IHttpRequestFeature))
|
||||
{
|
||||
_currentIHttpRequestFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpResponseFeature))
|
||||
{
|
||||
_currentIHttpResponseFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
||||
{
|
||||
_currentIHttpRequestIdentifierFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IServiceProvidersFeature))
|
||||
{
|
||||
_currentIServiceProvidersFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpRequestLifetimeFeature))
|
||||
{
|
||||
_currentIHttpRequestLifetimeFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpConnectionFeature))
|
||||
{
|
||||
_currentIHttpConnectionFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpAuthenticationFeature))
|
||||
{
|
||||
_currentIHttpAuthenticationFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IQueryFeature))
|
||||
{
|
||||
_currentIQueryFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IFormFeature))
|
||||
{
|
||||
_currentIFormFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpUpgradeFeature))
|
||||
{
|
||||
_currentIHttpUpgradeFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttp2StreamIdFeature))
|
||||
{
|
||||
_currentIHttp2StreamIdFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
|
||||
{
|
||||
_currentIResponseCookiesFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IItemsFeature))
|
||||
{
|
||||
_currentIItemsFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(ITlsConnectionFeature))
|
||||
{
|
||||
_currentITlsConnectionFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpWebSocketFeature))
|
||||
{
|
||||
_currentIHttpWebSocketFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(ISessionFeature))
|
||||
{
|
||||
_currentISessionFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMaxRequestBodySizeFeature))
|
||||
{
|
||||
_currentIHttpMaxRequestBodySizeFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMinRequestBodyDataRateFeature))
|
||||
{
|
||||
_currentIHttpMinRequestBodyDataRateFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMinResponseDataRateFeature))
|
||||
{
|
||||
_currentIHttpMinResponseDataRateFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpBodyControlFeature))
|
||||
{
|
||||
_currentIHttpBodyControlFeature = feature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
||||
{
|
||||
_currentIHttpSendFileFeature = feature;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExtraFeatureSet(typeof(TFeature), feature);
|
||||
}
|
||||
}
|
||||
|
||||
TFeature IFeatureCollection.Get<TFeature>()
|
||||
{
|
||||
TFeature feature = default;
|
||||
if (typeof(TFeature) == typeof(IHttpRequestFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpRequestFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpResponseFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpResponseFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpRequestIdentifierFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpRequestIdentifierFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IServiceProvidersFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIServiceProvidersFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpRequestLifetimeFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpRequestLifetimeFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpConnectionFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpConnectionFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpAuthenticationFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpAuthenticationFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IQueryFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIQueryFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IFormFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIFormFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpUpgradeFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpUpgradeFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttp2StreamIdFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttp2StreamIdFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IResponseCookiesFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIResponseCookiesFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IItemsFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIItemsFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(ITlsConnectionFeature))
|
||||
{
|
||||
feature = (TFeature)_currentITlsConnectionFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpWebSocketFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpWebSocketFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(ISessionFeature))
|
||||
{
|
||||
feature = (TFeature)_currentISessionFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMaxRequestBodySizeFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpMaxRequestBodySizeFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMinRequestBodyDataRateFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpMinRequestBodyDataRateFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpMinResponseDataRateFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpMinResponseDataRateFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpBodyControlFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpBodyControlFeature;
|
||||
}
|
||||
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
|
||||
{
|
||||
feature = (TFeature)_currentIHttpSendFileFeature;
|
||||
}
|
||||
else if (MaybeExtra != null)
|
||||
{
|
||||
feature = (TFeature)(ExtraFeatureGet(typeof(TFeature)));
|
||||
}
|
||||
|
||||
if (feature == null)
|
||||
{
|
||||
feature = ConnectionFeatures.Get<TFeature>();
|
||||
}
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
|
||||
{
|
||||
if (_currentIHttpRequestFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpRequestFeatureType, _currentIHttpRequestFeature as IHttpRequestFeature);
|
||||
}
|
||||
if (_currentIHttpResponseFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature as IHttpResponseFeature);
|
||||
}
|
||||
if (_currentIHttpRequestIdentifierFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpRequestIdentifierFeatureType, _currentIHttpRequestIdentifierFeature as IHttpRequestIdentifierFeature);
|
||||
}
|
||||
if (_currentIServiceProvidersFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IServiceProvidersFeatureType, _currentIServiceProvidersFeature as IServiceProvidersFeature);
|
||||
}
|
||||
if (_currentIHttpRequestLifetimeFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpRequestLifetimeFeatureType, _currentIHttpRequestLifetimeFeature as IHttpRequestLifetimeFeature);
|
||||
}
|
||||
if (_currentIHttpConnectionFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpConnectionFeatureType, _currentIHttpConnectionFeature as IHttpConnectionFeature);
|
||||
}
|
||||
if (_currentIHttpAuthenticationFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpAuthenticationFeatureType, _currentIHttpAuthenticationFeature as IHttpAuthenticationFeature);
|
||||
}
|
||||
if (_currentIQueryFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IQueryFeatureType, _currentIQueryFeature as IQueryFeature);
|
||||
}
|
||||
if (_currentIFormFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IFormFeatureType, _currentIFormFeature as IFormFeature);
|
||||
}
|
||||
if (_currentIHttpUpgradeFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpUpgradeFeatureType, _currentIHttpUpgradeFeature as IHttpUpgradeFeature);
|
||||
}
|
||||
if (_currentIHttp2StreamIdFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttp2StreamIdFeatureType, _currentIHttp2StreamIdFeature as IHttp2StreamIdFeature);
|
||||
}
|
||||
if (_currentIResponseCookiesFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IResponseCookiesFeatureType, _currentIResponseCookiesFeature as IResponseCookiesFeature);
|
||||
}
|
||||
if (_currentIItemsFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IItemsFeatureType, _currentIItemsFeature as IItemsFeature);
|
||||
}
|
||||
if (_currentITlsConnectionFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(ITlsConnectionFeatureType, _currentITlsConnectionFeature as ITlsConnectionFeature);
|
||||
}
|
||||
if (_currentIHttpWebSocketFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpWebSocketFeatureType, _currentIHttpWebSocketFeature as IHttpWebSocketFeature);
|
||||
}
|
||||
if (_currentISessionFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(ISessionFeatureType, _currentISessionFeature as ISessionFeature);
|
||||
}
|
||||
if (_currentIHttpMaxRequestBodySizeFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpMaxRequestBodySizeFeatureType, _currentIHttpMaxRequestBodySizeFeature as IHttpMaxRequestBodySizeFeature);
|
||||
}
|
||||
if (_currentIHttpMinRequestBodyDataRateFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpMinRequestBodyDataRateFeatureType, _currentIHttpMinRequestBodyDataRateFeature as IHttpMinRequestBodyDataRateFeature);
|
||||
}
|
||||
if (_currentIHttpMinResponseDataRateFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpMinResponseDataRateFeatureType, _currentIHttpMinResponseDataRateFeature as IHttpMinResponseDataRateFeature);
|
||||
}
|
||||
if (_currentIHttpBodyControlFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as IHttpBodyControlFeature);
|
||||
}
|
||||
if (_currentIHttpSendFileFeature != null)
|
||||
{
|
||||
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as IHttpSendFileFeature);
|
||||
}
|
||||
|
||||
if (MaybeExtra != null)
|
||||
{
|
||||
foreach(var item in MaybeExtra)
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,104 @@
|
|||
// 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class HttpRequestHeaders : HttpHeaders
|
||||
{
|
||||
private static long ParseContentLength(string value)
|
||||
{
|
||||
long parsed;
|
||||
if (!HeaderUtilities.TryParseNonNegativeInt64(value, out parsed))
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidContentLength, value);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void SetValueUnknown(string key, in StringValues value)
|
||||
{
|
||||
Unknown[key] = value;
|
||||
}
|
||||
|
||||
public unsafe void Append(Span<byte> name, string value)
|
||||
{
|
||||
fixed (byte* namePtr = &MemoryMarshal.GetReference(name))
|
||||
{
|
||||
Append(namePtr, name.Length, value);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe void AppendUnknownHeaders(byte* pKeyBytes, int keyLength, string value)
|
||||
{
|
||||
string key = new string('\0', keyLength);
|
||||
fixed (char* keyBuffer = key)
|
||||
{
|
||||
if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, keyLength))
|
||||
{
|
||||
BadHttpRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
|
||||
}
|
||||
}
|
||||
|
||||
StringValues existing;
|
||||
Unknown.TryGetValue(key, out existing);
|
||||
Unknown[key] = AppendValue(existing, value);
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
protected override IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>
|
||||
{
|
||||
private readonly HttpRequestHeaders _collection;
|
||||
private readonly long _bits;
|
||||
private int _state;
|
||||
private KeyValuePair<string, StringValues> _current;
|
||||
private readonly bool _hasUnknown;
|
||||
private Dictionary<string, StringValues>.Enumerator _unknownEnumerator;
|
||||
|
||||
internal Enumerator(HttpRequestHeaders collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_bits = collection._bits;
|
||||
_state = 0;
|
||||
_current = default(KeyValuePair<string, StringValues>);
|
||||
_hasUnknown = collection.MaybeUnknown != null;
|
||||
_unknownEnumerator = _hasUnknown
|
||||
? collection.MaybeUnknown.GetEnumerator()
|
||||
: default(Dictionary<string, StringValues>.Enumerator);
|
||||
}
|
||||
|
||||
public KeyValuePair<string, StringValues> Current => _current;
|
||||
|
||||
object IEnumerator.Current => _current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_state = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
// 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.IO;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
internal class HttpRequestStream : ReadOnlyStream
|
||||
{
|
||||
private readonly IHttpBodyControlFeature _bodyControl;
|
||||
private MessageBody _body;
|
||||
private HttpStreamState _state;
|
||||
private Exception _error;
|
||||
|
||||
public HttpRequestStream(IHttpBodyControlFeature bodyControl)
|
||||
{
|
||||
_bodyControl = bodyControl;
|
||||
_state = HttpStreamState.Closed;
|
||||
}
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override long Length
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!_bodyControl.AllowSynchronousIO)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.SynchronousReadsDisallowed);
|
||||
}
|
||||
|
||||
return ReadAsync(buffer, offset, count).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = ReadAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return ((Task<int>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<int>(state);
|
||||
var task = ReadAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<int>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(task2.Result);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return ReadAsyncInternal(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return ReadAsyncInternal(destination, cancellationToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
private async ValueTask<int> ReadAsyncInternal(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _body.ReadAsync(buffer, cancellationToken);
|
||||
}
|
||||
catch (ConnectionAbortedException ex)
|
||||
{
|
||||
throw new TaskCanceledException("The request was aborted", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
}
|
||||
if (bufferSize <= 0)
|
||||
{
|
||||
throw new ArgumentException(CoreStrings.PositiveNumberRequired, nameof(bufferSize));
|
||||
}
|
||||
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return CopyToAsyncInternal(destination, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task CopyToAsyncInternal(Stream destination, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _body.CopyToAsync(destination, cancellationToken);
|
||||
}
|
||||
catch (ConnectionAbortedException ex)
|
||||
{
|
||||
throw new TaskCanceledException("The request was aborted", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartAcceptingReads(MessageBody body)
|
||||
{
|
||||
// Only start if not aborted
|
||||
if (_state == HttpStreamState.Closed)
|
||||
{
|
||||
_state = HttpStreamState.Open;
|
||||
_body = body;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAcceptingReads()
|
||||
{
|
||||
// Can't use dispose (or close) as can be disposed too early by user code
|
||||
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
|
||||
_state = HttpStreamState.Closed;
|
||||
_body = null;
|
||||
}
|
||||
|
||||
public void Abort(Exception error = null)
|
||||
{
|
||||
// We don't want to throw an ODE until the app func actually completes.
|
||||
// If the request is aborted, we throw a TaskCanceledException instead,
|
||||
// unless error is not null, in which case we throw it.
|
||||
if (_state != HttpStreamState.Closed)
|
||||
{
|
||||
_state = HttpStreamState.Aborted;
|
||||
_error = error;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateState(CancellationToken cancellationToken)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case HttpStreamState.Open:
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
break;
|
||||
case HttpStreamState.Closed:
|
||||
throw new ObjectDisposedException(nameof(HttpRequestStream));
|
||||
case HttpStreamState.Aborted:
|
||||
if (_error != null)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(_error).Throw();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new TaskCanceledException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum HttpRequestTarget
|
||||
{
|
||||
Unknown = -1,
|
||||
// origin-form is the most common
|
||||
OriginForm,
|
||||
AbsoluteForm,
|
||||
AuthorityForm,
|
||||
AsteriskForm
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public partial class HttpResponseHeaders : HttpHeaders
|
||||
{
|
||||
private static readonly byte[] _CrLf = new[] { (byte)'\r', (byte)'\n' };
|
||||
private static readonly byte[] _colonSpace = new[] { (byte)':', (byte)' ' };
|
||||
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
protected override IEnumerator<KeyValuePair<string, StringValues>> GetEnumeratorFast()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
internal void CopyTo(ref BufferWriter<PipeWriter> buffer)
|
||||
{
|
||||
CopyToFast(ref buffer);
|
||||
if (MaybeUnknown != null)
|
||||
{
|
||||
foreach (var kv in MaybeUnknown)
|
||||
{
|
||||
foreach (var value in kv.Value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
buffer.Write(_CrLf);
|
||||
PipelineExtensions.WriteAsciiNoValidation(ref buffer, kv.Key);
|
||||
buffer.Write(_colonSpace);
|
||||
PipelineExtensions.WriteAsciiNoValidation(ref buffer, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long ParseContentLength(string value)
|
||||
{
|
||||
long parsed;
|
||||
if (!HeaderUtilities.TryParseNonNegativeInt64(value, out parsed))
|
||||
{
|
||||
ThrowInvalidContentLengthException(value);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void SetValueUnknown(string key, in StringValues value)
|
||||
{
|
||||
ValidateHeaderCharacters(key);
|
||||
Unknown[key] = value;
|
||||
}
|
||||
|
||||
private static void ThrowInvalidContentLengthException(string value)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.FormatInvalidContentLength_InvalidNumber(value));
|
||||
}
|
||||
|
||||
public partial struct Enumerator : IEnumerator<KeyValuePair<string, StringValues>>
|
||||
{
|
||||
private readonly HttpResponseHeaders _collection;
|
||||
private readonly long _bits;
|
||||
private int _state;
|
||||
private KeyValuePair<string, StringValues> _current;
|
||||
private readonly bool _hasUnknown;
|
||||
private Dictionary<string, StringValues>.Enumerator _unknownEnumerator;
|
||||
|
||||
internal Enumerator(HttpResponseHeaders collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_bits = collection._bits;
|
||||
_state = 0;
|
||||
_current = default;
|
||||
_hasUnknown = collection.MaybeUnknown != null;
|
||||
_unknownEnumerator = _hasUnknown
|
||||
? collection.MaybeUnknown.GetEnumerator()
|
||||
: default;
|
||||
}
|
||||
|
||||
public KeyValuePair<string, StringValues> Current => _current;
|
||||
|
||||
object IEnumerator.Current => _current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_state = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
internal class HttpResponseStream : WriteOnlyStream
|
||||
{
|
||||
private readonly IHttpBodyControlFeature _bodyControl;
|
||||
private readonly IHttpResponseControl _httpResponseControl;
|
||||
private HttpStreamState _state;
|
||||
|
||||
public HttpResponseStream(IHttpBodyControlFeature bodyControl, IHttpResponseControl httpResponseControl)
|
||||
{
|
||||
_bodyControl = bodyControl;
|
||||
_httpResponseControl = httpResponseControl;
|
||||
_state = HttpStreamState.Closed;
|
||||
}
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override long Length
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
FlushAsync(default(CancellationToken)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return _httpResponseControl.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (!_bodyControl.AllowSynchronousIO)
|
||||
{
|
||||
throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
|
||||
}
|
||||
|
||||
WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
var task = WriteAsync(buffer, offset, count, default(CancellationToken), state);
|
||||
if (callback != null)
|
||||
{
|
||||
task.ContinueWith(t => callback.Invoke(t));
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
((Task<object>)asyncResult).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>(state);
|
||||
var task = WriteAsync(buffer, offset, count, cancellationToken);
|
||||
task.ContinueWith((task2, state2) =>
|
||||
{
|
||||
var tcs2 = (TaskCompletionSource<object>)state2;
|
||||
if (task2.IsCanceled)
|
||||
{
|
||||
tcs2.SetCanceled();
|
||||
}
|
||||
else if (task2.IsFaulted)
|
||||
{
|
||||
tcs2.SetException(task2.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs2.SetResult(null);
|
||||
}
|
||||
}, tcs, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return _httpResponseControl.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ValidateState(cancellationToken);
|
||||
|
||||
return new ValueTask(_httpResponseControl.WriteAsync(source, cancellationToken));
|
||||
}
|
||||
#endif
|
||||
|
||||
public void StartAcceptingWrites()
|
||||
{
|
||||
// Only start if not aborted
|
||||
if (_state == HttpStreamState.Closed)
|
||||
{
|
||||
_state = HttpStreamState.Open;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAcceptingWrites()
|
||||
{
|
||||
// Can't use dispose (or close) as can be disposed too early by user code
|
||||
// As exampled in EngineTests.ZeroContentLengthNotSetAutomaticallyForCertainStatusCodes
|
||||
_state = HttpStreamState.Closed;
|
||||
}
|
||||
|
||||
public void Abort()
|
||||
{
|
||||
// We don't want to throw an ODE until the app func actually completes.
|
||||
if (_state != HttpStreamState.Closed)
|
||||
{
|
||||
_state = HttpStreamState.Aborted;
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateState(CancellationToken cancellationToken)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case HttpStreamState.Open:
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
break;
|
||||
case HttpStreamState.Closed:
|
||||
throw new ObjectDisposedException(nameof(HttpResponseStream), CoreStrings.WritingToResponseBodyAfterResponseCompleted);
|
||||
case HttpStreamState.Aborted:
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Aborted state only throws on write if cancellationToken requests it
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum HttpScheme
|
||||
{
|
||||
Unknown = -1,
|
||||
Http = 0,
|
||||
Https = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
enum HttpStreamState
|
||||
{
|
||||
Open,
|
||||
Closed,
|
||||
Aborted
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
internal class HttpUpgradeStream : Stream
|
||||
{
|
||||
private readonly Stream _requestStream;
|
||||
private readonly Stream _responseStream;
|
||||
|
||||
public HttpUpgradeStream(Stream requestStream, Stream responseStream)
|
||||
{
|
||||
_requestStream = requestStream;
|
||||
_responseStream = responseStream;
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
return _requestStream.CanRead;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get
|
||||
{
|
||||
return _requestStream.CanSeek;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return _responseStream.CanTimeout || _requestStream.CanTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
return _responseStream.CanWrite;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return _requestStream.Length;
|
||||
}
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get
|
||||
{
|
||||
return _requestStream.Position;
|
||||
}
|
||||
set
|
||||
{
|
||||
_requestStream.Position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override int ReadTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return _requestStream.ReadTimeout;
|
||||
}
|
||||
set
|
||||
{
|
||||
_requestStream.ReadTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override int WriteTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
return _responseStream.WriteTimeout;
|
||||
}
|
||||
set
|
||||
{
|
||||
_responseStream.WriteTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_requestStream.Dispose();
|
||||
_responseStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_responseStream.Flush();
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _responseStream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
_requestStream.Close();
|
||||
_responseStream.Close();
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
return _requestStream.BeginRead(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override int EndRead(IAsyncResult asyncResult)
|
||||
{
|
||||
return _requestStream.EndRead(asyncResult);
|
||||
}
|
||||
|
||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||
{
|
||||
return _responseStream.BeginWrite(buffer, offset, count, callback, state);
|
||||
}
|
||||
|
||||
public override void EndWrite(IAsyncResult asyncResult)
|
||||
{
|
||||
_responseStream.EndWrite(asyncResult);
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return _requestStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _requestStream.ReadAsync(destination, cancellationToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
|
||||
{
|
||||
return _requestStream.CopyToAsync(destination, bufferSize, cancellationToken);
|
||||
}
|
||||
|
||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
return _responseStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
#if NETCOREAPP2_1
|
||||
public override ValueTask WriteAsync(ReadOnlyMemory<byte> source, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _responseStream.WriteAsync(source, cancellationToken);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return _requestStream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
_requestStream.SetLength(value);
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return _requestStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
return _requestStream.ReadByte();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
_responseStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
_responseStream.WriteByte(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum HttpVersion
|
||||
{
|
||||
Unknown = -1,
|
||||
Http10 = 0,
|
||||
Http11 = 1,
|
||||
Http2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpHeadersHandler
|
||||
{
|
||||
void OnHeader(Span<byte> name, Span<byte> value);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Connections;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpOutputProducer : IDisposable
|
||||
{
|
||||
void Abort(ConnectionAbortedException abortReason);
|
||||
Task WriteAsync<T>(Func<PipeWriter, T, long> callback, T state);
|
||||
Task FlushAsync(CancellationToken cancellationToken);
|
||||
Task Write100ContinueAsync(CancellationToken cancellationToken);
|
||||
void WriteResponseHeaders(int statusCode, string ReasonPhrase, HttpResponseHeaders responseHeaders);
|
||||
// The reason this is ReadOnlySpan and not ReadOnlyMemory is because writes are always
|
||||
// synchronous. Flushing to get back pressure is the only time we truly go async but
|
||||
// that's after the buffer is copied
|
||||
Task WriteDataAsync(ReadOnlySpan<byte> data, CancellationToken cancellationToken);
|
||||
Task WriteStreamSuffixAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpParser<TRequestHandler> where TRequestHandler : IHttpHeadersHandler, IHttpRequestLineHandler
|
||||
{
|
||||
bool ParseRequestLine(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined);
|
||||
|
||||
bool ParseHeaders(TRequestHandler handler, in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined, out int consumedBytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// 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.Net;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpProtocolContext
|
||||
{
|
||||
string ConnectionId { get; set; }
|
||||
ServiceContext ServiceContext { get; set; }
|
||||
IFeatureCollection ConnectionFeatures { get; set; }
|
||||
MemoryPool<byte> MemoryPool { get; set; }
|
||||
IPEndPoint RemoteEndPoint { get; set; }
|
||||
IPEndPoint LocalEndPoint { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpRequestLineHandler
|
||||
{
|
||||
void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public interface IHttpResponseControl
|
||||
{
|
||||
void ProduceContinue();
|
||||
Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);
|
||||
Task FlushAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public abstract class MessageBody
|
||||
{
|
||||
private static readonly MessageBody _zeroContentLengthClose = new ForZeroContentLength(keepAlive: false);
|
||||
private static readonly MessageBody _zeroContentLengthKeepAlive = new ForZeroContentLength(keepAlive: true);
|
||||
|
||||
private readonly HttpProtocol _context;
|
||||
|
||||
private bool _send100Continue = true;
|
||||
|
||||
protected MessageBody(HttpProtocol context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
|
||||
|
||||
public static MessageBody ZeroContentLengthKeepAlive => _zeroContentLengthKeepAlive;
|
||||
|
||||
public bool RequestKeepAlive { get; protected set; }
|
||||
|
||||
public bool RequestUpgrade { get; protected set; }
|
||||
|
||||
public virtual bool IsEmpty => false;
|
||||
|
||||
protected IKestrelTrace Log => _context.ServiceContext.Log;
|
||||
|
||||
public virtual async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
TryInit();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await _context.RequestBodyPipe.Reader.ReadAsync();
|
||||
var readableBuffer = result.Buffer;
|
||||
var consumed = readableBuffer.End;
|
||||
|
||||
try
|
||||
{
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
// buffer.Count is int
|
||||
var actual = (int) Math.Min(readableBuffer.Length, buffer.Length);
|
||||
var slice = readableBuffer.Slice(0, actual);
|
||||
consumed = readableBuffer.GetPosition(actual);
|
||||
slice.CopyTo(buffer.Span);
|
||||
return actual;
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context.RequestBodyPipe.Reader.AdvanceTo(consumed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
TryInit();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await _context.RequestBodyPipe.Reader.ReadAsync();
|
||||
var readableBuffer = result.Buffer;
|
||||
var consumed = readableBuffer.End;
|
||||
|
||||
try
|
||||
{
|
||||
if (!readableBuffer.IsEmpty)
|
||||
{
|
||||
foreach (var memory in readableBuffer)
|
||||
{
|
||||
// REVIEW: This *could* be slower if 2 things are true
|
||||
// - The WriteAsync(ReadOnlyMemory<byte>) isn't overridden on the destination
|
||||
// - We change the Kestrel Memory Pool to not use pinned arrays but instead use native memory
|
||||
#if NETCOREAPP2_1
|
||||
await destination.WriteAsync(memory);
|
||||
#else
|
||||
var array = memory.GetArray();
|
||||
await destination.WriteAsync(array.Array, array.Offset, array.Count, cancellationToken);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_context.RequestBodyPipe.Reader.AdvanceTo(consumed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Task ConsumeAsync()
|
||||
{
|
||||
TryInit();
|
||||
|
||||
return OnConsumeAsync();
|
||||
}
|
||||
|
||||
protected abstract Task OnConsumeAsync();
|
||||
|
||||
public abstract Task StopAsync();
|
||||
|
||||
protected void TryProduceContinue()
|
||||
{
|
||||
if (_send100Continue)
|
||||
{
|
||||
_context.HttpResponseControl.ProduceContinue();
|
||||
_send100Continue = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryInit()
|
||||
{
|
||||
if (!_context.HasStartedConsumingRequestBody)
|
||||
{
|
||||
OnReadStarting();
|
||||
_context.HasStartedConsumingRequestBody = true;
|
||||
OnReadStarted();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnReadStarting()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnReadStarted()
|
||||
{
|
||||
}
|
||||
|
||||
private class ForZeroContentLength : MessageBody
|
||||
{
|
||||
public ForZeroContentLength(bool keepAlive)
|
||||
: base(null)
|
||||
{
|
||||
RequestKeepAlive = keepAlive;
|
||||
}
|
||||
|
||||
public override bool IsEmpty => true;
|
||||
|
||||
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default(CancellationToken)) => new ValueTask<int>(0);
|
||||
|
||||
public override Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default(CancellationToken)) => Task.CompletedTask;
|
||||
|
||||
public override Task ConsumeAsync() => Task.CompletedTask;
|
||||
|
||||
public override Task StopAsync() => Task.CompletedTask;
|
||||
|
||||
protected override Task OnConsumeAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public static class PathNormalizer
|
||||
{
|
||||
private const byte ByteSlash = (byte)'/';
|
||||
private const byte ByteDot = (byte)'.';
|
||||
|
||||
// In-place implementation of the algorithm from https://tools.ietf.org/html/rfc3986#section-5.2.4
|
||||
public static unsafe int RemoveDotSegments(Span<byte> input)
|
||||
{
|
||||
fixed (byte* start = &MemoryMarshal.GetReference(input))
|
||||
{
|
||||
var end = start + input.Length;
|
||||
return RemoveDotSegments(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe int RemoveDotSegments(byte* start, byte* end)
|
||||
{
|
||||
if (!ContainsDotSegments(start, end))
|
||||
{
|
||||
return (int)(end - start);
|
||||
}
|
||||
|
||||
var src = start;
|
||||
var dst = start;
|
||||
|
||||
while (src < end)
|
||||
{
|
||||
var ch1 = *src;
|
||||
Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");
|
||||
|
||||
byte ch2, ch3, ch4;
|
||||
|
||||
switch (end - src)
|
||||
{
|
||||
case 1:
|
||||
break;
|
||||
case 2:
|
||||
ch2 = *(src + 1);
|
||||
|
||||
if (ch2 == ByteDot)
|
||||
{
|
||||
// B. if the input buffer begins with a prefix of "/./" or "/.",
|
||||
// where "." is a complete path segment, then replace that
|
||||
// prefix with "/" in the input buffer; otherwise,
|
||||
src += 1;
|
||||
*src = ByteSlash;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case 3:
|
||||
ch2 = *(src + 1);
|
||||
ch3 = *(src + 2);
|
||||
|
||||
if (ch2 == ByteDot && ch3 == ByteDot)
|
||||
{
|
||||
// C. if the input buffer begins with a prefix of "/../" or "/..",
|
||||
// where ".." is a complete path segment, then replace that
|
||||
// prefix with "/" in the input buffer and remove the last
|
||||
// segment and its preceding "/" (if any) from the output
|
||||
// buffer; otherwise,
|
||||
src += 2;
|
||||
*src = ByteSlash;
|
||||
|
||||
if (dst > start)
|
||||
{
|
||||
do
|
||||
{
|
||||
dst--;
|
||||
} while (dst > start && *dst != ByteSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (ch2 == ByteDot && ch3 == ByteSlash)
|
||||
{
|
||||
// B. if the input buffer begins with a prefix of "/./" or "/.",
|
||||
// where "." is a complete path segment, then replace that
|
||||
// prefix with "/" in the input buffer; otherwise,
|
||||
src += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
ch2 = *(src + 1);
|
||||
ch3 = *(src + 2);
|
||||
ch4 = *(src + 3);
|
||||
|
||||
if (ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash)
|
||||
{
|
||||
// C. if the input buffer begins with a prefix of "/../" or "/..",
|
||||
// where ".." is a complete path segment, then replace that
|
||||
// prefix with "/" in the input buffer and remove the last
|
||||
// segment and its preceding "/" (if any) from the output
|
||||
// buffer; otherwise,
|
||||
src += 3;
|
||||
|
||||
if (dst > start)
|
||||
{
|
||||
do
|
||||
{
|
||||
dst--;
|
||||
} while (dst > start && *dst != ByteSlash);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (ch2 == ByteDot && ch3 == ByteSlash)
|
||||
{
|
||||
// B. if the input buffer begins with a prefix of "/./" or "/.",
|
||||
// where "." is a complete path segment, then replace that
|
||||
// prefix with "/" in the input buffer; otherwise,
|
||||
src += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// E. move the first path segment in the input buffer to the end of
|
||||
// the output buffer, including the initial "/" character (if
|
||||
// any) and any subsequent characters up to, but not including,
|
||||
// the next "/" character or the end of the input buffer.
|
||||
do
|
||||
{
|
||||
*dst++ = ch1;
|
||||
ch1 = *++src;
|
||||
} while (src < end && ch1 != ByteSlash);
|
||||
}
|
||||
|
||||
if (dst == start)
|
||||
{
|
||||
*dst++ = ByteSlash;
|
||||
}
|
||||
|
||||
return (int)(dst - start);
|
||||
}
|
||||
|
||||
public static unsafe bool ContainsDotSegments(byte* start, byte* end)
|
||||
{
|
||||
var src = start;
|
||||
var dst = start;
|
||||
|
||||
while (src < end)
|
||||
{
|
||||
var ch1 = *src;
|
||||
Debug.Assert(ch1 == '/', "Path segment must always start with a '/'");
|
||||
|
||||
byte ch2, ch3, ch4;
|
||||
|
||||
switch (end - src)
|
||||
{
|
||||
case 1:
|
||||
break;
|
||||
case 2:
|
||||
ch2 = *(src + 1);
|
||||
|
||||
if (ch2 == ByteDot)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
case 3:
|
||||
ch2 = *(src + 1);
|
||||
ch3 = *(src + 2);
|
||||
|
||||
if ((ch2 == ByteDot && ch3 == ByteDot) ||
|
||||
(ch2 == ByteDot && ch3 == ByteSlash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
ch2 = *(src + 1);
|
||||
ch3 = *(src + 2);
|
||||
ch4 = *(src + 3);
|
||||
|
||||
if ((ch2 == ByteDot && ch3 == ByteDot && ch4 == ByteSlash) ||
|
||||
(ch2 == ByteDot && ch3 == ByteSlash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
ch1 = *++src;
|
||||
} while (src < end && ch1 != ByteSlash);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public static class PipelineExtensions
|
||||
{
|
||||
private const int _maxULongByteLength = 20;
|
||||
|
||||
[ThreadStatic]
|
||||
private static byte[] _numericBytesScratch;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ReadOnlySpan<byte> ToSpan(this ReadOnlySequence<byte> buffer)
|
||||
{
|
||||
if (buffer.IsSingleSegment)
|
||||
{
|
||||
return buffer.First.Span;
|
||||
}
|
||||
return buffer.ToArray();
|
||||
}
|
||||
|
||||
public static ArraySegment<byte> GetArray(this Memory<byte> buffer)
|
||||
{
|
||||
return ((ReadOnlyMemory<byte>)buffer).GetArray();
|
||||
}
|
||||
|
||||
public static ArraySegment<byte> GetArray(this ReadOnlyMemory<byte> memory)
|
||||
{
|
||||
if (!MemoryMarshal.TryGetArray(memory, out var result))
|
||||
{
|
||||
throw new InvalidOperationException("Buffer backed by array was expected");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static unsafe void WriteAsciiNoValidation(ref this BufferWriter<PipeWriter> buffer, string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dest = buffer.Span;
|
||||
var destLength = dest.Length;
|
||||
var sourceLength = data.Length;
|
||||
|
||||
// Fast path, try copying to the available memory directly
|
||||
if (sourceLength <= destLength)
|
||||
{
|
||||
fixed (char* input = data)
|
||||
fixed (byte* output = &MemoryMarshal.GetReference(dest))
|
||||
{
|
||||
EncodeAsciiCharsToBytes(input, output, sourceLength);
|
||||
}
|
||||
|
||||
buffer.Advance(sourceLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteAsciiMultiWrite(ref buffer, data);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static unsafe void WriteNumeric(ref this BufferWriter<PipeWriter> buffer, ulong number)
|
||||
{
|
||||
const byte AsciiDigitStart = (byte)'0';
|
||||
|
||||
var span = buffer.Span;
|
||||
var bytesLeftInBlock = span.Length;
|
||||
|
||||
// Fast path, try copying to the available memory directly
|
||||
var simpleWrite = true;
|
||||
fixed (byte* output = &MemoryMarshal.GetReference(span))
|
||||
{
|
||||
var start = output;
|
||||
if (number < 10 && bytesLeftInBlock >= 1)
|
||||
{
|
||||
*(start) = (byte)(((uint)number) + AsciiDigitStart);
|
||||
buffer.Advance(1);
|
||||
}
|
||||
else if (number < 100 && bytesLeftInBlock >= 2)
|
||||
{
|
||||
var val = (uint)number;
|
||||
var tens = (byte)((val * 205u) >> 11); // div10, valid to 1028
|
||||
|
||||
*(start) = (byte)(tens + AsciiDigitStart);
|
||||
*(start + 1) = (byte)(val - (tens * 10) + AsciiDigitStart);
|
||||
buffer.Advance(2);
|
||||
}
|
||||
else if (number < 1000 && bytesLeftInBlock >= 3)
|
||||
{
|
||||
var val = (uint)number;
|
||||
var digit0 = (byte)((val * 41u) >> 12); // div100, valid to 1098
|
||||
var digits01 = (byte)((val * 205u) >> 11); // div10, valid to 1028
|
||||
|
||||
*(start) = (byte)(digit0 + AsciiDigitStart);
|
||||
*(start + 1) = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
|
||||
*(start + 2) = (byte)(val - (digits01 * 10) + AsciiDigitStart);
|
||||
buffer.Advance(3);
|
||||
}
|
||||
else
|
||||
{
|
||||
simpleWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!simpleWrite)
|
||||
{
|
||||
WriteNumericMultiWrite(ref buffer, number);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void WriteNumericMultiWrite(ref this BufferWriter<PipeWriter> buffer, ulong number)
|
||||
{
|
||||
const byte AsciiDigitStart = (byte)'0';
|
||||
|
||||
var value = number;
|
||||
var position = _maxULongByteLength;
|
||||
var byteBuffer = NumericBytesScratch;
|
||||
do
|
||||
{
|
||||
// Consider using Math.DivRem() if available
|
||||
var quotient = value / 10;
|
||||
byteBuffer[--position] = (byte)(AsciiDigitStart + (value - quotient * 10)); // 0x30 = '0'
|
||||
value = quotient;
|
||||
}
|
||||
while (value != 0);
|
||||
|
||||
var length = _maxULongByteLength - position;
|
||||
buffer.Write(new ReadOnlySpan<byte>(byteBuffer, position, length));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private unsafe static void WriteAsciiMultiWrite(ref this BufferWriter<PipeWriter> buffer, string data)
|
||||
{
|
||||
var remaining = data.Length;
|
||||
|
||||
fixed (char* input = data)
|
||||
{
|
||||
var inputSlice = input;
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
var writable = Math.Min(remaining, buffer.Span.Length);
|
||||
|
||||
if (writable == 0)
|
||||
{
|
||||
buffer.Ensure();
|
||||
continue;
|
||||
}
|
||||
|
||||
fixed (byte* output = &MemoryMarshal.GetReference(buffer.Span))
|
||||
{
|
||||
EncodeAsciiCharsToBytes(inputSlice, output, writable);
|
||||
}
|
||||
|
||||
inputSlice += writable;
|
||||
remaining -= writable;
|
||||
|
||||
buffer.Advance(writable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void EncodeAsciiCharsToBytes(char* input, byte* output, int length)
|
||||
{
|
||||
// Note: Not BIGENDIAN or check for non-ascii
|
||||
const int Shift16Shift24 = (1 << 16) | (1 << 24);
|
||||
const int Shift8Identity = (1 << 8) | (1);
|
||||
|
||||
// Encode as bytes upto the first non-ASCII byte and return count encoded
|
||||
int i = 0;
|
||||
// Use Intrinsic switch
|
||||
if (IntPtr.Size == 8) // 64 bit
|
||||
{
|
||||
if (length < 4) goto trailing;
|
||||
|
||||
int unaligned = (int)(((ulong)input) & 0x7) >> 1;
|
||||
// Unaligned chars
|
||||
for (; i < unaligned; i++)
|
||||
{
|
||||
char ch = *(input + i);
|
||||
*(output + i) = (byte)ch; // Cast convert
|
||||
}
|
||||
|
||||
// Aligned
|
||||
int ulongDoubleCount = (length - i) & ~0x7;
|
||||
for (; i < ulongDoubleCount; i += 8)
|
||||
{
|
||||
ulong inputUlong0 = *(ulong*)(input + i);
|
||||
ulong inputUlong1 = *(ulong*)(input + i + 4);
|
||||
// Pack 16 ASCII chars into 16 bytes
|
||||
*(uint*)(output + i) =
|
||||
((uint)((inputUlong0 * Shift16Shift24) >> 24) & 0xffff) |
|
||||
((uint)((inputUlong0 * Shift8Identity) >> 24) & 0xffff0000);
|
||||
*(uint*)(output + i + 4) =
|
||||
((uint)((inputUlong1 * Shift16Shift24) >> 24) & 0xffff) |
|
||||
((uint)((inputUlong1 * Shift8Identity) >> 24) & 0xffff0000);
|
||||
}
|
||||
if (length - 4 > i)
|
||||
{
|
||||
ulong inputUlong = *(ulong*)(input + i);
|
||||
// Pack 8 ASCII chars into 8 bytes
|
||||
*(uint*)(output + i) =
|
||||
((uint)((inputUlong * Shift16Shift24) >> 24) & 0xffff) |
|
||||
((uint)((inputUlong * Shift8Identity) >> 24) & 0xffff0000);
|
||||
i += 4;
|
||||
}
|
||||
|
||||
trailing:
|
||||
for (; i < length; i++)
|
||||
{
|
||||
char ch = *(input + i);
|
||||
*(output + i) = (byte)ch; // Cast convert
|
||||
}
|
||||
}
|
||||
else // 32 bit
|
||||
{
|
||||
// Unaligned chars
|
||||
if ((unchecked((int)input) & 0x2) != 0)
|
||||
{
|
||||
char ch = *input;
|
||||
i = 1;
|
||||
*(output) = (byte)ch; // Cast convert
|
||||
}
|
||||
|
||||
// Aligned
|
||||
int uintCount = (length - i) & ~0x3;
|
||||
for (; i < uintCount; i += 4)
|
||||
{
|
||||
uint inputUint0 = *(uint*)(input + i);
|
||||
uint inputUint1 = *(uint*)(input + i + 2);
|
||||
// Pack 4 ASCII chars into 4 bytes
|
||||
*(ushort*)(output + i) = (ushort)(inputUint0 | (inputUint0 >> 8));
|
||||
*(ushort*)(output + i + 2) = (ushort)(inputUint1 | (inputUint1 >> 8));
|
||||
}
|
||||
if (length - 1 > i)
|
||||
{
|
||||
uint inputUint = *(uint*)(input + i);
|
||||
// Pack 2 ASCII chars into 2 bytes
|
||||
*(ushort*)(output + i) = (ushort)(inputUint | (inputUint >> 8));
|
||||
i += 2;
|
||||
}
|
||||
|
||||
if (i < length)
|
||||
{
|
||||
char ch = *(input + i);
|
||||
*(output + i) = (byte)ch; // Cast convert
|
||||
i = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] NumericBytesScratch => _numericBytesScratch ?? CreateNumericBytesScratch();
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static byte[] CreateNumericBytesScratch()
|
||||
{
|
||||
var bytes = new byte[_maxULongByteLength];
|
||||
_numericBytesScratch = bytes;
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum ProduceEndType
|
||||
{
|
||||
SocketShutdown,
|
||||
SocketDisconnect,
|
||||
ConnectionKeepAlive,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public static class ReasonPhrases
|
||||
{
|
||||
private static readonly byte[] _bytesStatus100 = CreateStatusBytes(StatusCodes.Status100Continue);
|
||||
private static readonly byte[] _bytesStatus101 = CreateStatusBytes(StatusCodes.Status101SwitchingProtocols);
|
||||
private static readonly byte[] _bytesStatus102 = CreateStatusBytes(StatusCodes.Status102Processing);
|
||||
|
||||
private static readonly byte[] _bytesStatus200 = CreateStatusBytes(StatusCodes.Status200OK);
|
||||
private static readonly byte[] _bytesStatus201 = CreateStatusBytes(StatusCodes.Status201Created);
|
||||
private static readonly byte[] _bytesStatus202 = CreateStatusBytes(StatusCodes.Status202Accepted);
|
||||
private static readonly byte[] _bytesStatus203 = CreateStatusBytes(StatusCodes.Status203NonAuthoritative);
|
||||
private static readonly byte[] _bytesStatus204 = CreateStatusBytes(StatusCodes.Status204NoContent);
|
||||
private static readonly byte[] _bytesStatus205 = CreateStatusBytes(StatusCodes.Status205ResetContent);
|
||||
private static readonly byte[] _bytesStatus206 = CreateStatusBytes(StatusCodes.Status206PartialContent);
|
||||
private static readonly byte[] _bytesStatus207 = CreateStatusBytes(StatusCodes.Status207MultiStatus);
|
||||
private static readonly byte[] _bytesStatus208 = CreateStatusBytes(StatusCodes.Status208AlreadyReported);
|
||||
private static readonly byte[] _bytesStatus226 = CreateStatusBytes(StatusCodes.Status226IMUsed);
|
||||
|
||||
private static readonly byte[] _bytesStatus300 = CreateStatusBytes(StatusCodes.Status300MultipleChoices);
|
||||
private static readonly byte[] _bytesStatus301 = CreateStatusBytes(StatusCodes.Status301MovedPermanently);
|
||||
private static readonly byte[] _bytesStatus302 = CreateStatusBytes(StatusCodes.Status302Found);
|
||||
private static readonly byte[] _bytesStatus303 = CreateStatusBytes(StatusCodes.Status303SeeOther);
|
||||
private static readonly byte[] _bytesStatus304 = CreateStatusBytes(StatusCodes.Status304NotModified);
|
||||
private static readonly byte[] _bytesStatus305 = CreateStatusBytes(StatusCodes.Status305UseProxy);
|
||||
private static readonly byte[] _bytesStatus306 = CreateStatusBytes(StatusCodes.Status306SwitchProxy);
|
||||
private static readonly byte[] _bytesStatus307 = CreateStatusBytes(StatusCodes.Status307TemporaryRedirect);
|
||||
private static readonly byte[] _bytesStatus308 = CreateStatusBytes(StatusCodes.Status308PermanentRedirect);
|
||||
|
||||
private static readonly byte[] _bytesStatus400 = CreateStatusBytes(StatusCodes.Status400BadRequest);
|
||||
private static readonly byte[] _bytesStatus401 = CreateStatusBytes(StatusCodes.Status401Unauthorized);
|
||||
private static readonly byte[] _bytesStatus402 = CreateStatusBytes(StatusCodes.Status402PaymentRequired);
|
||||
private static readonly byte[] _bytesStatus403 = CreateStatusBytes(StatusCodes.Status403Forbidden);
|
||||
private static readonly byte[] _bytesStatus404 = CreateStatusBytes(StatusCodes.Status404NotFound);
|
||||
private static readonly byte[] _bytesStatus405 = CreateStatusBytes(StatusCodes.Status405MethodNotAllowed);
|
||||
private static readonly byte[] _bytesStatus406 = CreateStatusBytes(StatusCodes.Status406NotAcceptable);
|
||||
private static readonly byte[] _bytesStatus407 = CreateStatusBytes(StatusCodes.Status407ProxyAuthenticationRequired);
|
||||
private static readonly byte[] _bytesStatus408 = CreateStatusBytes(StatusCodes.Status408RequestTimeout);
|
||||
private static readonly byte[] _bytesStatus409 = CreateStatusBytes(StatusCodes.Status409Conflict);
|
||||
private static readonly byte[] _bytesStatus410 = CreateStatusBytes(StatusCodes.Status410Gone);
|
||||
private static readonly byte[] _bytesStatus411 = CreateStatusBytes(StatusCodes.Status411LengthRequired);
|
||||
private static readonly byte[] _bytesStatus412 = CreateStatusBytes(StatusCodes.Status412PreconditionFailed);
|
||||
private static readonly byte[] _bytesStatus413 = CreateStatusBytes(StatusCodes.Status413PayloadTooLarge);
|
||||
private static readonly byte[] _bytesStatus414 = CreateStatusBytes(StatusCodes.Status414UriTooLong);
|
||||
private static readonly byte[] _bytesStatus415 = CreateStatusBytes(StatusCodes.Status415UnsupportedMediaType);
|
||||
private static readonly byte[] _bytesStatus416 = CreateStatusBytes(StatusCodes.Status416RangeNotSatisfiable);
|
||||
private static readonly byte[] _bytesStatus417 = CreateStatusBytes(StatusCodes.Status417ExpectationFailed);
|
||||
private static readonly byte[] _bytesStatus418 = CreateStatusBytes(StatusCodes.Status418ImATeapot);
|
||||
private static readonly byte[] _bytesStatus419 = CreateStatusBytes(StatusCodes.Status419AuthenticationTimeout);
|
||||
private static readonly byte[] _bytesStatus421 = CreateStatusBytes(StatusCodes.Status421MisdirectedRequest);
|
||||
private static readonly byte[] _bytesStatus422 = CreateStatusBytes(StatusCodes.Status422UnprocessableEntity);
|
||||
private static readonly byte[] _bytesStatus423 = CreateStatusBytes(StatusCodes.Status423Locked);
|
||||
private static readonly byte[] _bytesStatus424 = CreateStatusBytes(StatusCodes.Status424FailedDependency);
|
||||
private static readonly byte[] _bytesStatus426 = CreateStatusBytes(StatusCodes.Status426UpgradeRequired);
|
||||
private static readonly byte[] _bytesStatus428 = CreateStatusBytes(StatusCodes.Status428PreconditionRequired);
|
||||
private static readonly byte[] _bytesStatus429 = CreateStatusBytes(StatusCodes.Status429TooManyRequests);
|
||||
private static readonly byte[] _bytesStatus431 = CreateStatusBytes(StatusCodes.Status431RequestHeaderFieldsTooLarge);
|
||||
private static readonly byte[] _bytesStatus451 = CreateStatusBytes(StatusCodes.Status451UnavailableForLegalReasons);
|
||||
|
||||
private static readonly byte[] _bytesStatus500 = CreateStatusBytes(StatusCodes.Status500InternalServerError);
|
||||
private static readonly byte[] _bytesStatus501 = CreateStatusBytes(StatusCodes.Status501NotImplemented);
|
||||
private static readonly byte[] _bytesStatus502 = CreateStatusBytes(StatusCodes.Status502BadGateway);
|
||||
private static readonly byte[] _bytesStatus503 = CreateStatusBytes(StatusCodes.Status503ServiceUnavailable);
|
||||
private static readonly byte[] _bytesStatus504 = CreateStatusBytes(StatusCodes.Status504GatewayTimeout);
|
||||
private static readonly byte[] _bytesStatus505 = CreateStatusBytes(StatusCodes.Status505HttpVersionNotsupported);
|
||||
private static readonly byte[] _bytesStatus506 = CreateStatusBytes(StatusCodes.Status506VariantAlsoNegotiates);
|
||||
private static readonly byte[] _bytesStatus507 = CreateStatusBytes(StatusCodes.Status507InsufficientStorage);
|
||||
private static readonly byte[] _bytesStatus508 = CreateStatusBytes(StatusCodes.Status508LoopDetected);
|
||||
private static readonly byte[] _bytesStatus510 = CreateStatusBytes(StatusCodes.Status510NotExtended);
|
||||
private static readonly byte[] _bytesStatus511 = CreateStatusBytes(StatusCodes.Status511NetworkAuthenticationRequired);
|
||||
|
||||
private static byte[] CreateStatusBytes(int statusCode)
|
||||
{
|
||||
var reasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(statusCode);
|
||||
Debug.Assert(!string.IsNullOrEmpty(reasonPhrase));
|
||||
|
||||
return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture) + " " + reasonPhrase);
|
||||
}
|
||||
|
||||
public static byte[] ToStatusBytes(int statusCode, string reasonPhrase = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(reasonPhrase))
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case StatusCodes.Status100Continue:
|
||||
return _bytesStatus100;
|
||||
case StatusCodes.Status101SwitchingProtocols:
|
||||
return _bytesStatus101;
|
||||
case StatusCodes.Status102Processing:
|
||||
return _bytesStatus102;
|
||||
|
||||
case StatusCodes.Status200OK:
|
||||
return _bytesStatus200;
|
||||
case StatusCodes.Status201Created:
|
||||
return _bytesStatus201;
|
||||
case StatusCodes.Status202Accepted:
|
||||
return _bytesStatus202;
|
||||
case StatusCodes.Status203NonAuthoritative:
|
||||
return _bytesStatus203;
|
||||
case StatusCodes.Status204NoContent:
|
||||
return _bytesStatus204;
|
||||
case StatusCodes.Status205ResetContent:
|
||||
return _bytesStatus205;
|
||||
case StatusCodes.Status206PartialContent:
|
||||
return _bytesStatus206;
|
||||
case StatusCodes.Status207MultiStatus:
|
||||
return _bytesStatus207;
|
||||
case StatusCodes.Status208AlreadyReported:
|
||||
return _bytesStatus208;
|
||||
case StatusCodes.Status226IMUsed:
|
||||
return _bytesStatus226;
|
||||
|
||||
case StatusCodes.Status300MultipleChoices:
|
||||
return _bytesStatus300;
|
||||
case StatusCodes.Status301MovedPermanently:
|
||||
return _bytesStatus301;
|
||||
case StatusCodes.Status302Found:
|
||||
return _bytesStatus302;
|
||||
case StatusCodes.Status303SeeOther:
|
||||
return _bytesStatus303;
|
||||
case StatusCodes.Status304NotModified:
|
||||
return _bytesStatus304;
|
||||
case StatusCodes.Status305UseProxy:
|
||||
return _bytesStatus305;
|
||||
case StatusCodes.Status306SwitchProxy:
|
||||
return _bytesStatus306;
|
||||
case StatusCodes.Status307TemporaryRedirect:
|
||||
return _bytesStatus307;
|
||||
case StatusCodes.Status308PermanentRedirect:
|
||||
return _bytesStatus308;
|
||||
|
||||
case StatusCodes.Status400BadRequest:
|
||||
return _bytesStatus400;
|
||||
case StatusCodes.Status401Unauthorized:
|
||||
return _bytesStatus401;
|
||||
case StatusCodes.Status402PaymentRequired:
|
||||
return _bytesStatus402;
|
||||
case StatusCodes.Status403Forbidden:
|
||||
return _bytesStatus403;
|
||||
case StatusCodes.Status404NotFound:
|
||||
return _bytesStatus404;
|
||||
case StatusCodes.Status405MethodNotAllowed:
|
||||
return _bytesStatus405;
|
||||
case StatusCodes.Status406NotAcceptable:
|
||||
return _bytesStatus406;
|
||||
case StatusCodes.Status407ProxyAuthenticationRequired:
|
||||
return _bytesStatus407;
|
||||
case StatusCodes.Status408RequestTimeout:
|
||||
return _bytesStatus408;
|
||||
case StatusCodes.Status409Conflict:
|
||||
return _bytesStatus409;
|
||||
case StatusCodes.Status410Gone:
|
||||
return _bytesStatus410;
|
||||
case StatusCodes.Status411LengthRequired:
|
||||
return _bytesStatus411;
|
||||
case StatusCodes.Status412PreconditionFailed:
|
||||
return _bytesStatus412;
|
||||
case StatusCodes.Status413PayloadTooLarge:
|
||||
return _bytesStatus413;
|
||||
case StatusCodes.Status414UriTooLong:
|
||||
return _bytesStatus414;
|
||||
case StatusCodes.Status415UnsupportedMediaType:
|
||||
return _bytesStatus415;
|
||||
case StatusCodes.Status416RangeNotSatisfiable:
|
||||
return _bytesStatus416;
|
||||
case StatusCodes.Status417ExpectationFailed:
|
||||
return _bytesStatus417;
|
||||
case StatusCodes.Status418ImATeapot:
|
||||
return _bytesStatus418;
|
||||
case StatusCodes.Status419AuthenticationTimeout:
|
||||
return _bytesStatus419;
|
||||
case StatusCodes.Status421MisdirectedRequest:
|
||||
return _bytesStatus421;
|
||||
case StatusCodes.Status422UnprocessableEntity:
|
||||
return _bytesStatus422;
|
||||
case StatusCodes.Status423Locked:
|
||||
return _bytesStatus423;
|
||||
case StatusCodes.Status424FailedDependency:
|
||||
return _bytesStatus424;
|
||||
case StatusCodes.Status426UpgradeRequired:
|
||||
return _bytesStatus426;
|
||||
case StatusCodes.Status428PreconditionRequired:
|
||||
return _bytesStatus428;
|
||||
case StatusCodes.Status429TooManyRequests:
|
||||
return _bytesStatus429;
|
||||
case StatusCodes.Status431RequestHeaderFieldsTooLarge:
|
||||
return _bytesStatus431;
|
||||
case StatusCodes.Status451UnavailableForLegalReasons:
|
||||
return _bytesStatus451;
|
||||
|
||||
case StatusCodes.Status500InternalServerError:
|
||||
return _bytesStatus500;
|
||||
case StatusCodes.Status501NotImplemented:
|
||||
return _bytesStatus501;
|
||||
case StatusCodes.Status502BadGateway:
|
||||
return _bytesStatus502;
|
||||
case StatusCodes.Status503ServiceUnavailable:
|
||||
return _bytesStatus503;
|
||||
case StatusCodes.Status504GatewayTimeout:
|
||||
return _bytesStatus504;
|
||||
case StatusCodes.Status505HttpVersionNotsupported:
|
||||
return _bytesStatus505;
|
||||
case StatusCodes.Status506VariantAlsoNegotiates:
|
||||
return _bytesStatus506;
|
||||
case StatusCodes.Status507InsufficientStorage:
|
||||
return _bytesStatus507;
|
||||
case StatusCodes.Status508LoopDetected:
|
||||
return _bytesStatus508;
|
||||
case StatusCodes.Status510NotExtended:
|
||||
return _bytesStatus510;
|
||||
case StatusCodes.Status511NetworkAuthenticationRequired:
|
||||
return _bytesStatus511;
|
||||
|
||||
default:
|
||||
var predefinedReasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(statusCode);
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.1.2 requires trailing whitespace regardless of reason phrase
|
||||
var formattedStatusCode = statusCode.ToString(CultureInfo.InvariantCulture) + " ";
|
||||
return string.IsNullOrEmpty(predefinedReasonPhrase)
|
||||
? Encoding.ASCII.GetBytes(formattedStatusCode)
|
||||
: Encoding.ASCII.GetBytes(formattedStatusCode + predefinedReasonPhrase);
|
||||
|
||||
}
|
||||
}
|
||||
return Encoding.ASCII.GetBytes(statusCode.ToString(CultureInfo.InvariantCulture) + " " + reasonPhrase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum RequestProcessingStatus
|
||||
{
|
||||
RequestPending,
|
||||
ParsingRequestLine,
|
||||
ParsingHeaders,
|
||||
AppStarted,
|
||||
ResponseStarted
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
public enum RequestRejectionReason
|
||||
{
|
||||
UnrecognizedHTTPVersion,
|
||||
InvalidRequestLine,
|
||||
InvalidRequestHeader,
|
||||
InvalidRequestHeadersNoCRLF,
|
||||
MalformedRequestInvalidHeaders,
|
||||
InvalidContentLength,
|
||||
MultipleContentLengths,
|
||||
UnexpectedEndOfRequestContent,
|
||||
BadChunkSuffix,
|
||||
BadChunkSizeData,
|
||||
ChunkedRequestIncomplete,
|
||||
InvalidRequestTarget,
|
||||
InvalidCharactersInHeaderName,
|
||||
RequestLineTooLong,
|
||||
HeadersExceedMaxTotalSize,
|
||||
TooManyHeaders,
|
||||
RequestBodyTooLarge,
|
||||
RequestHeadersTimeout,
|
||||
RequestBodyTimeout,
|
||||
FinalTransferCodingNotChunked,
|
||||
LengthRequired,
|
||||
LengthRequiredHttp10,
|
||||
OptionsMethodRequired,
|
||||
ConnectMethodRequired,
|
||||
MissingHostHeader,
|
||||
MultipleHostHeaders,
|
||||
InvalidHostHeader,
|
||||
UpgradeRequestCannotHavePayload,
|
||||
RequestBodyExceedsContentLength
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||
{
|
||||
[Flags]
|
||||
public enum TransferCoding
|
||||
{
|
||||
None,
|
||||
Chunked,
|
||||
Other
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Connections.Abstractions
|
||||
{
|
||||
internal class UrlDecoder
|
||||
{
|
||||
static bool[] IsAllowed = new bool[0x7F + 1];
|
||||
|
||||
/// <summary>
|
||||
/// Unescape a URL path
|
||||
/// </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>
|
||||
/// <returns>The length of the byte sequence of the unescaped url path.</returns>
|
||||
public static int Decode(ReadOnlySpan<byte> source, Span<byte> destination)
|
||||
{
|
||||
if (destination.Length < source.Length)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Lenghth of the destination byte span is less then the source.",
|
||||
nameof(destination));
|
||||
}
|
||||
|
||||
// This requires the destination span to be larger or equal to source span
|
||||
source.CopyTo(destination);
|
||||
return DecodeInPlace(destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unescape a URL path in place.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The byte span represents a UTF8 encoding url path.</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)
|
||||
{
|
||||
// the slot to read the input
|
||||
var sourceIndex = 0;
|
||||
|
||||
// the slot to write the unescaped byte
|
||||
var destinationIndex = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (sourceIndex == buffer.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (buffer[sourceIndex] == '%')
|
||||
{
|
||||
var decodeIndex = sourceIndex;
|
||||
|
||||
// If decoding process succeeds, the writer iterator will be moved
|
||||
// to the next write-ready location. On the other hand if the scanned
|
||||
// percent-encodings cannot be interpreted as sequence of UTF-8 octets,
|
||||
// these bytes should be copied to output as is.
|
||||
// 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))
|
||||
{
|
||||
Copy(sourceIndex, decodeIndex, ref destinationIndex, buffer);
|
||||
}
|
||||
|
||||
sourceIndex = decodeIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[destinationIndex++] = buffer[sourceIndex++];
|
||||
}
|
||||
}
|
||||
|
||||
return destinationIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unescape the percent-encodings
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
// 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);
|
||||
if (byte1 == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (byte1 == 0)
|
||||
{
|
||||
throw new InvalidOperationException("The path contains null characters.");
|
||||
}
|
||||
|
||||
if (byte1 <= 0x7F)
|
||||
{
|
||||
// first byte < U+007f, it is a single byte ASCII
|
||||
buffer[destinationIndex++] = (byte)byte1;
|
||||
return true;
|
||||
}
|
||||
|
||||
int byte2 = 0, byte3 = 0, byte4 = 0;
|
||||
|
||||
// anticipate more bytes
|
||||
var currentDecodeBits = 0;
|
||||
var byteCount = 1;
|
||||
var expectValueMin = 0;
|
||||
if ((byte1 & 0xE0) == 0xC0)
|
||||
{
|
||||
// 110x xxxx, expect one more byte
|
||||
currentDecodeBits = byte1 & 0x1F;
|
||||
byteCount = 2;
|
||||
expectValueMin = 0x80;
|
||||
}
|
||||
else if ((byte1 & 0xF0) == 0xE0)
|
||||
{
|
||||
// 1110 xxxx, expect two more bytes
|
||||
currentDecodeBits = byte1 & 0x0F;
|
||||
byteCount = 3;
|
||||
expectValueMin = 0x800;
|
||||
}
|
||||
else if ((byte1 & 0xF8) == 0xF0)
|
||||
{
|
||||
// 1111 0xxx, expect three more bytes
|
||||
currentDecodeBits = byte1 & 0x07;
|
||||
byteCount = 4;
|
||||
expectValueMin = 0x10000;
|
||||
}
|
||||
else
|
||||
{
|
||||
// invalid first byte
|
||||
return false;
|
||||
}
|
||||
|
||||
var remainingBytes = byteCount - 1;
|
||||
while (remainingBytes > 0)
|
||||
{
|
||||
// read following three chars
|
||||
if (sourceIndex == buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextSourceIndex = sourceIndex;
|
||||
var nextByte = UnescapePercentEncoding(ref nextSourceIndex, buffer);
|
||||
if (nextByte == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((nextByte & 0xC0) != 0x80)
|
||||
{
|
||||
// the follow up byte is not in form of 10xx xxxx
|
||||
return false;
|
||||
}
|
||||
|
||||
currentDecodeBits = (currentDecodeBits << 6) | (nextByte & 0x3F);
|
||||
remainingBytes--;
|
||||
|
||||
if (remainingBytes == 1 && currentDecodeBits >= 0x360 && currentDecodeBits <= 0x37F)
|
||||
{
|
||||
// this is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that
|
||||
// are not allowed in UTF-8;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remainingBytes == 2 && currentDecodeBits >= 0x110)
|
||||
{
|
||||
// this is going to be out of the upper Unicode bound 0x10FFFF.
|
||||
return false;
|
||||
}
|
||||
|
||||
sourceIndex = nextSourceIndex;
|
||||
if (byteCount - remainingBytes == 2)
|
||||
{
|
||||
byte2 = nextByte;
|
||||
}
|
||||
else if (byteCount - remainingBytes == 3)
|
||||
{
|
||||
byte3 = nextByte;
|
||||
}
|
||||
else if (byteCount - remainingBytes == 4)
|
||||
{
|
||||
byte4 = nextByte;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentDecodeBits < expectValueMin)
|
||||
{
|
||||
// overlong encoding (e.g. using 2 bytes to encode something that only needed 1).
|
||||
return false;
|
||||
}
|
||||
|
||||
// all bytes are verified, write to the output
|
||||
// TODO: measure later to determine if the performance of following logic can be improved
|
||||
// the idea is to combine the bytes into short/int and write to span directly to avoid
|
||||
// range check cost
|
||||
if (byteCount > 0)
|
||||
{
|
||||
buffer[destinationIndex++] = (byte)byte1;
|
||||
}
|
||||
if (byteCount > 1)
|
||||
{
|
||||
buffer[destinationIndex++] = (byte)byte2;
|
||||
}
|
||||
if (byteCount > 2)
|
||||
{
|
||||
buffer[destinationIndex++] = (byte)byte3;
|
||||
}
|
||||
if (byteCount > 3)
|
||||
{
|
||||
buffer[destinationIndex++] = (byte)byte4;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Copy(int begin, int end, ref int writer, Span<byte> buffer)
|
||||
{
|
||||
while (begin != end)
|
||||
{
|
||||
buffer[writer++] = buffer[begin++];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the percent-encoding and try unescape it.
|
||||
///
|
||||
/// The operation first peek at the character the <paramref name="scan"/>
|
||||
/// iterator points at. If it is % the <paramref name="scan"/> is then
|
||||
/// moved on to scan the following to characters. If the two following
|
||||
/// characters are hexadecimal literals they will be unescaped and the
|
||||
/// value will be returned.
|
||||
///
|
||||
/// If the first character is not % the <paramref name="scan"/> iterator
|
||||
/// will be removed beyond the location of % and -1 will be returned.
|
||||
///
|
||||
/// If the following two characters can't be successfully unescaped the
|
||||
/// <paramref name="scan"/> iterator will be move behind the % and -1
|
||||
/// will be returned.
|
||||
/// </summary>
|
||||
/// <param name="scan">The value to read</param>
|
||||
/// <param name="buffer">The byte array</param>
|
||||
/// <returns>The unescaped byte if success. Otherwise return -1.</returns>
|
||||
private static int UnescapePercentEncoding(ref int scan, Span<byte> buffer)
|
||||
{
|
||||
if (buffer[scan++] != '%')
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var probe = scan;
|
||||
|
||||
var value1 = ReadHex(ref probe, buffer);
|
||||
if (value1 == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var value2 = ReadHex(ref probe, buffer);
|
||||
if (value2 == -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (SkipUnescape(value1, value2))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
scan = probe;
|
||||
return (value1 << 4) + value2;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Read the next char and convert it into hexadecimal value.
|
||||
///
|
||||
/// The <paramref name="scan"/> index will be moved to the next
|
||||
/// byte no matter no matter whether the operation successes.
|
||||
/// </summary>
|
||||
/// <param name="scan">The index of the byte in the buffer to read</param>
|
||||
/// <param name="buffer">The byte span from which the hex to be read</param>
|
||||
/// <returns>The hexadecimal value if successes, otherwise -1.</returns>
|
||||
private static int ReadHex(ref int scan, Span<byte> buffer)
|
||||
{
|
||||
if (scan == buffer.Length)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var value = buffer[scan++];
|
||||
var isHead = ((value >= '0') && (value <= '9')) ||
|
||||
((value >= 'A') && (value <= 'F')) ||
|
||||
((value >= 'a') && (value <= 'f'));
|
||||
|
||||
if (!isHead)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (value <= '9')
|
||||
{
|
||||
return value - '0';
|
||||
}
|
||||
else if (value <= 'F')
|
||||
{
|
||||
return (value - 'A') + 10;
|
||||
}
|
||||
else // a - f
|
||||
{
|
||||
return (value - 'a') + 10;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SkipUnescape(int value1, int value2)
|
||||
{
|
||||
// skip %2F - '/'
|
||||
if (value1 == 2 && value2 == 15)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static UrlDecoder()
|
||||
{
|
||||
// Unreserved
|
||||
IsAllowed['A'] = true;
|
||||
IsAllowed['B'] = true;
|
||||
IsAllowed['C'] = true;
|
||||
IsAllowed['D'] = true;
|
||||
IsAllowed['E'] = true;
|
||||
IsAllowed['F'] = true;
|
||||
IsAllowed['G'] = true;
|
||||
IsAllowed['H'] = true;
|
||||
IsAllowed['I'] = true;
|
||||
IsAllowed['J'] = true;
|
||||
IsAllowed['K'] = true;
|
||||
IsAllowed['L'] = true;
|
||||
IsAllowed['M'] = true;
|
||||
IsAllowed['N'] = true;
|
||||
IsAllowed['O'] = true;
|
||||
IsAllowed['P'] = true;
|
||||
IsAllowed['Q'] = true;
|
||||
IsAllowed['R'] = true;
|
||||
IsAllowed['S'] = true;
|
||||
IsAllowed['T'] = true;
|
||||
IsAllowed['U'] = true;
|
||||
IsAllowed['V'] = true;
|
||||
IsAllowed['W'] = true;
|
||||
IsAllowed['X'] = true;
|
||||
IsAllowed['Y'] = true;
|
||||
IsAllowed['Z'] = true;
|
||||
|
||||
IsAllowed['a'] = true;
|
||||
IsAllowed['b'] = true;
|
||||
IsAllowed['c'] = true;
|
||||
IsAllowed['d'] = true;
|
||||
IsAllowed['e'] = true;
|
||||
IsAllowed['f'] = true;
|
||||
IsAllowed['g'] = true;
|
||||
IsAllowed['h'] = true;
|
||||
IsAllowed['i'] = true;
|
||||
IsAllowed['j'] = true;
|
||||
IsAllowed['k'] = true;
|
||||
IsAllowed['l'] = true;
|
||||
IsAllowed['m'] = true;
|
||||
IsAllowed['n'] = true;
|
||||
IsAllowed['o'] = true;
|
||||
IsAllowed['p'] = true;
|
||||
IsAllowed['q'] = true;
|
||||
IsAllowed['r'] = true;
|
||||
IsAllowed['s'] = true;
|
||||
IsAllowed['t'] = true;
|
||||
IsAllowed['u'] = true;
|
||||
IsAllowed['v'] = true;
|
||||
IsAllowed['w'] = true;
|
||||
IsAllowed['x'] = true;
|
||||
IsAllowed['y'] = true;
|
||||
IsAllowed['z'] = true;
|
||||
|
||||
IsAllowed['0'] = true;
|
||||
IsAllowed['1'] = true;
|
||||
IsAllowed['2'] = true;
|
||||
IsAllowed['3'] = true;
|
||||
IsAllowed['4'] = true;
|
||||
IsAllowed['5'] = true;
|
||||
IsAllowed['6'] = true;
|
||||
IsAllowed['7'] = true;
|
||||
IsAllowed['8'] = true;
|
||||
IsAllowed['9'] = true;
|
||||
|
||||
IsAllowed['-'] = true;
|
||||
IsAllowed['_'] = true;
|
||||
IsAllowed['.'] = true;
|
||||
IsAllowed['~'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
public class DynamicTable
|
||||
{
|
||||
private HeaderField[] _buffer;
|
||||
private int _maxSize;
|
||||
private int _size;
|
||||
private int _count;
|
||||
private int _insertIndex;
|
||||
private int _removeIndex;
|
||||
|
||||
public DynamicTable(int maxSize)
|
||||
{
|
||||
_buffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
|
||||
public int Count => _count;
|
||||
|
||||
public int Size => _size;
|
||||
|
||||
public int MaxSize => _maxSize;
|
||||
|
||||
public HeaderField this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index >= _count)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
return _buffer[_insertIndex == 0 ? _buffer.Length - 1 : _insertIndex - index - 1];
|
||||
}
|
||||
}
|
||||
|
||||
public void Insert(Span<byte> name, Span<byte> value)
|
||||
{
|
||||
var entryLength = HeaderField.GetLength(name.Length, value.Length);
|
||||
EnsureAvailable(entryLength);
|
||||
|
||||
if (entryLength > _maxSize)
|
||||
{
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.4.4
|
||||
// It is not an error to attempt to add an entry that is larger than the maximum size;
|
||||
// an attempt to add an entry larger than the maximum size causes the table to be emptied
|
||||
// of all existing entries and results in an empty table.
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = new HeaderField(name, value);
|
||||
_buffer[_insertIndex] = entry;
|
||||
_insertIndex = (_insertIndex + 1) % _buffer.Length;
|
||||
_size += entry.Length;
|
||||
_count++;
|
||||
}
|
||||
|
||||
public void Resize(int maxSize)
|
||||
{
|
||||
if (maxSize > _maxSize)
|
||||
{
|
||||
var newBuffer = new HeaderField[maxSize / HeaderField.RfcOverhead];
|
||||
|
||||
for (var i = 0; i < Count; i++)
|
||||
{
|
||||
newBuffer[i] = _buffer[i];
|
||||
}
|
||||
|
||||
_buffer = newBuffer;
|
||||
_maxSize = maxSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
EnsureAvailable(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureAvailable(int available)
|
||||
{
|
||||
while (_count > 0 && _maxSize - _size < available)
|
||||
{
|
||||
_size -= _buffer[_removeIndex].Length;
|
||||
_count--;
|
||||
_removeIndex = (_removeIndex + 1) % _buffer.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
public class HPackDecoder
|
||||
{
|
||||
private enum State
|
||||
{
|
||||
Ready,
|
||||
HeaderFieldIndex,
|
||||
HeaderNameIndex,
|
||||
HeaderNameLength,
|
||||
HeaderNameLengthContinue,
|
||||
HeaderName,
|
||||
HeaderValueLength,
|
||||
HeaderValueLengthContinue,
|
||||
HeaderValue,
|
||||
DynamicTableSizeUpdate
|
||||
}
|
||||
|
||||
// TODO: add new configurable limit
|
||||
public const int MaxStringOctets = 4096;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 1 | Index (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte IndexedHeaderFieldMask = 0x80;
|
||||
private const byte IndexedHeaderFieldRepresentation = 0x80;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 1 | Index (6+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingMask = 0xc0;
|
||||
private const byte LiteralHeaderFieldWithIncrementalIndexingRepresentation = 0x40;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 0 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldWithoutIndexingMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldWithoutIndexingRepresentation = 0x00;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 0 | 1 | Index (4+) |
|
||||
// +---+---+-----------------------+
|
||||
private const byte LiteralHeaderFieldNeverIndexedMask = 0xf0;
|
||||
private const byte LiteralHeaderFieldNeverIndexedRepresentation = 0x10;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.6.3
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | 0 | 0 | 1 | Max size (5+) |
|
||||
// +---+---------------------------+
|
||||
private const byte DynamicTableSizeUpdateMask = 0xe0;
|
||||
private const byte DynamicTableSizeUpdateRepresentation = 0x20;
|
||||
|
||||
// http://httpwg.org/specs/rfc7541.html#rfc.section.5.2
|
||||
// 0 1 2 3 4 5 6 7
|
||||
// +---+---+---+---+---+---+---+---+
|
||||
// | H | String Length (7+) |
|
||||
// +---+---------------------------+
|
||||
private const byte HuffmanMask = 0x80;
|
||||
|
||||
private const int IndexedHeaderFieldPrefix = 7;
|
||||
private const int LiteralHeaderFieldWithIncrementalIndexingPrefix = 6;
|
||||
private const int LiteralHeaderFieldWithoutIndexingPrefix = 4;
|
||||
private const int LiteralHeaderFieldNeverIndexedPrefix = 4;
|
||||
private const int DynamicTableSizeUpdatePrefix = 5;
|
||||
private const int StringLengthPrefix = 7;
|
||||
|
||||
private readonly int _maxDynamicTableSize;
|
||||
private readonly DynamicTable _dynamicTable;
|
||||
private readonly IntegerDecoder _integerDecoder = new IntegerDecoder();
|
||||
private readonly byte[] _stringOctets = new byte[MaxStringOctets];
|
||||
private readonly byte[] _headerNameOctets = new byte[MaxStringOctets];
|
||||
private readonly byte[] _headerValueOctets = new byte[MaxStringOctets];
|
||||
|
||||
private State _state = State.Ready;
|
||||
private byte[] _headerName;
|
||||
private int _stringIndex;
|
||||
private int _stringLength;
|
||||
private int _headerNameLength;
|
||||
private int _headerValueLength;
|
||||
private bool _index;
|
||||
private bool _huffman;
|
||||
|
||||
public HPackDecoder(int maxDynamicTableSize)
|
||||
: this(maxDynamicTableSize, new DynamicTable(maxDynamicTableSize))
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
}
|
||||
|
||||
// For testing.
|
||||
internal HPackDecoder(int maxDynamicTableSize, DynamicTable dynamicTable)
|
||||
{
|
||||
_maxDynamicTableSize = maxDynamicTableSize;
|
||||
_dynamicTable = dynamicTable;
|
||||
}
|
||||
|
||||
public void Decode(Span<byte> data, bool endHeaders, IHttpHeadersHandler handler)
|
||||
{
|
||||
for (var i = 0; i < data.Length; i++)
|
||||
{
|
||||
OnByte(data[i], handler);
|
||||
}
|
||||
|
||||
if (endHeaders && _state != State.Ready)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackErrorIncompleteHeaderBlock);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnByte(byte b, IHttpHeadersHandler handler)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
case State.Ready:
|
||||
if ((b & IndexedHeaderFieldMask) == IndexedHeaderFieldRepresentation)
|
||||
{
|
||||
var val = b & ~IndexedHeaderFieldMask;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)val, IndexedHeaderFieldPrefix))
|
||||
{
|
||||
OnIndexedHeaderField(_integerDecoder.Value, handler);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderFieldIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithIncrementalIndexingMask) == LiteralHeaderFieldWithIncrementalIndexingRepresentation)
|
||||
{
|
||||
_index = true;
|
||||
var val = b & ~LiteralHeaderFieldWithIncrementalIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithIncrementalIndexingPrefix))
|
||||
{
|
||||
OnIndexedHeaderName(_integerDecoder.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldWithoutIndexingMask) == LiteralHeaderFieldWithoutIndexingRepresentation)
|
||||
{
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldWithoutIndexingMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldWithoutIndexingPrefix))
|
||||
{
|
||||
OnIndexedHeaderName(_integerDecoder.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & LiteralHeaderFieldNeverIndexedMask) == LiteralHeaderFieldNeverIndexedRepresentation)
|
||||
{
|
||||
_index = false;
|
||||
var val = b & ~LiteralHeaderFieldNeverIndexedMask;
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
_state = State.HeaderNameLength;
|
||||
}
|
||||
else if (_integerDecoder.BeginDecode((byte)val, LiteralHeaderFieldNeverIndexedPrefix))
|
||||
{
|
||||
OnIndexedHeaderName(_integerDecoder.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameIndex;
|
||||
}
|
||||
}
|
||||
else if ((b & DynamicTableSizeUpdateMask) == DynamicTableSizeUpdateRepresentation)
|
||||
{
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~DynamicTableSizeUpdateMask), DynamicTableSizeUpdatePrefix))
|
||||
{
|
||||
// TODO: validate that it's less than what's defined via SETTINGS
|
||||
_dynamicTable.Resize(_integerDecoder.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.DynamicTableSizeUpdate;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Can't happen
|
||||
throw new HPackDecodingException($"Byte value {b} does not encode a valid header field representation.");
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderFieldIndex:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
OnIndexedHeaderField(_integerDecoder.Value, handler);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameIndex:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
OnIndexedHeaderName(_integerDecoder.Value);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
||||
{
|
||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderNameLengthContinue;
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderNameLengthContinue:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderName);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderName:
|
||||
_stringOctets[_stringIndex++] = b;
|
||||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
OnString(nextState: State.HeaderValueLength);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValueLength:
|
||||
_huffman = (b & HuffmanMask) != 0;
|
||||
|
||||
if (_integerDecoder.BeginDecode((byte)(b & ~HuffmanMask), StringLengthPrefix))
|
||||
{
|
||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
||||
if (_integerDecoder.Value == 0)
|
||||
{
|
||||
ProcessHeaderValue(handler);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = State.HeaderValueLengthContinue;
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValueLengthContinue:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
OnStringLength(_integerDecoder.Value, nextState: State.HeaderValue);
|
||||
if (_integerDecoder.Value == 0)
|
||||
{
|
||||
ProcessHeaderValue(handler);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case State.HeaderValue:
|
||||
_stringOctets[_stringIndex++] = b;
|
||||
|
||||
if (_stringIndex == _stringLength)
|
||||
{
|
||||
ProcessHeaderValue(handler);
|
||||
}
|
||||
|
||||
break;
|
||||
case State.DynamicTableSizeUpdate:
|
||||
if (_integerDecoder.Decode(b))
|
||||
{
|
||||
if (_integerDecoder.Value > _maxDynamicTableSize)
|
||||
{
|
||||
throw new HPackDecodingException(
|
||||
CoreStrings.FormatHPackErrorDynamicTableSizeUpdateTooLarge(_integerDecoder.Value, _maxDynamicTableSize));
|
||||
}
|
||||
|
||||
_dynamicTable.Resize(_integerDecoder.Value);
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
// Can't happen
|
||||
throw new HPackDecodingException("The HPACK decoder reached an invalid state.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessHeaderValue(IHttpHeadersHandler handler)
|
||||
{
|
||||
OnString(nextState: State.Ready);
|
||||
|
||||
var headerNameSpan = new Span<byte>(_headerName, 0, _headerNameLength);
|
||||
var headerValueSpan = new Span<byte>(_headerValueOctets, 0, _headerValueLength);
|
||||
|
||||
handler.OnHeader(headerNameSpan, headerValueSpan);
|
||||
|
||||
if (_index)
|
||||
{
|
||||
_dynamicTable.Insert(headerNameSpan, headerValueSpan);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderField(int index, IHttpHeadersHandler handler)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
handler.OnHeader(new Span<byte>(header.Name), new Span<byte>(header.Value));
|
||||
_state = State.Ready;
|
||||
}
|
||||
|
||||
private void OnIndexedHeaderName(int index)
|
||||
{
|
||||
var header = GetHeader(index);
|
||||
_headerName = header.Name;
|
||||
_headerNameLength = header.Name.Length;
|
||||
_state = State.HeaderValueLength;
|
||||
}
|
||||
|
||||
private void OnStringLength(int length, State nextState)
|
||||
{
|
||||
if (length > _stringOctets.Length)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackStringLengthTooLarge(length, _stringOctets.Length));
|
||||
}
|
||||
|
||||
_stringLength = length;
|
||||
_stringIndex = 0;
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private void OnString(State nextState)
|
||||
{
|
||||
int Decode(byte[] dst)
|
||||
{
|
||||
if (_huffman)
|
||||
{
|
||||
return Huffman.Decode(_stringOctets, 0, _stringLength, dst);
|
||||
}
|
||||
else
|
||||
{
|
||||
Buffer.BlockCopy(_stringOctets, 0, dst, 0, _stringLength);
|
||||
return _stringLength;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_state == State.HeaderName)
|
||||
{
|
||||
_headerName = _headerNameOctets;
|
||||
_headerNameLength = Decode(_headerNameOctets);
|
||||
}
|
||||
else
|
||||
{
|
||||
_headerValueLength = Decode(_headerValueOctets);
|
||||
}
|
||||
}
|
||||
catch (HuffmanDecodingException ex)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.HPackHuffmanError, ex);
|
||||
}
|
||||
|
||||
_state = nextState;
|
||||
}
|
||||
|
||||
private HeaderField GetHeader(int index)
|
||||
{
|
||||
try
|
||||
{
|
||||
return index <= StaticTable.Instance.Count
|
||||
? StaticTable.Instance[index - 1]
|
||||
: _dynamicTable[index - StaticTable.Instance.Count - 1];
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
{
|
||||
throw new HPackDecodingException(CoreStrings.FormatHPackErrorIndexOutOfRange(index), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
public class HPackDecodingException : Exception
|
||||
{
|
||||
public HPackDecodingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
public HPackDecodingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// 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.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2.HPack
|
||||
{
|
||||
public class HPackEncoder
|
||||
{
|
||||
private IEnumerator<KeyValuePair<string, string>> _enumerator;
|
||||
|
||||
public bool BeginEncode(IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
return Encode(buffer, out length);
|
||||
}
|
||||
|
||||
public bool BeginEncode(int statusCode, IEnumerable<KeyValuePair<string, string>> headers, Span<byte> buffer, out int length)
|
||||
{
|
||||
_enumerator = headers.GetEnumerator();
|
||||
_enumerator.MoveNext();
|
||||
|
||||
var statusCodeLength = EncodeStatusCode(statusCode, buffer);
|
||||
var done = Encode(buffer.Slice(statusCodeLength), out var headersLength);
|
||||
length = statusCodeLength + headersLength;
|
||||
|
||||
return done;
|
||||
}
|
||||
|
||||
public bool Encode(Span<byte> buffer, out int length)
|
||||
{
|
||||
length = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (!EncodeHeader(_enumerator.Current.Key, _enumerator.Current.Value, buffer.Slice(length), out var headerLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
length += headerLength;
|
||||
} while (_enumerator.MoveNext());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int EncodeStatusCode(int statusCode, Span<byte> buffer)
|
||||
{
|
||||
switch (statusCode)
|
||||
{
|
||||
case 200:
|
||||
case 204:
|
||||
case 206:
|
||||
case 304:
|
||||
case 400:
|
||||
case 404:
|
||||
case 500:
|
||||
buffer[0] = (byte)(0x80 | StaticTable.Instance.StatusIndex[statusCode]);
|
||||
return 1;
|
||||
default:
|
||||
// Send as Literal Header Field Without Indexing - Indexed Name
|
||||
buffer[0] = 0x08;
|
||||
|
||||
var statusBytes = StatusCodes.ToStatusBytes(statusCode);
|
||||
buffer[1] = (byte)statusBytes.Length;
|
||||
((Span<byte>)statusBytes).CopyTo(buffer.Slice(2));
|
||||
|
||||
return 2 + statusBytes.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private bool EncodeHeader(string name, string value, Span<byte> buffer, out int length)
|
||||
{
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = 0;
|
||||
|
||||
if (i == buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(name, buffer.Slice(i), out var nameLength, lowercase: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EncodeString(value, buffer.Slice(i), out var valueLength, lowercase: false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += valueLength;
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool EncodeString(string s, Span<byte> buffer, out int length, bool lowercase)
|
||||
{
|
||||
const int toLowerMask = 0x20;
|
||||
|
||||
var i = 0;
|
||||
length = 0;
|
||||
|
||||
if (buffer.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[0] = 0;
|
||||
|
||||
if (!IntegerEncoder.Encode(s.Length, 7, buffer, out var nameLength))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
i += nameLength;
|
||||
|
||||
// TODO: use huffman encoding
|
||||
for (var j = 0; j < s.Length; j++)
|
||||
{
|
||||
if (i >= buffer.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer[i++] = (byte)(s[j] | (lowercase ? toLowerMask : 0));
|
||||
}
|
||||
|
||||
length = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue