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

Commit dc355e9

Browse files
authored
Add folded response header support to SocketsHttpHandler (#27727)
* Add folded response header support to SocketsHttpHandler The feature is deprecated by RFC 7230, but some servers still utilize it, and if they do, SocketsHttpHandler currently fails (WinHttpHandler and the netfx handler both allow it, CurlHandler currently fails as well). This commit updates the header parsing logic to allow for them. * Address PR feedback
1 parent f5e9b75 commit dc355e9

File tree

4 files changed

+156
-78
lines changed

4 files changed

+156
-78
lines changed

src/Common/tests/System/Net/Http/LoopbackServer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ public static Task CreateServerAsync(Func<LoopbackServer, Uri, Task> funcAsync,
7474
return CreateServerAsync(server => funcAsync(server, server.Uri), options);
7575
}
7676

77-
public static Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<LoopbackServer, Task> serverFunc)
77+
public static Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<LoopbackServer, Task> serverFunc, Options options = null)
7878
{
7979
return CreateServerAsync(async server =>
8080
{
8181
Task clientTask = clientFunc(server.Uri);
8282
Task serverTask = serverFunc(server);
8383

8484
await new Task[] { clientTask, serverTask }.WhenAllOrAnyFailed();
85-
});
85+
}, options);
8686
}
8787

8888
public async Task AcceptConnectionAsync(Func<Connection, Task> funcAsync)

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

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ internal partial class HttpConnection : IDisposable
6666

6767
public HttpConnection(
6868
HttpConnectionPool pool,
69-
Stream stream,
69+
Stream stream,
7070
TransportContext transportContext)
7171
{
7272
Debug.Assert(pool != null);
@@ -413,7 +413,7 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
413413

414414
// Parse the response status line.
415415
var response = new HttpResponseMessage() { RequestMessage = request, Content = new HttpConnectionResponseContent() };
416-
ParseStatusLine(await ReadNextLineAsync().ConfigureAwait(false), response);
416+
ParseStatusLine(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false), response);
417417

418418
// If we sent an Expect: 100-continue header, handle the response accordingly.
419419
if (allowExpect100ToContinue != null)
@@ -441,12 +441,12 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
441441
if (response.StatusCode == HttpStatusCode.Continue)
442442
{
443443
// We got our continue header. Read the subsequent empty line and parse the additional status line.
444-
if (!LineIsEmpty(await ReadNextLineAsync().ConfigureAwait(false)))
444+
if (!LineIsEmpty(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false)))
445445
{
446446
ThrowInvalidHttpResponse();
447447
}
448448

