Skip to content

Commit

Permalink
Get tests running for HTTP/3 (dotnet#31898)
Browse files Browse the repository at this point in the history
Update generic tests to use a Version rather than boolean IsHttp11/IsHttp20, and update some HTTP/2 to work for HTTP/3.
Enable tests for HTTP/3, behind a conditional feature test.
Fix QPackDecoder lzcnt assuming an 8-bit test.
Rename test QPACK classes from QPackEncoder/QPackDecoder -> QPackTestEncoder/QPackTestDecoder to avoid naming confusion with product code classes.
Fix QPackTestDecoder bit flag checks.
Fix a double call to QuicConnection.CloseAsync(). Update to shutdown QuicConnection in a background task.
Fix test cert usage.
  • Loading branch information
scalablecory authored Feb 14, 2020
1 parent 66b69cd commit 556fb40
Show file tree
Hide file tree
Showing 26 changed files with 379 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private void OnByte(byte b, IHttpHeadersHandler handler)
}
break;
case State.CompressedHeaders:
switch (BitOperations.LeadingZeroCount(b) - 24)
switch (BitOperations.LeadingZeroCount(b) - 24) // byte 'b' is extended to uint, so will have 24 extra 0s.
{
case 0: // Indexed Header Field
prefixInt = IndexedHeaderFieldPrefixMask & b;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using Xunit;
Expand All @@ -23,6 +25,7 @@ public static partial class Certificates
private static readonly X509Certificate2 s_noEKUCertificate;
private static readonly X509Certificate2 s_selfSignedServerCertificate;
private static readonly X509Certificate2 s_selfSignedClientCertificate;
private static X509Certificate2 s_selfSigned13ServerCertificate;

static Certificates()
{
Expand Down Expand Up @@ -69,6 +72,44 @@ static Certificates()
public static X509Certificate2 GetNoEKUCertificate() => new X509Certificate2(s_noEKUCertificate);
public static X509Certificate2 GetSelfSignedServerCertificate() => new X509Certificate2(s_selfSignedServerCertificate);
public static X509Certificate2 GetSelfSignedClientCertificate() => new X509Certificate2(s_selfSignedClientCertificate);

public static X509Certificate2 GetSelfSigned13ServerCertificate()
{
if (s_selfSigned13ServerCertificate == null)
{
X509Certificate2 cert;

using (ECDsa dsa = ECDsa.Create())
{
var certReq = new CertificateRequest("CN=testservereku.contoso.com", dsa, HashAlgorithmName.SHA256);
certReq.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false));
certReq.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
certReq.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false));

X509Certificate2 innerCert = certReq.CreateSelfSigned(DateTimeOffset.UtcNow.AddMonths(-1), DateTimeOffset.UtcNow.AddMonths(1));

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
using (innerCert)
{
cert = new X509Certificate2(innerCert.Export(X509ContentType.Pfx));
}
}
else
{
cert = innerCert;
}
}

if (Interlocked.CompareExchange(ref s_selfSigned13ServerCertificate, cert, null) != null)
{
// Lost a race to create.
cert.Dispose();
}
}

return new X509Certificate2(s_selfSigned13ServerCertificate);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ public abstract class LoopbackServerFactory
public abstract GenericLoopbackServer CreateServer(GenericLoopbackOptions options = null);
public abstract Task CreateServerAsync(Func<GenericLoopbackServer, Uri, Task> funcAsync, int millisecondsTimeout = 60_000, GenericLoopbackOptions options = null);

// TODO: just expose a version property.
public abstract bool IsHttp11 { get; }
public abstract bool IsHttp2 { get; }
public abstract bool IsHttp3 { get; }
public abstract Version Version { get; }

// Common helper methods

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ public async Task<HeadersFrame> ReadRequestHeaderFrameAsync()

private static (int bytesConsumed, int value) DecodeInteger(ReadOnlySpan<byte> headerBlock, byte prefixMask)
{
return QPackDecoder.DecodeInteger(headerBlock, prefixMask);
return QPackTestDecoder.DecodeInteger(headerBlock, prefixMask);
}

