diff --git a/src/Servers/Kestrel/Core/src/CoreStrings.resx b/src/Servers/Kestrel/Core/src/CoreStrings.resx
index 0951807eede5..289c8ecacc68 100644
--- a/src/Servers/Kestrel/Core/src/CoreStrings.resx
+++ b/src/Servers/Kestrel/Core/src/CoreStrings.resx
@@ -390,34 +390,34 @@
The client sent a {frameType} frame to idle stream ID {streamId}.
-
+
The client sent trailers containing one or more pseudo-header fields.
-
+
The client sent a header with uppercase characters in its name.
-
+
The client sent a trailer with uppercase characters in its name.
The client sent a HEADERS frame containing trailers without setting the END_STREAM flag.
-
+
Request headers missing one or more mandatory pseudo-header fields.
-
+
Pseudo-header field found in request headers after regular header fields.
-
+
Request headers contain unknown pseudo-header field.
-
+
Request headers contain response-specific pseudo-header field.
-
+
Request headers contain duplicate pseudo-header field.
-
+
Request headers contain connection-specific header field.
@@ -680,4 +680,4 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
The client created multiple inbound {streamType} streams for the connection.
-
+
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
index 6b48daff78d3..900ed0b82ba3 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
@@ -1026,7 +1026,7 @@ private void StartStream()
// All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header
// fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header
// fields is malformed (Section 8.1.2.6).
- throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
}
if (_clientActiveStreamCount == _serverSettings.MaxConcurrentStreams)
@@ -1327,7 +1327,7 @@ private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value)
if (IsConnectionSpecificHeaderField(name, value))
{
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
@@ -1338,11 +1338,11 @@ private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
}
}
@@ -1398,13 +1398,13 @@ implementations to these vulnerabilities.*/
// All pseudo-header fields MUST appear in the header block before regular header fields.
// Any request or response that contains a pseudo-header field that appears in a header
// block after a regular header field MUST be treated as malformed (Section 8.1.2.6).
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR);
}
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
// Pseudo-header fields MUST NOT appear in trailers.
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
_requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields;
@@ -1413,21 +1413,21 @@ implementations to these vulnerabilities.*/
{
// Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header
// fields as malformed (Section 8.1.2.6).
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
if (headerField == PseudoHeaderFields.Status)
{
// Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields
// defined for responses MUST NOT appear in requests.
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
if ((_parsedPseudoHeaderFields & headerField) == headerField)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3
// All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
- throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
+ throw new Http2ConnectionErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}
if (headerField == PseudoHeaderFields.Method)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 9c3a3e52f600..1c38bf462d1b 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -32,6 +32,9 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler,
private static ReadOnlySpan TrailersBytes => new byte[8] { (byte)'t', (byte)'r', (byte)'a', (byte)'i', (byte)'l', (byte)'e', (byte)'r', (byte)'s' };
private static ReadOnlySpan ConnectBytes => new byte[7] { (byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T' };
+ private static readonly PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
+ PseudoHeaderFields.Method | PseudoHeaderFields.Path | PseudoHeaderFields.Scheme;
+
private readonly Http3FrameWriter _frameWriter;
private readonly Http3OutputProducer _http3Output;
private int _isClosed;
@@ -42,6 +45,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler,
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
protected RequestHeaderParsingState _requestHeaderParsingState;
private PseudoHeaderFields _parsedPseudoHeaderFields;
+ private int _totalParsedHeaderSize;
private bool _isMethodConnect;
private readonly Http3Connection _http3Connection;
@@ -148,7 +152,14 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan value)
public override void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
{
- // TODO MaxRequestHeadersTotalSize?
+ // https://tools.ietf.org/html/rfc7540#section-6.5.2
+ // "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
+ _totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
+ if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
+ {
+ throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
+ }
+
ValidateHeader(name, value);
try
{
@@ -165,11 +176,11 @@ public override void OnHeader(ReadOnlySpan name, ReadOnlySpan value)
}
catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre)
{
- throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.MessageError);
}
catch (InvalidOperationException)
{
- throw new Http3StreamErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http3ErrorCode.MessageError);
}
}
@@ -177,11 +188,10 @@ private void ValidateHeader(ReadOnlySpan name, ReadOnlySpan value)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1
/*
- Intermediaries that process HTTP requests or responses (i.e., any
- intermediary not acting as a tunnel) MUST NOT forward a malformed
- request or response. Malformed requests or responses that are
- detected MUST be treated as a stream error (Section 5.4.2) of type
- PROTOCOL_ERROR.
+ Intermediaries that process HTTP requests or responses
+ (i.e., any intermediary not acting as a tunnel) MUST NOT forward a
+ malformed request or response. Malformed requests or responses that
+ are detected MUST be treated as a stream error of type H3_MESSAGE_ERROR.
For malformed requests, a server MAY send an HTTP response prior to
closing or resetting the stream. Clients MUST NOT accept a malformed
@@ -193,39 +203,43 @@ implementations to these vulnerabilities.*/
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Headers)
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-4
// All pseudo-header fields MUST appear in the header block before regular header fields.
// Any request or response that contains a pseudo-header field that appears in a header
- // block after a regular header field MUST be treated as malformed (Section 8.1.2.6).
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http3ErrorCode.ProtocolError);
+ // block after a regular header field MUST be treated as malformed.
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http3ErrorCode.MessageError);
}
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Pseudo-header fields MUST NOT appear in trailers.
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http3ErrorCode.MessageError);
}
_requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields;
if (headerField == PseudoHeaderFields.Unknown)
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header
- // fields as malformed (Section 8.1.2.6).
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorUnknownPseudoHeaderField, Http3ErrorCode.ProtocolError);
+ // fields as malformed.
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http3ErrorCode.MessageError);
}
if (headerField == PseudoHeaderFields.Status)
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields
// defined for responses MUST NOT appear in requests.
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorResponsePseudoHeaderField, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http3ErrorCode.MessageError);
}
if ((_parsedPseudoHeaderFields & headerField) == headerField)
{
- // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3
- // All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorDuplicatePseudoHeaderField, Http3ErrorCode.ProtocolError);
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-7
+ // All HTTP/3 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http3ErrorCode.MessageError);
}
if (headerField == PseudoHeaderFields.Method)
@@ -242,22 +256,22 @@ implementations to these vulnerabilities.*/
if (IsConnectionSpecificHeaderField(name, value))
{
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http3ErrorCode.MessageError);
}
- // http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
- // A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6).
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1-3
+ // A request or response containing uppercase header field names MUST be treated as malformed.
for (var i = 0; i < name.Length; i++)
{
if (name[i] >= 65 && name[i] <= 90)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http3ErrorCode.MessageError);
}
else
{
- throw new Http3StreamErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http3ErrorCode.ProtocolError);
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http3ErrorCode.MessageError);
}
}
}
@@ -521,6 +535,15 @@ private async Task ProcessHeadersFrameAsync(IHttpApplication
await OnEndStreamReceived();
}
+ if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields)
+ {
+ // All HTTP/3 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header
+ // fields, unless it is a CONNECT request. An HTTP request that omits mandatory pseudo-header
+ // fields is malformed.
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1
+ throw new Http3StreamErrorException(CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields, Http3ErrorCode.MessageError);
+ }
+
_appCompleted = new TaskCompletionSource();
ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
index 1c0f85b27560..b683b6b08e68 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
@@ -2413,7 +2413,7 @@ await WaitForConnectionErrorAsync(
ignoreNonGoAwayFrames: false,
expectedLastStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
- expectedErrorMessage: CoreStrings.Http2ErrorHeaderNameUppercase);
+ expectedErrorMessage: CoreStrings.HttpErrorHeaderNameUppercase);
}
[Fact]
@@ -2427,7 +2427,7 @@ public Task HEADERS_Received_HeaderBlockContainsUnknownPseudoHeaderField_Connect
new KeyValuePair(":unknown", "0"),
};
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.Http2ErrorUnknownPseudoHeaderField);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorUnknownPseudoHeaderField);
}
[Fact]
@@ -2441,14 +2441,14 @@ public Task HEADERS_Received_HeaderBlockContainsResponsePseudoHeaderField_Connec
new KeyValuePair(HeaderNames.Status, "200"),
};
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.Http2ErrorResponsePseudoHeaderField);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorResponsePseudoHeaderField);
}
[Theory]
[MemberData(nameof(DuplicatePseudoHeaderFieldData))]
public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_ConnectionError(IEnumerable> headers)
{
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.Http2ErrorDuplicatePseudoHeaderField);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorDuplicatePseudoHeaderField);
}
[Theory]
@@ -2471,7 +2471,7 @@ await ExpectAsync(Http2FrameType.HEADERS,
[MemberData(nameof(PseudoHeaderFieldAfterRegularHeadersData))]
public Task HEADERS_Received_HeaderBlockContainsPseudoHeaderFieldAfterRegularHeaders_ConnectionError(IEnumerable> headers)
{
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, expectedErrorMessage: CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders);
}
private async Task HEADERS_Received_InvalidHeaderFields_ConnectionError(IEnumerable> headers, string expectedErrorMessage)
@@ -2495,7 +2495,7 @@ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeade
await WaitForStreamErrorAsync(
expectedStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
- expectedErrorMessage: CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields);
+ expectedErrorMessage: CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields);
// Verify that the stream ID can't be re-used
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, _browserRequestHeaders);
@@ -2572,7 +2572,7 @@ public Task HEADERS_Received_HeaderBlockContainsConnectionHeader_ConnectionError
new KeyValuePair("connection", "keep-alive")
};
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.Http2ErrorConnectionSpecificHeaderField);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField);
}
[Fact]
@@ -2586,7 +2586,7 @@ public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsNotTrailers_Conn
new KeyValuePair("te", "trailers, deflate")
};
- return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.Http2ErrorConnectionSpecificHeaderField);
+ return HEADERS_Received_InvalidHeaderFields_ConnectionError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField);
}
[Fact]
@@ -4291,7 +4291,7 @@ public async Task CONTINUATION_Received_HeaderBlockDoesNotContainMandatoryPseudo
await WaitForStreamErrorAsync(
expectedStreamId: 1,
expectedErrorCode: Http2ErrorCode.PROTOCOL_ERROR,
- expectedErrorMessage: CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields);
+ expectedErrorMessage: CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields);
// Verify that the stream ID can't be re-used
await SendHeadersAsync(1, Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM, headers);
@@ -5239,22 +5239,22 @@ public static TheoryData IllegalTrailerData
var data = new TheoryData();
// Indexed Header Field - :method: GET
- data.Add(new byte[] { 0x82 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
+ data.Add(new byte[] { 0x82 }, CoreStrings.HttpErrorTrailersContainPseudoHeaderField);
// Indexed Header Field - :path: /
- data.Add(new byte[] { 0x84 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
+ data.Add(new byte[] { 0x84 }, CoreStrings.HttpErrorTrailersContainPseudoHeaderField);
// Indexed Header Field - :scheme: http
- data.Add(new byte[] { 0x86 }, CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
+ data.Add(new byte[] { 0x86 }, CoreStrings.HttpErrorTrailersContainPseudoHeaderField);
// Literal Header Field without Indexing - Indexed Name - :authority: 127.0.0.1
- data.Add(new byte[] { 0x01, 0x09 }.Concat(Encoding.ASCII.GetBytes("127.0.0.1")).ToArray(), CoreStrings.Http2ErrorTrailersContainPseudoHeaderField);
+ data.Add(new byte[] { 0x01, 0x09 }.Concat(Encoding.ASCII.GetBytes("127.0.0.1")).ToArray(), CoreStrings.HttpErrorTrailersContainPseudoHeaderField);
// Literal Header Field without Indexing - New Name - contains-Uppercase: 0
data.Add(new byte[] { 0x00, 0x12 }
.Concat(Encoding.ASCII.GetBytes("contains-Uppercase"))
.Concat(new byte[] { 0x01, (byte)'0' })
- .ToArray(), CoreStrings.Http2ErrorTrailerNameUppercase);
+ .ToArray(), CoreStrings.HttpErrorTrailerNameUppercase);
return data;
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index 0e86c84104ea..aaea53fd9f36 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@@ -1837,5 +1839,267 @@ await requestStream.WaitForStreamErrorAsync(
Http3ErrorCode.UnexpectedFrame,
expectedErrorMessage: CoreStrings.FormatHttp3ErrorUnsupportedFrameOnServer(frame.FormattedType));
}
+
+ [Fact]
+ public Task HEADERS_Received_HeaderBlockContainsUnknownPseudoHeaderField_ConnectionError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(":unknown", "0"),
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, expectedErrorMessage: CoreStrings.HttpErrorUnknownPseudoHeaderField);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_HeaderBlockContainsResponsePseudoHeaderField_ConnectionError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair(HeaderNames.Status, "200"),
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, expectedErrorMessage: CoreStrings.HttpErrorResponsePseudoHeaderField);
+ }
+
+ public static TheoryData>> DuplicatePseudoHeaderFieldData
+ {
+ get
+ {
+ var data = new TheoryData>>();
+ var requestHeaders = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Authority, "127.0.0.1"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+
+ foreach (var headerField in requestHeaders)
+ {
+ var headers = requestHeaders.Concat(new[] { new KeyValuePair(headerField.Key, headerField.Value) });
+ data.Add(headers);
+ }
+
+ return data;
+ }
+ }
+
+ public static TheoryData>> ConnectMissingPseudoHeaderFieldData
+ {
+ get
+ {
+ var data = new TheoryData>>();
+ var methodHeader = new KeyValuePair(HeaderNames.Method, "CONNECT");
+ var headers = new[] { methodHeader };
+ data.Add(headers);
+
+ return data;
+ }
+ }
+
+ public static TheoryData>> PseudoHeaderFieldAfterRegularHeadersData
+ {
+ get
+ {
+ var data = new TheoryData>>();
+ var requestHeaders = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Authority, "127.0.0.1"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("content-length", "0")
+ };
+
+ foreach (var headerField in requestHeaders.Where(h => h.Key.StartsWith(':')))
+ {
+ var headers = requestHeaders.Except(new[] { headerField }).Concat(new[] { headerField });
+ data.Add(headers);
+ }
+
+ return data;
+ }
+ }
+
+ public static TheoryData>> MissingPseudoHeaderFieldData
+ {
+ get
+ {
+ var data = new TheoryData>>();
+ var requestHeaders = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ };
+
+ foreach (var headerField in requestHeaders)
+ {
+ var headers = requestHeaders.Except(new[] { headerField });
+ data.Add(headers);
+ }
+
+ return data;
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(DuplicatePseudoHeaderFieldData))]
+ public Task HEADERS_Received_HeaderBlockContainsDuplicatePseudoHeaderField_ConnectionError(IEnumerable> headers)
+ {
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, expectedErrorMessage: CoreStrings.HttpErrorDuplicatePseudoHeaderField);
+ }
+
+ [Theory]
+ [MemberData(nameof(ConnectMissingPseudoHeaderFieldData))]
+ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_MethodIsCONNECT_NoError(IEnumerable> headers)
+ {
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
+
+ await requestStream.SendHeadersAsync(headers, endStream: true);
+
+ await requestStream.ExpectHeadersAsync();
+
+ await requestStream.ExpectReceiveEndOfStream();
+ }
+
+ [Theory]
+ [MemberData(nameof(PseudoHeaderFieldAfterRegularHeadersData))]
+ public Task HEADERS_Received_HeaderBlockContainsPseudoHeaderFieldAfterRegularHeaders_ConnectionError(IEnumerable> headers)
+ {
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, expectedErrorMessage: CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders);
+ }
+
+ private async Task HEADERS_Received_InvalidHeaderFields_StreamError(IEnumerable> headers, string expectedErrorMessage, Http3ErrorCode? errorCode = null)
+ {
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
+ await requestStream.SendHeadersAsync(headers, endStream: true);
+
+ await requestStream.WaitForStreamErrorAsync(
+ errorCode ?? Http3ErrorCode.MessageError,
+ expectedErrorMessage);
+ }
+
+ [Theory]
+ [MemberData(nameof(MissingPseudoHeaderFieldData))]
+ public async Task HEADERS_Received_HeaderBlockDoesNotContainMandatoryPseudoHeaderField_StreamError(IEnumerable> headers)
+ {
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
+
+ await requestStream.SendHeadersAsync(headers, endStream: true);
+ await requestStream.WaitForStreamErrorAsync(
+ Http3ErrorCode.MessageError,
+ expectedErrorMessage: CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_HeaderBlockOverLimit_ConnectionError()
+ {
+ // > 32kb
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("a", _4kHeaderValue),
+ new KeyValuePair("b", _4kHeaderValue),
+ new KeyValuePair("c", _4kHeaderValue),
+ new KeyValuePair("d", _4kHeaderValue),
+ new KeyValuePair("e", _4kHeaderValue),
+ new KeyValuePair("f", _4kHeaderValue),
+ new KeyValuePair("g", _4kHeaderValue),
+ new KeyValuePair("h", _4kHeaderValue),
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_TooManyHeaders_ConnectionError()
+ {
+ // > MaxRequestHeaderCount (100)
+ var headers = new List>();
+ headers.AddRange(new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ });
+ for (var i = 0; i < 100; i++)
+ {
+ headers.Add(new KeyValuePair(i.ToString(CultureInfo.InvariantCulture), i.ToString(CultureInfo.InvariantCulture)));
+ }
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_TooManyHeaders);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_InvalidCharacters_ConnectionError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("Custom", "val\0ue"),
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.BadRequest_MalformedRequestInvalidHeaders);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_HeaderBlockContainsConnectionHeader_ConnectionError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("connection", "keep-alive")
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField);
+ }
+
+ [Fact]
+ public Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsNotTrailers_ConnectionError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("te", "trailers, deflate")
+ };
+
+ return HEADERS_Received_InvalidHeaderFields_StreamError(headers, CoreStrings.HttpErrorConnectionSpecificHeaderField);
+ }
+
+ [Fact]
+ public async Task HEADERS_Received_HeaderBlockContainsTEHeader_ValueIsTrailers_NoError()
+ {
+ var headers = new[]
+ {
+ new KeyValuePair(HeaderNames.Method, "GET"),
+ new KeyValuePair(HeaderNames.Path, "/"),
+ new KeyValuePair(HeaderNames.Scheme, "http"),
+ new KeyValuePair("te", "trailers")
+ };
+
+ var requestStream = await InitializeConnectionAndStreamsAsync(_noopApplication);
+
+ await requestStream.SendHeadersAsync(headers, endStream: true);
+
+ await requestStream.ExpectHeadersAsync();
+
+ await requestStream.ExpectReceiveEndOfStream();
+ }
}
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
index d4efefeecac6..6000ab347849 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3TestBase.cs
@@ -36,6 +36,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
public class Http3TestBase : TestApplicationErrorLoggerLoggedTest, IDisposable
{
+ protected static readonly int MaxRequestHeaderFieldSize = 16 * 1024;
+ protected static readonly string _4kHeaderValue = new string('a', 4096);
+
internal TestServiceContext _serviceContext;
internal readonly TimeoutControl _timeoutControl;
internal readonly Mock _mockKestrelTrace = new Mock();
@@ -370,7 +373,7 @@ internal class Http3RequestStream : Http3StreamBase, IHttpHeadersHandler, IProto
public bool Disposed => _testStreamContext.Disposed;
public long Error { get; set; }
- private readonly byte[] _headerEncodingBuffer = new byte[16 * 1024];
+ private readonly byte[] _headerEncodingBuffer = new byte[64 * 1024];
private QPackEncoder _qpackEncoder = new QPackEncoder();
private QPackDecoder _qpackDecoder = new QPackDecoder(8192);
protected readonly Dictionary _decodedHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
diff --git a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
index 8cb1c21513d8..33869c9254ae 100644
--- a/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
+++ b/src/Shared/runtime/Http3/Helpers/VariableLengthIntegerHelper.cs
@@ -33,7 +33,7 @@ internal static class VariableLengthIntegerHelper
// public for internal use in aspnetcore
public const uint OneByteLimit = (1U << 6) - 1;
- public const uint TwoByteLimit = (1U << 16) - 1;
+ public const uint TwoByteLimit = (1U << 14) - 1;
public const uint FourByteLimit = (1U << 30) - 1;
public const long EightByteLimit = (1L << 62) - 1;
diff --git a/src/Shared/runtime/Http3/QPack/HeaderField.cs b/src/Shared/runtime/Http3/QPack/HeaderField.cs
index e299c3cb3334..b4b80c53798f 100644
--- a/src/Shared/runtime/Http3/QPack/HeaderField.cs
+++ b/src/Shared/runtime/Http3/QPack/HeaderField.cs
@@ -5,6 +5,10 @@ namespace System.Net.Http.QPack
{
internal readonly struct HeaderField
{
+ // https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.3-1
+ // public for internal use in aspnetcore
+ public const int RfcOverhead = 32;
+
public HeaderField(byte[] name, byte[] value)
{
Name = name;