Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 85c943f

Browse files
author
Geoff Kizer
committed
support LF line endings and trailer headers
1 parent 7c4d35b commit 85c943f

File tree

5 files changed

+78
-78
lines changed

5 files changed

+78
-78
lines changed

src/System.Net.Http/src/System/Net/Http/Managed/ChunkedEncodingReadStream.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,23 @@ private async Task<bool> TryGetNextChunk(CancellationToken cancellationToken)
3434
return true;
3535
}
3636

37-
// Indicates end of response body. We expect final CRLF after this.
38-
await _connection.ReadCrLfAsync(cancellationToken).ConfigureAwait(false);
37+
// We received a chunk size of 0, which indicates end of response body.
38+
// Read and discard any trailing headers, until we see an empty line.
39+
while (!LineIsEmpty(await _connection.ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
40+
;
41+
3942
_connection.ReturnConnectionToPool();
4043
_connection = null;
4144
return false;
4245
}
4346

4447
private ulong ParseHexSize(ArraySegment<byte> line)
4548
{
49+
if (line.Count == 0)
50+
{
51+
throw new IOException(SR.net_http_invalid_response);
52+
}
53+
4654
ulong size = 0;
4755
try
4856
{
@@ -63,10 +71,6 @@ private ulong ParseHexSize(ArraySegment<byte> line)
6371
}
6472
else
6573
{
66-
if (c == '\r' && i > 0)
67-
{
68-
break;
69-
}
7074
throw new IOException(SR.net_http_invalid_response);
7175
}
7276
}
@@ -84,7 +88,7 @@ private async Task ConsumeChunkBytes(ulong bytesConsumed, CancellationToken canc
8488
_chunkBytesRemaining -= bytesConsumed;
8589
if (_chunkBytesRemaining == 0)
8690
{
87-
await _connection.ReadCrLfAsync(cancellationToken).ConfigureAwait(false);
91+
await _connection.ReadEmptyLineAsync(cancellationToken).ConfigureAwait(false);
8892
}
8993
}
9094

src/System.Net.Http/src/System/Net/Http/Managed/HttpConnection.cs

Lines changed: 32 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,8 @@ await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11N
407407
// And if this was 100 continue, deal with the extra headers.
408408
if (response.StatusCode == HttpStatusCode.Continue)
409409
{
410-
// We got our continue header. Read the subsequent \r\n and parse the additional status line.
411-
if (!LineIsEmpty(await ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
412-
{
413-
ThrowInvalidHttpResponse();
414-
}
415-
410+
// We got our continue header. Read the subsequent empty line and parse the additional status line.
411+
await ReadEmptyLineAsync(cancellationToken).ConfigureAwait(false);
416412
ParseStatusLine(await ReadNextLineAsync(cancellationToken).ConfigureAwait(false), response);
417413
}
418414
}
@@ -511,8 +507,7 @@ await WriteBytesAsync(isHttp10 ? s_spaceHttp10NewlineAsciiBytes : s_spaceHttp11N
511507

512508
private static bool LineIsEmpty(ArraySegment<byte> line)
513509
{
514-
Debug.Assert(line.Count >= 2, "Lines should always be \r\n terminated.");
515-
return line.Count == 2;
510+
return line.Count == 0;
516511
}
517512

518513
private async Task SendRequestContentAsync(Task copyTask, HttpContentWriteStream stream)
@@ -572,7 +567,7 @@ private void ParseStatusLine(Span<byte> line, HttpResponseMessage response)
572567
// We sent the request version as either 1.0 or 1.1.
573568
// We expect a response version of the form 1.X, where X is a single digit as per RFC.
574569

575-
if (line.Length < 14 || // "HTTP/1.1 123\r\n" with optional phrase before the crlf
570+
if (line.Length < 12 || // "HTTP/1.1 123" with optional phrase
576571
line[0] != 'H' ||
577572
line[1] != 'T' ||
578573
line[2] != 'T' ||
@@ -604,39 +599,25 @@ private void ParseStatusLine(Span<byte> line, HttpResponseMessage response)
604599
(HttpStatusCode)(100 * (status1 - '0') + 10 * (status2 - '0') + (status3 - '0'));
605600

606601
// Parse (optional) reason phrase
607-
byte c = line[12];
608-
if (c == '\r')
602+
if (line.Length == 12)
609603
{
610604
response.ReasonPhrase = string.Empty;
611605
}
612-
else if (c != ' ')
606+
else if (line[12] != ' ')
613607
{
614608
ThrowInvalidHttpResponse();
615609
}
616610
else
617611
{
618-
Span<byte> reasonBytes = line.Slice(13, line.Length - 13 - 2); // 2 == \r\n ending trimmed off
612+
Span<byte> reasonBytes = line.Slice(13);
619613
string knownReasonPhrase = HttpStatusDescription.Get(response.StatusCode);
620614
if (knownReasonPhrase != null && EqualsOrdinal(knownReasonPhrase, reasonBytes))
621615
{
622616
response.ReasonPhrase = knownReasonPhrase;
623617
}
624618
else
625619
{
626-
unsafe
627-
{
628-
try
629-
{
630-
fixed (byte* reasonPtr = &MemoryMarshal.GetReference(reasonBytes))
631-
{
632-
response.ReasonPhrase = Encoding.ASCII.GetString(reasonPtr, reasonBytes.Length);
633-
}
634-
}
635-
catch (FormatException e)
636-
{
637-
ThrowInvalidHttpResponse(e);
638-
}
639-
}
620+
response.ReasonPhrase = Encoding.ASCII.GetString(reasonBytes);
640621
}
641622
}
642623
}
@@ -649,6 +630,8 @@ private void ParseHeaderNameValue(ArraySegment<byte> line, HttpResponseMessage r
649630

650631
private void ParseHeaderNameValue(Span<byte> line, HttpResponseMessage response)
651632
{
633+
Debug.Assert(line.Length > 0);
634+
652635
int pos = 0;
653636
while (line[pos] != (byte)':' && line[pos] != (byte)' ')
654637
{
@@ -666,8 +649,6 @@ private void ParseHeaderNameValue(Span<byte> line, HttpResponseMessage response)
666649
return;
667650
}
668651

669-
// CONSIDER: trailing whitespace?
670-
671652
if (!HeaderDescriptor.TryGet(line.Slice(0, pos), out HeaderDescriptor descriptor))
672653
{
673654
// Ignore invalid header name
@@ -692,12 +673,12 @@ private void ParseHeaderNameValue(Span<byte> line, HttpResponseMessage response)
692673
}
693674

694675
// Skip whitespace after colon
695-
while (pos < line.Length && (line[pos] == (byte)' ' || line[pos] == '\t'))
676+
while (pos < line.Length && (line[pos] == (byte)' ' || line[pos] == (byte)'\t'))
696677
{
697678
pos++;
698679
}
699680

700-
string headerValue = descriptor.GetHeaderValue(line.Slice(pos, line.Length - pos - 2)); // trim trailing \r\n
681+
string headerValue = descriptor.GetHeaderValue(line.Slice(pos));
701682

702683
// Note we ignore the return value from TryAddWithoutValidation;
703684
// if the header can't be added, we silently drop it.
@@ -933,54 +914,37 @@ private Task WriteToStreamAsync(ReadOnlyMemory<byte> source, CancellationToken c
933914

934915
private async ValueTask<ArraySegment<byte>> ReadNextLineAsync(CancellationToken cancellationToken)
935916
{
936-
int searchOffset = 0;
917+
int previouslyScannedBytes = 0;
937918
while (true)
938919
{
939-
int startIndex = _readOffset + searchOffset;
940-
int length = _readLength - startIndex;
941-
int crPos = Array.IndexOf(_readBuffer, (byte)'\r', startIndex, length);
942-
if (crPos < 0)
943-
{
944-
// Couldn't find a \r. Read more.
945-
searchOffset = length;
946-
await FillAsync(cancellationToken).ConfigureAwait(false);
947-
}
948-
else if (crPos + 1 >= _readLength)
920+
int scanOffset = _readOffset + previouslyScannedBytes;
921+
int endIndex = Array.IndexOf(_readBuffer, (byte)'\n', scanOffset, _readLength - scanOffset);
922+
if (endIndex >= 0)
949923
{
950-
// We found a \r, but we don't have enough data buffered to read the \n.
951-
searchOffset = length - 1;
952-
await FillAsync(cancellationToken).ConfigureAwait(false);
953-
}
954-
else if (_readBuffer[crPos + 1] == '\n')
955-
{
956-
// We found a \r\n. Return the data up to and including it.
957-
int lineLength = crPos - _readOffset + 2;
958-
var result = new ArraySegment<byte>(_readBuffer, _readOffset, lineLength);
959-
_readOffset += lineLength;
960-
return result;
961-
}
962-
else
963-
{
964-
ThrowInvalidHttpResponse();
924+
int startIndex = _readOffset;
925+
_readOffset = endIndex + 1;
926+
927+
if (endIndex > startIndex && _readBuffer[endIndex - 1] == '\r')
928+
{
929+
endIndex--;
930+
}
931+
932+
return new ArraySegment<byte>(_readBuffer, startIndex, endIndex - startIndex);
965933
}
966-
}
967-
}
968934

969-
private async Task ReadCrLfAsync(CancellationToken cancellationToken)
970-
{
971-
while (_readLength - _readOffset < 2)
972-
{
973-
// We have fewer than 2 chars buffered. Get more.
935+
// Couldn't find LF. Read more.
936+
// Note this may cause _readOffset to change.
937+
previouslyScannedBytes = _readLength - _readOffset;
974938
await FillAsync(cancellationToken).ConfigureAwait(false);
975939
}
940+
}
976941

977-
// We expect \r\n. If so, consume them. If not, it's an error.
978-
if (_readBuffer[_readOffset] != '\r' || _readBuffer[_readOffset + 1] != '\n')
942+
private async Task ReadEmptyLineAsync(CancellationToken cancellationToken)
943+
{
944+
if (!LineIsEmpty(await ReadNextLineAsync(cancellationToken).ConfigureAwait(false)))
979945
{
980946
ThrowInvalidHttpResponse();
981947
}
982-
983-
_readOffset += 2;
984948
}
985949

986950
// Throws IOException on EOF. This is only called when we expect more data.

src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,15 +1159,15 @@ await TestHelper.WhenAllCompletedOrAnyFailed(
11591159
}
11601160

11611161
[OuterLoop] // TODO: Issue #11345
1162-
[ActiveIssue(17174, TestPlatforms.AnyUnix)] // https://github.com/curl/curl/issues/1354
11631162
[Theory]
11641163
[InlineData(false)]
11651164
[InlineData(true)]
11661165
public async Task GetAsync_TrailingHeaders_Ignored(bool includeTrailerHeader)
11671166
{
1168-
if (UseManagedHandler)
1167+
if (IsCurlHandler)
11691168
{
1170-
// TODO #23130: The managed handler isn't correctly handling trailing headers.
1169+
// ActiveIssue #17174: CurlHandler has a problem here
1170+
// https://github.com/curl/curl/issues/1354
11711171
return;
11721172
}
11731173

src/System.Net.Http/tests/FunctionalTests/HttpClientTestBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ static HttpClientTestBase()
3131

3232
protected virtual bool UseManagedHandler => false;
3333

34+
protected bool IsWinHttpHandler => !UseManagedHandler && PlatformDetection.IsWindows && !PlatformDetection.IsUap && !PlatformDetection.IsFullFramework;
35+
protected bool IsCurlHandler => !UseManagedHandler && !PlatformDetection.IsWindows;
36+
protected bool IsNetfxHandler => !UseManagedHandler && PlatformDetection.IsWindows && PlatformDetection.IsFullFramework;
37+
protected bool IsUapHandler => !UseManagedHandler && PlatformDetection.IsWindows && PlatformDetection.IsUap;
38+
3439
protected HttpClient CreateHttpClient() => new HttpClient(CreateHttpClientHandler());
3540

3641
protected HttpClientHandler CreateHttpClientHandler() => CreateHttpClientHandler(UseManagedHandler);

src/System.Net.Http/tests/FunctionalTests/HttpProtocolTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,33 @@ await LoopbackServer.CreateServerAsync(async (server, url) =>
478478
}
479479
});
480480
}
481+
482+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] // Does not support LF-only
483+
[Theory]
484+
[InlineData("\r\n")]
485+
[InlineData("\n")]
486+
public async Task GetAsync_ResponseHasNormalLineEndings_Success(string lineEnding)
487+
{
488+
await LoopbackServer.CreateServerAsync(async (server, url) =>
489+
{
490+
using (HttpClient client = CreateHttpClient())
491+
{
492+
Task<HttpResponseMessage> getResponseTask = client.GetAsync(url);
493+
Task<List<string>> serverTask = LoopbackServer.ReadRequestAndSendResponseAsync(server,
494+
$"HTTP/1.1 200 OK{lineEnding}Date: {DateTimeOffset.UtcNow:R}{lineEnding}Server: TestServer{lineEnding}Content-Length: 0{lineEnding}{lineEnding}",
495+
new LoopbackServer.Options { ResponseStreamWrapper = GetStream });
496+
497+
await TestHelper.WhenAllCompletedOrAnyFailed(getResponseTask, serverTask);
498+
499+
using (HttpResponseMessage response = await getResponseTask)
500+
{
501+
Assert.Equal(200, (int)response.StatusCode);
502+
Assert.Equal("OK", response.ReasonPhrase);
503+
Assert.Equal("TestServer", response.Headers.Server.ToString());
504+
}
505+
}
506+
});
507+
}
481508
}
482509

483510
public class HttpProtocolTests_Dribble : HttpProtocolTests

0 commit comments

Comments
 (0)