Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
30da832
downgrade proposal for HttpClient
May 24, 2022
7b03bc8
HttpClient to handle ws over h2
Jun 7, 2022
e7b8332
ClientWebSocket to handle ws over h2
Jun 7, 2022
a5e991a
Add property for protocol header and remove it from known headers
Jun 17, 2022
264c802
Update src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestM…
Jun 18, 2022
08d1f8c
Address review feedback
Jun 18, 2022
71cc887
Rename HttpVersion and HttpVersionPolicy
Jun 20, 2022
8019a3c
Apply suggestions from code review
Jun 21, 2022
8ef8b4c
address review feedback
Jun 21, 2022
ac966a8
Merge branch 'main' into ws-h2-draft
Jun 23, 2022
f6de72a
address review feedback
Jun 24, 2022
9221483
address review feedback
Jun 28, 2022
fafc84b
Apply suggestions from code review
Jun 28, 2022
0d8f8f7
fix race condition on setting enable connect
Jun 29, 2022
9be5b95
inherit h2 read and writes streams
Jun 29, 2022
502b051
Apply suggestions from code review
Jun 30, 2022
e635439
fix H2PACK encoding issue
Jun 30, 2022
ad66857
add timeout for waiting settings task
Jun 30, 2022
c58d993
generalized settings received task
Jun 30, 2022
76c9e20
Update src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpH…
Jul 1, 2022
2467449
Fixing HttpStream tests
Jul 1, 2022
d9248a6
Apply suggestions from code review
Jul 4, 2022
baf52d4
address review feedback
Jul 4, 2022
afaf5be
Apply suggestions from code review
Jul 8, 2022
918c6a8
Address review feedback
Jul 8, 2022
9734075
Adapt test to ValueTask.FromException
Jul 8, 2022
efdcdba
Apply suggestions from code review
Jul 10, 2022
6f7b101
Adding connect tests
Jul 10, 2022
595642c
Apply suggestions from code review
Jul 10, 2022
e05db11
feedback + skip tests on browser
Jul 10, 2022
54d0b08
Merge branch 'main' into ws-h2-draft
Jul 10, 2022
b7f543b
Feedback + test for websocket stream
Jul 12, 2022
73968ec
Update src/libraries/System.Net.Http/src/Resources/Strings.resx
Jul 12, 2022
4dbe1c2
Address review feedback
Jul 12, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
Assert.Equal(PlatformDetection.IsBrowser && !enableWasmStreaming, responseStream.CanSeek);

// Not supported operations
Assert.Throws<NotSupportedException>(() => responseStream.BeginWrite(new byte[1], 0, 1, null, null));
await Assert.ThrowsAsync<NotSupportedException>(async () => await Task.Factory.FromAsync(responseStream.BeginWrite, responseStream.EndWrite, new byte[1], 0, 1, null));
if (!responseStream.CanSeek)
{
Assert.Throws<NotSupportedException>(() => responseStream.Length);
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Net.Http/ref/System.Net.Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public HttpMethod(string method) { }
public static System.Net.Http.HttpMethod Post { get { throw null; } }
public static System.Net.Http.HttpMethod Put { get { throw null; } }
public static System.Net.Http.HttpMethod Trace { get { throw null; } }
public static System.Net.Http.HttpMethod Connect { get { throw null; } }
public bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] System.Net.Http.HttpMethod? other) { throw null; }
public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] object? obj) { throw null; }
public override int GetHashCode() { throw null; }
Expand Down Expand Up @@ -656,6 +657,7 @@ internal HttpRequestHeaders() { }
public System.DateTimeOffset? IfUnmodifiedSince { get { throw null; } set { } }
public int? MaxForwards { get { throw null; } set { } }
public System.Net.Http.Headers.HttpHeaderValueCollection<System.Net.Http.Headers.NameValueHeaderValue> Pragma { get { throw null; } }
public string? Protocol { get { throw null; } set { } }
public System.Net.Http.Headers.AuthenticationHeaderValue? ProxyAuthorization { get { throw null; } set { } }
public System.Net.Http.Headers.RangeHeaderValue? Range { get { throw null; } set { } }
public System.Uri? Referrer { get { throw null; } set { } }
Expand Down
8 changes: 7 additions & 1 deletion src/libraries/System.Net.Http/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@
<data name="net_http_content_readonly_stream" xml:space="preserve">
<value>The stream does not support writing.</value>
</data>
<data name="net_http_content_writeonly_stream" xml:space="preserve">
<value>The stream does not support reading.</value>
</data>
<data name="net_http_content_invalid_charset" xml:space="preserve">
<value>The character set provided in ContentType is invalid. Cannot read content as string using an invalid character set.</value>
</data>
Expand Down Expand Up @@ -564,4 +567,7 @@
<data name="net_http_chunk_too_large" xml:space="preserve">
<value>The HTTP/1.1 response chunk was too large.</value>
</data>
</root>
<data name="net_unsupported_extended_connect" xml:space="preserve">
<value>Extended CONNECT is not supported.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public int? MaxForwards
set { SetOrRemoveParsedValue(KnownHeaders.MaxForwards.Descriptor, value); }
}

