--- /dev/null
+<Project>
+ <Import Project="..\Directory.Build.props" />
+ <PropertyGroup>
+ <AssemblyVersion>4.2.1.0</AssemblyVersion>
+ <StrongNameKeyId>Microsoft</StrongNameKeyId>
+ <IsNETCoreApp>true</IsNETCoreApp>
+ </PropertyGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27213.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Quic.Tests", "tests\FunctionalTests\System.Net.Quic.Tests.csproj", "{8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}"
+ ProjectSection(ProjectDependencies) = postProject
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B} = {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Quic", "src\System.Net.Quic.csproj", "{43311AFB-D7C4-4E5A-B1DE-855407F90D1B}"
+ ProjectSection(ProjectDependencies) = postProject
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3} = {834E3534-6A11-4A8D-923F-35C1E71CCEC3}
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.Quic", "ref\System.Net.Quic.csproj", "{834E3534-6A11-4A8D-923F-35C1E71CCEC3}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1A2F9F4A-A032-433E-B914-ADD5992BB178}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E107E9C1-E893-4E87-987E-04EF0DCEAEFD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{2E666815-2EDB-464B-9DF6-380BF4789AD4}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Debug|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Debug|Any CPU
+ {8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Debug|Any CPU.Build.0 = netcoreapp-Windows_NT-Debug|Any CPU
+ {8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Release|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Release|Any CPU
+ {8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6}.Release|Any CPU.Build.0 = netcoreapp-Windows_NT-Release|Any CPU
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Debug|Any CPU
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Debug|Any CPU.Build.0 = netcoreapp-Windows_NT-Debug|Any CPU
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Release|Any CPU.ActiveCfg = netcoreapp-Windows_NT-Release|Any CPU
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B}.Release|Any CPU.Build.0 = netcoreapp-Windows_NT-Release|Any CPU
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3}.Debug|Any CPU.ActiveCfg = netcoreapp-Debug|Any CPU
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3}.Debug|Any CPU.Build.0 = netcoreapp-Debug|Any CPU
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3}.Release|Any CPU.ActiveCfg = netcoreapp-Release|Any CPU
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3}.Release|Any CPU.Build.0 = netcoreapp-Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {8CBA022C-635F-4C8D-9D29-CD8AAC68C8E6} = {1A2F9F4A-A032-433E-B914-ADD5992BB178}
+ {43311AFB-D7C4-4E5A-B1DE-855407F90D1B} = {E107E9C1-E893-4E87-987E-04EF0DCEAEFD}
+ {834E3534-6A11-4A8D-923F-35C1E71CCEC3} = {2E666815-2EDB-464B-9DF6-380BF4789AD4}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6E33D5E2-B9BF-49D1-99FA-3C9AC3412636}
+ EndGlobalSection
+EndGlobal
--- /dev/null
+<Project>
+ <PropertyGroup>
+ <BuildConfigurations>
+ netcoreapp;
+ </BuildConfigurations>
+ </PropertyGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+// ------------------------------------------------------------------------------
+// Changes to this file must follow the http://aka.ms/api-review process.
+// ------------------------------------------------------------------------------
+
+using System.Threading;
+
+namespace System.Net.Quic
+{
+ public sealed class QuicConnection : System.IDisposable
+ {
+ public QuicConnection(IPEndPoint remoteEndPoint, System.Net.Security.SslClientAuthenticationOptions sslClientAuthenticationOptions, IPEndPoint localEndPoint = null, bool mock = false) { }
+ public System.Threading.Tasks.ValueTask ConnectAsync(System.Threading.CancellationToken cancellationToken = default) { throw null; }
+ public bool Connected => throw null;
+ public IPEndPoint LocalEndPoint => throw null;
+ public IPEndPoint RemoteEndPoint => throw null;
+ public QuicStream OpenUnidirectionalStream() => throw null;
+ public QuicStream OpenBidirectionalStream() => throw null;
+ public System.Threading.Tasks.ValueTask<QuicStream> AcceptStreamAsync(System.Threading.CancellationToken cancellationToken = default) => throw null;
+ public void Close() => throw null;
+ public void Dispose() => throw null;
+ }
+ public sealed class QuicListener : IDisposable
+ {
+ public QuicListener(IPEndPoint listenEndPoint, System.Net.Security.SslServerAuthenticationOptions sslServerAuthenticationOptions, bool mock = false) { }
+ public IPEndPoint ListenEndPoint => throw null;
+ public System.Threading.Tasks.ValueTask<QuicConnection> AcceptConnectionAsync(System.Threading.CancellationToken cancellationToken = default) => throw null;
+ public void Close() => throw null;
+ public void Dispose() => throw null;
+ }
+ public sealed class QuicStream : System.IO.Stream
+ {
+ internal QuicStream() { }
+ public override bool CanSeek => throw null;
+ public override long Length => throw null;
+ public override long Seek(long offset, System.IO.SeekOrigin origin) => throw null;
+ public override void SetLength(long value) => throw null;
+ public override long Position { get => throw null; set => throw null; }
+ public override bool CanRead => throw null;
+ public override bool CanWrite => throw null;
+ public override void Flush() => throw null;
+ public override int Read(byte[] buffer, int offset, int count) => throw null;
+ public override void Write(byte[] buffer, int offset, int count) => throw null;
+ public long StreamId => throw null;
+ public void ShutdownRead() => throw null;
+ public void ShutdownWrite() => throw null;
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <Configurations>netcoreapp-Debug;netcoreapp-Release</Configurations>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="System.Net.Quic.cs" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\System.IO\ref\System.IO.csproj" />
+ <ProjectReference Include="..\..\System.IO.FileSystem.Primitives\ref\System.IO.FileSystem.Primitives.csproj" />
+ <ProjectReference Include="..\..\System.Net.Primitives\ref\System.Net.Primitives.csproj" />
+ <ProjectReference Include="..\..\System.Net.Security\ref\System.Net.Security.csproj" />
+ <ProjectReference Include="..\..\System.Runtime\ref\System.Runtime.csproj" />
+ <ProjectReference Include="..\..\System.Threading.Tasks\ref\System.Threading.Tasks.csproj" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+<Project>
+ <PropertyGroup>
+ <BuildConfigurations>
+ netcoreapp-Unix;
+ netcoreapp-Windows_NT;
+ </BuildConfigurations>
+ </PropertyGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+<root>
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+ <xsd:element name="root" msdata:IsDataSet="true">
+ <xsd:complexType>
+ <xsd:choice maxOccurs="unbounded">
+ <xsd:element name="metadata">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" />
+ </xsd:sequence>
+ <xsd:attribute name="name" use="required" type="xsd:string" />
+ <xsd:attribute name="type" type="xsd:string" />
+ <xsd:attribute name="mimetype" type="xsd:string" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="assembly">
+ <xsd:complexType>
+ <xsd:attribute name="alias" type="xsd:string" />
+ <xsd:attribute name="name" type="xsd:string" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="data">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+ <xsd:attribute ref="xml:space" />
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="resheader">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+ </xsd:sequence>
+ <xsd:attribute name="name" type="xsd:string" use="required" />
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ <resheader name="resmimetype">
+ <value>text/microsoft-resx</value>
+ </resheader>
+ <resheader name="version">
+ <value>2.0</value>
+ </resheader>
+ <resheader name="reader">
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+ <resheader name="writer">
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+ </resheader>
+
+ <data name="net_quic_placeholdertext" xml:space="preserve">
+ <value>Placeholder text</value>
+ </data>
+</root>
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <AssemblyName>System.Net.Quic</AssemblyName>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <Configurations>netcoreapp-Unix-Debug;netcoreapp-Unix-Release;netcoreapp-Windows_NT-Debug;netcoreapp-Windows_NT-Release</Configurations>
+ </PropertyGroup>
+ <ItemGroup>
+ <!-- All configurations -->
+ <Compile Include="System\Net\Quic\QuicConnection.cs" />
+ <Compile Include="System\Net\Quic\QuicListener.cs" />
+ <Compile Include="System\Net\Quic\QuicStream.cs" />
+ <Compile Include="$(CommonPath)\CoreLib\System\Threading\Tasks\TaskToApm.cs">
+ <Link>Common\CoreLib\System\Threading\Tasks\TaskToApm.cs</Link>
+ </Compile>
+ </ItemGroup>
+ <ItemGroup>
+ <Reference Include="System.Buffers" />
+ <Reference Include="System.Console" />
+ <Reference Include="System.Diagnostics.Debug" />
+ <Reference Include="System.Memory" />
+ <Reference Include="System.Net.NameResolution" />
+ <Reference Include="System.Net.Primitives" />
+ <Reference Include="System.Net.Security" />
+ <Reference Include="System.Net.Sockets" />
+ <Reference Include="System.Resources.ResourceManager" />
+ <Reference Include="System.Runtime" />
+ <Reference Include="System.Runtime.Extensions" />
+ <Reference Include="System.Runtime.InteropServices" />
+ <Reference Include="System.Threading" />
+ <Reference Include="System.Threading.Tasks" />
+ <Reference Include="System.Threading.ThreadPool" />
+ </ItemGroup>
+</Project>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Quic
+{
+ public sealed class QuicConnection : IDisposable
+ {
+ private readonly bool _isClient;
+ private bool _disposed = false;
+ private IPEndPoint _remoteEndPoint;
+ private IPEndPoint _localEndPoint;
+ private object _syncObject = new object();
+
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ private readonly bool _mock = false;
+ private Socket _socket = null;
+ private IPEndPoint _peerListenEndPoint = null;
+ private TcpListener _inboundListener = null;
+ private long _nextOutboundBidirectionalStream;
+ private long _nextOutboundUnidirectionalStream;
+
+ /// <summary>
+ /// Create an outbound QUIC connection.
+ /// </summary>
+ /// <param name="remoteEndPoint">The remote endpoint to connect to.</param>
+ /// <param name="sslClientAuthenticationOptions">TLS options</param>
+ /// <param name="localEndPoint">The local endpoint to connect from.</param>
+ /// <param name="mock">Use mock QUIC implementation.</param>
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT: Remove "mock" parameter before shipping
+ public QuicConnection(IPEndPoint remoteEndPoint, SslClientAuthenticationOptions sslClientAuthenticationOptions, IPEndPoint localEndPoint = null, bool mock = false)
+ {
+ // TODO: TLS handling
+
+ _mock = mock;
+ _remoteEndPoint = remoteEndPoint;
+ _localEndPoint = localEndPoint;
+
+ _isClient = true;
+ _nextOutboundBidirectionalStream = 0;
+ _nextOutboundUnidirectionalStream = 2;
+ }
+
+ // Constructor for accepted inbound QuicConnections
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ internal QuicConnection(Socket socket, IPEndPoint peerListenEndPoint, TcpListener inboundListener)
+ {
+ _mock = true;
+ _isClient = false;
+ _nextOutboundBidirectionalStream = 1;
+ _nextOutboundUnidirectionalStream = 3;
+ _socket = socket;
+ _peerListenEndPoint = peerListenEndPoint;
+ _inboundListener = inboundListener;
+ _localEndPoint = (IPEndPoint)socket.LocalEndPoint;
+ _remoteEndPoint = (IPEndPoint)socket.RemoteEndPoint;
+ }
+
+ /// <summary>
+ /// Indicates whether the QuicConnection is connected.
+ /// </summary>
+ public bool Connected
+ {
+ get
+ {
+ CheckDisposed();
+
+ if (_mock)
+ {
+ return _socket != null;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+
+ public IPEndPoint LocalEndPoint => new IPEndPoint(_localEndPoint.Address, _localEndPoint.Port);
+
+ public IPEndPoint RemoteEndPoint => new IPEndPoint(_remoteEndPoint.Address, _remoteEndPoint.Port);
+
+ /// <summary>
+ /// Connect to the remote endpoint.
+ /// </summary>
+ /// <param name="cancellationToken"></param>
+ /// <returns></returns>
+ public async ValueTask ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ CheckDisposed();
+
+ if (_mock)
+ {
+ if (Connected)
+ {
+ // TODO: Exception text
+ throw new InvalidOperationException("Already connected");
+ }
+
+ Socket socket = new Socket(_remoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
+ await socket.ConnectAsync(_remoteEndPoint).ConfigureAwait(false);
+ socket.NoDelay = true;
+
+ _localEndPoint = (IPEndPoint)socket.LocalEndPoint;
+
+ // Listen on a new local endpoint for inbound streams
+ TcpListener inboundListener = new TcpListener(_localEndPoint.Address, 0);
+ inboundListener.Start();
+ int inboundListenPort = ((IPEndPoint)inboundListener.LocalEndpoint).Port;
+
+ // Write inbound listen port to socket so server can read it
+ byte[] buffer = new byte[4];
+ BinaryPrimitives.WriteInt32LittleEndian(buffer, inboundListenPort);
+ await socket.SendAsync(buffer, SocketFlags.None).ConfigureAwait(false);
+
+ // Read first 4 bytes to get server listen port
+ int bytesRead = 0;
+ do
+ {
+ bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false);
+ } while (bytesRead != buffer.Length);
+
+ int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer);
+ IPEndPoint peerListenEndPoint = new IPEndPoint(((IPEndPoint)socket.RemoteEndPoint).Address, peerListenPort);
+
+ _socket = socket;
+ _peerListenEndPoint = peerListenEndPoint;
+ _inboundListener = inboundListener;
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Create an outbound unidirectional stream.
+ /// </summary>
+ /// <returns></returns>
+ public QuicStream OpenUnidirectionalStream()
+ {
+ if (_mock)
+ {
+ long streamId;
+ lock (_syncObject)
+ {
+ streamId = _nextOutboundUnidirectionalStream;
+ _nextOutboundUnidirectionalStream += 4;
+ }
+
+ return new QuicStream(this, streamId, bidirectional: false);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Create an outbound bidirectional stream.
+ /// </summary>
+ /// <returns></returns>
+ public QuicStream OpenBidirectionalStream()
+ {
+ if (_mock)
+ {
+ long streamId;
+ lock (_syncObject)
+ {
+ streamId = _nextOutboundBidirectionalStream;
+ _nextOutboundBidirectionalStream += 4;
+ }
+
+ return new QuicStream(this, streamId, bidirectional: true);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ internal async Task<Socket> CreateOutboundMockStreamAsync(long streamId)
+ {
+ Debug.Assert(_mock);
+ Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
+ await socket.ConnectAsync(_peerListenEndPoint).ConfigureAwait(false);
+ socket.NoDelay = true;
+
+ // Write stream ID to socket so server can read it
+ byte[] buffer = new byte[8];
+ BinaryPrimitives.WriteInt64LittleEndian(buffer, streamId);
+ await socket.SendAsync(buffer, SocketFlags.None).ConfigureAwait(false);
+
+ return socket;
+ }
+
+ /// <summary>
+ /// Accept an incoming stream.
+ /// </summary>
+ /// <returns></returns>
+ public async ValueTask<QuicStream> AcceptStreamAsync(CancellationToken cancellationToken = default)
+ {
+ CheckDisposed();
+
+ if (_mock)
+ {
+ Socket socket = await _inboundListener.AcceptSocketAsync().ConfigureAwait(false);
+
+ // Read first bytes to get stream ID
+ byte[] buffer = new byte[8];
+ int bytesRead = 0;
+ do
+ {
+ bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false);
+ } while (bytesRead != buffer.Length);
+
+ long streamId = BinaryPrimitives.ReadInt64LittleEndian(buffer);
+
+ bool clientInitiated = ((streamId & 0b01) == 0);
+ if (clientInitiated == _isClient)
+ {
+ throw new Exception($"Wrong initiator on accepted stream??? streamId={streamId}, _isClient={_isClient}");
+ }
+
+ bool bidirectional = ((streamId & 0b10) == 0);
+ return new QuicStream(socket, streamId, bidirectional: bidirectional);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Close the connection and terminate any active streams.
+ /// </summary>
+ public void Close()
+ {
+ Dispose();
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(QuicConnection));
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ if (_mock)
+ {
+ _socket?.Dispose();
+ _socket = null;
+
+ _inboundListener?.Stop();
+ _inboundListener = null;
+ }
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
+ // TODO: set large fields to null.
+
+ _disposed = true;
+ }
+ }
+
+ ~QuicConnection()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Net.Sockets;
+using System.Net.Security;
+using System.Threading.Tasks;
+using System.Threading;
+using System.Buffers.Binary;
+
+namespace System.Net.Quic
+{
+ public sealed class QuicListener : IDisposable
+ {
+ private bool _disposed = false;
+ private SslServerAuthenticationOptions _sslOptions;
+ private IPEndPoint _listenEndPoint;
+
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ private bool _mock = false;
+ private TcpListener _tcpListener = null;
+
+ /// <summary>
+ /// Create a QUIC listener on the specified local endpoint and start listening.
+ /// </summary>
+ /// <param name="listenEndPoint">The local endpoint to listen on.</param>
+ /// <param name="sslServerAuthenticationOptions">TLS options for the listener.</param>
+ /// <param name="mock">Use mock QUIC implementation.</param>
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT: Remove "mock" parameter before shipping
+ public QuicListener(IPEndPoint listenEndPoint, SslServerAuthenticationOptions sslServerAuthenticationOptions, bool mock = false)
+ {
+ if (sslServerAuthenticationOptions == null && !mock)
+ {
+ throw new ArgumentNullException(nameof(sslServerAuthenticationOptions));
+ }
+
+ if (listenEndPoint == null)
+ {
+ throw new ArgumentNullException(nameof(listenEndPoint));
+ }
+
+ _sslOptions = sslServerAuthenticationOptions;
+ _listenEndPoint = listenEndPoint;
+
+ _mock = mock;
+ if (mock)
+ {
+ _tcpListener = new TcpListener(listenEndPoint);
+ _tcpListener.Start();
+
+ if (listenEndPoint.Port == 0)
+ {
+ // Get auto-assigned port
+ _listenEndPoint = (IPEndPoint)_tcpListener.LocalEndpoint;
+ }
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ // IPEndPoint is mutable, so we must create a new instance every time this is retrieved.
+ public IPEndPoint ListenEndPoint => new IPEndPoint(_listenEndPoint.Address, _listenEndPoint.Port);
+
+ /// <summary>
+ /// Accept a connection.
+ /// </summary>
+ /// <returns></returns>
+ public async ValueTask<QuicConnection> AcceptConnectionAsync(CancellationToken cancellationToken = default)
+ {
+ CheckDisposed();
+
+ if (_mock)
+ {
+ Socket socket = await _tcpListener.AcceptSocketAsync().ConfigureAwait(false);
+ socket.NoDelay = true;
+
+ // Read first 4 bytes to get client listen port
+ byte[] buffer = new byte[4];
+ int bytesRead = 0;
+ do
+ {
+ bytesRead += await socket.ReceiveAsync(buffer.AsMemory().Slice(bytesRead), SocketFlags.None).ConfigureAwait(false);
+ } while (bytesRead != buffer.Length);
+
+ int peerListenPort = BinaryPrimitives.ReadInt32LittleEndian(buffer);
+ IPEndPoint peerListenEndPoint = new IPEndPoint(((IPEndPoint)socket.RemoteEndPoint).Address, peerListenPort);
+
+ // Listen on a new local endpoint for inbound streams
+ TcpListener inboundListener = new TcpListener(_listenEndPoint.Address, 0);
+ inboundListener.Start();
+ int inboundListenPort = ((IPEndPoint)inboundListener.LocalEndpoint).Port;
+
+ // Write inbound listen port to socket so client can read it
+ BinaryPrimitives.WriteInt32LittleEndian(buffer, inboundListenPort);
+ await socket.SendAsync(buffer, SocketFlags.None).ConfigureAwait(false);
+
+ return new QuicConnection(socket, peerListenEndPoint, inboundListener);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Stop listening and close the listener.
+ /// </summary>
+ public void Close()
+ {
+ Dispose();
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(QuicListener));
+ }
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _tcpListener?.Stop();
+ _tcpListener = null;
+ }
+
+ // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
+ // TODO: set large fields to null.
+
+ _disposed = true;
+ }
+ }
+
+ ~QuicListener()
+ {
+ Dispose(false);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.IO;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.Quic
+{
+ public sealed class QuicStream : Stream
+ {
+ private bool _disposed = false;
+ private readonly long _streamId;
+ private bool _canRead;
+ private bool _canWrite;
+ private QuicConnection _connection;
+
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ private readonly bool _mock = false;
+ private Socket _socket = null;
+
+ // Constructor for outbound streams
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ internal QuicStream(QuicConnection connection, long streamId, bool bidirectional)
+ {
+ _mock = true;
+ _connection = connection;
+ _streamId = streamId;
+ _canRead = bidirectional;
+ _canWrite = true;
+ }
+
+ // Constructor for inbound streams
+ // !!! TEMPORARY FOR QUIC MOCK SUPPORT
+ internal QuicStream(Socket socket, long streamId, bool bidirectional)
+ {
+ _mock = true;
+ _socket = socket;
+ _streamId = streamId;
+ _canRead = true;
+ _canWrite = bidirectional;
+ }
+
+ public override bool CanSeek => false;
+ public override long Length => throw new NotSupportedException();
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+
+ public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) =>
+ TaskToApm.Begin(ReadAsync(buffer, offset, count, default), callback, state);
+
+ public override int EndRead(IAsyncResult asyncResult) =>
+ TaskToApm.End<int>(asyncResult);
+
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) =>
+ TaskToApm.Begin(WriteAsync(buffer, offset, count, default), callback, state);
+
+ public override void EndWrite(IAsyncResult asyncResult) =>
+ TaskToApm.End(asyncResult);
+
+ private static void ValidateBufferArgs(byte[] buffer, int offset, int count)
+ {
+ if (buffer == null)
+ {
+ throw new ArgumentNullException(nameof(buffer));
+ }
+
+ if ((uint)offset > buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ }
+
+ if ((uint)count > buffer.Length - offset)
+ {
+ throw new ArgumentOutOfRangeException(nameof(count));
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ ValidateBufferArgs(buffer, offset, count);
+ return Read(buffer.AsSpan(offset, count));
+ }
+
+ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ ValidateBufferArgs(buffer, offset, count);
+ return ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ ValidateBufferArgs(buffer, offset, count);
+ Write(buffer.AsSpan(offset, count));
+ }
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ ValidateBufferArgs(buffer, offset, count);
+ return WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).AsTask();
+ }
+
+ private async ValueTask ConnectAsync(CancellationToken cancellationToken = default)
+ {
+ Debug.Assert(_mock);
+ Debug.Assert(_connection != null, "Stream not connected but no connection???");
+
+ _socket = await _connection.CreateOutboundMockStreamAsync(_streamId).ConfigureAwait(false);
+
+ // Don't need to hold on to the connection any longer.
+ _connection = null;
+ }
+
+ /// <summary>
+ /// QUIC stream ID.
+ /// </summary>
+ public long StreamId
+ {
+ get
+ {
+ CheckDisposed();
+ return _streamId;
+ }
+ }
+
+ public override bool CanRead => _canRead;
+
+ public override int Read(Span<byte> buffer)
+ {
+ CheckDisposed();
+
+ if (!_canRead)
+ {
+ throw new NotSupportedException();
+ }
+
+ if (_mock)
+ {
+ return _socket.Receive(buffer);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ CheckDisposed();
+
+ if (!_canRead)
+ {
+ throw new NotSupportedException();
+ }
+
+ if (_mock)
+ {
+ if (_socket == null)
+ {
+ await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ return await _socket.ReceiveAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override bool CanWrite => _canWrite;
+
+ public override void Write(ReadOnlySpan<byte> buffer)
+ {
+ CheckDisposed();
+
+ if (!_canWrite)
+ {
+ throw new NotSupportedException();
+ }
+
+ if (_mock)
+ {
+ _socket.Send(buffer);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
+ {
+ CheckDisposed();
+
+ if (!_canWrite)
+ {
+ throw new NotSupportedException();
+ }
+
+ if (_mock)
+ {
+ if (_socket == null)
+ {
+ await ConnectAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ await _socket.SendAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public override void Flush()
+ {
+ CheckDisposed();
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+
+ return Task.CompletedTask;
+ }
+
+ public void ShutdownRead()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ShutdownWrite()
+ {
+ CheckDisposed();
+
+ if (_mock)
+ {
+ _socket.Shutdown(SocketShutdown.Send);
+ }
+ else
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(nameof(QuicStream));
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+
+ if (_mock)
+ {
+ _socket?.Dispose();
+ _socket = null;
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+}
--- /dev/null
+<Project DefaultTargets="Build">
+ <PropertyGroup>
+ <BuildConfigurations>
+ netcoreapp-Windows_NT;
+ netcoreapp-Unix;
+ </BuildConfigurations>
+ </PropertyGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Net.Quic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Quic.Tests
+{
+ public class QuicConnectionTests
+ {
+ private static ReadOnlyMemory<byte> s_data = Encoding.UTF8.GetBytes("Hello world!");
+
+ [Fact]
+ public async Task BasicTest()
+ {
+ using (QuicListener listener = new QuicListener(new IPEndPoint(IPAddress.Loopback, 0), sslServerAuthenticationOptions: null, mock: true))
+ {
+ IPEndPoint listenEndPoint = listener.ListenEndPoint;
+
+ await Task.WhenAll(
+ Task.Run(async () =>
+ {
+ // Client code
+ using (QuicConnection connection = new QuicConnection(listenEndPoint, sslClientAuthenticationOptions: null, mock: true))
+ {
+ await connection.ConnectAsync();
+ using (QuicStream stream = connection.OpenBidirectionalStream())
+ {
+ await stream.WriteAsync(s_data);
+ }
+ }
+ }),
+ Task.Run(async () =>
+ {
+ // Server code
+ using (QuicConnection connection = await listener.AcceptConnectionAsync())
+ {
+ using (QuicStream stream = await connection.AcceptStreamAsync())
+ {
+ byte[] buffer = new byte[s_data.Length];
+ int bytesRead = await stream.ReadAsync(buffer);
+ Assert.Equal(s_data.Length, bytesRead);
+ Assert.True(s_data.Span.SequenceEqual(buffer));
+ }
+ }
+ }));
+ }
+ }
+
+ [Fact]
+ public async Task TestStreams()
+ {
+ using (QuicListener listener = new QuicListener(new IPEndPoint(IPAddress.Loopback, 0), sslServerAuthenticationOptions: null, mock: true))
+ {
+ IPEndPoint listenEndPoint = listener.ListenEndPoint;
+
+ using (QuicConnection clientConnection = new QuicConnection(listenEndPoint, sslClientAuthenticationOptions: null, mock: true))
+ {
+ Assert.False(clientConnection.Connected);
+ Assert.Equal(listenEndPoint, clientConnection.RemoteEndPoint);
+
+ ValueTask connectTask = clientConnection.ConnectAsync();
+ QuicConnection serverConnection = await listener.AcceptConnectionAsync();
+ await connectTask;
+
+ Assert.True(clientConnection.Connected);
+ Assert.True(serverConnection.Connected);
+ Assert.Equal(listenEndPoint, serverConnection.LocalEndPoint);
+ Assert.Equal(listenEndPoint, clientConnection.RemoteEndPoint);
+ Assert.Equal(clientConnection.LocalEndPoint, serverConnection.RemoteEndPoint);
+
+ await CreateAndTestBidirectionalStream(clientConnection, serverConnection);
+ await CreateAndTestBidirectionalStream(serverConnection, clientConnection);
+ await CreateAndTestUnidirectionalStream(serverConnection, clientConnection);
+ await CreateAndTestUnidirectionalStream(clientConnection, serverConnection);
+ }
+ }
+ }
+
+ private static async Task CreateAndTestBidirectionalStream(QuicConnection c1, QuicConnection c2)
+ {
+ using (QuicStream s1 = c1.OpenBidirectionalStream())
+ {
+ Assert.True(s1.CanRead);
+ Assert.True(s1.CanWrite);
+
+ ValueTask writeTask = s1.WriteAsync(s_data);
+ using (QuicStream s2 = await c2.AcceptStreamAsync())
+ {
+ await ReceiveDataAsync(s_data, s2);
+ await writeTask;
+ await TestBidirectionalStream(s1, s2);
+ }
+ }
+ }
+
+ private static async Task CreateAndTestUnidirectionalStream(QuicConnection c1, QuicConnection c2)
+ {
+ using (QuicStream s1 = c1.OpenUnidirectionalStream())
+ {
+ Assert.False(s1.CanRead);
+ Assert.True(s1.CanWrite);
+
+ ValueTask writeTask = s1.WriteAsync(s_data);
+ using (QuicStream s2 = await c2.AcceptStreamAsync())
+ {
+ await ReceiveDataAsync(s_data, s2);
+ await writeTask;
+ await TestUnidirectionalStream(s1, s2);
+ }
+ }
+ }
+
+ private static async Task TestBidirectionalStream(QuicStream s1, QuicStream s2)
+ {
+ Assert.True(s1.CanRead);
+ Assert.True(s1.CanWrite);
+ Assert.True(s2.CanRead);
+ Assert.True(s2.CanWrite);
+ Assert.Equal(s1.StreamId, s2.StreamId);
+
+ await SendAndReceiveDataAsync(s_data, s1, s2);
+ await SendAndReceiveDataAsync(s_data, s2, s1);
+ await SendAndReceiveDataAsync(s_data, s2, s1);
+ await SendAndReceiveDataAsync(s_data, s1, s2);
+
+ await SendAndReceiveEOFAsync(s1, s2);
+ await SendAndReceiveEOFAsync(s2, s1);
+ }
+
+ private static async Task TestUnidirectionalStream(QuicStream s1, QuicStream s2)
+ {
+ Assert.False(s1.CanRead);
+ Assert.True(s1.CanWrite);
+ Assert.True(s2.CanRead);
+ Assert.False(s2.CanWrite);
+ Assert.Equal(s1.StreamId, s2.StreamId);
+
+ await SendAndReceiveDataAsync(s_data, s1, s2);
+ await SendAndReceiveDataAsync(s_data, s1, s2);
+
+ await SendAndReceiveEOFAsync(s1, s2);
+ }
+
+ private static async Task SendAndReceiveDataAsync(ReadOnlyMemory<byte> data, QuicStream s1, QuicStream s2)
+ {
+ await s1.WriteAsync(data);
+ await ReceiveDataAsync(data, s2);
+ }
+
+ private static async Task ReceiveDataAsync(ReadOnlyMemory<byte> data, QuicStream s)
+ {
+ Memory<byte> readBuffer = new byte[data.Length];
+
+ int bytesRead = 0;
+ while (bytesRead < data.Length)
+ {
+ bytesRead += await s.ReadAsync(readBuffer.Slice(bytesRead));
+ }
+
+ Assert.True(data.Span.SequenceEqual(readBuffer.Span));
+ }
+
+ private static async Task SendAndReceiveEOFAsync(QuicStream s1, QuicStream s2)
+ {
+ byte[] readBuffer = new byte[1];
+
+ s1.ShutdownWrite();
+
+ int bytesRead = await s2.ReadAsync(readBuffer);
+ Assert.Equal(0, bytesRead);
+
+ // Another read should still give EOF
+ bytesRead = await s2.ReadAsync(readBuffer);
+ Assert.Equal(0, bytesRead);
+ }
+ }
+}
--- /dev/null
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ <Configurations>netcoreapp-Unix-Debug;netcoreapp-Unix-Release;netcoreapp-Windows_NT-Debug;netcoreapp-Windows_NT-Release</Configurations>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="QuicConnectionTests.cs" />
+ </ItemGroup>
+</Project>
\ No newline at end of file
"4.0.12.0": "4.3.0"
}
},
+ "System.Net.Quic": {
+ "InboxOn": {
+ "netcoreapp3.0": "4.2.1.0"
+ }
+ },
"System.Net.Requests": {
"StableVersions": [
"3.9.0",