Handle response content length mismatches (#175).

This commit is contained in:
Cesar Blum Silveira 2016-10-07 10:23:07 -07:00
parent 8c103f0f23
commit f8813a600d
13 changed files with 482 additions and 98 deletions

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -75,7 +76,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
protected readonly long _keepAliveMilliseconds;
private readonly long _requestHeadersTimeoutMilliseconds;
private int _responseBytesWritten;
protected long _responseBytesWritten;
public Frame(ConnectionContext context)
{
@ -516,8 +517,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public void Write(ArraySegment<byte> data)
{
VerifyAndUpdateWrite(data.Count);
ProduceStartAndFireOnStarting().GetAwaiter().GetResult();
_responseBytesWritten += data.Count;
if (_canHaveBody)
{
@ -547,7 +548,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return WriteAsyncAwaited(data, cancellationToken);
}
_responseBytesWritten += data.Count;
VerifyAndUpdateWrite(data.Count);
if (_canHaveBody)
{
@ -573,8 +574,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public async Task WriteAsyncAwaited(ArraySegment<byte> data, CancellationToken cancellationToken)
{
VerifyAndUpdateWrite(data.Count);
await ProduceStartAndFireOnStarting();
_responseBytesWritten += data.Count;
if (_canHaveBody)
{
@ -598,6 +600,23 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
private void VerifyAndUpdateWrite(int count)
{
var responseHeaders = FrameResponseHeaders;
if (responseHeaders != null &&
!responseHeaders.HasTransferEncoding &&
responseHeaders.HasContentLength &&
_responseBytesWritten + count > responseHeaders.HeaderContentLengthValue.Value)
{
_keepAlive = false;
throw new InvalidOperationException(
$"Response Content-Length mismatch: too many bytes written ({_responseBytesWritten + count} of {responseHeaders.HeaderContentLengthValue.Value}).");
}
_responseBytesWritten += count;
}
private void WriteChunked(ArraySegment<byte> data)
{
SocketOutput.Write(data, chunk: true);

View File

@ -3697,6 +3697,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
_bits = 0;
_headers = default(HeaderReferences);
MaybeUnknown?.Clear();
}
@ -5670,6 +5671,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
set
{
_contentLength = ParseContentLength(value);
_bits |= 2048L;
_headers._ContentLength = value;
_headers._rawContentLength = null;
@ -7384,6 +7386,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if ("Content-Length".Equals(key, StringComparison.OrdinalIgnoreCase))
{
_contentLength = ParseContentLength(value);
_bits |= 2048L;
_headers._ContentLength = value;
_headers._rawContentLength = null;
@ -7809,6 +7812,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
ThrowDuplicateKeyException();
}
_contentLength = ParseContentLength(value);
_bits |= 2048L;
_headers._ContentLength = value;
_headers._rawContentLength = null;
@ -8350,6 +8354,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
if (((_bits & 2048L) != 0))
{
_contentLength = null;
_bits &= ~2048L;
_headers._ContentLength = StringValues.Empty;
_headers._rawContentLength = null;
@ -8601,6 +8606,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{
_bits = 0;
_headers = default(HeaderReferences);
_contentLength = null;
MaybeUnknown?.Clear();
}

View File

@ -4,6 +4,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@ -232,6 +233,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
}
}
public static long ParseContentLength(StringValues value)
{
try
{
return long.Parse(value, NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite, CultureInfo.InvariantCulture);
}
catch (FormatException ex)
{
throw new InvalidOperationException("Content-Length value must be an integral number.", ex);
}
}
private static void ThrowInvalidHeaderCharacter(char ch)
{
throw new InvalidOperationException(string.Format("Invalid non-ASCII or control character in header: 0x{0:X4}", (ushort)ch));

View File

@ -92,6 +92,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
try
{
await _application.ProcessRequestAsync(context).ConfigureAwait(false);
var responseHeaders = FrameResponseHeaders;
if (!responseHeaders.HasTransferEncoding &&
responseHeaders.HasContentLength &&
_responseBytesWritten < responseHeaders.HeaderContentLengthValue.Value)
{
_keepAlive = false;
ReportApplicationError(new InvalidOperationException(
$"Response Content-Length mismatch: too few bytes written ({_responseBytesWritten} of {responseHeaders.HeaderContentLengthValue.Value})."));
}
}
catch (Exception ex)
{

View File

@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
private static readonly byte[] _CrLf = new[] { (byte)'\r', (byte)'\n' };
private static readonly byte[] _colonSpace = new[] { (byte)':', (byte)' ' };
private long? _contentLength;
public bool HasConnection => HeaderConnection.Count != 0;
public bool HasTransferEncoding => HeaderTransferEncoding.Count != 0;
@ -23,6 +25,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
public bool HasDate => HeaderDate.Count != 0;
public long? HeaderContentLengthValue => _contentLength;
public Enumerator GetEnumerator()
{
return new Enumerator(this);

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure
void ConnectionDisconnectedWrite(string connectionId, int count, Exception ex);
void ConnectionHeadResponseBodyWrite(string connectionId, int count);
void ConnectionHeadResponseBodyWrite(string connectionId, long count);
void ConnectionBadRequest(string connectionId, BadHttpRequestException ex);

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
private static readonly Action<ILogger, string, Exception> _applicationError;
private static readonly Action<ILogger, string, Exception> _connectionError;
private static readonly Action<ILogger, string, int, Exception> _connectionDisconnectedWrite;
private static readonly Action<ILogger, string, int, Exception> _connectionHeadResponseBodyWrite;
private static readonly Action<ILogger, string, long, Exception> _connectionHeadResponseBodyWrite;
private static readonly Action<ILogger, Exception> _notAllConnectionsClosedGracefully;
private static readonly Action<ILogger, string, string, Exception> _connectionBadRequest;
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_connectionDisconnectedWrite = LoggerMessage.Define<string, int>(LogLevel.Debug, 15, @"Connection id ""{ConnectionId}"" write of ""{count}"" bytes to disconnected client.");
_notAllConnectionsClosedGracefully = LoggerMessage.Define(LogLevel.Debug, 16, "Some connections failed to close gracefully during server shutdown.");
_connectionBadRequest = LoggerMessage.Define<string, string>(LogLevel.Information, 17, @"Connection id ""{ConnectionId}"" bad request data: ""{message}""");
_connectionHeadResponseBodyWrite = LoggerMessage.Define<string, int>(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response.");
_connectionHeadResponseBodyWrite = LoggerMessage.Define<string, long>(LogLevel.Debug, 18, @"Connection id ""{ConnectionId}"" write of ""{count}"" body bytes to non-body HEAD response.");
}
public KestrelTrace(ILogger logger)
@ -135,7 +135,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal
_connectionDisconnectedWrite(_logger, connectionId, count, ex);
}
public virtual void ConnectionHeadResponseBodyWrite(string connectionId, int count)
public virtual void ConnectionHeadResponseBodyWrite(string connectionId, long count)
{
_connectionHeadResponseBodyWrite(_logger, connectionId, count, null);
}

View File

@ -2,6 +2,7 @@
// 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.Linq;
using System.Net;
using System.Net.Http;
@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Internal.Http;
using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
@ -85,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
app.Run(async context =>
{
context.Response.Headers.Add(headerName, headerValue);
await context.Response.WriteAsync("");
});
});
@ -299,7 +301,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
[Fact]
public async Task ResponseBodyNotWrittenOnHeadResponse()
public async Task ResponseBodyNotWrittenOnHeadResponseAndLoggedOnlyOnce()
{
var mockKestrelTrace = new Mock<IKestrelTrace>();
@ -324,7 +326,285 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
}
mockKestrelTrace.Verify(kestrelTrace =>
kestrelTrace.ConnectionHeadResponseBodyWrite(It.IsAny<string>(), "hello, world".Length));
kestrelTrace.ConnectionHeadResponseBodyWrite(It.IsAny<string>(), "hello, world".Length), Times.Once);
}
[Fact]
public async Task WhenAppWritesMoreThanContentLengthWriteThrowsAndConnectionCloses()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(httpContext =>
{
httpContext.Response.ContentLength = 11;
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes("hello,"), 0, 6);
httpContext.Response.Body.Write(Encoding.ASCII.GetBytes(" world"), 0, 6);
return TaskCache.CompletedTask;
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 11",
"",
"hello,");
}
}
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal(
$"Response Content-Length mismatch: too many bytes written (12 of 11).",
logMessage.Exception.Message);
}
[Fact]
public async Task WhenAppWritesMoreThanContentLengthWriteAsyncThrowsAndConnectionCloses()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 11;
await httpContext.Response.WriteAsync("hello,");
await httpContext.Response.WriteAsync(" world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 11",
"",
"hello,");
}
}
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal(
$"Response Content-Length mismatch: too many bytes written (12 of 11).",
logMessage.Exception.Message);
}
[Fact]
public async Task WhenAppWritesMoreThanContentLengthAndResponseNotStarted500ResponseSentAndConnectionCloses()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 5;
await httpContext.Response.WriteAsync("hello, world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
$"HTTP/1.1 500 Internal Server Error",
"Connection: close",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
var logMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal(
$"Response Content-Length mismatch: too many bytes written (12 of 5).",
logMessage.Exception.Message);
}
[Fact]
public async Task WhenAppWritesLessThanContentLengthErrorLogged()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 13;
await httpContext.Response.WriteAsync("hello, world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 13",
"",
"hello, world");
}
}
var errorMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal(
$"Response Content-Length mismatch: too few bytes written (12 of 13).",
errorMessage.Exception.Message);
}
[Fact]
public async Task WhenAppSetsContentLengthButDoesNotWriteBody500ResponseSentAndConnectionCloses()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(httpContext =>
{
httpContext.Response.ContentLength = 5;
return TaskCache.CompletedTask;
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
$"HTTP/1.1 500 Internal Server Error",
"Connection: close",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
var errorMessage = Assert.Single(testLogger.Messages, message => message.LogLevel == LogLevel.Error);
Assert.Equal(
$"Response Content-Length mismatch: too few bytes written (0 of 5).",
errorMessage.Exception.Message);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task WhenAppSetsContentLengthToZeroAndDoesNotWriteNoErrorIsThrown(bool flushResponse)
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.ContentLength = 0;
if (flushResponse)
{
await httpContext.Response.Body.FlushAsync();
}
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Content-Length: 0",
"",
"");
}
}
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
}
// https://tools.ietf.org/html/rfc7230#section-3.3.3
// If a message is received with both a Transfer-Encoding and a
// Content-Length header field, the Transfer-Encoding overrides the
// Content-Length.
[Fact]
public async Task WhenAppSetsTransferEncodingAndContentLengthWritingLessIsNotAnError()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Transfer-Encoding"] = "chunked";
httpContext.Response.ContentLength = 13;
await httpContext.Response.WriteAsync("hello, world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"Content-Length: 13",
"",
"hello, world");
}
}
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
}
// https://tools.ietf.org/html/rfc7230#section-3.3.3
// If a message is received with both a Transfer-Encoding and a
// Content-Length header field, the Transfer-Encoding overrides the
// Content-Length.
[Fact]
public async Task WhenAppSetsTransferEncodingAndContentLengthWritingMoreIsNotAnError()
{
var testLogger = new TestApplicationErrorLogger();
var serviceContext = new TestServiceContext { Log = new TestKestrelTrace(testLogger) };
using (var server = new TestServer(async httpContext =>
{
httpContext.Response.Headers["Transfer-Encoding"] = "chunked";
httpContext.Response.ContentLength = 11;
await httpContext.Response.WriteAsync("hello, world");
}, serviceContext))
{
using (var connection = server.CreateConnection())
{
await connection.Send(
"GET / HTTP/1.1",
"",
"");
await connection.Receive(
$"HTTP/1.1 200 OK",
$"Date: {server.Context.DateHeaderValue}",
"Transfer-Encoding: chunked",
"Content-Length: 11",
"",
"hello, world");
}
}
Assert.Equal(0, testLogger.ApplicationErrorsLogged);
}
public static TheoryData<string, StringValues, string> NullHeaderData