private static (int bytesConsumed, string value) DecodeString(ReadOnlySpan<byte> headerBlock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,7 @@ public override async Task CreateServerAsync(Func<GenericLoopbackServer, Uri, Ta
}
}

public override bool IsHttp11 => false;
public override bool IsHttp2 => true;
public override bool IsHttp3 => false;
public override Version Version => HttpVersion.Version20;
}

public enum ProtocolErrors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,27 @@ namespace System.Net.Test.Common
{
public sealed class Http3LoopbackConnection : GenericLoopbackConnection
{
public const long H3_NO_ERROR = 0x100;
public const long H3_GENERAL_PROTOCOL_ERROR = 0x101;
public const long H3_INTERNAL_ERROR = 0x102;
public const long H3_STREAM_CREATION_ERROR = 0x103;
public const long H3_CLOSED_CRITICAL_STREAM = 0x104;
public const long H3_FRAME_UNEXPECTED = 0x105;
public const long H3_FRAME_ERROR = 0x106;
public const long H3_EXCESSIVE_LOAD = 0x107;
public const long H3_ID_ERROR = 0x108;
public const long H3_SETTINGS_ERROR = 0x109;
public const long H3_MISSING_SETTINGS = 0x10a;
public const long H3_REQUEST_REJECTED = 0x10b;
public const long H3_REQUEST_CANCELLED = 0x10c;
public const long H3_REQUEST_INCOMPLETE = 0x10d;
public const long H3_CONNECT_ERROR = 0x10f;
public const long H3_VERSION_FALLBACK = 0x110;

private readonly QuicConnection _connection;
private readonly Dictionary<int, Http3LoopbackStream> _openStreams = new Dictionary<int, Http3LoopbackStream>();
private Http3LoopbackStream _currentStream;
private bool _closed;

public Http3LoopbackConnection(QuicConnection connection)
{
Expand All @@ -29,9 +47,20 @@ public override void Dispose()
stream.Dispose();
}

if (!_closed)
{
CloseAsync(H3_INTERNAL_ERROR).GetAwaiter().GetResult();
}

_connection.Dispose();
}

public async Task CloseAsync(long errorCode)
{
await _connection.CloseAsync(errorCode).ConfigureAwait(false);
_closed = true;
}

public Http3LoopbackStream OpenUnidirectionalStream()
{
return new Http3LoopbackStream(_connection.OpenUnidirectionalStream());
Expand Down Expand Up @@ -73,7 +102,14 @@ public override async Task<byte[]> ReadRequestBodyAsync()

public override async Task<HttpRequestData> ReadRequestDataAsync(bool readBody = true)
{
Http3LoopbackStream stream = await AcceptStreamAsync().ConfigureAwait(false);
Http3LoopbackStream stream;

do
{
stream = await AcceptStreamAsync().ConfigureAwait(false);
}
while (!stream.CanWrite); // skip control stream.

return await stream.ReadRequestDataAsync(readBody).ConfigureAwait(false);
}

Expand All @@ -94,7 +130,7 @@ public override async Task SendResponseBodyAsync(byte[] content, bool isFinal =

if (isFinal)
{
stream.ShutdownSend();
await stream.ShutdownSendAsync().ConfigureAwait(false);
stream.Dispose();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ public Http3LoopbackServer(GenericLoopbackOptions options = null)
{
options ??= new GenericLoopbackOptions();

_cert = Configuration.Certificates.GetServerCertificate();
_cert = Configuration.Certificates.GetSelfSigned13ServerCertificate();

var sslOpts = new SslServerAuthenticationOptions
{
EnabledSslProtocols = options.SslProtocols,
ApplicationProtocols = new List<SslApplicationProtocol> { SslApplicationProtocol.Http3 },
ServerCertificate = _cert,
//ServerCertificate = _cert,
ClientCertificateRequired = false
};

_listener = new QuicListener(new IPEndPoint(options.Address, 0), sslOpts);
_listener.Start();
}

public override void Dispose()
Expand All @@ -55,10 +56,11 @@ public override async Task AcceptConnectionAsync(Func<GenericLoopbackConnection,

public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = "")
{
using GenericLoopbackConnection con = await EstablishGenericConnectionAsync().ConfigureAwait(false);
using var con = (Http3LoopbackConnection)await EstablishGenericConnectionAsync().ConfigureAwait(false);

HttpRequestData request = await con.ReadRequestDataAsync().ConfigureAwait(false);
await con.SendResponseAsync(statusCode, headers, content).ConfigureAwait(false);
await con.CloseAsync(Http3LoopbackConnection.H3_NO_ERROR);
return request;
}
}
Expand All @@ -67,11 +69,7 @@ public sealed class Http3LoopbackServerFactory : LoopbackServerFactory
{
public static Http3LoopbackServerFactory Singleton { get; } = new Http3LoopbackServerFactory();

public override bool IsHttp11 => false;

public override bool IsHttp2 => false;

public override bool IsHttp3 => true;
public override Version Version => HttpVersion.Version30;

public override GenericLoopbackServer CreateServer(GenericLoopbackOptions options = null)
{
Expand Down
22 changes: 11 additions & 11 deletions src/libraries/Common/tests/System/Net/Http/Http3LoopbackStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,22 @@ public async Task SendSettingsFrameAsync(ICollection<(long settingId, long setti

public async Task SendHeadersFrameAsync(ICollection<HttpHeaderData> headers)
{
int bufferLength = QPackEncoder.MaxPrefixLength;
int bufferLength = QPackTestEncoder.MaxPrefixLength;

foreach (HttpHeaderData header in headers)
{
// Two varints for length, and double the name/value lengths to account for expanding Huffman coding.
bufferLength += QPackEncoder.MaxVarIntLength * 2 + header.Name.Length * 2 + header.Value.Length * 2;
bufferLength += QPackTestEncoder.MaxVarIntLength * 2 + header.Name.Length * 2 + header.Value.Length * 2;
}

var buffer = new byte[bufferLength];
int bytesWritten = 0;

bytesWritten += QPackEncoder.EncodePrefix(buffer.AsSpan(bytesWritten), 0, 0);
bytesWritten += QPackTestEncoder.EncodePrefix(buffer.AsSpan(bytesWritten), 0, 0);

foreach (HttpHeaderData header in headers)
{
bytesWritten += QPackEncoder.EncodeHeader(buffer.AsSpan(bytesWritten), header.Name, header.Value, header.HuffmanEncoded ? QPackFlags.HuffmanEncode : QPackFlags.None);
bytesWritten += QPackTestEncoder.EncodeHeader(buffer.AsSpan(bytesWritten), header.Name, header.Value, header.HuffmanEncoded ? QPackFlags.HuffmanEncode : QPackFlags.None);
}

await SendFrameAsync(HeadersFrame, buffer.AsMemory(0, bytesWritten)).ConfigureAwait(false);
Expand All @@ -100,9 +100,10 @@ public async Task SendFrameAsync(long frameType, ReadOnlyMemory<byte> framePaylo
await _stream.WriteAsync(framePayload).ConfigureAwait(false);
}

public void ShutdownSend()
public async Task ShutdownSendAsync()
{
_stream.Shutdown();
await _stream.ShutdownWriteCompleted().ConfigureAwait(false);
}

static int EncodeHttpInteger(long longToEncode, Span<byte> buffer)
Expand Down Expand Up @@ -176,14 +177,14 @@ private HttpRequestData ParseHeaders(ReadOnlySpan<byte> buffer)
{
HttpRequestData request = new HttpRequestData { RequestId = Http3LoopbackConnection.GetRequestId(_stream) };

(int prefixLength, int requiredInsertCount, int deltaBase) = QPackDecoder.DecodePrefix(buffer);
(int prefixLength, int requiredInsertCount, int deltaBase) = QPackTestDecoder.DecodePrefix(buffer);
if (requiredInsertCount != 0 || deltaBase != 0) throw new Exception("QPack dynamic table not yet supported.");

buffer = buffer.Slice(prefixLength);

while (!buffer.IsEmpty)
{
(int headerLength, HttpHeaderData header) = QPackDecoder.DecodeHeader(buffer);
(int headerLength, HttpHeaderData header) = QPackTestDecoder.DecodeHeader(buffer);

request.Headers.Add(header);
buffer = buffer.Slice(headerLength);
Expand Down Expand Up @@ -247,22 +248,21 @@ public async Task WaitForCancellationAsync(bool ignoreIncomingData = true)
public async Task<long?> ReadInteger()
{
byte[] buffer = new byte[MaximumVarIntBytes];
int bufferAvailableOffset = -1;
int bufferActiveLength = 0;

long integerValue;
int bytesRead;

do
{
bytesRead = await _stream.ReadAsync(buffer.AsMemory(++bufferAvailableOffset, 1)).ConfigureAwait(false);
bytesRead = await _stream.ReadAsync(buffer.AsMemory(bufferActiveLength++, 1)).ConfigureAwait(false);
if (bytesRead == 0)
{
return bufferActiveLength == 0 ? (long?)null : throw new Exception("Unable to read varint; unexpected end of stream.");
return bufferActiveLength == 1 ? (long?)null : throw new Exception("Unable to read varint; unexpected end of stream.");
}
Debug.Assert(bytesRead == 1);
}
while (!TryDecodeHttpInteger(buffer.AsSpan(0, ++bufferActiveLength), out integerValue, out bytesRead));
while (!TryDecodeHttpInteger(buffer.AsSpan(0, bufferActiveLength), out integerValue, out bytesRead));

Debug.Assert(bytesRead == bufferActiveLength);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(outp
[InlineData(true, CancellationMode.Token)]
public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode)
{
if (LoopbackServerFactory.IsHttp2 && chunkedTransfer)
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && chunkedTransfer)
{
// There is no chunked encoding in HTTP/2
// There is no chunked encoding in HTTP/2 and later
return;
}

Expand Down Expand Up @@ -80,9 +80,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
[MemberData(nameof(OneBoolAndCancellationMode))]
public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode)
{
if (LoopbackServerFactory.IsHttp2 && connectionClose)
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && connectionClose)
{
// There is no Connection header in HTTP/2
// There is no Connection header in HTTP/2 and later
return;
}

Expand Down Expand Up @@ -130,9 +130,9 @@ await ValidateClientCancellationAsync(async () =>
[MemberData(nameof(TwoBoolsAndCancellationMode))]
public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, CancellationMode mode)
{
if (LoopbackServerFactory.IsHttp2 && (chunkedTransfer || connectionClose))
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
{
// There is no chunked encoding or connection header in HTTP/2
// There is no chunked encoding or connection header in HTTP/2 and later
return;
}

Expand Down Expand Up @@ -186,9 +186,9 @@ await ValidateClientCancellationAsync(async () =>
[MemberData(nameof(ThreeBools))]
public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync)
{
if (LoopbackServerFactory.IsHttp2 && (chunkedTransfer || connectionClose))
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
{
// There is no chunked encoding or connection header in HTTP/2
// There is no chunked encoding or connection header in HTTP/2 and later
return;
}

Expand Down Expand Up @@ -312,10 +312,10 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
[ConditionalFact]
public async Task MaxConnectionsPerServer_WaitingConnectionsAreCancelable()
{
if (LoopbackServerFactory.IsHttp2)
if (LoopbackServerFactory.Version >= HttpVersion.Version20)
{
// HTTP/2 does not use connection limits.
throw new SkipTestException("Not supported on HTTP/2");
throw new SkipTestException("Not supported on HTTP/2 and later");
}

using (HttpClientHandler handler = CreateHttpClientHandler())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(

private string GetCookieValue(HttpRequestData request)
{
if (!LoopbackServerFactory.IsHttp2)
if (LoopbackServerFactory.Version < HttpVersion.Version20)
{
// HTTP/1.x must have only one value.
return request.GetSingleHeaderValue("Cookie");
Expand Down
Loading

0 comments on commit 556fb40

Please sign in to comment.