public string? Protocol { get; set; }

public AuthenticationHeaderValue? ProxyAuthorization
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public static HttpMethod Patch
// Don't expose CONNECT as static property, since it's used by the transport to connect to a proxy.
// CONNECT is not used by users directly.

internal static HttpMethod Connect
public static HttpMethod Connect
{
get { return s_connectMethod; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ public override string ToString()

internal bool WasRedirected() => (_sendStatus & MessageIsRedirect) != 0;

internal bool IsWebSocketH2Request() => _version.Major == 2 && Method == HttpMethod.Connect && HasHeaders && Headers.Protocol == "websocket";

#region IDisposable Members

protected virtual void Dispose(bool disposing)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using System.IO;
using System.Net.Http.Headers;
using System.Net.Http.HPack;
using System.Net.Security;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
Expand All @@ -19,6 +18,8 @@ namespace System.Net.Http
{
internal sealed partial class Http2Connection : HttpConnectionBase
{
private static ReadOnlySpan<byte> ProtocolLiteralHeaderBytes => new byte[] { 0x0, 0x9, 0x3a, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c };

private readonly HttpConnectionPool _pool;
private readonly Stream _stream;

Expand Down Expand Up @@ -826,17 +827,33 @@ private void ProcessSettingsFrame(FrameHeader frameHeader, bool initialFrame = f
// We don't actually store this value; we always send frames of the minimum size (16K).
break;

case SettingId.EnableConnect:
if (settingValue == 1)
{
IsConnectEnabled = true;
}
break;

default:
// All others are ignored because we don't care about them.
// Note, per RFC, unknown settings IDs should be ignored.
break;
}
}

if (initialFrame && !maxConcurrentStreamsReceived)
if (initialFrame)
{
// Set to 'infinite' because MaxConcurrentStreams was not set on the initial SETTINGS frame.
ChangeMaxConcurrentStreams(int.MaxValue);
if (!maxConcurrentStreamsReceived)
{
// Set to 'infinite' because MaxConcurrentStreams was not set on the initial SETTINGS frame.
ChangeMaxConcurrentStreams(int.MaxValue);
}

if (_initialSettingsReceived is null)
{
Interlocked.CompareExchange(ref _initialSettingsReceived, s_settingsReceivedSingleton, null);
}
InitialSettingsReceived.TrySetResult(true);
}

_incomingBuffer.Discard(frameHeader.PayloadLength);
Expand Down Expand Up @@ -1448,6 +1465,14 @@ private void WriteHeaders(HttpRequestMessage request, ref ArrayBuffer headerBuff

if (request.HasHeaders)
{
if (request.Headers.Protocol != null)
{
HttpHeaders.CheckContainsNewLine(request.Headers.Protocol);
WriteBytes(ProtocolLiteralHeaderBytes, ref headerBuffer);
Encoding? protocolEncoding = _pool.Settings._requestHeaderEncodingSelector?.Invoke(":protocol", request);
WriteLiteralHeaderValue(request.Headers.Protocol, protocolEncoding, ref headerBuffer);
}

WriteHeaderCollection(request, request.Headers, ref headerBuffer);
}

Expand Down Expand Up @@ -1888,8 +1913,26 @@ private enum SettingId : ushort
MaxConcurrentStreams = 0x3,
InitialWindowSize = 0x4,
MaxFrameSize = 0x5,
MaxHeaderListSize = 0x6
MaxHeaderListSize = 0x6,
EnableConnect = 0x8
}

private static readonly TaskCompletionSourceWithCancellation<bool> s_settingsReceivedSingleton = CreateSuccessfullyCompleted();

internal TaskCompletionSourceWithCancellation<bool> InitialSettingsReceived =>
_initialSettingsReceived ??
Interlocked.CompareExchange(ref _initialSettingsReceived, new(), null) ??
_initialSettingsReceived;

private TaskCompletionSourceWithCancellation<bool>? _initialSettingsReceived;

private static TaskCompletionSourceWithCancellation<bool> CreateSuccessfullyCompleted()
{
var tcs = new TaskCompletionSourceWithCancellation<bool>();
tcs.TrySetResult(true);
return tcs;
}
internal bool IsConnectEnabled { get; private set; }

// Note that this is safe to be called concurrently by multiple threads.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;

Expand Down Expand Up @@ -44,6 +45,7 @@ private sealed class Http2Stream : IValueTaskSource, IHttpStreamHeadersHandler,
private StreamCompletionState _requestCompletionState;
private StreamCompletionState _responseCompletionState;
private ResponseProtocolState _responseProtocolState;
private bool _webSocketEstablished;

// If this is not null, then we have received a reset from the server
// (i.e. RST_STREAM or general IO error processing the connection)
Expand Down Expand Up @@ -106,6 +108,10 @@ public Http2Stream(HttpRequestMessage request, Http2Connection connection)
if (_request.Content == null)
{
_requestCompletionState = StreamCompletionState.Completed;
if (_request.IsWebSocketH2Request())
{
_requestBodyCancellationSource = new CancellationTokenSource();
}
}
else
{
Expand Down Expand Up @@ -629,6 +635,11 @@ private void OnStatus(int statusCode)
}
else
{
if (statusCode == 200 && _response.RequestMessage!.IsWebSocketH2Request())
{
_webSocketEstablished = true;
}

_responseProtocolState = ResponseProtocolState.ExpectingHeaders;

// If we are waiting for a 100-continue response, signal the waiter now.
Expand Down Expand Up @@ -1036,6 +1047,10 @@ public async Task ReadResponseHeadersAsync(CancellationToken cancellationToken)
MoveTrailersToResponseMessage(_response);
responseContent.SetStream(EmptyReadStream.Instance);
}
else if (_webSocketEstablished)
{
responseContent.SetStream(new Http2ReadWriteStream(this));
}
else
{
responseContent.SetStream(new Http2ReadStream(this));
Expand Down Expand Up @@ -1417,20 +1432,69 @@ private enum StreamCompletionState : byte
Failed
}

private sealed class Http2ReadStream : HttpBaseStream
private sealed class Http2ReadStream : Http2ReadWriteStream
{
public Http2ReadStream(Http2Stream http2Stream) : base(http2Stream)
{
base.CloseResponseBodyOnDispose = true;
}

public override bool CanWrite => false;

public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.net_http_content_readonly_stream);

public override ValueTask WriteAsync(ReadOnlyMemory<byte> destination, CancellationToken cancellationToken) => ValueTask.FromException(new NotSupportedException(SR.net_http_content_readonly_stream));
}

private sealed class Http2WriteStream : Http2ReadWriteStream
{
public long BytesWritten { get; private set; }

public long ContentLength { get; }

public Http2WriteStream(Http2Stream http2Stream, long contentLength) : base(http2Stream)
{
Debug.Assert(contentLength >= -1);
ContentLength = contentLength;
}

public override bool CanRead => false;

public override int Read(Span<byte> buffer) => throw new NotSupportedException(SR.net_http_content_writeonly_stream);

public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken) => ValueTask.FromException<int>(new NotSupportedException(SR.net_http_content_writeonly_stream));

public override void CopyTo(Stream destination, int bufferSize) => throw new NotSupportedException(SR.net_http_content_writeonly_stream);

public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => Task.FromException(new NotSupportedException(SR.net_http_content_writeonly_stream));

public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
BytesWritten += buffer.Length;

if ((ulong)BytesWritten > (ulong)ContentLength) // If ContentLength == -1, this will always be false
{
return ValueTask.FromException(new HttpRequestException(SR.net_http_content_write_larger_than_content_length));
}

return base.WriteAsync(buffer, cancellationToken);
}
}

