Improve implementation of IHttpUpgradeFeature

After upgrade has been accepted by the server:
 - Reads to HttpRequest.Body always return 0
 - Writes to HttpResponse.Body always throw
 - The only valid way to communicate is to use the stream returned by IHttpUpgradeFeature.UpgradeAsync()

Also, Kestrel returns HTTP 400 if requests attempt to send a request body along with Connection: Upgrade
This commit is contained in:
Nate McMaster 2017-04-17 17:09:48 -07:00
parent bebba2a113
commit ee9feedc27
22 changed files with 824 additions and 93 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ TestResults/
.nuget/
*.sln.ide/
_ReSharper.*/
.idea/
packages/
artifacts/
PublishProfiles/

View File

@ -89,6 +89,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
case RequestRejectionReason.InvalidHostHeader:
ex = new BadHttpRequestException("Invalid Host header.", StatusCodes.Status400BadRequest);
break;
case RequestRejectionReason.UpgradeRequestCannotHavePayload:
ex = new BadHttpRequestException("Requests with 'Connection: Upgrade' cannot have content in the request body.", StatusCodes.Status400BadRequest);
break;
default:
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
break;

View File

@ -0,0 +1,123 @@
<?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="ResponseStreamWasUpgraded" xml:space="preserve">
<value>Cannot write to response body after connection has been upgraded.</value>
</data>
</root>

View File

@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
await FlushAsync(default(CancellationToken));
return DuplexStream;
return _frameStreams.Upgrade();
}
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();

View File

@ -241,8 +241,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
public IHeaderDictionary ResponseHeaders { get; set; }
public Stream ResponseBody { get; set; }
public Stream DuplexStream { get; set; }
public CancellationToken RequestAborted
{
get
@ -323,31 +321,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_frameStreams = new Streams(this);
}
RequestBody = _frameStreams.RequestBody;
ResponseBody = _frameStreams.ResponseBody;
DuplexStream = _frameStreams.DuplexStream;
_frameStreams.RequestBody.StartAcceptingReads(messageBody);
_frameStreams.ResponseBody.StartAcceptingWrites();
(RequestBody, ResponseBody) = _frameStreams.Start(messageBody);
}
public void PauseStreams()
{
_frameStreams.RequestBody.PauseAcceptingReads();
_frameStreams.ResponseBody.PauseAcceptingWrites();
}
public void PauseStreams() => _frameStreams.Pause();
public void ResumeStreams()
{
_frameStreams.RequestBody.ResumeAcceptingReads();
_frameStreams.ResponseBody.ResumeAcceptingWrites();
}
public void ResumeStreams() => _frameStreams.Resume();
public void StopStreams()
{
_frameStreams.RequestBody.StopAcceptingReads();
_frameStreams.ResponseBody.StopAcceptingWrites();
}
public void StopStreams() => _frameStreams.Stop();
public void Reset()
{
@ -455,8 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_requestProcessingStopping = true;
_frameStreams?.RequestBody.Abort(error);
_frameStreams?.ResponseBody.Abort();
_frameStreams?.Abort(error);
LifetimeControl.End(ProduceEndType.SocketDisconnect);

View File

@ -10,7 +10,7 @@ using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
class FrameDuplexStream : Stream
internal class FrameDuplexStream : Stream
{
private readonly Stream _requestStream;
private readonly Stream _responseStream;

View File

@ -5,11 +5,12 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
class FrameRequestStream : Stream
internal class FrameRequestStream : ReadOnlyStream
{
private MessageBody _body;
private FrameStreamState _state;
@ -20,30 +21,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_state = FrameStreamState.Closed;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length
{
get
{
throw new NotSupportedException();
}
}
=> throw new NotSupportedException();
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
@ -145,11 +131,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return task;
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public void StartAcceptingReads(MessageBody body)
{
// Only start if not aborted

View File

@ -5,10 +5,11 @@ using System;
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
{
class FrameResponseStream : Stream
internal class FrameResponseStream : WriteOnlyStream
{
private IFrameControl _frameControl;
private FrameStreamState _state;
@ -19,30 +20,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_state = FrameStreamState.Closed;
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length
{
get
{
throw new NotSupportedException();
}
}
=> throw new NotSupportedException();
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
@ -72,11 +58,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
ValidateState(default(CancellationToken));

View File

@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_context = context;
}
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
public bool RequestKeepAlive { get; protected set; }
public bool RequestUpgrade { get; protected set; }
@ -237,15 +239,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
var keepAlive = httpVersion != HttpVersion.Http10;
var connection = headers.HeaderConnection;
var upgrade = false;
if (connection.Count > 0)
{
var connectionOptions = FrameHeaders.ParseConnection(connection);
if ((connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade)
{
return new ForRemainingData(true, context);
}
upgrade = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade;
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
}
@ -265,16 +264,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString());
}
if (upgrade)
{
context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload);
}
return new ForChunkedEncoding(keepAlive, headers, context);
}
if (headers.ContentLength.HasValue)
{
var contentLength = headers.ContentLength.Value;
if (contentLength == 0)
{
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
}
else if (upgrade)
{
context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload);
}
return new ForContentLength(keepAlive, contentLength, context);
}
@ -291,15 +300,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
}
}
if (upgrade)
{
return new ForUpgrade(context);
}
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
}
private class ForRemainingData : MessageBody
private class ForUpgrade : MessageBody
{
public ForRemainingData(bool upgrade, Frame context)
public ForUpgrade(Frame context)
: base(context)
{
RequestUpgrade = upgrade;
RequestUpgrade = true;
}
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)