View File

@ -620,35 +620,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task WriteOnHeadResponseLoggedOnlyOnce(TestServiceContext testContext)
{
using (var server = new TestServer(async httpContext =>
{
await httpContext.Response.WriteAsync("hello, ");
await httpContext.Response.WriteAsync("world");
await httpContext.Response.WriteAsync("!");
}, testContext))
{
using (var connection = server.CreateConnection())
{
await connection.SendEnd(
"HEAD / HTTP/1.1",
"",
"");
await connection.ReceiveEnd(
"HTTP/1.1 200 OK",
$"Date: {testContext.DateHeaderValue}",
"",
"");
}
Assert.Equal(1, ((TestKestrelTrace)testContext.Log).HeadResponseWrites);
Assert.Equal(13, ((TestKestrelTrace)testContext.Log).HeadResponseWriteByteCount);
}
}
[Theory]
[MemberData(nameof(ConnectionFilterData))]
public async Task ThrowingResultsIn500Response(TestServiceContext testContext)
@ -697,11 +668,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"Content-Length: 0",
"",
"");
Assert.False(onStartingCalled);
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
}
}
Assert.False(onStartingCalled);
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
}
[Theory]
@ -739,11 +710,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"Content-Length: 11",
"",
"Hello World");
Assert.True(onStartingCalled);
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
}
}
Assert.True(onStartingCalled);
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
}
[Theory]
@ -781,11 +752,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"Content-Length: 11",
"",
"Hello");
Assert.True(onStartingCalled);
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
}
}
Assert.True(onStartingCalled);
Assert.Equal(1, testLogger.ApplicationErrorsLogged);
}
[Theory]
@ -925,16 +896,14 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"Content-Length: 0",
"",
"");
Assert.Equal(2, onStartingCallCount2);
// The first registered OnStarting callback should not be called,
// since they are called LIFO and the other one failed.
Assert.Equal(0, onStartingCallCount1);
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
}
}
// The first registered OnStarting callback should not be called,
// since they are called LIFO and the other one failed.
Assert.Equal(0, onStartingCallCount1);
Assert.Equal(2, onStartingCallCount2);
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
}
[Theory]
@ -979,12 +948,12 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
"",
"Hello World");
}
// All OnCompleted callbacks should be called even if they throw.
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
Assert.True(onCompletedCalled1);
Assert.True(onCompletedCalled2);
}
// All OnCompleted callbacks should be called even if they throw.
Assert.Equal(2, testLogger.ApplicationErrorsLogged);
Assert.True(onCompletedCalled1);
Assert.True(onCompletedCalled2);
}
[Theory]