public class Http2ReadWriteStream : HttpBaseStream
{
private Http2Stream? _http2Stream;
private readonly HttpResponseMessage _responseMessage;

public Http2ReadStream(Http2Stream http2Stream)
public Http2ReadWriteStream(Http2Stream http2Stream)
{
Debug.Assert(http2Stream != null);
Debug.Assert(http2Stream._response != null);
_http2Stream = http2Stream;
_responseMessage = _http2Stream._response;
}

~Http2ReadStream()
~Http2ReadWriteStream()
{
if (NetEventSource.Log.IsEnabled()) _http2Stream?.Trace("");
try
Expand All @@ -1443,6 +1507,8 @@ public Http2ReadStream(Http2Stream http2Stream)
}
}

protected bool CloseResponseBodyOnDispose { get; set; }

protected override void Dispose(bool disposing)
{
Http2Stream? http2Stream = Interlocked.Exchange(ref _http2Stream, null);
Expand All @@ -1456,14 +1522,16 @@ protected override void Dispose(bool disposing)
// protocol, we have little choice: if someone drops the Http2ReadStream without
// disposing of it, we need to a) signal to the server that the stream is being
// canceled, and b) clean up the associated state in the Http2Connection.

http2Stream.CloseResponseBody();
if (CloseResponseBodyOnDispose)
{
http2Stream.CloseResponseBody();
}

base.Dispose(disposing);
}