View File

@ -30,5 +30,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
MissingHostHeader,
MultipleHostHeaders,
InvalidHostHeader,
UpgradeRequestCannotHavePayload,
}
}

View File

@ -0,0 +1,29 @@
// 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.Infrastructure
{
public abstract class ReadOnlyStream : Stream
{
public override bool CanRead => true;
public override bool CanWrite => false;
public override int WriteTimeout
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
}

View File

@ -1,21 +1,84 @@
// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
class Streams
internal class Streams
{
private static readonly ThrowingWriteOnlyStream _throwingResponseStream
= new ThrowingWriteOnlyStream(new InvalidOperationException(CoreStrings.ResponseStreamWasUpgraded));
private readonly FrameResponseStream _response;
private readonly FrameRequestStream _request;
private readonly WrappingStream _upgradeableResponse;
private readonly FrameRequestStream _emptyRequest;
private readonly Stream _upgradeStream;
public Streams(IFrameControl frameControl)
{
RequestBody = new FrameRequestStream();
ResponseBody = new FrameResponseStream(frameControl);
DuplexStream = new FrameDuplexStream(RequestBody, ResponseBody);
_request = new FrameRequestStream();
_emptyRequest = new FrameRequestStream();
_response = new FrameResponseStream(frameControl);
_upgradeableResponse = new WrappingStream(_response);
_upgradeStream = new FrameDuplexStream(_request, _response);
}
public FrameRequestStream RequestBody { get; }
public FrameResponseStream ResponseBody { get; }
public FrameDuplexStream DuplexStream { get; }
public Stream Upgrade()
{
// causes writes to context.Response.Body to throw
_upgradeableResponse.SetInnerStream(_throwingResponseStream);
// _upgradeStream always uses _response
return _upgradeStream;
}
public (Stream request, Stream response) Start(MessageBody body)
{
_request.StartAcceptingReads(body);
_emptyRequest.StartAcceptingReads(MessageBody.ZeroContentLengthClose);
_response.StartAcceptingWrites();
if (body.RequestUpgrade)
{
// until Upgrade() is called, context.Response.Body should use the normal output stream
_upgradeableResponse.SetInnerStream(_response);
// upgradeable requests should never have a request body
return (_emptyRequest, _upgradeableResponse);
}
else
{
return (_request, _response);
}
}
public void Pause()
{
_request.PauseAcceptingReads();
_emptyRequest.PauseAcceptingReads();
_response.PauseAcceptingWrites();
}
public void Resume()
{
_request.ResumeAcceptingReads();
_emptyRequest.ResumeAcceptingReads();
_response.ResumeAcceptingWrites();
}
public void Stop()
{
_request.StopAcceptingReads();
_emptyRequest.StopAcceptingReads();
_response.StopAcceptingWrites();
}
public void Abort(Exception error)
{
_request.Abort(error);
_emptyRequest.Abort(error);
_response.Abort();
}
}
}