View File

@ -78,24 +78,29 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
{
var responseHeaders = new FrameResponseHeaders();
Assert.Throws<InvalidOperationException>(() => {
Assert.Throws<InvalidOperationException>(() =>
{
((IHeaderDictionary)responseHeaders)[key] = value;
});
Assert.Throws<InvalidOperationException>(() => {
Assert.Throws<InvalidOperationException>(() =>
{
((IHeaderDictionary)responseHeaders)[key] = new StringValues(new[] { "valid", value });
});
Assert.Throws<InvalidOperationException>(() => {
Assert.Throws<InvalidOperationException>(() =>
{
((IDictionary<string, StringValues>)responseHeaders)[key] = value;
});
Assert.Throws<InvalidOperationException>(() => {
Assert.Throws<InvalidOperationException>(() =>
{
var kvp = new KeyValuePair<string, StringValues>(key, value);
((ICollection<KeyValuePair<string, StringValues>>)responseHeaders).Add(kvp);
});
Assert.Throws<InvalidOperationException>(() => {
Assert.Throws<InvalidOperationException>(() =>
{
var kvp = new KeyValuePair<string, StringValues>(key, value);
((IDictionary<string, StringValues>)responseHeaders).Add(key, value);
});
@ -142,5 +147,83 @@ namespace Microsoft.AspNetCore.Server.KestrelTests
Assert.Throws<InvalidOperationException>(() => dictionary.Clear());
}
[Fact]
public void ThrowsWhenAddingContentLengthWithNonNumericValue()
{
var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers;
Assert.Throws<InvalidOperationException>(() => dictionary.Add("Content-Length", new[] { "bad" }));
}
[Fact]
public void ThrowsWhenSettingContentLengthToNonNumericValue()
{
var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers;
Assert.Throws<InvalidOperationException>(() => ((IHeaderDictionary)headers)["Content-Length"] = "bad");
}
[Fact]
public void ThrowsWhenAssigningHeaderContentLengthToNonNumericValue()
{
var headers = new FrameResponseHeaders();
Assert.Throws<InvalidOperationException>(() => headers.HeaderContentLength = "bad");
}
[Fact]
public void ContentLengthValueCanBeReadAsLongAfterAddingHeader()
{
var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary.Add("Content-Length", "42");
Assert.Equal(42, headers.HeaderContentLengthValue);
}
[Fact]
public void ContentLengthValueCanBeReadAsLongAfterSettingHeader()
{
var headers = new FrameResponseHeaders();
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary["Content-Length"] = "42";
Assert.Equal(42, headers.HeaderContentLengthValue);
}
[Fact]
public void ContentLengthValueCanBeReadAsLongAfterAssigningHeader()
{
var headers = new FrameResponseHeaders();
headers.HeaderContentLength = "42";
Assert.Equal(42, headers.HeaderContentLengthValue);
}
[Fact]
public void ContentLengthValueClearedWhenHeaderIsRemoved()
{
var headers = new FrameResponseHeaders();
headers.HeaderContentLength = "42";
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary.Remove("Content-Length");
Assert.Equal(null, headers.HeaderContentLengthValue);
}
[Fact]
public void ContentLengthValueClearedWhenHeadersCleared()
{
var headers = new FrameResponseHeaders();
headers.HeaderContentLength = "42";
var dictionary = (IDictionary<string, StringValues>)headers;
dictionary.Clear();
Assert.Equal(null, headers.HeaderContentLengthValue);
}
}
}

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Server.Kestrel.Internal;
using Microsoft.Extensions.Logging;
@ -12,11 +14,13 @@ namespace Microsoft.AspNetCore.Testing
// Application errors are logged using 13 as the eventId.
private const int ApplicationErrorEventId = 13;
public int TotalErrorsLogged { get; set; }
public List<LogMessage> Messages { get; } = new List<LogMessage>();
public int CriticalErrorsLogged { get; set; }
public int TotalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Error);
public int ApplicationErrorsLogged { get; set; }
public int CriticalErrorsLogged => Messages.Count(message => message.LogLevel == LogLevel.Critical);
public int ApplicationErrorsLogged => Messages.Count(message => message.EventId.Id == ApplicationErrorEventId);
public IDisposable BeginScope<TState>(TState state)
{
@ -34,20 +38,14 @@ namespace Microsoft.AspNetCore.Testing
Console.WriteLine($"Log {logLevel}[{eventId}]: {formatter(state, exception)} {exception?.Message}");
#endif
if (eventId.Id == ApplicationErrorEventId)
{
ApplicationErrorsLogged++;
}
Messages.Add(new LogMessage { LogLevel = logLevel, EventId = eventId, Exception = exception });
}
if (logLevel == LogLevel.Error)
{
TotalErrorsLogged++;
}
if (logLevel == LogLevel.Critical)
{
CriticalErrorsLogged++;
}
public class LogMessage
{
public LogLevel LogLevel { get; set; }
public EventId EventId { get; set; }
public Exception Exception { get; set; }
}
}
}

View File

@ -13,10 +13,6 @@ namespace Microsoft.AspNetCore.Testing
{
}
public int HeadResponseWrites { get; set; }
public int HeadResponseWriteByteCount { get; set; }
public override void ConnectionRead(string connectionId, int count)
{
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" recv {count} bytes.", connectionId, count);
@ -31,11 +27,5 @@ namespace Microsoft.AspNetCore.Testing
{
//_logger.LogDebug(1, @"Connection id ""{ConnectionId}"" send finished with status {status}.", connectionId, status);
}
public override void ConnectionHeadResponseBodyWrite(string connectionId, int count)
{
HeadResponseWrites++;
HeadResponseWriteByteCount = count;
}
}
}

View File

@ -14,6 +14,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.GeneratedCode
return values.Any() ? values.Select(formatter).Aggregate((a, b) => a + b) : "";
}
static string If(bool condition, Func<string> formatter)
{
return condition ? formatter() : "";
}
class KnownHeader
{
public string Name { get; set; }
@ -228,7 +233,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
return StringValues.Empty;
}}
set
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{header.SetBit()};
_headers._{header.Identifier} = value; {(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
@ -304,7 +310,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
case {byLength.Key}:
{{{Each(byLength, header => $@"
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{header.SetBit()};
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
@ -328,7 +335,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if ({header.TestBit()})
{{
ThrowDuplicateKeyException();
}}
}}{
If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{header.SetBit()};
_headers._{header.Identifier} = value;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
@ -349,7 +358,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
if (""{header.Name}"".Equals(key, StringComparison.OrdinalIgnoreCase))
{{
if ({header.TestBit()})
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = null;")}
{header.ClearBit()};
_headers._{header.Identifier} = StringValues.Empty;{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}
@ -369,6 +379,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
{{
_bits = 0;
_headers = default(HeaderReferences);
{(loop.ClassName == "FrameResponseHeaders" ? "_contentLength = null;" : "")}
MaybeUnknown?.Clear();
}}
@ -435,7 +446,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http
_headers._{header.Identifier} = AppendValue(_headers._{header.Identifier}, value);
}}
else
{{
{{{If(loop.ClassName == "FrameResponseHeaders" && header.Identifier == "ContentLength", () => @"
_contentLength = ParseContentLength(value);")}
{header.SetBit()};
_headers._{header.Identifier} = new StringValues(value);{(header.EnhancedSetter == false ? "" : $@"
_headers._raw{header.Identifier} = null;")}