449-
ParseStatusLine(await ReadNextLineAsync().ConfigureAwait(false), response);
449+
ParseStatusLine(await ReadNextResponseHeaderLineAsync().ConfigureAwait(false), response);
450450
}
451451
}
452452
}
@@ -463,7 +463,7 @@ public async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request,
463463
// Parse the response headers.
464464
while (true)
465465
{
466-
ArraySegment<byte> line = await ReadNextLineAsync().ConfigureAwait(false);
466+
ArraySegment<byte> line = await ReadNextResponseHeaderLineAsync(foldedHeadersAllowed: true).ConfigureAwait(false);
467467
if (LineIsEmpty(line))
468468
{
469469
break;
@@ -600,7 +600,7 @@ public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool doRe
600600
}
601601

602602
private HttpContentWriteStream CreateRequestContentStream(HttpRequestMessage request)
603-
{
603+
{
604604
bool requestTransferEncodingChunked = request.HasHeaders && request.Headers.TransferEncodingChunked == true;
605605
HttpContentWriteStream requestContentStream = requestTransferEncodingChunked ? (HttpContentWriteStream)
606606
new ChunkedEncodingWriteStream(this) :
@@ -1060,16 +1060,13 @@ private bool TryReadNextLine(out ReadOnlySpan<byte> line)
10601060
int bytesConsumed = length + 1;
10611061
_readOffset += bytesConsumed;
10621062
_allowedReadLineBytes -= bytesConsumed;
1063-
if (_allowedReadLineBytes < 0)
1064-
{
1065-
ThrowInvalidHttpResponse();
1066-
}
1063+
ThrowIfExceededAllowedReadLineBytes();
10671064

10681065
line = buffer.Slice(0, length > 0 && buffer[length - 1] == '\r' ? length - 1 : length);
10691066
return true;
10701067
}
10711068

1072-
private async ValueTask<ArraySegment<byte>> ReadNextLineAsync()
1069+
private async ValueTask<ArraySegment<byte>> ReadNextResponseHeaderLineAsync(bool foldedHeadersAllowed = false)
10731070
{
10741071
int previouslyScannedBytes = 0;
10751072
while (true)
@@ -1080,34 +1077,94 @@ private async ValueTask<ArraySegment<byte>> ReadNextLineAsync()
10801077
{
10811078
int startIndex = _readOffset;
10821079
int length = lfIndex - startIndex;
1083-
if (length > 0 && _readBuffer[startIndex + length - 1] == '\r')
1080+
if (lfIndex > 0 && _readBuffer[lfIndex - 1] == '\r')
10841081
{
10851082
length--;
10861083
}
10871084

1088-
// Advance read position past the LF
1089-
_allowedReadLineBytes -= lfIndex + 1 - scanOffset;
1090-
if (_allowedReadLineBytes < 0)
1085+
// If this isn't the ending header, we need to account for the possibility
1086+
// of folded headers, which per RFC2616 are headers split across multiple
1087+
// lines, where the continuation line begins with a space or horizontal tab.
1088+
// The feature was deprecated in RFC 7230 3.2.4, but some servers still use it.
1089+
if (foldedHeadersAllowed && length > 0)
10911090
{
1092-
ThrowInvalidHttpResponse();
1091+
// If the newline is the last character we've buffered, we need at least
1092+
// one more character in order to see whether it's space/tab, in which
1093+
// case it's a folded header.
1094+
if (lfIndex + 1 == _readLength)
1095+
{
1096+
// The LF is at the end of the buffer, so we need to read more
1097+
// to determine whether there's a continuation. We'll read
1098+
// and then loop back around again, but to avoid needing to
1099+
// rescan the whole header, reposition to one character before
1100+
// the newline so that we'll find it quickly.
1101+
int backPos = _readBuffer[lfIndex - 1] == '\r' ? lfIndex - 2 : lfIndex - 1;
1102+
Debug.Assert(backPos >= 0);
1103+
previouslyScannedBytes = backPos - _readOffset;
1104+
_allowedReadLineBytes -= backPos - scanOffset;
1105+
ThrowIfExceededAllowedReadLineBytes();
1106+
await FillAsync().ConfigureAwait(false);
1107+
continue;
1108+
}
1109+
1110+
// We have at least one more character we can look at.
1111+
Debug.Assert(lfIndex + 1 < _readLength);
1112+
char nextChar = (char)_readBuffer[lfIndex + 1];
1113+
if (nextChar == ' ' || nextChar == '\t')
1114+
{
1115+
// The next header is a continuation.
1116+
1117+
// Folded headers are only allowed within header field values, not within header field names,
1118+
// so if we haven't seen a colon, this is invalid.
1119+
if (Array.IndexOf(_readBuffer, (byte)':', _readOffset, lfIndex - _readOffset) == -1)
1120+
{
1121+
ThrowInvalidHttpResponse();
1122+
}
1123+
1124+
// When we return the line, we need the interim newlines filtered out. According
1125+
// to RFC 7230 3.2.4, a valid approach to dealing with them is to "replace each
1126+
// received obs-fold with one or more SP octets prior to interpreting the field
1127+
// value or forwarding the message downstream", so that's what we do.
1128+
_readBuffer[lfIndex] = (byte)' ';
1129+
if (_readBuffer[lfIndex - 1] == '\r')
1130+
{
1131+
_readBuffer[lfIndex - 1] = (byte)' ';
1132+
}
1133+
1134+
// Update how much we've read, and simply go back to search for the next newline.
1135+
previouslyScannedBytes = (lfIndex + 1 - _readOffset);
1136+
_allowedReadLineBytes -= (lfIndex + 1 - scanOffset);
1137+
ThrowIfExceededAllowedReadLineBytes();
1138+
continue;
1139+
}
1140+
1141+
// Not at the end of a header with a continuation.
10931142
}
1143+
1144+
// Advance read position past the LF
1145+
_allowedReadLineBytes -= lfIndex + 1 - scanOffset;
1146+
ThrowIfExceededAllowedReadLineBytes();
10941147
_readOffset = lfIndex + 1;
10951148

10961149
return new ArraySegment<byte>(_readBuffer, startIndex, length);
10971150
}
10981151

1099-
// Couldn't find LF. Read more.
1100-
// Note this may cause _readOffset to change.
1152+
// Couldn't find LF. Read more. Note this may cause _readOffset to change.
11011153
previouslyScannedBytes = _readLength - _readOffset;
11021154
_allowedReadLineBytes -= _readLength - scanOffset;
1103-
if (_allowedReadLineBytes < 0)
1104-
{
1105-
ThrowInvalidHttpResponse();
1106-
}
1155+
ThrowIfExceededAllowedReadLineBytes();
11071156
await FillAsync().ConfigureAwait(false);
11081157
}
11091158
}
11101159

1160+
private void ThrowIfExceededAllowedReadLineBytes()
1161+
{
1162+
if (_allowedReadLineBytes < 0)
1163+
{
1164+
ThrowInvalidHttpResponse();
1165+
}
1166+
}
1167+
11111168
// Throws IOException on EOF. This is only called when we expect more data.
11121169
private async Task FillAsync()
11131170
{

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
2626
for (int i = 0; i < count; i++)
2727
{
2828
await _wrapped.WriteAsync(buffer, offset + i, 1);
29+
await _wrapped.FlushAsync();
2930
await Task.Yield(); // introduce short delays, enough to send packets individually but not so long as to extend test duration significantly
3031
}
3132
}
@@ -41,6 +42,7 @@ public override void Write(byte[] buffer, int offset, int count)
4142
for (int i = 0; i < count; i++)
4243
{
4344
_wrapped.Write(buffer, offset + i, 1);
45+
_wrapped.Flush();
4446
}
4547
}
4648
catch (IOException) when (_clientDisconnectAllowed)

0 commit comments

Comments
 (0)