View File

@ -0,0 +1,45 @@
// 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.Infrastructure
{
public class ThrowingWriteOnlyStream : WriteOnlyStream
{
private readonly Exception _exception;
public ThrowingWriteOnlyStream(Exception exception)
{
_exception = exception;
}
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 Write(byte[] buffer, int offset, int count)
=> throw _exception;
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> throw _exception;
public override void Flush()
=> throw _exception;
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
}
}

View File

@ -0,0 +1,140 @@
// 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;
#if NET46
using System.Runtime.Remoting;
#endif
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
{
internal class WrappingStream : Stream
{
private Stream _inner;
private bool _disposed;
public WrappingStream(Stream inner)
{
_inner = inner;
}
public void SetInnerStream(Stream inner)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(WrappingStream));
}
_inner = inner;
}
public override bool CanRead => _inner.CanRead;
public override bool CanSeek => _inner.CanSeek;
public override bool CanWrite => _inner.CanWrite;
public override bool CanTimeout => _inner.CanTimeout;
public override long Length => _inner.Length;
public override long Position
{
get => _inner.Position;
set => _inner.Position = value;
}
public override int ReadTimeout
{
get => _inner.ReadTimeout;
set => _inner.ReadTimeout = value;
}
public override int WriteTimeout
{
get => _inner.WriteTimeout;
set => _inner.WriteTimeout = value;
}
public override void Flush()
=> _inner.Flush();
public override Task FlushAsync(CancellationToken cancellationToken)
=> _inner.FlushAsync(cancellationToken);
public override int Read(byte[] buffer, int offset, int count)
=> _inner.Read(buffer, offset, count);
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _inner.ReadAsync(buffer, offset, count, cancellationToken);
public override int ReadByte()
=> _inner.ReadByte();
public override long Seek(long offset, SeekOrigin origin)
=> _inner.Seek(offset, origin);
public override void SetLength(long value)
=> _inner.SetLength(value);
public override void Write(byte[] buffer, int offset, int count)
=> _inner.Write(buffer, offset, count);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _inner.WriteAsync(buffer, offset, count, cancellationToken);
public override void WriteByte(byte value)
=> _inner.WriteByte(value);
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
=> _inner.CopyToAsync(destination, bufferSize, cancellationToken);
#if NET46
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> _inner.BeginRead(buffer, offset, count, callback, state);
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> _inner.BeginWrite(buffer, offset, count, callback, state);
public override int EndRead(IAsyncResult asyncResult)
=> _inner.EndRead(asyncResult);
public override void EndWrite(IAsyncResult asyncResult)
=> _inner.EndWrite(asyncResult);
public override ObjRef CreateObjRef(Type requestedType)
=> _inner.CreateObjRef(requestedType);
public override object InitializeLifetimeService()
=> _inner.InitializeLifetimeService();
public override void Close()
=> _inner.Close();
#elif NETSTANDARD1_3
#else
#error Target framework should be updated
#endif
public override bool Equals(object obj)
=> _inner.Equals(obj);
public override int GetHashCode()
=> _inner.GetHashCode();
public override string ToString()
=> _inner.ToString();
protected override void Dispose(bool disposing)
{
if (disposing)
{
_disposed = true;
_inner.Dispose();
}
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Infrastructure
{
public abstract class WriteOnlyStream : Stream
{
public override bool CanRead => false;
public override bool CanWrite => true;
public override int ReadTimeout
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> throw new NotSupportedException();
}
}

View File

@ -20,6 +20,7 @@
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
</ItemGroup>
@ -33,4 +34,10 @@
<PackageReference Include="System.Threading.ThreadPool" Version="$(CoreFxVersion)" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="CoreStrings.resx">
<Generator></Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
// <auto-generated />
namespace Microsoft.AspNetCore.Server.Kestrel.Core
{
using System.Globalization;
using System.Reflection;
using System.Resources;
internal static class CoreStrings
{
private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.AspNetCore.Server.Kestrel.Core.CoreStrings", typeof(CoreStrings).GetTypeInfo().Assembly);
/// <summary>
/// Cannot write to response body after connection has been upgraded.
/// </summary>
internal static string ResponseStreamWasUpgraded
{
get => GetString("ResponseStreamWasUpgraded");
}
/// <summary>
/// Cannot write to response body after connection has been upgraded.
/// </summary>
internal static string FormatResponseStreamWasUpgraded()
=> GetString("ResponseStreamWasUpgraded");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
System.Diagnostics.Debug.Assert(value != null);
if (formatterNames != null)
{
for (var i = 0; i < formatterNames.Length; i++)
{
value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
}
}
return value;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Moq;

View File

@ -256,10 +256,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var originalRequestBody = _frame.RequestBody;
var originalResponseBody = _frame.ResponseBody;
var originalDuplexStream = _frame.DuplexStream;
_frame.RequestBody = new MemoryStream();
_frame.ResponseBody = new MemoryStream();
_frame.DuplexStream = new MemoryStream();
// Act
_frame.InitializeStreams(messageBody);
@ -267,7 +265,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
// Assert
Assert.Same(originalRequestBody, _frame.RequestBody);
Assert.Same(originalResponseBody, _frame.ResponseBody);
Assert.Same(originalDuplexStream, _frame.DuplexStream);
}
[Theory]

View File

@ -0,0 +1,89 @@
// 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;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class StreamsTests
{
[Fact]
public async Task StreamsThrowAfterAbort()
{
var streams = new Streams(Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody());
var ex = new Exception("My error");
streams.Abort(ex);
await response.WriteAsync(new byte[1], 0, 1);
Assert.Same(ex,
await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));
}
[Fact]
public async Task StreamsThrowOnAbortAfterUpgrade()
{
var streams = new Streams(Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
var upgrade = streams.Upgrade();
var ex = new Exception("My error");
streams.Abort(ex);
var writeEx = await Assert.ThrowsAsync<InvalidOperationException>(() => response.WriteAsync(new byte[1], 0, 1));
Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, writeEx.Message);
Assert.Same(ex,
await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));
Assert.Same(ex,
await Assert.ThrowsAsync<Exception>(() => upgrade.ReadAsync(new byte[1], 0, 1)));
await upgrade.WriteAsync(new byte[1], 0, 1);
}
[Fact]
public async Task StreamsThrowOnUpgradeAfterAbort()
{
var streams = new Streams(Mock.Of<IFrameControl>());
var (request, response) = streams.Start(new MockMessageBody(upgradeable: true));
var ex = new Exception("My error");
streams.Abort(ex);
var upgrade = streams.Upgrade();
var writeEx = await Assert.ThrowsAsync<InvalidOperationException>(() => response.WriteAsync(new byte[1], 0, 1));
Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, writeEx.Message);
Assert.Same(ex,
await Assert.ThrowsAsync<Exception>(() => request.ReadAsync(new byte[1], 0, 1)));
Assert.Same(ex,
await Assert.ThrowsAsync<Exception>(() => upgrade.ReadAsync(new byte[1], 0, 1)));
await upgrade.WriteAsync(new byte[1], 0, 1);
}
private class MockMessageBody : MessageBody
{
public MockMessageBody(bool upgradeable = false)
: base(null)
{
RequestUpgrade = upgradeable;
}
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)
{
return new ValueTask<ArraySegment<byte>>(new ArraySegment<byte>(new byte[1]));
}
}
}
}

