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:
parent
bebba2a113
commit
ee9feedc27
|
|
@ -4,6 +4,7 @@ TestResults/
|
||||||
.nuget/
|
.nuget/
|
||||||
*.sln.ide/
|
*.sln.ide/
|
||||||
_ReSharper.*/
|
_ReSharper.*/
|
||||||
|
.idea/
|
||||||
packages/
|
packages/
|
||||||
artifacts/
|
artifacts/
|
||||||
PublishProfiles/
|
PublishProfiles/
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
|
||||||
case RequestRejectionReason.InvalidHostHeader:
|
case RequestRejectionReason.InvalidHostHeader:
|
||||||
ex = new BadHttpRequestException("Invalid Host header.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Invalid Host header.", StatusCodes.Status400BadRequest);
|
||||||
break;
|
break;
|
||||||
|
case RequestRejectionReason.UpgradeRequestCannotHavePayload:
|
||||||
|
ex = new BadHttpRequestException("Requests with 'Connection: Upgrade' cannot have content in the request body.", StatusCodes.Status400BadRequest);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
|
ex = new BadHttpRequestException("Bad request.", StatusCodes.Status400BadRequest);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
|
|
||||||
await FlushAsync(default(CancellationToken));
|
await FlushAsync(default(CancellationToken));
|
||||||
|
|
||||||
return DuplexStream;
|
return _frameStreams.Upgrade();
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
|
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
|
||||||
|
|
|
||||||
|
|
@ -241,8 +241,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
public IHeaderDictionary ResponseHeaders { get; set; }
|
public IHeaderDictionary ResponseHeaders { get; set; }
|
||||||
public Stream ResponseBody { get; set; }
|
public Stream ResponseBody { get; set; }
|
||||||
|
|
||||||
public Stream DuplexStream { get; set; }
|
|
||||||
|
|
||||||
public CancellationToken RequestAborted
|
public CancellationToken RequestAborted
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -323,31 +321,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_frameStreams = new Streams(this);
|
_frameStreams = new Streams(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
RequestBody = _frameStreams.RequestBody;
|
(RequestBody, ResponseBody) = _frameStreams.Start(messageBody);
|
||||||
ResponseBody = _frameStreams.ResponseBody;
|
|
||||||
DuplexStream = _frameStreams.DuplexStream;
|
|
||||||
|
|
||||||
_frameStreams.RequestBody.StartAcceptingReads(messageBody);
|
|
||||||
_frameStreams.ResponseBody.StartAcceptingWrites();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PauseStreams()
|
public void PauseStreams() => _frameStreams.Pause();
|
||||||
{
|
|
||||||
_frameStreams.RequestBody.PauseAcceptingReads();
|
|
||||||
_frameStreams.ResponseBody.PauseAcceptingWrites();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResumeStreams()
|
public void ResumeStreams() => _frameStreams.Resume();
|
||||||
{
|
|
||||||
_frameStreams.RequestBody.ResumeAcceptingReads();
|
|
||||||
_frameStreams.ResponseBody.ResumeAcceptingWrites();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopStreams()
|
public void StopStreams() => _frameStreams.Stop();
|
||||||
{
|
|
||||||
_frameStreams.RequestBody.StopAcceptingReads();
|
|
||||||
_frameStreams.ResponseBody.StopAcceptingWrites();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
|
@ -455,8 +436,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
_requestProcessingStopping = true;
|
_requestProcessingStopping = true;
|
||||||
|
|
||||||
_frameStreams?.RequestBody.Abort(error);
|
_frameStreams?.Abort(error);
|
||||||
_frameStreams?.ResponseBody.Abort();
|
|
||||||
|
|
||||||
LifetimeControl.End(ProduceEndType.SocketDisconnect);
|
LifetimeControl.End(ProduceEndType.SocketDisconnect);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
class FrameDuplexStream : Stream
|
internal class FrameDuplexStream : Stream
|
||||||
{
|
{
|
||||||
private readonly Stream _requestStream;
|
private readonly Stream _requestStream;
|
||||||
private readonly Stream _responseStream;
|
private readonly Stream _responseStream;
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
class FrameRequestStream : Stream
|
internal class FrameRequestStream : ReadOnlyStream
|
||||||
{
|
{
|
||||||
private MessageBody _body;
|
private MessageBody _body;
|
||||||
private FrameStreamState _state;
|
private FrameStreamState _state;
|
||||||
|
|
@ -20,30 +21,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_state = FrameStreamState.Closed;
|
_state = FrameStreamState.Closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanRead => true;
|
|
||||||
|
|
||||||
public override bool CanSeek => false;
|
public override bool CanSeek => false;
|
||||||
|
|
||||||
public override bool CanWrite => false;
|
|
||||||
|
|
||||||
public override long Length
|
public override long Length
|
||||||
{
|
=> throw new NotSupportedException();
|
||||||
get
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long Position
|
public override long Position
|
||||||
{
|
{
|
||||||
get
|
get => throw new NotSupportedException();
|
||||||
{
|
set => throw new NotSupportedException();
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
|
|
@ -145,11 +131,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(byte[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartAcceptingReads(MessageBody body)
|
public void StartAcceptingReads(MessageBody body)
|
||||||
{
|
{
|
||||||
// Only start if not aborted
|
// Only start if not aborted
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
{
|
{
|
||||||
class FrameResponseStream : Stream
|
internal class FrameResponseStream : WriteOnlyStream
|
||||||
{
|
{
|
||||||
private IFrameControl _frameControl;
|
private IFrameControl _frameControl;
|
||||||
private FrameStreamState _state;
|
private FrameStreamState _state;
|
||||||
|
|
@ -19,30 +20,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_state = FrameStreamState.Closed;
|
_state = FrameStreamState.Closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanRead => false;
|
|
||||||
|
|
||||||
public override bool CanSeek => false;
|
public override bool CanSeek => false;
|
||||||
|
|
||||||
public override bool CanWrite => true;
|
|
||||||
|
|
||||||
public override long Length
|
public override long Length
|
||||||
{
|
=> throw new NotSupportedException();
|
||||||
get
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override long Position
|
public override long Position
|
||||||
{
|
{
|
||||||
get
|
get => throw new NotSupportedException();
|
||||||
{
|
set => throw new NotSupportedException();
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Flush()
|
public override void Flush()
|
||||||
|
|
@ -72,11 +58,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
throw new NotSupportedException();
|
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)
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
{
|
{
|
||||||
ValidateState(default(CancellationToken));
|
ValidateState(default(CancellationToken));
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MessageBody ZeroContentLengthClose => _zeroContentLengthClose;
|
||||||
|
|
||||||
public bool RequestKeepAlive { get; protected set; }
|
public bool RequestKeepAlive { get; protected set; }
|
||||||
|
|
||||||
public bool RequestUpgrade { 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 keepAlive = httpVersion != HttpVersion.Http10;
|
||||||
|
|
||||||
var connection = headers.HeaderConnection;
|
var connection = headers.HeaderConnection;
|
||||||
|
var upgrade = false;
|
||||||
if (connection.Count > 0)
|
if (connection.Count > 0)
|
||||||
{
|
{
|
||||||
var connectionOptions = FrameHeaders.ParseConnection(connection);
|
var connectionOptions = FrameHeaders.ParseConnection(connection);
|
||||||
|
|
||||||
if ((connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade)
|
upgrade = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade;
|
||||||
{
|
|
||||||
return new ForRemainingData(true, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
|
keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,16 +264,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString());
|
context.RejectRequest(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (upgrade)
|
||||||
|
{
|
||||||
|
context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload);
|
||||||
|
}
|
||||||
|
|
||||||
return new ForChunkedEncoding(keepAlive, headers, context);
|
return new ForChunkedEncoding(keepAlive, headers, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (headers.ContentLength.HasValue)
|
if (headers.ContentLength.HasValue)
|
||||||
{
|
{
|
||||||
var contentLength = headers.ContentLength.Value;
|
var contentLength = headers.ContentLength.Value;
|
||||||
|
|
||||||
if (contentLength == 0)
|
if (contentLength == 0)
|
||||||
{
|
{
|
||||||
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
|
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
|
||||||
}
|
}
|
||||||
|
else if (upgrade)
|
||||||
|
{
|
||||||
|
context.RejectRequest(RequestRejectionReason.UpgradeRequestCannotHavePayload);
|
||||||
|
}
|
||||||
|
|
||||||
return new ForContentLength(keepAlive, contentLength, context);
|
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;
|
return keepAlive ? _zeroContentLengthKeepAlive : _zeroContentLengthClose;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ForRemainingData : MessageBody
|
private class ForUpgrade : MessageBody
|
||||||
{
|
{
|
||||||
public ForRemainingData(bool upgrade, Frame context)
|
public ForUpgrade(Frame context)
|
||||||
: base(context)
|
: base(context)
|
||||||
{
|
{
|
||||||
RequestUpgrade = upgrade;
|
RequestUpgrade = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)
|
protected override ValueTask<ArraySegment<byte>> PeekAsync(CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
|
||||||
MissingHostHeader,
|
MissingHostHeader,
|
||||||
MultipleHostHeaders,
|
MultipleHostHeaders,
|
||||||
InvalidHostHeader,
|
InvalidHostHeader,
|
||||||
|
UpgradeRequestCannotHavePayload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,84 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
|
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)
|
public Streams(IFrameControl frameControl)
|
||||||
{
|
{
|
||||||
RequestBody = new FrameRequestStream();
|
_request = new FrameRequestStream();
|
||||||
ResponseBody = new FrameResponseStream(frameControl);
|
_emptyRequest = new FrameRequestStream();
|
||||||
DuplexStream = new FrameDuplexStream(RequestBody, ResponseBody);
|
_response = new FrameResponseStream(frameControl);
|
||||||
|
_upgradeableResponse = new WrappingStream(_response);
|
||||||
|
_upgradeStream = new FrameDuplexStream(_request, _response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FrameRequestStream RequestBody { get; }
|
public Stream Upgrade()
|
||||||
public FrameResponseStream ResponseBody { get; }
|
{
|
||||||
public FrameDuplexStream DuplexStream { get; }
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.Extensions.TaskCache.Sources" Version="$(AspNetCoreVersion)" PrivateAssets="All" />
|
||||||
|
|
||||||
|
<PackageReference Include="System.ValueTuple" Version="$(CoreFxVersion)" />
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(CoreFxVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
@ -33,4 +34,10 @@
|
||||||
<PackageReference Include="System.Threading.ThreadPool" Version="$(CoreFxVersion)" />
|
<PackageReference Include="System.Threading.ThreadPool" Version="$(CoreFxVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="CoreStrings.resx">
|
||||||
|
<Generator></Generator>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
44
src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
generated
Normal file
44
src/Microsoft.AspNetCore.Server.Kestrel.Core/Properties/CoreStrings.Designer.cs
generated
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
|
|
||||||
|
|
@ -256,10 +256,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
|
|
||||||
var originalRequestBody = _frame.RequestBody;
|
var originalRequestBody = _frame.RequestBody;
|
||||||
var originalResponseBody = _frame.ResponseBody;
|
var originalResponseBody = _frame.ResponseBody;
|
||||||
var originalDuplexStream = _frame.DuplexStream;
|
|
||||||
_frame.RequestBody = new MemoryStream();
|
_frame.RequestBody = new MemoryStream();
|
||||||
_frame.ResponseBody = new MemoryStream();
|
_frame.ResponseBody = new MemoryStream();
|
||||||
_frame.DuplexStream = new MemoryStream();
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
_frame.InitializeStreams(messageBody);
|
_frame.InitializeStreams(messageBody);
|
||||||
|
|
@ -267,7 +265,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(originalRequestBody, _frame.RequestBody);
|
Assert.Same(originalRequestBody, _frame.RequestBody);
|
||||||
Assert.Same(originalResponseBody, _frame.ResponseBody);
|
Assert.Same(originalResponseBody, _frame.ResponseBody);
|
||||||
Assert.Same(originalDuplexStream, _frame.DuplexStream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue