Implement ITlsConnectionFeature (#1390)
This commit is contained in:
parent
5e896ca506
commit
ece5ad36e2
|
|
@ -6,7 +6,9 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -24,13 +26,15 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
IHttpRequestLifetimeFeature,
|
IHttpRequestLifetimeFeature,
|
||||||
IHttpAuthenticationFeature,
|
IHttpAuthenticationFeature,
|
||||||
IServerVariablesFeature,
|
IServerVariablesFeature,
|
||||||
IHttpBufferingFeature
|
IHttpBufferingFeature,
|
||||||
|
ITlsConnectionFeature
|
||||||
{
|
{
|
||||||
// NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation,
|
// NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation,
|
||||||
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
// then the list of `implementedFeatures` in the generated code project MUST also be updated.
|
||||||
|
|
||||||
private int _featureRevision;
|
private int _featureRevision;
|
||||||
private string _httpProtocolVersion = null;
|
private string _httpProtocolVersion = null;
|
||||||
|
private X509Certificate2 _certificate;
|
||||||
|
|
||||||
private List<KeyValuePair<Type, object>> MaybeExtra;
|
private List<KeyValuePair<Type, object>> MaybeExtra;
|
||||||
public void ResetFeatureCollection()
|
public void ResetFeatureCollection()
|
||||||
|
|
@ -276,7 +280,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
|
ReasonPhrase = ReasonPhrases.GetReasonPhrase(StatusCodes.Status101SwitchingProtocols);
|
||||||
|
|
||||||
// If we started reading before calling Upgrade Task should be completed at this point
|
// If we started reading before calling Upgrade Task should be completed at this point
|
||||||
// because read would return 0 syncronosly
|
// because read would return 0 synchronously
|
||||||
Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted);
|
Debug.Assert(_readBodyTask == null || _readBodyTask.IsCompleted);
|
||||||
|
|
||||||
// Reset reading status to allow restarting with new IO
|
// Reset reading status to allow restarting with new IO
|
||||||
|
|
@ -290,6 +294,35 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
return new DuplexStream(RequestBody, ResponseBody);
|
return new DuplexStream(RequestBody, ResponseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task<X509Certificate2> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult(((ITlsConnectionFeature)this).ClientCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe X509Certificate2 ITlsConnectionFeature.ClientCertificate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_certificate == null &&
|
||||||
|
NativeRequest->pSslInfo != null &&
|
||||||
|
NativeRequest->pSslInfo->pClientCertInfo != null &&
|
||||||
|
NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded != null &&
|
||||||
|
NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize != 0)
|
||||||
|
{
|
||||||
|
// Based off of from https://referencesource.microsoft.com/#system/net/System/Net/HttpListenerRequest.cs,1037c8ec82879ba0,references
|
||||||
|
var rawCertificateCopy = new byte[NativeRequest->pSslInfo->pClientCertInfo->CertEncodedSize];
|
||||||
|
Marshal.Copy((IntPtr)NativeRequest->pSslInfo->pClientCertInfo->pCertEncoded, rawCertificateCopy, 0, rawCertificateCopy.Length);
|
||||||
|
_certificate = new X509Certificate2(rawCertificateCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _certificate;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_certificate = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
|
IEnumerator<KeyValuePair<Type, object>> IEnumerable<KeyValuePair<Type, object>>.GetEnumerator() => FastEnumerable().GetEnumerator();
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator();
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
||||||
_currentIHttpAuthenticationFeature = this;
|
_currentIHttpAuthenticationFeature = this;
|
||||||
_currentIServerVariablesFeature = this;
|
_currentIServerVariablesFeature = this;
|
||||||
_currentIHttpBufferingFeature = this;
|
_currentIHttpBufferingFeature = this;
|
||||||
|
_currentITlsConnectionFeature = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object FastFeatureGet(Type key)
|
internal object FastFeatureGet(Type key)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ namespace Microsoft.AspNetCore.Server.IIS
|
||||||
|
|
||||||
public static extern bool CloseHandle(IntPtr handle);
|
public static extern bool CloseHandle(IntPtr handle);
|
||||||
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
|
{
|
||||||
|
public class ClientCertificateFixture : IDisposable
|
||||||
|
{
|
||||||
|
public ClientCertificateFixture()
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
|
||||||
|
foreach (var cert in store.Certificates)
|
||||||
|
{
|
||||||
|
if (cert.Issuer != "CN=IISIntegrationTest_Root")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Certificate = cert;
|
||||||
|
store.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentKey = CreateKeyMaterial(2048);
|
||||||
|
|
||||||
|
// On first run of the test, creates the certificate in the trusted root certificate authorities.
|
||||||
|
var parentRequest = new CertificateRequest("CN=IISIntegrationTest_Root", parentKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
|
|
||||||
|
parentRequest.CertificateExtensions.Add(
|
||||||
|
new X509BasicConstraintsExtension(
|
||||||
|
certificateAuthority: true,
|
||||||
|
hasPathLengthConstraint: false,
|
||||||
|
pathLengthConstraint: 0,
|
||||||
|
critical: true));
|
||||||
|
|
||||||
|
parentRequest.CertificateExtensions.Add(
|
||||||
|
new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation, critical: true));
|
||||||
|
|
||||||
|
parentRequest.CertificateExtensions.Add(
|
||||||
|
new X509SubjectKeyIdentifierExtension(parentRequest.PublicKey, false));
|
||||||
|
|
||||||
|
var notBefore = DateTimeOffset.Now.AddDays(-1);
|
||||||
|
var notAfter = DateTimeOffset.Now.AddYears(5);
|
||||||
|
|
||||||
|
var parentCert = parentRequest.CreateSelfSigned(notBefore, notAfter);
|
||||||
|
|
||||||
|
// Need to export/import the certificate to associate the private key with the cert.
|
||||||
|
var imported = parentCert;
|
||||||
|
|
||||||
|
var export = parentCert.Export(X509ContentType.Pkcs12, "");
|
||||||
|
imported = new X509Certificate2(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
|
||||||
|
Array.Clear(export, 0, export.Length);
|
||||||
|
|
||||||
|
// Add the cert to the cert store
|
||||||
|
Certificate = imported;
|
||||||
|
|
||||||
|
store.Add(certificate: imported);
|
||||||
|
store.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate2 Certificate { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
using (var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
|
||||||
|
{
|
||||||
|
store.Open(OpenFlags.ReadWrite);
|
||||||
|
store.Remove(Certificate);
|
||||||
|
store.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RSA CreateKeyMaterial(int minimumKeySize)
|
||||||
|
{
|
||||||
|
var rsa = RSA.Create(minimumKeySize);
|
||||||
|
if (rsa.KeySize < minimumKeySize)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Failed to create a key with a size of {minimumKeySize} bits");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests
|
||||||
|
{
|
||||||
|
[Collection(PublishedSitesCollection.Name)]
|
||||||
|
[SkipIfNotAdmin]
|
||||||
|
public class ClientCertificateTests : IISFunctionalTestBase
|
||||||
|
{
|
||||||
|
private readonly PublishedSitesFixture _fixture;
|
||||||
|
private readonly ClientCertificateFixture _certFixture;
|
||||||
|
|
||||||
|
public ClientCertificateTests(PublishedSitesFixture fixture, ClientCertificateFixture certFixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
_certFixture = certFixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestMatrix TestVariants
|
||||||
|
=> TestMatrix.ForServers(DeployerSelector.ServerType)
|
||||||
|
.WithTfms(Tfm.NetCoreApp22, Tfm.Net461)
|
||||||
|
.WithAllApplicationTypes()
|
||||||
|
.WithAllAncmVersions()
|
||||||
|
.WithAllHostingModels();
|
||||||
|
|
||||||
|
[ConditionalTheory]
|
||||||
|
[MemberData(nameof(TestVariants))]
|
||||||
|
public Task HttpsNoClientCert_NoClientCert(TestVariant variant)
|
||||||
|
{
|
||||||
|
return ClientCertTest(variant, sendClientCert: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[ConditionalTheory]
|
||||||
|
[MemberData(nameof(TestVariants))]
|
||||||
|
public Task HttpsClientCert_GetCertInformation(TestVariant variant)
|
||||||
|
{
|
||||||
|
return ClientCertTest(variant, sendClientCert: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClientCertTest(TestVariant variant, bool sendClientCert)
|
||||||
|
{
|
||||||
|
var port = TestPortHelper.GetNextSSLPort();
|
||||||
|
var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant);
|
||||||
|
deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
|
||||||
|
deploymentParameters.AddHttpsToServerConfig();
|
||||||
|
|
||||||
|
var deploymentResult = await DeployAsync(deploymentParameters);
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
|
||||||
|
ClientCertificateOptions = ClientCertificateOption.Manual,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sendClientCert)
|
||||||
|
{
|
||||||
|
Assert.NotNull(_certFixture.Certificate);
|
||||||
|
handler.ClientCertificates.Add(_certFixture.Certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = deploymentResult.CreateClient(handler);
|
||||||
|
var response = await client.GetAsync("GetClientCert");
|
||||||
|
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (sendClientCert)
|
||||||
|
{
|
||||||
|
Assert.Equal($"Enabled;{_certFixture.Certificate.GetCertHashString()}", responseText);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal("Disabled", responseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
||||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||||
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
/// This type just maps collection names to available fixtures
|
/// This type just maps collection names to available fixtures
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[CollectionDefinition(Name)]
|
[CollectionDefinition(Name)]
|
||||||
public class PublishedSitesCollection : ICollectionFixture<PublishedSitesFixture>
|
public class PublishedSitesCollection : ICollectionFixture<PublishedSitesFixture>, ICollectionFixture<ClientCertificateFixture>
|
||||||
{
|
{
|
||||||
public const string Name = nameof(PublishedSitesCollection);
|
public const string Name = nameof(PublishedSitesCollection);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public sealed class SkipIfNotAdminAttribute : Attribute, ITestCondition
|
||||||
|
{
|
||||||
|
public bool IsMet
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var identity = WindowsIdentity.GetCurrent();
|
||||||
|
var principal = new WindowsPrincipal(identity);
|
||||||
|
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SkipReason => "The current process is not running as admin.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
|
||||||
|
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
||||||
|
{
|
||||||
|
[Collection(PublishedSitesCollection.Name)]
|
||||||
|
public class HttpsTests : IISFunctionalTestBase
|
||||||
|
{
|
||||||
|
private readonly PublishedSitesFixture _fixture;
|
||||||
|
|
||||||
|
public HttpsTests(PublishedSitesFixture fixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestMatrix TestVariants
|
||||||
|
=> TestMatrix.ForServers(DeployerSelector.ServerType)
|
||||||
|
.WithTfms(Tfm.NetCoreApp22, Tfm.Net461)
|
||||||
|
.WithAllApplicationTypes()
|
||||||
|
.WithAllAncmVersions()
|
||||||
|
.WithAllHostingModels();
|
||||||
|
|
||||||
|
[ConditionalTheory]
|
||||||
|
[MemberData(nameof(TestVariants))]
|
||||||
|
public async Task HttpsHelloWorld(TestVariant variant)
|
||||||
|
{
|
||||||
|
var port = TestPortHelper.GetNextSSLPort();
|
||||||
|
var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant);
|
||||||
|
deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
|
||||||
|
deploymentParameters.AddHttpsToServerConfig();
|
||||||
|
|
||||||
|
var deploymentResult = await DeployAsync(deploymentParameters);
|
||||||
|
|
||||||
|
var handler = new HttpClientHandler
|
||||||
|
{
|
||||||
|
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
|
||||||
|
};
|
||||||
|
var client = deploymentResult.CreateClient(handler);
|
||||||
|
var response = await client.GetAsync("HttpsHelloWorld");
|
||||||
|
var responseText = await response.Content.ReadAsStringAsync();
|
||||||
|
if (variant.HostingModel == HostingModel.OutOfProcess)
|
||||||
|
{
|
||||||
|
Assert.Equal("Scheme:https; Original:http", responseText);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal("Scheme:https; Original:", responseText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,148 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.Net.Http;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Server.IIS.FunctionalTests.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Server.IntegrationTesting;
|
|
||||||
using Microsoft.AspNetCore.Server.IntegrationTesting.Common;
|
|
||||||
using Microsoft.AspNetCore.Server.IntegrationTesting.IIS;
|
|
||||||
using Microsoft.AspNetCore.Testing.xunit;
|
|
||||||
using Xunit;
|
|
||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests
|
|
||||||
{
|
|
||||||
// IIS Express preregisteres 44300-44399 ports with SSL bindings.
|
|
||||||
// So these tests always have to use ports in this range, and we can't rely on OS-allocated ports without a whole lot of ceremony around
|
|
||||||
// creating self-signed certificates and registering SSL bindings with HTTP.sys
|
|
||||||
// Test specific to IISExpress
|
|
||||||
[Collection(PublishedSitesCollection.Name)]
|
|
||||||
public class HttpsTest : IISFunctionalTestBase
|
|
||||||
{
|
|
||||||
private readonly PublishedSitesFixture _fixture;
|
|
||||||
|
|
||||||
public HttpsTest(PublishedSitesFixture fixture)
|
|
||||||
{
|
|
||||||
_fixture = fixture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TestMatrix TestVariants
|
|
||||||
=> TestMatrix.ForServers(DeployerSelector.ServerType)
|
|
||||||
.WithTfms(Tfm.NetCoreApp22, Tfm.Net461)
|
|
||||||
.WithAllAncmVersions();
|
|
||||||
|
|
||||||
[ConditionalTheory]
|
|
||||||
[MemberData(nameof(TestVariants))]
|
|
||||||
public async Task HttpsHelloWorld(TestVariant variant)
|
|
||||||
{
|
|
||||||
var port = TestPortHelper.GetNextSSLPort();
|
|
||||||
var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant);
|
|
||||||
deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
|
|
||||||
deploymentParameters.AddHttpsToServerConfig();
|
|
||||||
|
|
||||||
var deploymentResult = await DeployAsync(deploymentParameters);
|
|
||||||
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = (a, b, c, d) => true
|
|
||||||
};
|
|
||||||
var client = deploymentResult.CreateClient(handler);
|
|
||||||
var response = await client.GetAsync("HttpsHelloWorld");
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
Assert.Equal("Scheme:https; Original:http", responseText);
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConditionalTheory]
|
|
||||||
[MemberData(nameof(TestVariants))]
|
|
||||||
public Task HttpsHelloWorld_NoClientCert(TestVariant variant)
|
|
||||||
{
|
|
||||||
return HttpsHelloWorldCerts(variant, sendClientCert: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma warning disable xUnit1004 // Test methods should not be skipped
|
|
||||||
[ConditionalTheory(Skip = "Manual test only, selecting a client cert is non-determanistic on different machines.")]
|
|
||||||
[MemberData(nameof(TestVariants))]
|
|
||||||
#pragma warning restore xUnit1004 // Test methods should not be skipped
|
|
||||||
public Task HttpsHelloWorld_ClientCert(TestVariant variant)
|
|
||||||
{
|
|
||||||
return HttpsHelloWorldCerts(variant, sendClientCert: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HttpsHelloWorldCerts(TestVariant variant, bool sendClientCert)
|
|
||||||
{
|
|
||||||
var port = TestPortHelper.GetNextSSLPort();
|
|
||||||
var deploymentParameters = _fixture.GetBaseDeploymentParameters(variant);
|
|
||||||
deploymentParameters.ApplicationBaseUriHint = $"https://localhost:{port}/";
|
|
||||||
deploymentParameters.AddHttpsToServerConfig();
|
|
||||||
|
|
||||||
var deploymentResult = await DeployAsync(deploymentParameters);
|
|
||||||
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback = (a, b, c, d) => true,
|
|
||||||
ClientCertificateOptions = ClientCertificateOption.Manual
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sendClientCert)
|
|
||||||
{
|
|
||||||
X509Certificate2 clientCert = FindClientCert();
|
|
||||||
Assert.NotNull(clientCert);
|
|
||||||
handler.ClientCertificates.Add(clientCert);
|
|
||||||
}
|
|
||||||
|
|
||||||
var client = deploymentResult.CreateClient(handler);
|
|
||||||
|
|
||||||
// Request to base address and check if various parts of the body are rendered & measure the cold startup time.
|
|
||||||
var response = await client.GetAsync("checkclientcert");
|
|
||||||
|
|
||||||
var responseText = await response.Content.ReadAsStringAsync();
|
|
||||||
if (sendClientCert)
|
|
||||||
{
|
|
||||||
Assert.Equal("Scheme:https; Original:http; has cert? True", responseText);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Assert.Equal("Scheme:https; Original:http; has cert? False", responseText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate2 FindClientCert()
|
|
||||||
{
|
|
||||||
var store = new X509Store();
|
|
||||||
store.Open(OpenFlags.ReadOnly);
|
|
||||||
|
|
||||||
foreach (var cert in store.Certificates)
|
|
||||||
{
|
|
||||||
bool isClientAuth = false;
|
|
||||||
bool isSmartCard = false;
|
|
||||||
foreach (var extension in cert.Extensions)
|
|
||||||
{
|
|
||||||
var eku = extension as X509EnhancedKeyUsageExtension;
|
|
||||||
if (eku != null)
|
|
||||||
{
|
|
||||||
foreach (var oid in eku.EnhancedKeyUsages)
|
|
||||||
{
|
|
||||||
if (oid.FriendlyName == "Client Authentication")
|
|
||||||
{
|
|
||||||
isClientAuth = true;
|
|
||||||
}
|
|
||||||
else if (oid.FriendlyName == "Smart Card Logon")
|
|
||||||
{
|
|
||||||
isSmartCard = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isClientAuth && !isSmartCard)
|
|
||||||
{
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -681,5 +681,8 @@ namespace TestSite
|
||||||
{
|
{
|
||||||
await ctx.Response.WriteAsync(string.Join("|", Environment.GetCommandLineArgs().Skip(1)));
|
await ctx.Response.WriteAsync(string.Join("|", Environment.GetCommandLineArgs().Skip(1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task HttpsHelloWorld(HttpContext ctx) =>
|
||||||
|
ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,6 @@ namespace TestSite
|
||||||
public Task HttpsHelloWorld(HttpContext ctx) =>
|
public Task HttpsHelloWorld(HttpContext ctx) =>
|
||||||
ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]);
|
ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]);
|
||||||
|
|
||||||
public Task CheckClientCert(HttpContext ctx) =>
|
|
||||||
ctx.Response.WriteAsync("Scheme:" + ctx.Request.Scheme + "; Original:" + ctx.Request.Headers["x-original-proto"]
|
|
||||||
+ "; has cert? " + (ctx.Connection.ClientCertificate != null));
|
|
||||||
|
|
||||||
public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated);
|
public Task Anonymous(HttpContext context) => context.Response.WriteAsync("Anonymous?" + !context.User.Identity.IsAuthenticated);
|
||||||
|
|
||||||
public Task Restricted(HttpContext context)
|
public Task Restricted(HttpContext context)
|
||||||
|
|
|
||||||
|
|
@ -43,5 +43,11 @@ namespace TestSite
|
||||||
|
|
||||||
await ctx.Response.WriteAsync(authScheme?.Name ?? "null");
|
await ctx.Response.WriteAsync(authScheme?.Name ?? "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task GetClientCert(HttpContext context)
|
||||||
|
{
|
||||||
|
var clientCert = context.Connection.ClientCertificate;
|
||||||
|
await context.Response.WriteAsync(clientCert != null ? $"Enabled;{clientCert.GetCertHashString()}" : "Disabled");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue