diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs index 50c00ae379e3c..b84cb6cce4267 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/MsQuicConfiguration.cs @@ -42,6 +42,13 @@ public static MsQuicSafeHandle Create(QuicClientConnectionOptions options) { certificate = selectedCertificate; } + else + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(options, $"'{certificate}' not selected because it doesn't have a private key."); + } + } } else if (authenticationOptions.ClientCertificates != null) { @@ -52,6 +59,13 @@ public static MsQuicSafeHandle Create(QuicClientConnectionOptions options) certificate = clientCertificate; break; } + else + { + if (NetEventSource.Log.IsEnabled()) + { + NetEventSource.Info(options, $"'{certificate}' not selected because it doesn't have a private key."); + } + } } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs index 975bd86b1a6fa..3ec8f5348d636 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ResettableValueTaskSource.cs @@ -15,7 +15,8 @@ internal sealed class ResettableValueTaskSource : IValueTaskSource { // None -> [TryGetValueTask] -> Awaiting -> [TrySetResult|TrySetException(final: false)] -> Ready -> [GetResult] -> None // None -> [TrySetResult|TrySetException(final: false)] -> Ready -> [TryGetValueTask] -> [GetResult] -> None - // None|Awaiting -> [TrySetResult|TrySetException(final: true)] -> Final(never leaves this state) + // None|Awaiting -> [TrySetResult|TrySetException(final: true)] -> Completed(never leaves this state) + // Ready -> [GetResult: TrySet*(final: true) was called] -> Completed(never leaves this state) private enum State { None, @@ -30,7 +31,7 @@ private enum State private Action? _cancellationAction; private GCHandle _keepAlive; - private FinalTaskSource _finalTaskSource; + private TaskCompletionSource _finalTaskSource; public ResettableValueTaskSource(bool runContinuationsAsynchronously = true) { @@ -39,7 +40,8 @@ public ResettableValueTaskSource(bool runContinuationsAsynchronously = true) _cancellationRegistration = default; _keepAlive = default; - _finalTaskSource = new FinalTaskSource(runContinuationsAsynchronously); + // TODO: defer instantiation only after Task is retrieved + _finalTaskSource = new TaskCompletionSource(runContinuationsAsynchronously ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None); } /// @@ -49,11 +51,21 @@ public ResettableValueTaskSource(bool runContinuationsAsynchronously = true) public Action CancellationAction { init { _cancellationAction = value; } } /// - /// Returns true is this task source has entered its final state, i.e. or + /// Returns true is this task source has entered its final state, i.e. or /// was called with final set to true and the result was propagated. /// public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As(ref _state)) == State.Completed; + /// + /// Tries to get a value task representing this task source. If this task source is , it'll also transition it into state. + /// It prevents concurrent operations from being invoked since it'll return false if the task source was already in state. + /// In other states, it'll return a value task representing this task source without any other work. So to determine whether to invoke a P/Invoke operation or not, + /// the state of must also be checked. + /// + /// A value task representing the result. Only meaningful in case this method returns true. Might already be completed. + /// An object to hold during a P/Invoke call. It'll get release with setting the result/exception. + /// A cancellation token which might cancel the value task. + /// true if this is not an overlapping call (task source transitioned or was already set); otherwise, false. public bool TryGetValueTask(out ValueTask valueTask, object? keepAlive = null, CancellationToken cancellationToken = default) { lock (this) @@ -66,10 +78,11 @@ public bool TryGetValueTask(out ValueTask valueTask, object? keepAlive = null, C { _cancellationRegistration = cancellationToken.UnsafeRegister(static (obj, cancellationToken) => { - (ResettableValueTaskSource parent, object? target) = ((ResettableValueTaskSource, object?))obj!; - if (parent.TrySetException(new OperationCanceledException(cancellationToken))) + (ResettableValueTaskSource thisRef, object? target) = ((ResettableValueTaskSource, object?))obj!; + // This will transition the state to Ready. + if (thisRef.TrySetException(new OperationCanceledException(cancellationToken))) { - parent._cancellationAction?.Invoke(target); + thisRef._cancellationAction?.Invoke(target); } }, (this, keepAlive)); } @@ -118,45 +131,54 @@ private bool TryComplete(Exception? exception, bool final) { State state = _state; - // None,Awaiting: clean up and finish the task source. - if (state == State.Awaiting || - state == State.None) + // Completed: nothing to do. + if (state == State.Completed) + { + return false; + } + + // If the _valueTaskSource has already been set, we don't want to lose the result by overwriting it. + // So keep it as is and store the result in _finalTaskSource. + if (state == State.None || + state == State.Awaiting) { _state = final ? State.Completed : State.Ready; + } - // Swap the cancellation registration so the one that's been registered gets eventually Disposed. - // Ideally, we would dispose it here, but if the callbacks kicks in, it tries to take the lock held by this thread leading to deadlock. - cancellationRegistration = _cancellationRegistration; - _cancellationRegistration = default; + // Swap the cancellation registration so the one that's been registered gets eventually Disposed. + // Ideally, we would dispose it here, but if the callbacks kicks in, it tries to take the lock held by this thread leading to deadlock. + cancellationRegistration = _cancellationRegistration; + _cancellationRegistration = default; - // Unblock the current task source and in case of a final also the final task source. - if (exception is not null) + // Unblock the current task source and in case of a final also the final task source. + if (exception is not null) + { + // Set up the exception stack strace for the caller. + exception = exception.StackTrace is null ? ExceptionDispatchInfo.SetCurrentStackTrace(exception) : exception; + if (state == State.None || + state == State.Awaiting) { - // Set up the exception stack strace for the caller. - exception = exception.StackTrace is null ? ExceptionDispatchInfo.SetCurrentStackTrace(exception) : exception; _valueTaskSource.SetException(exception); } - else + if (final) + { + return _finalTaskSource.TrySetException(exception); + } + return state != State.Ready; + } + else + { + if (state == State.None || + state == State.Awaiting) { _valueTaskSource.SetResult(final); } - if (final) { - _finalTaskSource.TryComplete(exception); - _finalTaskSource.TrySignal(out _); + return _finalTaskSource.TrySetResult(); } - - return true; - } - - // Final: remember the first final result to set it once the current non-final result gets retrieved. - if (final) - { - return _finalTaskSource.TryComplete(exception); + return state != State.Ready; } - - return false; } finally { @@ -176,11 +198,24 @@ private bool TryComplete(Exception? exception, bool final) } } + /// + /// Tries to transition from to either or , depending on the value of . + /// Only the first call (with either value for ) is able to do that. I.e.: TrySetResult() followed by TrySetResult(true) will both return true. + /// + /// Whether this is the final transition to or just a transition into from which the task source can be reset back to . + /// true if this is the first call that set the result; otherwise, false. public bool TrySetResult(bool final = false) { return TryComplete(null, final); } + /// + /// Tries to transition from to either or , depending on the value of . + /// Only the first call is able to do that with the exception of TrySetResult() followed by TrySetResult(true), which will both return true. + /// + /// Whether this is the final transition to or just a transition into from which the task source can be reset back to . + /// The exception to set as a result of the value task. + /// true if this is the first call that set the result; otherwise, false. public bool TrySetException(Exception exception, bool final = false) { return TryComplete(exception, final); @@ -194,9 +229,11 @@ void IValueTaskSource.OnCompleted(Action continuation, object? state, s void IValueTaskSource.GetResult(short token) { + bool successful = false; try { _valueTaskSource.GetResult(token); + successful = true; } finally { @@ -207,75 +244,31 @@ void IValueTaskSource.GetResult(short token) if (state == State.Ready) { _valueTaskSource.Reset(); - if (_finalTaskSource.TrySignal(out Exception? exception)) + _state = State.None; + + // Propagate the _finalTaskSource result into _valueTaskSource if completed. + if (_finalTaskSource.Task.IsCompleted) { _state = State.Completed; - - if (exception is not null) + if (_finalTaskSource.Task.IsCompletedSuccessfully) { - _valueTaskSource.SetException(exception); + _valueTaskSource.SetResult(true); } else { - _valueTaskSource.SetResult(true); + // We know it's always going to be a single exception since we're the ones setting it. + _valueTaskSource.SetException(_finalTaskSource.Task.Exception?.InnerException!); + } + + // In case the _valueTaskSource was successful, we want the potential error from _finalTaskSource to surface immediately. + // In other words, if _valueTaskSource was set with success while final exception arrived, this will throw that exception right away. + if (successful) + { + _valueTaskSource.GetResult(_valueTaskSource.Version); } - } - else - { - _state = State.None; } } } } } - - private struct FinalTaskSource - { - private TaskCompletionSource _finalTaskSource; - private bool _isCompleted; - private Exception? _exception; - - public FinalTaskSource(bool runContinuationsAsynchronously = true) - { - // TODO: defer instantiation only after Task is retrieved - _finalTaskSource = new TaskCompletionSource(runContinuationsAsynchronously ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None); - _isCompleted = false; - _exception = null; - } - - public Task Task => _finalTaskSource.Task; - - public bool TryComplete(Exception? exception = null) - { - if (_isCompleted) - { - return false; - } - - _exception = exception; - _isCompleted = true; - return true; - } - - public bool TrySignal(out Exception? exception) - { - if (!_isCompleted) - { - exception = default; - return false; - } - - if (_exception is not null) - { - _finalTaskSource.SetException(_exception); - } - else - { - _finalTaskSource.SetResult(); - } - - exception = _exception; - return true; - } - } } diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs index 4a6c49b5df560..a6e40dbf7ea8a 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/Internal/ValueTaskSource.cs @@ -35,9 +35,22 @@ public ValueTaskSource(bool runContinuationsAsynchronously = true) _keepAlive = default; } + /// + /// Returns true is this task source was completed, i.e. or was called. + /// public bool IsCompleted => (State)Volatile.Read(ref Unsafe.As(ref _state)) == State.Completed; + /// + /// Returns true is this task source was completed successfully, i.e. was called and set the result. + /// public bool IsCompletedSuccessfully => IsCompleted && _valueTaskSource.GetStatus(_valueTaskSource.Version) == ValueTaskSourceStatus.Succeeded; + /// + /// Tries to transition from to . Only the first call is able to do that so the result can be used to determine whether to invoke an operation or not. + /// + /// A value task representing the result. In case this method returns false, it'll still contain a value task that'll get set with the original operation. + /// An object to hold during a P/Invoke call. It'll get release with setting the result/exception. + /// A cancellation token which might cancel the value task. + /// true if this is the first call; otherwise, false. public bool TryInitialize(out ValueTask valueTask, object? keepAlive = null, CancellationToken cancellationToken = default) { lock (this) @@ -53,8 +66,8 @@ public bool TryInitialize(out ValueTask valueTask, object? keepAlive = null, Can { _cancellationRegistration = cancellationToken.UnsafeRegister(static (obj, cancellationToken) => { - ValueTaskSource parent = (ValueTaskSource)obj!; - parent.TrySetException(new OperationCanceledException(cancellationToken)); + ValueTaskSource thisRef = (ValueTaskSource)obj!; + thisRef.TrySetException(new OperationCanceledException(cancellationToken)); }, this); } } @@ -134,11 +147,20 @@ private bool TryComplete(Exception? exception) } } + /// + /// Tries to transition from to . Only the first call is able to do that. + /// + /// true if this is the first call that set the result; otherwise, false. public bool TrySetResult() { return TryComplete(null); } + /// + /// Tries to transition from to . Only the first call is able to do that. + /// + /// The exception to set as a result of the value task. + /// true if this is the first call that set the result; otherwise, false. public bool TrySetException(Exception exception) { return TryComplete(exception); diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs index a064e3899602c..390c47d4eed0f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnection.cs @@ -34,9 +34,6 @@ namespace System.Net.Quic; /// /// Each connection can then open outbound stream: , /// or accept an inbound stream: . -/// -/// After all the streams have been finished, connection should be properly closed with an application code: . -/// If not, the connection will not send the peer information about being closed and the peer's connection will have to wait on its idle timeout. /// public sealed partial class QuicConnection : IAsyncDisposable { @@ -177,6 +174,7 @@ public X509Certificate? RemoteCertificate /// public SslApplicationProtocol NegotiatedApplicationProtocol => _negotiatedApplicationProtocol; + /// public override string ToString() => _handle.ToString(); /// diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs index 30dd916404dc7..538630cc21b5f 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicConnectionOptions.cs @@ -37,6 +37,7 @@ internal QuicConnectionOptions() /// /// Error code used when the stream needs to abort read or write side of the stream internally. + /// This property is mandatory and not setting it will result in validation error when establishing a connection. /// // QUIC doesn't allow negative value: https://www.rfc-editor.org/rfc/rfc9000.html#integer-encoding // We can safely use this to distinguish if user provided value during validation. @@ -44,8 +45,11 @@ internal QuicConnectionOptions() /// /// Error code used for when the connection gets disposed. - /// To use different close error code, call explicitly before disposing. + /// This property is mandatory and not setting it will result in validation error when establishing a connection. /// + /// + /// To use different close error code, call explicitly before disposing. + /// // QUIC doesn't allow negative value: https://www.rfc-editor.org/rfc/rfc9000.html#integer-encoding // We can safely use this to distinguish if user provided value during validation. public long DefaultCloseErrorCode { get; set; } = -1; @@ -95,11 +99,13 @@ public QuicClientConnectionOptions() /// /// Client authentication options to use when establishing a new connection. + /// This property is mandatory and not setting it will result in validation error when establishing a connection. /// public SslClientAuthenticationOptions ClientAuthenticationOptions { get; set; } = null!; /// /// The remote endpoint to connect to. May be both , which will get resolved to an IP before connecting, or directly . + /// This property is mandatory and not setting it will result in validation error when establishing a connection. /// public EndPoint RemoteEndPoint { get; set; } = null!; @@ -144,6 +150,7 @@ public QuicServerConnectionOptions() /// /// Server authentication options to use when accepting a new connection. + /// This property is mandatory and not setting it will result in validation error when establishing a connection. /// public SslServerAuthenticationOptions ServerAuthenticationOptions { get; set; } = null!; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs index f1b9ee1e40c29..7ee161adeb313 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListener.cs @@ -93,6 +93,7 @@ public static ValueTask ListenAsync(QuicListenerOptions options, C /// public IPEndPoint LocalEndPoint { get; } + /// public override string ToString() => _handle.ToString(); /// diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs index 24a2b1e6050ec..e65d0bfe4e1b4 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicListenerOptions.cs @@ -15,11 +15,13 @@ public sealed class QuicListenerOptions { /// /// The endpoint to listen on. + /// This property is mandatory and not setting it will result in validation error when starting the listener. /// public IPEndPoint ListenEndPoint { get; set; } = null!; /// /// List of application protocols which the listener will accept. At least one must be specified. + /// This property is mandatory and not setting it will result in validation error when starting the listener. /// public List ApplicationProtocols { get; set; } = null!; diff --git a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs index a974a54257ebb..4a6ea5b0c52fa 100644 --- a/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs +++ b/src/libraries/System.Net.Quic/src/System/Net/Quic/QuicStream.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.Quic; using static System.Net.Quic.MsQuicHelpers; -using static System.Net.Quic.QuicDefaults; using static Microsoft.Quic.MsQuic; using START_COMPLETE = Microsoft.Quic.QUIC_STREAM_EVENT._Anonymous_e__Union._START_COMPLETE_e__Struct; @@ -22,6 +20,37 @@ namespace System.Net.Quic; +/// +/// Represents a QUIC stream, see RFC 9000: Streams for more details. +/// can be unidirectional, i.e.: write-only for the opening side, +/// or bidirectional which allows both side to write. +/// +/// +/// can be used in a same way as any other . +/// Apart from stream API, also exposes QUIC specific features: +/// +/// +/// +/// Allows to close the writing side of the stream as a single operation with the write itself. +/// +/// +/// +/// Close the writing side of the stream. +/// +/// +/// +/// Aborts either the writing or the reading side of the stream. +/// +/// +/// +/// A that will get completed when the stream writing side has been closed (gracefully or abortively). +/// +/// +/// +/// A that will get completed when the stream reading side has been closed (gracefully or abortively). +/// +/// +/// public sealed partial class QuicStream { /// @@ -89,10 +118,24 @@ public sealed partial class QuicStream /// public QuicStreamType Type => _type; + /// + /// A that will get completed once reading side has been closed. + /// Which might be by reading till end of stream ( will return 0), + /// or when for is called, + /// or when the peer called for . + /// public Task ReadsClosed => _receiveTcs.GetFinalTask(); + /// + /// A that will get completed once writing side has been closed. + /// Which might be by closing the write side via + /// or with completeWrites: true and getting acknowledgement from the peer for it, + /// or when for is called, + /// or when the peer called for . + /// public Task WritesClosed => _sendTcs.GetFinalTask(); + /// public override string ToString() => _handle.ToString(); /// @@ -175,6 +218,13 @@ internal unsafe QuicStream(QuicConnection.State connectionState, MsQuicContextSa _startedTcs.TrySetResult(); } + /// + /// Starts the stream, but doesn't send anything to the peer yet. + /// If no more concurrent streams can be opened at the moment, the operation will wait until it can, + /// either by closing some existing streams or receiving more available stream ids from the peer. + /// + /// A cancellation token that can be used to cancel the asynchronous operation. + /// An asynchronous task that completes with the opened . internal ValueTask StartAsync(CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed == 1, this); @@ -197,6 +247,7 @@ internal ValueTask StartAsync(CancellationToken cancellationToken = default) return valueTask; } + /// public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed == 1, this); @@ -270,9 +321,15 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation return totalCopied; } + /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => WriteAsync(buffer, completeWrites: false, cancellationToken); + + /// + /// The region of memory to write data from. + /// The token to monitor for cancellation requests. The default value is . + /// Notifies the peer about gracefully closing the write side, i.e.: sends FIN flag with the data. public ValueTask WriteAsync(ReadOnlyMemory buffer, bool completeWrites, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed == 1, this); @@ -347,7 +404,15 @@ public ValueTask WriteAsync(ReadOnlyMemory buffer, bool completeWrites, Ca return valueTask; } - + /// + /// Aborts either reading, writing or both sides of the stream. + /// + /// + /// Corresponds to STOP_SENDING + /// and RESET_STREAM QUIC frames. + /// + /// The direction of the stream to abort. + /// The error code with which to abort the stream, this value is application protocol (layer above QUIC) dependent. public void Abort(QuicAbortDirection abortDirection, long errorCode) { if (_disposed == 1) @@ -387,6 +452,13 @@ public void Abort(QuicAbortDirection abortDirection, long errorCode) } } + /// + /// Gracefully completes the writing side of the stream. + /// Equivalent to using with completeWrites: true. + /// + /// + /// Corresponds to an empty STREAM frame with FIN flag set to true. + /// public void CompleteWrites() { ObjectDisposedException.ThrowIf(_disposed == 1, this); @@ -547,7 +619,7 @@ private static unsafe int NativeCallback(QUIC_HANDLE* connection, void* context, } /// - /// If the read side is not fully consumed, i.e.: is completed and/or returned 0, + /// If the read side is not fully consumed, i.e.: is not completed and/or hasn't returned 0, /// dispose will abort the read side with provided . /// If the write side hasn't been closed, it'll be closed gracefully as if was called. /// Finally, all resources associated with the stream will be released. diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs index d27aae22484ac..52806fdf9c6ec 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicConnectionTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -293,5 +294,56 @@ await RunClientServer( await AssertThrowsQuicExceptionAsync(QuicError.ConnectionAborted, async () => await serverStream.WriteAsync(new byte[1])); }, listenerOptions: listenerOptions); } + + [Fact] + public async Task AcceptAsync_NoCapacity_Throws() + { + await RunClientServer( + async clientConnection => + { + await Assert.ThrowsAsync(async () => await clientConnection.AcceptInboundStreamAsync()); + }, + _ => Task.CompletedTask); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Connect_PeerCertificateDisposed(bool useGetter) + { + await using QuicListener listener = await CreateQuicListener(); + + QuicClientConnectionOptions clientOptions = CreateQuicClientOptions(listener.LocalEndPoint); + X509Certificate? peerCertificate = null; + clientOptions.ClientAuthenticationOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => + { + peerCertificate = certificate; + return true; + }; + + ValueTask connectTask = CreateQuicConnection(clientOptions); + ValueTask acceptTask = listener.AcceptConnectionAsync(); + + await new Task[] { connectTask.AsTask(), acceptTask.AsTask() }.WhenAllOrAnyFailed(PassingTestTimeoutMilliseconds); + await using QuicConnection serverConnection = acceptTask.Result; + QuicConnection clientConnection = connectTask.Result; + + Assert.NotNull(peerCertificate); + if (useGetter) + { + Assert.Equal(peerCertificate, clientConnection.RemoteCertificate); + } + // Dispose connection, if we touched RemoteCertificate (useGetter), the cert should not be disposed; otherwise, it should be disposed. + await clientConnection.DisposeAsync(); + if (useGetter) + { + Assert.NotEqual(IntPtr.Zero, peerCertificate.Handle); + } + else + { + Assert.Equal(IntPtr.Zero, peerCertificate.Handle); + } + peerCertificate.Dispose(); + } } } diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs index 922cbe490b4c1..e220decb1bd40 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/QuicStreamTests.cs @@ -703,14 +703,14 @@ await RunBidirectionalClientServer( }, async serverStream => { - var writeCompletionTask = ReleaseOnWriteCompletionAsync(); + var writesClosedTask = ReleaseOnWritesClosedAsync(); int received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(1, received); received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(0, received); - Assert.False(writeCompletionTask.IsCompleted, "Server is still writing."); + Assert.False(writesClosedTask.IsCompleted, "Server is still writing."); // Tell client that data has been read and it can abort its reads. sem.Release(); @@ -718,9 +718,9 @@ await RunBidirectionalClientServer( long sendAbortErrorCode = await waitForAbortTcs.Task; Assert.Equal(ExpectedErrorCode, sendAbortErrorCode); - await writeCompletionTask; + await writesClosedTask; - async ValueTask ReleaseOnWriteCompletionAsync() + async ValueTask ReleaseOnWritesClosedAsync() { try { @@ -766,7 +766,7 @@ await RunBidirectionalClientServer( } [Fact] - public async Task WaitForWriteCompletionAsync_ServerWriteAborted_Throws() + public async Task WaitForWritesClosedAsync_ServerWriteAborted_Throws() { const int ExpectedErrorCode = 0xfffffff; SemaphoreSlim sem = new SemaphoreSlim(0); @@ -781,27 +781,77 @@ await RunBidirectionalClientServer( }, async serverStream => { - var writeCompletionTask = ReleaseOnWriteCompletionAsync(); + var writesClosedTask = ReleaseOnWritesClosedAsync(); int received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(1, received); received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(0, received); - Assert.False(writeCompletionTask.IsCompleted, "Server is still writing."); + Assert.False(writesClosedTask.IsCompleted, "Server is still writing."); serverStream.Abort(QuicAbortDirection.Write, ExpectedErrorCode); sem.Release(); await waitForAbortTcs.Task; - await writeCompletionTask; + await writesClosedTask; - async ValueTask ReleaseOnWriteCompletionAsync() + async ValueTask ReleaseOnWritesClosedAsync() { try { await serverStream.WritesClosed; - waitForAbortTcs.SetException(new Exception("WaitForWriteCompletionAsync didn't throw stream aborted.")); + waitForAbortTcs.SetException(new Exception("WaitForWriteCompletionAsync didn't throw operation aborted.")); + } + catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted) + { + waitForAbortTcs.SetResult(); + } + catch (Exception ex) + { + waitForAbortTcs.SetException(ex); + } + }; + }); + } + + [Fact] + public async Task WaitForReadsClosedAsync_ServerReadAborted_Throws() + { + const int ExpectedErrorCode = 0xfffffff; + SemaphoreSlim sem = new SemaphoreSlim(0); + + TaskCompletionSource waitForAbortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await RunBidirectionalClientServer( + async clientStream => + { + Assert.Equal(1, await clientStream.ReadAsync(new byte[1])); + await clientStream.ReadsClosed; + Assert.Equal(0, await clientStream.ReadAsync(new byte[1])); + await sem.WaitAsync(); + }, + async serverStream => + { + var readsClosedTask = ReleaseOnReadsClosedAsync(); + + await serverStream.WriteAsync(new byte[1], completeWrites: true); + await serverStream.WritesClosed; + + Assert.False(readsClosedTask.IsCompleted, "Server is still reading."); + + serverStream.Abort(QuicAbortDirection.Read, ExpectedErrorCode); + sem.Release(); + + await waitForAbortTcs.Task; + await readsClosedTask; + + async ValueTask ReleaseOnReadsClosedAsync() + { + try + { + await serverStream.ReadsClosed; + waitForAbortTcs.SetException(new Exception("ReadsClosed didn't throw operation aborted.")); } catch (QuicException ex) when (ex.QuicError == QuicError.OperationAborted) { @@ -816,7 +866,107 @@ async ValueTask ReleaseOnWriteCompletionAsync() } [Fact] - public async Task WaitForWriteCompletionAsync_ServerShutdown_Success() + public async Task WaitForWritesClosedAsync_ClientReadAborted_Throws() + { + const int ExpectedErrorCode = 0xfffffff; + SemaphoreSlim sem = new SemaphoreSlim(0); + + TaskCompletionSource waitForAbortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await RunBidirectionalClientServer( + async clientStream => + { + await clientStream.WriteAsync(new byte[1], completeWrites: true); + await sem.WaitAsync(); + clientStream.Abort(QuicAbortDirection.Read, ExpectedErrorCode); + }, + async serverStream => + { + var writesClosedTask = ReleaseOnWritesClosedAsync(); + + int received = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(1, received); + received = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(0, received); + + Assert.False(writesClosedTask.IsCompleted, "Server is still writing."); + + sem.Release(); + + await waitForAbortTcs.Task; + await writesClosedTask; + + async ValueTask ReleaseOnWritesClosedAsync() + { + try + { + await serverStream.WritesClosed; + waitForAbortTcs.SetException(new Exception("WritesClosed didn't throw stream aborted.")); + } + catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted && ex.ApplicationErrorCode == ExpectedErrorCode) + { + waitForAbortTcs.SetResult(); + } + catch (Exception ex) + { + waitForAbortTcs.SetException(ex); + } + }; + }); + } + + [Fact] + public async Task WaitForReadsClosedAsync_ClientWriteAborted_Throws() + { + const int ExpectedErrorCode = 0xfffffff; + SemaphoreSlim sem = new SemaphoreSlim(0); + + TaskCompletionSource waitForAbortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await RunBidirectionalClientServer( + async clientStream => + { + Assert.Equal(1, await clientStream.ReadAsync(new byte[1])); + await clientStream.ReadsClosed; + Assert.Equal(0, await clientStream.ReadAsync(new byte[1])); + await sem.WaitAsync(); + clientStream.Abort(QuicAbortDirection.Write, ExpectedErrorCode); + }, + async serverStream => + { + var readsClosedTask = ReleaseOnReadsClosedAsync(); + + await serverStream.WriteAsync(new byte[1], completeWrites: true); + await serverStream.WritesClosed; + + Assert.False(readsClosedTask.IsCompleted, "Server is still reading."); + + sem.Release(); + + await waitForAbortTcs.Task; + await readsClosedTask; + + async ValueTask ReleaseOnReadsClosedAsync() + { + try + { + await serverStream.ReadsClosed; + waitForAbortTcs.SetException(new Exception("ReadsClosed didn't throw stream aborted.")); + } + catch (QuicException ex) when (ex.QuicError == QuicError.StreamAborted && ex.ApplicationErrorCode == ExpectedErrorCode) + { + waitForAbortTcs.SetResult(); + } + catch (Exception ex) + { + waitForAbortTcs.SetException(ex); + } + }; + }); + } + + [Fact] + public async Task WaitForWritesClosedAsync_ServerShutdown_Success() { await RunBidirectionalClientServer( async clientStream => @@ -831,7 +981,7 @@ await RunBidirectionalClientServer( }, async serverStream => { - var writeCompletionTask = serverStream.WritesClosed; + var writesClosedTask = serverStream.WritesClosed; int received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(1, received); @@ -840,16 +990,51 @@ await RunBidirectionalClientServer( await serverStream.WriteAsync(new byte[1]); - Assert.False(writeCompletionTask.IsCompleted, "Server is still writing."); + Assert.False(writesClosedTask.IsCompleted, "Server is still writing."); serverStream.CompleteWrites(); - await writeCompletionTask; + await writesClosedTask; + }); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task WaitForReadsClosedAsync_ClientCompleteWrites_Success(bool extraCall) + { + await RunBidirectionalClientServer( + async clientStream => + { + Assert.Equal(1, await clientStream.ReadAsync(new byte[1])); + await clientStream.ReadsClosed; + Assert.Equal(0, await clientStream.ReadAsync(new byte[1])); + + await clientStream.WriteAsync(new byte[1], completeWrites: !extraCall); + if (extraCall) + { + clientStream.CompleteWrites(); + } + }, + async serverStream => + { + var readsClosedTask = serverStream.ReadsClosed; + + await serverStream.WriteAsync(new byte[1], completeWrites: true); + await serverStream.WritesClosed; + + Assert.False(readsClosedTask.IsCompleted, "Server is still reading."); + + var readCount = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(1, readCount); + readCount = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(0, readCount); + Assert.True(readsClosedTask.IsCompletedSuccessfully); }); } [Fact] - public async Task WaitForWriteCompletionAsync_GracefulShutdown_Success() + public async Task WaitForWritesClosedAsync_GracefulShutdown_Success() { await RunBidirectionalClientServer( async clientStream => @@ -864,23 +1049,53 @@ await RunBidirectionalClientServer( }, async serverStream => { - var writeCompletionTask = serverStream.WritesClosed; + var writesClosedTask = serverStream.WritesClosed; int received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(1, received); received = await serverStream.ReadAsync(new byte[1]); Assert.Equal(0, received); - Assert.False(writeCompletionTask.IsCompleted, "Server is still writing."); + Assert.False(writesClosedTask.IsCompleted, "Server is still writing."); + + await serverStream.WriteAsync(new byte[1], completeWrites: true); + + await writesClosedTask; + }); + } + + [Fact] + public async Task WaitForReadsClosedAsync_GracefulShutdown_Success() + { + await RunBidirectionalClientServer( + async clientStream => + { + Assert.Equal(1, await clientStream.ReadAsync(new byte[1])); + await clientStream.ReadsClosed; + Assert.Equal(0, await clientStream.ReadAsync(new byte[1])); + + await clientStream.WriteAsync(new byte[1]); + // Let DisposeAsync gracefully shutdown the write side. + }, + async serverStream => + { + var readsClosedTask = serverStream.ReadsClosed; await serverStream.WriteAsync(new byte[1], completeWrites: true); + await serverStream.WritesClosed; + + Assert.False(readsClosedTask.IsCompleted, "Server is still reading."); - await writeCompletionTask; + var readCount = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(1, readCount); + readCount = await serverStream.ReadAsync(new byte[1]); + Assert.Equal(0, readCount); + Assert.True(readsClosedTask.IsCompletedSuccessfully); }); } [Fact] - public async Task WaitForWriteCompletionAsync_ConnectionClosed_Throws() + public async Task WaitForWritesClosedAsync_ConnectionClosed_Throws() { const int ExpectedErrorCode = 0xfffffff; @@ -892,7 +1107,7 @@ await RunClientServer( { await using QuicStream stream = await connection.AcceptInboundStreamAsync(); - var writeCompletionTask = ReleaseOnWriteCompletionAsync(); + var writesClosedTask = ReleaseOnWritesClosedAsync(); int received = await stream.ReadAsync(new byte[1]); Assert.Equal(1, received); @@ -905,14 +1120,14 @@ await RunClientServer( long closeErrorCode = await waitForAbortTcs.Task; Assert.Equal(ExpectedErrorCode, closeErrorCode); - await writeCompletionTask; + await writesClosedTask; - async ValueTask ReleaseOnWriteCompletionAsync() + async ValueTask ReleaseOnWritesClosedAsync() { try { await stream.WritesClosed; - waitForAbortTcs.SetException(new Exception("WaitForWriteCompletionAsync didn't throw connection aborted.")); + waitForAbortTcs.SetException(new Exception("WritesClosed didn't throw connection aborted.")); } catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) { @@ -935,5 +1150,63 @@ async ValueTask ReleaseOnWriteCompletionAsync() } ); } + + [Fact] + public async Task WaitForReadsClosedAsync_ConnectionClosed_Throws() + { + const int ExpectedErrorCode = 0xfffffff; + + using SemaphoreSlim sem = new SemaphoreSlim(0); + TaskCompletionSource waitForAbortTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + await RunClientServer( + serverFunction: async connection => + { + await using QuicStream stream = await connection.AcceptInboundStreamAsync(); + + var readsClosedTask = ReleaseOnReadsClosedAsync(); + + await stream.WriteAsync(new byte[1], completeWrites: true); + + // Signal that the server has read data + sem.Release(); + + long closeErrorCode = await waitForAbortTcs.Task; + Assert.Equal(ExpectedErrorCode, closeErrorCode); + + await readsClosedTask; + + async ValueTask ReleaseOnReadsClosedAsync() + { + try + { + await stream.ReadsClosed; + waitForAbortTcs.SetException(new Exception("ReadsClosed didn't throw connection aborted.")); + } + catch (QuicException ex) when (ex.QuicError == QuicError.ConnectionAborted) + { + waitForAbortTcs.SetResult(ex.ApplicationErrorCode.Value); + QuicException readEx = await Assert.ThrowsAsync(async () => await stream.ReadAsync(new byte[1])); + Assert.Equal(ex, readEx); + } + }; + }, + clientFunction: async connection => + { + await using QuicStream stream = await connection.OpenOutboundStreamAsync(QuicStreamType.Bidirectional); + + await stream.WriteAsync(new byte[1]); + + Assert.Equal(1, await stream.ReadAsync(new byte[1])); + await stream.ReadsClosed; + Assert.Equal(0, await stream.ReadAsync(new byte[1])); + + // Wait for the server to write data before closing the connection + await sem.WaitAsync(); + + await connection.CloseAsync(ExpectedErrorCode); + } + ); + } } }