public override bool CanRead => _http2Stream != null;
public override bool CanWrite => false;
public override bool CanWrite => _http2Stream != null;

public override int Read(Span<byte> destination)
{
Expand Down Expand Up @@ -1506,53 +1574,8 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio
http2Stream.CopyToAsync(_responseMessage, destination, bufferSize, cancellationToken);
}

public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.net_http_content_readonly_stream);

public override ValueTask WriteAsync(ReadOnlyMemory<byte> destination, CancellationToken cancellationToken) => throw new NotSupportedException();
}

private sealed class Http2WriteStream : HttpBaseStream
{
private Http2Stream? _http2Stream;

public long BytesWritten { get; private set; }

public long ContentLength { get; private set; }

public Http2WriteStream(Http2Stream http2Stream, long contentLength)
{
Debug.Assert(http2Stream != null);
Debug.Assert(contentLength >= -1);
_http2Stream = http2Stream;
ContentLength = contentLength;
}

protected override void Dispose(bool disposing)
{
Http2Stream? http2Stream = Interlocked.Exchange(ref _http2Stream, null);
if (http2Stream == null)
{
return;
}

base.Dispose(disposing);
}

public override bool CanRead => false;
public override bool CanWrite => _http2Stream != null;

public override int Read(Span<byte> buffer) => throw new NotSupportedException();

public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken) => throw new NotSupportedException();

public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
BytesWritten += buffer.Length;

if ((ulong)BytesWritten > (ulong)ContentLength) // If ContentLength == -1, this will always be false
{
return ValueTask.FromException(new HttpRequestException(SR.net_http_content_write_larger_than_content_length));
}

Http2Stream? http2Stream = _http2Stream;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
// Use HTTP/3 if possible.
if (IsHttp3Supported() && // guard to enable trimming HTTP/3 support
_http3Enabled &&
!request.IsWebSocketH2Request() &&
(request.Version.Major >= 3 || (request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher && IsSecure)))
{
Debug.Assert(async);
Expand All @@ -1021,6 +1022,16 @@ public async ValueTask<HttpResponseMessage> SendWithVersionDetectionAndRetryAsyn
Debug.Assert(connection is not null || !_http2Enabled);
if (connection is not null)
{
if (request.IsWebSocketH2Request())
{
if (await connection.InitialSettingsReceived.WaitWithCancellationAsync(cancellationToken).ConfigureAwait(false) && !connection.IsConnectEnabled)
{
HttpRequestException exception = new(SR.net_unsupported_extended_connect);
exception.Data["SETTINGS_ENABLE_CONNECT_PROTOCOL"] = false;
throw exception;
}
}

response = await connection.SendAsync(request, async, cancellationToken).ConfigureAwait(false);
}
}
Expand Down
Loading