View File

@ -0,0 +1,29 @@
// 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.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class ThrowingWriteOnlyStreamTests
{
[Fact]
public async Task ThrowsOnWrite()
{
var ex = new Exception("my error");
var stream = new ThrowingWriteOnlyStream(ex);
Assert.True(stream.CanWrite);
Assert.False(stream.CanRead);
Assert.False(stream.CanSeek);
Assert.False(stream.CanTimeout);
Assert.Same(ex, Assert.Throws<Exception>(() => stream.Write(new byte[1], 0, 1)));
Assert.Same(ex, await Assert.ThrowsAsync<Exception>(() => stream.WriteAsync(new byte[1], 0, 1)));
Assert.Same(ex, Assert.Throws<Exception>(() => stream.Flush()));
Assert.Same(ex, await Assert.ThrowsAsync<Exception>(() => stream.FlushAsync()));
}
}
}

View File

@ -0,0 +1,174 @@
// 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.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Xunit;
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
{
public class UpgradeTests
{
[Fact]
public async Task ResponseThrowsAfterUpgrade()
{
var upgrade = new TaskCompletionSource<bool>();
using (var server = new TestServer(async context =>
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
var stream = await feature.UpgradeAsync();
var ex = Assert.Throws<InvalidOperationException>(() => context.Response.Body.WriteByte((byte)' '));
Assert.Equal(CoreStrings.ResponseStreamWasUpgraded, ex.Message);
using (var writer = new StreamWriter(stream))
{
writer.WriteLine("New protocol data");
}
upgrade.TrySetResult(true);
}))
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
await connection.Receive("New protocol data");
await upgrade.Task.TimeoutAfter(TimeSpan.FromSeconds(30));
}
}
}
[Fact]
public async Task RequestBodyAlwaysEmptyAfterUpgrade()
{
const string send = "Custom protocol send";
const string recv = "Custom protocol recv";
var upgrade = new TaskCompletionSource<bool>();
using (var server = new TestServer(async context =>
{
try
{
var feature = context.Features.Get<IHttpUpgradeFeature>();
var stream = await feature.UpgradeAsync();
var buffer = new byte[128];
var read = await context.Request.Body.ReadAsync(buffer, 0, 128).TimeoutAfter(TimeSpan.FromSeconds(10));
Assert.Equal(0, read);
using (var reader = new StreamReader(stream))
using (var writer = new StreamWriter(stream))
{
var line = await reader.ReadLineAsync();
Assert.Equal(send, line);
await writer.WriteLineAsync(recv);
}
upgrade.TrySetResult(true);
}
catch (Exception ex)
{
upgrade.SetException(ex);
throw;
}
}))
{
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 101 Switching Protocols",
"Connection: Upgrade",
$"Date: {server.Context.DateHeaderValue}",
"",
"");
await connection.Send(send + "\r\n");
await connection.Receive(recv);
await upgrade.Task.TimeoutAfter(TimeSpan.FromSeconds(30));
}
}
}
[Fact]
public async Task RejectsRequestWithContentLengthAndUpgrade()
{
using (var server = new TestServer(context => TaskCache.CompletedTask))
using (var connection = server.CreateConnection())
{
await connection.Send("POST / HTTP/1.1",
"Host:",
"Content-Length: 1",
"Connection: Upgrade",
"",
"1");
await connection.Receive("HTTP/1.1 400 Bad Request");
}
}
[Fact]
public async Task AcceptsRequestWithNoContentLengthAndUpgrade()
{
using (var server = new TestServer(context => TaskCache.CompletedTask))
{
using (var connection = server.CreateConnection())
{
await connection.Send("POST / HTTP/1.1",
"Host:",
"Content-Length: 0",
"Connection: Upgrade, keep-alive",
"",
"");
await connection.Receive("HTTP/1.1 200 OK");
}
using (var connection = server.CreateConnection())
{
await connection.Send("GET / HTTP/1.1",
"Host:",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 200 OK");
}
}
}
[Fact]
public async Task RejectsRequestWithChunkedEncodingAndUpgrade()
{
using (var server = new TestServer(context => TaskCache.CompletedTask))
using (var connection = server.CreateConnection())
{
await connection.Send("POST / HTTP/1.1",
"Host:",
"Transfer-Encoding: chunked",
"Connection: Upgrade",
"",
"");
await connection.Receive("HTTP/1.1 400 Bad Request");
}
}
}
}