diff --git a/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj b/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj index bafb556b55d3..ded7faa4d510 100644 --- a/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj +++ b/sdk/core/Azure.Core.TestFramework/src/Azure.Core.TestFramework.csproj @@ -4,7 +4,9 @@ true - + + + diff --git a/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs b/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs index a71ca1ac8a2f..e6f7e3f61899 100644 --- a/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs +++ b/sdk/core/Azure.Core.TestFramework/src/MockTransport.cs @@ -21,6 +21,8 @@ public class MockTransport : HttpPipelineTransport public bool? ExpectSyncPipeline { get; set; } + public List TransportUpdates { get; } = []; + public MockTransport() { RequestGate = new AsyncGate(); @@ -38,7 +40,7 @@ public MockTransport(params MockResponse[] responses) }; } - public MockTransport(Func responseFactory): this(req => responseFactory((MockRequest)req.Request)) + public MockTransport(Func responseFactory) : this(req => responseFactory((MockRequest)req.Request)) { } @@ -72,6 +74,11 @@ public override async ValueTask ProcessAsync(HttpMessage message) await ProcessCore(message); } + public override void Update(HttpPipelineTransportOptions options) + { + TransportUpdates.Add(options); + } + private async Task ProcessCore(HttpMessage message) { if (!(message.Request is MockRequest request)) diff --git a/sdk/core/Azure.Core/api/Azure.Core.net462.cs b/sdk/core/Azure.Core/api/Azure.Core.net462.cs index 8136f6d21825..c9a2bb5e336d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net462.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net462.cs @@ -306,6 +306,8 @@ public partial struct AccessToken public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; } + public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; } + public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } } public System.DateTimeOffset ExpiresOn { get { throw null; } } public System.DateTimeOffset? RefreshOn { get { throw null; } } public string Token { get { throw null; } } @@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans { public static readonly Azure.Core.Pipeline.HttpClientTransport Shared; public HttpClientTransport() { } + public HttpClientTransport(System.Func clientFactory) { } + public HttpClientTransport(System.Func handlerFactory) { } public HttpClientTransport(System.Net.Http.HttpClient client) { } public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { } public sealed override Azure.Core.Request CreateRequest() { throw null; } public void Dispose() { } public override void Process(Azure.Core.HttpMessage message) { } public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipeline { @@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { } public abstract Azure.Core.Request CreateRequest(); public abstract void Process(Azure.Core.HttpMessage message); public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message); + public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipelineTransportOptions { diff --git a/sdk/core/Azure.Core/api/Azure.Core.net472.cs b/sdk/core/Azure.Core/api/Azure.Core.net472.cs index 8136f6d21825..c9a2bb5e336d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net472.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net472.cs @@ -306,6 +306,8 @@ public partial struct AccessToken public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; } + public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; } + public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } } public System.DateTimeOffset ExpiresOn { get { throw null; } } public System.DateTimeOffset? RefreshOn { get { throw null; } } public string Token { get { throw null; } } @@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans { public static readonly Azure.Core.Pipeline.HttpClientTransport Shared; public HttpClientTransport() { } + public HttpClientTransport(System.Func clientFactory) { } + public HttpClientTransport(System.Func handlerFactory) { } public HttpClientTransport(System.Net.Http.HttpClient client) { } public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { } public sealed override Azure.Core.Request CreateRequest() { throw null; } public void Dispose() { } public override void Process(Azure.Core.HttpMessage message) { } public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipeline { @@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { } public abstract Azure.Core.Request CreateRequest(); public abstract void Process(Azure.Core.HttpMessage message); public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message); + public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipelineTransportOptions { diff --git a/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs index 3eb502afc9ff..44dddb16c14d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net8.0.cs @@ -315,6 +315,8 @@ public partial struct AccessToken public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; } + public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; } + public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } } public System.DateTimeOffset ExpiresOn { get { throw null; } } public System.DateTimeOffset? RefreshOn { get { throw null; } } public string Token { get { throw null; } } @@ -1055,12 +1057,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans { public static readonly Azure.Core.Pipeline.HttpClientTransport Shared; public HttpClientTransport() { } + public HttpClientTransport(System.Func clientFactory) { } + public HttpClientTransport(System.Func handlerFactory) { } public HttpClientTransport(System.Net.Http.HttpClient client) { } public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { } public sealed override Azure.Core.Request CreateRequest() { throw null; } public void Dispose() { } public override void Process(Azure.Core.HttpMessage message) { } public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipeline { @@ -1117,6 +1122,7 @@ protected HttpPipelineTransport() { } public abstract Azure.Core.Request CreateRequest(); public abstract void Process(Azure.Core.HttpMessage message); public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message); + public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipelineTransportOptions { diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 8136f6d21825..c9a2bb5e336d 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -306,6 +306,8 @@ public partial struct AccessToken public AccessToken(string accessToken, System.DateTimeOffset expiresOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn) { throw null; } public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType) { throw null; } + public AccessToken(string accessToken, System.DateTimeOffset expiresOn, System.DateTimeOffset? refreshOn, string tokenType, System.Security.Cryptography.X509Certificates.X509Certificate2 bindingCertificate) { throw null; } + public System.Security.Cryptography.X509Certificates.X509Certificate2? BindingCertificate { get { throw null; } } public System.DateTimeOffset ExpiresOn { get { throw null; } } public System.DateTimeOffset? RefreshOn { get { throw null; } } public string Token { get { throw null; } } @@ -1042,12 +1044,15 @@ public partial class HttpClientTransport : Azure.Core.Pipeline.HttpPipelineTrans { public static readonly Azure.Core.Pipeline.HttpClientTransport Shared; public HttpClientTransport() { } + public HttpClientTransport(System.Func clientFactory) { } + public HttpClientTransport(System.Func handlerFactory) { } public HttpClientTransport(System.Net.Http.HttpClient client) { } public HttpClientTransport(System.Net.Http.HttpMessageHandler messageHandler) { } public sealed override Azure.Core.Request CreateRequest() { throw null; } public void Dispose() { } public override void Process(Azure.Core.HttpMessage message) { } public override System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message) { throw null; } + public override void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipeline { @@ -1103,6 +1108,7 @@ protected HttpPipelineTransport() { } public abstract Azure.Core.Request CreateRequest(); public abstract void Process(Azure.Core.HttpMessage message); public abstract System.Threading.Tasks.ValueTask ProcessAsync(Azure.Core.HttpMessage message); + public virtual void Update(Azure.Core.Pipeline.HttpPipelineTransportOptions options) { } } public partial class HttpPipelineTransportOptions { diff --git a/sdk/core/Azure.Core/src/AccessToken.cs b/sdk/core/Azure.Core/src/AccessToken.cs index a23b64964119..0cc3d8d63028 100644 --- a/sdk/core/Azure.Core/src/AccessToken.cs +++ b/sdk/core/Azure.Core/src/AccessToken.cs @@ -3,6 +3,7 @@ using System; using System.ClientModel.Primitives; +using System.Security.Cryptography.X509Certificates; namespace Azure.Core { @@ -52,6 +53,23 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset? TokenType = tokenType; } + /// + /// Creates a new instance of using the provided and . + /// + /// The access token value. + /// The access token expiry date. + /// Specifies the time when the cached token should be proactively refreshed. + /// The access token type. + /// The binding certificate for the access token. + public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset? refreshOn, string tokenType, X509Certificate2 bindingCertificate) + { + Token = accessToken; + ExpiresOn = expiresOn; + RefreshOn = refreshOn; + TokenType = tokenType; + BindingCertificate = bindingCertificate; + } + /// /// Get the access token value. /// @@ -72,6 +90,13 @@ public AccessToken(string accessToken, DateTimeOffset expiresOn, DateTimeOffset? /// public string TokenType { get; } + /// + /// Gets or sets the binding certificate for the access token. + /// This is used when authenticating via Proof of Possession (PoP). + /// + /// + public X509Certificate2? BindingCertificate { get; } + /// public override bool Equals(object? obj) { diff --git a/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs b/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs index b65bbb1ccf05..e0c15f4854b2 100644 --- a/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs +++ b/sdk/core/Azure.Core/src/Diagnostics/AzureCoreEventSource.cs @@ -36,6 +36,7 @@ internal sealed class AzureCoreEventSource : AzureEventSource private const int RequestRedirectCountExceededEvent = 22; private const int PipelineTransportOptionsNotAppliedEvent = 23; private const int FailedToDecodeCaeChallengeClaimsEvent = 24; + private const int FailedToUpdateTransportEvent = 25; private AzureCoreEventSource() : base(EventSourceName) { } @@ -333,5 +334,14 @@ public void FailedToDecodeCaeChallengeClaims(string? encodedClaims, string excep { WriteEvent(FailedToDecodeCaeChallengeClaimsEvent, encodedClaims, exception); } + + [Event(FailedToUpdateTransportEvent, Level = EventLevel.Error, Message = "Failed to update transport: {0}")] + public void FailedToUpdateTransport(string reason) + { + if (IsEnabled(EventLevel.Error, EventKeywords.None)) + { + WriteEvent(FailedToUpdateTransportEvent, reason); + } + } } } diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs index 07f21a103c9a..a3ee696c596a 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpClientTransport.cs @@ -10,6 +10,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; +using Azure.Core.Diagnostics; namespace Azure.Core.Pipeline { @@ -18,25 +19,86 @@ namespace Azure.Core.Pipeline /// public partial class HttpClientTransport : HttpPipelineTransport, IDisposable { + /// + /// Reference-counted wrapper around HttpClient to ensure safe disposal during concurrent access. + /// + private sealed class HttpClientWrapper + { + private readonly HttpClient _client; + private volatile int _refCount = 1; // Start with 1 reference (the transport itself) + + public HttpClientWrapper(HttpClient client, bool disableRefCounting = false) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + IsRefCountingEnabled = !disableRefCounting; + } + + public HttpClient Client => _client; + + public bool IsRefCountingEnabled { get; set; } + + /// + /// Atomically increment reference count if not disposed. + /// + /// True if reference was successfully added, false if already disposed. + public bool TryAddRef() + { + int currentCount; + do + { + currentCount = _refCount; + if (currentCount == 0) return false; // Already disposed + } + while (Interlocked.CompareExchange(ref _refCount, currentCount + 1, currentCount) != currentCount); + + return true; + } + + /// + /// Atomically decrement reference count and dispose if needed. + /// + public void Release() + { + if (Interlocked.Decrement(ref _refCount) == 0) + { + _client.Dispose(); + } + } + } + /// /// A shared instance of with default parameters. /// public static readonly HttpClientTransport Shared = new HttpClientTransport(); + private volatile HttpClientWrapper _clientWrapper; + // The transport's private HttpClient is internal because it is used by tests. - internal HttpClient Client { get; } + internal HttpClient Client => _clientWrapper.Client ?? throw new ObjectDisposedException(nameof(HttpClientTransport)); + + internal Func? _clientFactory { get; } + internal Func? _handlerFactory { get; } + + /// + /// Internal property for testing: indicates whether reference counting is enabled for this transport instance. + /// + internal bool IsRefCountingEnabled => _clientWrapper?.IsRefCountingEnabled ?? false; /// /// Creates a new instance using default configuration. /// public HttpClientTransport() : this(CreateDefaultClient()) - { } + { + _clientFactory = CreateDefaultClient; + _clientWrapper.IsRefCountingEnabled = true; + } /// /// Creates a new instance using default configuration. /// /// The that to configure the behavior of the transport. - internal HttpClientTransport(HttpPipelineTransportOptions? options = null) : this(CreateDefaultClient(options)) + internal HttpClientTransport(HttpPipelineTransportOptions? options = null) + : this(_ => CreateDefaultClient(options)) { } /// @@ -45,7 +107,19 @@ internal HttpClientTransport(HttpPipelineTransportOptions? options = null) : thi /// The instance of to use. public HttpClientTransport(HttpMessageHandler messageHandler) { - Client = new HttpClient(messageHandler) ?? throw new ArgumentNullException(nameof(messageHandler)); + var client = new HttpClient(messageHandler ?? throw new ArgumentNullException(nameof(messageHandler))); + _clientWrapper = new HttpClientWrapper(client, true); + } + + /// + /// Creates a new instance of using the provided handler factory. + /// + /// The factory function to create the message handler. + public HttpClientTransport(Func handlerFactory) + : this(handlerFactory.Invoke(new HttpPipelineTransportOptions())) + { + _handlerFactory = handlerFactory; + _clientWrapper.IsRefCountingEnabled = true; } /// @@ -54,7 +128,55 @@ public HttpClientTransport(HttpMessageHandler messageHandler) /// The instance of to use. public HttpClientTransport(HttpClient client) { - Client = client ?? throw new ArgumentNullException(nameof(client)); + _clientWrapper = new HttpClientWrapper(client ?? throw new ArgumentNullException(nameof(client)), true); + } + + /// + /// Creates a new instance of using the provided factory. + /// + /// The factory function to create the client. + public HttpClientTransport(Func clientFactory) : this(clientFactory.Invoke(new HttpPipelineTransportOptions())) + { + _clientFactory = clientFactory; + _clientWrapper.IsRefCountingEnabled = true; + } + + /// + public override void Update(HttpPipelineTransportOptions options) + { + if (this == Shared) + { + throw new InvalidOperationException("Cannot update the shared HttpClientTransport instance."); + } + + HttpClient? newClient = null; + if (_clientFactory != null && _clientFactory != CreateDefaultClient) + { + newClient = _clientFactory(options); + } + else if (_handlerFactory != null) + { + var handler = _handlerFactory(options); + newClient = new HttpClient(handler); + } + else + { + // No factory to create a new client, so we cannot update. + if (_clientFactory != CreateDefaultClient) + { + AzureCoreEventSource.Singleton.FailedToUpdateTransport("No factory available to create a new HttpClient instance."); + } else + { + AzureCoreEventSource.Singleton.FailedToUpdateTransport("Skipping transport update because no custom factory is available to create a new HttpClient instance."); + } + return; + } + + var newWrapper = new HttpClientWrapper(newClient); + var oldWrapper = Interlocked.Exchange(ref _clientWrapper!, newWrapper); + + // Release the transport's reference to the old client + oldWrapper?.Release(); } /// @@ -86,8 +208,17 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async) HttpResponseMessage responseMessage; Stream? contentStream = null; message.ClearResponse(); + + // Get reference-counted access to the client + var clientWrapper = _clientWrapper; + if (clientWrapper.IsRefCountingEnabled && !clientWrapper.TryAddRef()) + { + throw new ObjectDisposedException(nameof(HttpClientTransport)); + } + try { + var localClient = clientWrapper.Client; #if NET5_0_OR_GREATER if (!async) { @@ -95,14 +226,14 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async) // HttpClient.Send would throw a NotSupported exception instead of GetAwaiter().GetResult() // throwing a System.Threading.SynchronizationLockException: Cannot wait on monitors on this runtime. #pragma warning disable CA1416 // 'HttpClient.Send(HttpRequestMessage, HttpCompletionOption, CancellationToken)' is unsupported on 'browser' - responseMessage = Client.Send(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken); + responseMessage = localClient.Send(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken); #pragma warning restore CA1416 } else #endif { #pragma warning disable AZC0110 // DO NOT use await keyword in possibly synchronous scope. - responseMessage = await Client.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken) + responseMessage = await localClient.SendAsync(httpRequest, HttpCompletionOption.ResponseHeadersRead, message.CancellationToken) #pragma warning restore AZC0110 // DO NOT use await keyword in possibly synchronous scope. .ConfigureAwait(false); } @@ -134,6 +265,14 @@ private async ValueTask ProcessSyncOrAsync(HttpMessage message, bool async) { throw new RequestFailedException(e.Message, e); } + finally + { + if (clientWrapper.IsRefCountingEnabled) + { + // Release the reference to the client wrapper + clientWrapper.Release(); + } + } message.Response = new HttpClientTransportResponse(message.Request.ClientRequestId, responseMessage, contentStream); } @@ -253,7 +392,7 @@ public void Dispose() { if (this != Shared) { - Client.Dispose(); + _clientWrapper?.Release(); } GC.SuppressFinalize(this); diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs index e0c42f79fd5a..acebf2fcbf9a 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipeline.cs @@ -58,6 +58,16 @@ public HttpPipeline(HttpPipelineTransport transport, HttpPipelinePolicy[]? polic ClientDiagnostics.CreateMessageSanitizer(new DiagnosticsOptions())); policies.CopyTo(all, 0); + for (int i = 0; i < policies.Length; i++) + { + // If the policy implements ISupportsTransportCertificateUpdate, we need to subscribe to its TransportUpdated event + if (policies[i] is ISupportsTransportUpdate transportUpdated) + { + transportUpdated.TransportOptionsChanged += options => _transport.Update(options); + break; + } + } + _pipeline = all; } @@ -78,6 +88,16 @@ internal HttpPipeline( _perCallIndex = perCallIndex; _perRetryIndex = perRetryIndex; _internallyConstructed = true; + + for (int i = 0; i < pipeline.Length; i++) + { + // If the policy implements ISupportsTransportCertificateUpdate, we need to subscribe to its TransportUpdated event + if (pipeline[i] is ISupportsTransportUpdate transportUpdated) + { + transportUpdated.TransportOptionsChanged += options => _transport.Update(options); + break; + } + } } /// diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs index a9daa7f45007..99c8dd456026 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransport.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -30,6 +32,16 @@ public abstract class HttpPipelineTransport /// public abstract Request CreateRequest(); + /// + /// Updates the transport with the provided . + /// + /// The options to use for updating the transport. + + public virtual void Update(HttpPipelineTransportOptions options) + { + throw new NotSupportedException("This transport does not support updating options."); + } + /// /// Creates the default based on the current environment and configuration. /// diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs index 3d16d4171f6f..ad558d5fd925 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpPipelineTransportOptions.cs @@ -34,12 +34,33 @@ public HttpPipelineTransportOptions() /// public IList ClientCertificates { get; } - /// + /// /// Gets or sets a value that indicates whether the redirect policy should follow redirection responses. /// /// /// true if the redirect policy should follow redirection responses; otherwise false. The default value is false. /// public bool IsClientRedirectEnabled { get; set; } + + /// + /// Creates a clone of the current instance with the same settings. + /// + /// + /// + /// A new instance with the same settings as the current instance. + /// + internal HttpPipelineTransportOptions Clone() + { + var clone = new HttpPipelineTransportOptions + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidationCallback, + IsClientRedirectEnabled = IsClientRedirectEnabled, + }; + foreach (X509Certificate2 certificate in ClientCertificates) + { + clone.ClientCertificates.Add(certificate); + } + return clone; + } } } diff --git a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs index 3b69d4529cfe..3f66f9cbd84f 100644 --- a/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs +++ b/sdk/core/Azure.Core/src/Pipeline/HttpWebRequestTransport.cs @@ -3,6 +3,7 @@ using System; using System.Net; +using System.Threading; using System.Threading.Tasks; namespace Azure.Core.Pipeline @@ -13,9 +14,10 @@ namespace Azure.Core.Pipeline /// internal partial class HttpWebRequestTransport : HttpPipelineTransport { - private readonly Action _configureRequest; + internal volatile Action _configureRequest; public static readonly HttpWebRequestTransport Shared = new HttpWebRequestTransport(); private readonly IWebProxy? _environmentProxy; + internal Func? TransportFactory { get; set; } /// /// Creates a new instance of @@ -37,6 +39,19 @@ internal HttpWebRequestTransport(Action configureRequest) } } + /// + public override void Update(HttpPipelineTransportOptions options) + { + if (this == Shared) + { + throw new InvalidOperationException("Cannot update the shared HttpWebRequestTransport instance."); + } + + Action newConfigureRequest = req => ApplyOptionsToRequest(req, options); + + Interlocked.Exchange(ref _configureRequest, newConfigureRequest); + } + /// public override void Process(HttpMessage message) { diff --git a/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs b/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs new file mode 100644 index 000000000000..19087b502d29 --- /dev/null +++ b/sdk/core/Azure.Core/src/Pipeline/Internal/ISupportsTransportUpdate.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Azure.Core.Pipeline; + +/// +/// Indicates that the transport supports updating its transport options. +/// +internal interface ISupportsTransportUpdate +{ + /// + /// Event that is triggered when the transport needs to be updated. + /// + public event Action? TransportOptionsChanged; +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs b/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs index 89e951496ae1..7aee3b5c5c71 100644 --- a/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs +++ b/sdk/core/Azure.Core/tests/HttpClientTransportFunctionalTest.cs @@ -22,7 +22,6 @@ namespace Azure.Core.Tests [NonParallelizable] public class HttpClientTransportFunctionalTest : TransportFunctionalTests { - private static RemoteCertificateValidationCallback certCallback = (_, _, _, _) => true; public HttpClientTransportFunctionalTest(bool isAsync) : base(isAsync) { } @@ -78,7 +77,7 @@ public void UsesHttpClientTransportOnNetStandart() [Test] public void DisposeDisposesClient() { - var transport = (HttpClientTransport)GetTransport(options:new HttpPipelineTransportOptions()); + var transport = (HttpClientTransport)GetTransport(options: new HttpPipelineTransportOptions()); transport.Dispose(); Assert.Throws(() => transport.Client.CancelPendingRequests()); } diff --git a/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs b/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs new file mode 100644 index 000000000000..b3b7a7d4f67a --- /dev/null +++ b/sdk/core/Azure.Core/tests/HttpClientTransportReferenceCountingTests.cs @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + [TestFixture(true)] + [TestFixture(false)] + public class HttpClientTransportReferenceCountingTests : SyncAsyncTestBase + { + public HttpClientTransportReferenceCountingTests(bool isAsync) : base(isAsync) + { + } + + [Test] + public void HttpClientConstructor_CannotBeUpdated_IndicatesNoReferenceCountingNeeded() + { + // Arrange + using var client = new HttpClient(); + + // Act + using var transport = new HttpClientTransport(client); + + // Assert - Reference counting should be disabled for direct HttpClient constructor + Assert.IsFalse(transport.IsRefCountingEnabled, + "Reference counting should be disabled when using direct HttpClient constructor"); + + // Update should not throw, but should be a no-op + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public void HttpMessageHandlerConstructor_CannotBeUpdated_IndicatesNoReferenceCountingNeeded() + { + // Arrange + using var handler = new HttpClientHandler(); + + // Act + using var transport = new HttpClientTransport(handler); + + // Assert - Reference counting should be disabled for direct HttpMessageHandler constructor + Assert.IsFalse(transport.IsRefCountingEnabled, + "Reference counting should be disabled when using direct HttpMessageHandler constructor"); + + // Update should not throw, but should be a no-op + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public void ClientFactoryConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled() + { + // Arrange + Func clientFactory = _ => new HttpClient(); + + // Act + using var transport = new HttpClientTransport(clientFactory); + + // Assert - Reference counting should be enabled and updates should work + Assert.IsTrue(transport.IsRefCountingEnabled, + "Reference counting should be enabled when using client factory constructor"); + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public void HandlerFactoryConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled() + { + // Arrange + Func handlerFactory = _ => new HttpClientHandler(); + + // Act + using var transport = new HttpClientTransport(handlerFactory); + + // Assert - Reference counting should be enabled and updates should work + Assert.IsTrue(transport.IsRefCountingEnabled, + "Reference counting should be enabled when using handler factory constructor"); + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public void DefaultConstructor_CanBeUpdated_IndicatesReferenceCountingEnabled() + { + // Act + using var transport = new HttpClientTransport(); + + // Assert - Default constructor should enable reference counting and updates via factory + Assert.IsTrue(transport.IsRefCountingEnabled, + "Reference counting should be enabled when using default constructor"); + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public async Task ProcessAsync_WithRefCountingDisabled_DoesNotCallTryAddRef() + { + // Arrange + var mockHandler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK)); + using var transport = new HttpClientTransport(mockHandler); + + var request = transport.CreateRequest(); + request.Uri.Reset(new Uri("https://example.com")); + var message = new HttpMessage(request, ResponseClassifier.Shared); + + // Act & Assert - This should not throw even though we're not managing ref counts + await ProcessSyncOrAsync(transport, message); + + Assert.AreEqual(200, message.Response.Status); + } + + [Test] + public async Task ProcessAsync_WithRefCountingEnabled_CallsTryAddRefAndRelease() + { + // Arrange + var mockHandler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK)); + Func handlerFactory = _ => mockHandler; + using var transport = new HttpClientTransport(handlerFactory); + + var request = transport.CreateRequest(); + request.Uri.Reset(new Uri("https://example.com")); + var message = new HttpMessage(request, ResponseClassifier.Shared); + + // Act + await ProcessSyncOrAsync(transport, message); + + // Assert - Should succeed with ref counting + Assert.AreEqual(200, message.Response.Status); + } + + [Test] + public async Task ConcurrentRequestsWithUpdate_RefCountingEnabled_SafeDisposal() + { + // Arrange + var requestCount = 0; + var responseCount = 0; + Func clientFactory = _ => + { + var handler = new MockHttpHandler(req => + { + Interlocked.Increment(ref requestCount); + // Simulate some processing time + Thread.Sleep(10); + Interlocked.Increment(ref responseCount); + return new HttpResponseMessage(System.Net.HttpStatusCode.OK); + }); + return new HttpClient(handler); + }; + + using var transport = new HttpClientTransport(clientFactory); + + // Act - Start multiple concurrent requests and update transport during processing + var tasks = new List(); + + // Start several requests + for (int i = 0; i < 5; i++) + { + tasks.Add(Task.Run(async () => + { + var request = transport.CreateRequest(); + request.Uri.Reset(new Uri("https://example.com")); + var message = new HttpMessage(request, ResponseClassifier.Shared); + await ProcessSyncOrAsync(transport, message); + Assert.AreEqual(200, message.Response.Status); + })); + } + + // Update transport while requests are in flight + await Task.Delay(5); // Let some requests start + transport.Update(new HttpPipelineTransportOptions()); + + // Wait for all requests to complete + await Task.WhenAll(tasks); + + // Assert - All requests should complete successfully + Assert.AreEqual(5, responseCount, "All 5 requests should have completed"); + } + + [Test] + public void Update_WithRefCountingDisabled_DoesNotThrowForDirectClientConstructor() + { + // Arrange + using var client = new HttpClient(); + using var transport = new HttpClientTransport(client); + + // Act & Assert - Update should not throw for direct client, but should be a no-op + Assert.IsFalse(transport.IsRefCountingEnabled, + "Reference counting should be disabled for direct client constructor"); + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public void Update_WithRefCountingEnabled_SucceedsWithFactory() + { + // Arrange + Func clientFactory = _ => new HttpClient(); + using var transport = new HttpClientTransport(clientFactory); + + // Act & Assert - Reference counting should be enabled and updates should succeed + Assert.IsTrue(transport.IsRefCountingEnabled, + "Reference counting should be enabled when using client factory"); + Assert.DoesNotThrow(() => transport.Update(new HttpPipelineTransportOptions())); + } + + [Test] + public async Task DisposedTransportWithRefCounting_ThrowsObjectDisposedException() + { + // Arrange + Func clientFactory = _ => new HttpClient(); + var transport = new HttpClientTransport(clientFactory); + + var request = transport.CreateRequest(); + request.Uri.Reset(new Uri("https://example.com")); + var message = new HttpMessage(request, ResponseClassifier.Shared); + + // Act - Dispose the transport + transport.Dispose(); + + // Assert - Subsequent requests should throw + var ex = await AssertThrowsAsync( + async () => await ProcessSyncOrAsync(transport, message)); + Assert.AreEqual(nameof(HttpClientTransport), ex.ObjectName); + } + + [Test] + public async Task MultipleUpdatesWithConcurrentRequests_HandlesSafeDisposal() + { + // Arrange + var updateCount = 0; + Func clientFactory = _ => + { + Interlocked.Increment(ref updateCount); + var handler = new MockHttpHandler(req => new HttpResponseMessage(System.Net.HttpStatusCode.OK)); + return new HttpClient(handler); + }; + + using var transport = new HttpClientTransport(clientFactory); + + // Act - Multiple updates and concurrent requests + var tasks = new List(); + + // Start continuous requests + for (int i = 0; i < 10; i++) + { + tasks.Add(Task.Run(async () => + { + for (int j = 0; j < 5; j++) + { + var request = transport.CreateRequest(); + request.Uri.Reset(new Uri("https://example.com")); + var message = new HttpMessage(request, ResponseClassifier.Shared); + await ProcessSyncOrAsync(transport, message); + Assert.AreEqual(200, message.Response.Status); + await Task.Delay(1); // Small delay between requests + } + })); + } + + // Perform updates while requests are running + for (int i = 0; i < 3; i++) + { + await Task.Delay(5); + transport.Update(new HttpPipelineTransportOptions()); + } + + // Wait for all requests to complete + await Task.WhenAll(tasks); + + // Assert - Should have created multiple clients due to updates + Assert.GreaterOrEqual(updateCount, 4, "Should have created multiple clients due to updates"); + } + + #region Helper Methods + + private async Task ProcessSyncOrAsync(HttpClientTransport transport, HttpMessage message) + { + if (IsAsync) + { + await transport.ProcessAsync(message); + } + else + { + transport.Process(message); + } + } + + private static async Task AssertThrowsAsync(Func action) where T : Exception + { + try + { + await action(); + Assert.Fail($"Expected exception of type {typeof(T).Name} but none was thrown"); + return null; // Never reached + } + catch (T ex) + { + return ex; + } + catch (Exception ex) + { + Assert.Fail($"Expected exception of type {typeof(T).Name} but got {ex.GetType().Name}: {ex.Message}"); + return null; // Never reached + } + } + + #endregion + + #region Mock Classes + + private class MockDisposableHttpClient : HttpClient + { + private readonly Action _onDispose; + + public MockDisposableHttpClient(Action onDispose) : base(new HttpClientHandler()) + { + _onDispose = onDispose; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _onDispose?.Invoke(); + } + base.Dispose(disposing); + } + } + + private class MockHttpHandler : HttpMessageHandler + { + private readonly Func _responseFactory; + + public MockHttpHandler(Func responseFactory) + { + _responseFactory = responseFactory; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(_responseFactory(request)); + } + +#if NET5_0_OR_GREATER + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return _responseFactory(request); + } +#endif + } + + #endregion + } +} \ No newline at end of file diff --git a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs index 4c297eefd861..319c0acea610 100644 --- a/sdk/core/Azure.Core/tests/HttpPipelineTests.cs +++ b/sdk/core/Azure.Core/tests/HttpPipelineTests.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -334,6 +335,28 @@ public async Task RequestContextDefault_IsErrorIsSet(int code, bool isError) Assert.AreEqual(isError, response.IsError); } + [Test] + public void TransportIsUpdatedWhenPolicyFiresTransportUpdatedEvent() + { + var policy = new TransportUpdatingPolicy(); + var options = new HttpPipelineTransportOptions(); + var certificate = GetTestCertificate(); + options.ClientCertificates.Add(certificate); + + var mockTransport = new MockTransport( + new MockResponse(1)); + + var _ = new HttpPipeline(mockTransport, [ + policy + ], responseClassifier: new CustomResponseClassifier()); + + Assert.That(mockTransport.TransportUpdates, Is.Empty); + + policy.UpdateTransport(options); + + Assert.AreEqual(certificate, options.ClientCertificates[0]); + } + #region Helpers public class AddHeaderPolicy : HttpPipelineSynchronousPolicy { @@ -377,6 +400,44 @@ public override bool IsErrorResponse(HttpMessage message) // How classifiers will be generated in DPG. private static ResponseClassifier _responseClassifier200204304; private static ResponseClassifier ResponseClassifier200204304 => _responseClassifier200204304 ??= new StatusCodeClassifier(stackalloc ushort[] { 200, 204, 304 }); + + public class TransportUpdatingPolicy : HttpPipelinePolicy, ISupportsTransportUpdate + { + public event Action TransportOptionsChanged; + + public void UpdateTransport(HttpPipelineTransportOptions options) + { + TransportOptionsChanged?.Invoke(options); + } + + public TransportUpdatingPolicy() + { + } + + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + ProcessNext(message, pipeline); + } + } + + private X509Certificate2 GetTestCertificate() + { + byte[] cer = Convert.FromBase64String(Pfx); + +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12(cer, null); +#else + return new X509Certificate2(cer); +#endif + } + + private const string Pfx = @" +MIIQzwIBAzCCEIUGCSqGSIb3DQEHAaCCEHYEghByMIIQbjCCBloGCSqGSIb3DQEHBqCCBkswggZHAgEAMIIGQAYJKoZIhvcNAQcBMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBC7yCMtx6oDATpCaQVf5XC5AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQa2eU6J3q3Uso5/ADdc5jpICCBdDtAYxjkhyCrCI7iKVUDTsu9qY/ZBkAZtndloT18lYUjOiV/eP5Mg6x+bxzWaN6z32ZZ/GIlqihemVuhwSBsp28tA4dkeK7md2xbCrr1WaAdX0VgUG0/3CpYmI8023o37rD3mq8I2vpB8svnkrFeu1vbDyGbTNmdym3UUIvyawLAXxzEjTKjRlYKyD+TwEzqWqYS1xEFDtG9g7OVdsbc7nqJiH84I2JqpYHwGaWKrli0R0YIOhbiMXo8MNPLP5DjH0JzJ0OkL059qQSYACWLrfNggSu7VMo/AXIorx3gWSfeUolBW1HMU4UwoOHy4VuNBv9yF108plAYAlrfYbeY+ZuBP5IWjvQh2svms7M7Eutol+O4moibN1HeKVPTob3qHX+GMW8rhr55Ii6t2JzQbR447ZCw+5FfLLN9QjWGbVuz2e1Kq/zgMvKCnMjjNJuJPWgTRTo9h02JTt/af2YwB/005p4O4q3Se3HfB1hTQv2dBWvAnUbaqMt0aplQjklWgbczhZMgj5X/t93+/jACbZSEhVgnK8iaTHGndSJnIwVSAG6eMvF+wdFNZjGghaglzfP+72PBXHndsSWYdGwzQZtFlW3b9eA9UxmMbV7VipXTC2V8/jmeAt5dY8X/mG8wZFsTaxAs2Z5cTJFP3KvoSAYegXUOHLWzA5CDug9mARzkLACFLsOo4STINsUI1BDItBJ3dbDuZ/5++zIwcr4ediG2GKeyFaw1gAOgWXi5/Qc3p9MHCB5EGTPaXPjq6qMZJJAyF2K7zlOxRjYTXLdWeK3fe6N6EvQfORGtDlEmrd0gLgOoAMfoZFNtHgpwAe7IChx6Cz+DUzIzi7dZT0OOhXK2eB92FZ4EkJjwH/Njv+AZwypqC1txrdRzI+gEnIeGEkh3OAfWu/WUMeNLUrJk87tIkGgHqPqp6/86/nZe362O8DmbX4BTiYUz2nDiIxfq95uvzLKz20cj3kp6nCUq04utvpYATN+ZogIQjHmxaZqKp7MvCx3L91id+P7gxDZxd22kEjpJnKjgDoZU6eO8MvE/Wb5cr7fGurRRhpzmZ23/JGJ2caMEaLSyV0yxiEB5jS+ikvMAvupcUHnirDGA+76GV6GuL2/2fS/ottGqtp3O2jL69onCGx3UMA5jjIRPVZseCSmERdjrSnetJTGc61VTw39xbvSewsOuYJlL+ruddL8UJzRmu5/NcIw9xq/4waX6g2QJvnfO9xZHJdAat5XG4ie5ePmYNtok+xONV8ZhWKAv3UmuLmnzcHuqN0mxBniiLsFJBSfay4/UbPhL+vXeE4Al/gyk70I07w2fIVhxG7h5MTo+juWv8OqmbnQ1p96Aq6QY5DcV9Tg9FW2coivzkX7aA5nHUGGt/GowgEDePdJjBgaXOq1XXDZpgJotYWyFqKYHvI+hvFaHrqSIlbyariG+LTInjhmJa1/WcJGoEB1ngFhX9up0tqBQdxaocxB/2jprNww9ioqbIwE9vTEBfM/WFfprHjx+beVEKJW3KCWDkyQMItD7V/ps8KvrAhm7cR9yOXfl3KOya1qCPo77TkFR3WrWFXtufCzYMW1ewj2MxNxMmkb8+/TA+FxW/NkOmlPFekfzsegjTyT4m2DJo0t3MrSJLsEsL4MOtO+8dRM7rP8HFMZmkpiiy1IYZ69n1jKl6De1Y6R5h1vhdGtMpgmwF9GGpFCeX7ErbF8duH8oiHwobtvv3GL0XBN73sFpYIaDFvsGmXGh/UZ1LKCznenteWg5UN+hgQ5jW8f/hKd92E2kyl30jXwOnJLbryN/BfP8db3+Opz55A17zxPMMKYKBHiSGp9ncbFJ/zg65bNiUXuJam/P5/6VjEii+gLqnzMLY4D94y/kEVvU/hoT1X7+tNYF42xxPcDOQs4ew5pe4caGpo9oW2yLdsXUhWWNBrxg+13/w6hXC2924YoeyeHCA6BEQSmlD3uLC7vBmHuea3eTCOwrP6MrVOm3HIwggoMBgkqhkiG9w0BBwGgggn9BIIJ+TCCCfUwggnxBgsqhkiG9w0BDAoBAqCCCbkwggm1MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBC7i0BgvYExXtovt+QPCERsAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ0lWlUzm/ZafkFQD8G75nGQSCCVCfn5F3SRe4xqHuY93dD4dzbmRUpToVa6MC6jbJwP4jcDEth1BCZnIcdDxNOrwlXBeoFudsDOjfa2mVaSi96hxJTxLCqCIauXXKyg2JPYiHQaMvT3BGDpFOe6b9MgN0nVwi8LnvN3GWTCR1bYDZnitIxkmwIfSayPSYuG0qdn9lVRrEA8qevMyPZvxxU6OZadhFTurd9+GwaqF29t9k90m15Sj0ZgR+Ox956k9xdaJ/WakbF/5YE806qfibU7mn1+5EGY2rTp8tEvE30eaUrM1sIg2L6pOx9oh4Rdm2tTN/DkamBaCPWLhPOJ8LiIRtWAOTvJ3slG+A12Xnot698R0BtixkQCE6UiCsDMU6uTb7wAUn5m/VPYDQo2y+sftOiBSve9s+1Sui+M72HXJMFVXXZAivUGapUOr19sNS06IP44OzeHNwXQL2cUfFqU0JKK6sqEFlTdQYkwu7Fr7s5XxiGIykj7C6Z592POL4hj4gFQC4Tn5vatYWaRr6oNmTnP8a1AFl6LhQBCgbiiYw4fczsG4sqi3Z+6B9024L9kwIUeQCnXyZRb8AFukTgR1o9brBR0mcpANb2w0mESysTti7cwvC+PCrfY6SyBsJLtFX5SkQLK9SBhMczlvwhfVOSsUiN4K+BEAZjCnjoXuV5xjoUHF5//q8fQkLxQsAIvwgVydkekhXoEiGJ7BQ8NbW5R86ibvgXRW5V+cAEsQZcMXMGEBvWAVGHAOCu5zpDkTFSyED9SjmWbMNW9PNrJZFFTLI4HNtwwdhOaABEvcYlP9vhZj79rfbdPu+cDoDzo+KtzpSzq10uNupUo6ODojmuq3onCiB2bFl/e29MhD9+8E0yx+Tkd4RaJSXnQqDEhpJiuBT12lAWKAjyDIGs0Cr9uD5AQR4vpIJf1/MROIr2vmSS5b5kLkkA8T9L0VJQ9FaL2ZTG+D+e0JnhO7U8JnngRktZsu3XQkbIwYP0dsRpSOcuvmAO92mmPtBQgrrPAKoMhvwl+CX1EBTNybQ5BCXF0dgO/1htyg7TtdnokBx+gCexsMASs4dcOVCUtNK9D2s8OvZGIAGvGB4mQc61gp5qpzGwsPGaImvqhdT0dMJB4V1XIxwM4wED9kfRW01Unhk8XAktL9wwM+pRHCy7+RnFVNH8+b6/6Q+ryYm0wyLmjL3nEPTggkrOHdh+NkNfgQRj9l9PWNo+BJ5uRpPNmE5z0IQkmhJVcLCdgsmlSZZmmBA3VnVUim03pImV6t978plH/HfH58pK762TN09tdb1fCUCePgg8HFqR9zWtPHrmHhax5hEFYxlYt4RBUyxcDYXTYUnbOBb6hPrsmJcQs+IMP2vhjsKdZz267srqegB5GVdBkSyn35NE2w7iDzuyFvb8UsYjqBg9WCVy7pmLJ5mv/3SerrumRda0ILiL2hwrQIrKgYEZJY8giiCvWjru2NJW3q5PbIwyM27DTCkQXPp6NN09FBXwLhi+qXeNMmzQuREE9QnTQcTGbIBfiRc7Avn0y4jz4etLqTTqJLPLX9lU0JwN1LJ5grjNdNWFNHc3NpxfOEesmIkyPkmVkSgYL4ldEicDM7ur05Fr8kH0Lzpnj0ZmeRdX4Ggrx4OKuyKJ5EWTVQSrroJqIrLxWvzq70ANI6uO3nk8hdTEkL6F9pX79mjyJuSj5cNRGtgLvliZTm4wSk/qNBEUySj1fhtWHcLb8ggurnVcmBCvM8IZCVVjW+hi20mPt3jp+Es7pyKq6DI5h/gn9gIXZAN1RWVtEakK6w4p1DqRFM4twc4VY77hUKRb03P0XPsewON1tCGIwScZ2+fo/1f5kvhlXf2Mw03O5kAIzCd11QOnLs/3NOWoV7xEQP8+pp8HkwAiiy2KUS1JDVH2Wo1pfw9wUDkdu5Si+7XzALy6aIFlHiYGs5gEWZZiSVRbetSeCeD2PJlRuaosfozxJa2eNgdERC9eOoAEKbn0iwFGcNswMsaZoSLBvHMByxoYYF++uSRJIR60hOAPxT4Qi4UVU/wkDztEM1tLve0ByIGLQAkyv7xfHzPuRhWT0MZCsLMoJW9iCZwa2DSB9Jkkotf1jHO84UCt0OBDLYMNht6+FIHw/BEmRTn22kdKpskrD+VCdrxYL5UNEt5d57vz3M++hcFE4dNEYzv0TfYl7GNRRvw6qLz3j1JIgCXoxhQ8a/DxHe91hJk3NcM8inQ1v6a0f+BxL/xFx4wNvdoDZsp6u++MRsDj+dwG51x/ysFwtDIKuhOzG0chJcLWxmqglfjpL6A6OyBu0o9O/ZmdUgfEBMkxCHuVR7Xk2ASmMoBgAVHKsd2UuPhVIX6T4ONLfhFICSTTnHLiGkeoO4BHdaGRd09caNQEc0GBIUfwEP7L7hifqZOTAK9gmJZcfCzu2qiqY7kOferQpw76GwfVW/kOV0FmXD4IQR6myXt/AkqsJgim6dswWI66iKQ2ec30ApVv855D+0i40RfsUGSbGvcwxxdG78Tc4hafBwA3F9fKYOrrDGRCMrnktGG1caJRpSWmNEv4+q15ceSLF05NEO9bqtc52g1t5mtdlDDrMu91kmvCs4X9nLao3y+klwZwIK4ttQRNWD8bWN7EUns/iM8P7E3IrXZgBIAdxCalOD3PnU65He8CeMCh/ZnmPIz5eBwRFRKj5N2S4U8XCBzme3oG5jBH74r5hCsOOZt93ulejV8hJosLW5Rq9fHZfPi6IvcKSQbHg+muNOlSRb9aSihDSy9FitPoOdX4mLXmPKC0OlcXOMVcG6RvoEtOs6yKnD+uPdOmnZkmEWnxQ/EmgDreRIieFgLfNkO52aIaWgg2nTU530O9uTWBSk3QSCKF4n/NWCWmjZiWg5O3CmUcxoKI4eRmZ58oYvAl+wdI4k+89rV1jUmmts8O3eftAHWeUiXWils2MdzWbFk0iwdsutavh8prEbGZfAM6nnRhR0L4ppN5dvSRWtjI5eyxfuODnGUskGcSAXD0zdqfJTMqg1ey/qF51vYDKwyk9uq8jSESdgA9Rc1oCBMqRfFgDHmPnlfdmIMO2bp8VrZKWNTPBTzCIf00jUlUpNNtwnMb0dpZwk3rPHfgIdMLQ8SNxkts+VqTTEmIIb2dmC1353s5AMNydrtyNAXYHSSp9hxwP8oESZdQRlUjivFusGPpIS/spcEt6JBu7aiMmSfKTElMCMGCSqGSIb3DQEJFTEWBBQLglDeakGMXukKkw1Ak5ac4o4cLzBBMDEwDQYJYIZIAWUDBAIBBQAEIJUPNvPN11c4V/Bq+3kIUB9hj37CScdpGtoQMf3htjMqBAhX75eHkXy8wgICCAA="; #endregion } diff --git a/sdk/core/Azure.Core/tests/PipelineTestBase.cs b/sdk/core/Azure.Core/tests/PipelineTestBase.cs index d75ad0518e97..9c3312c4c32a 100644 --- a/sdk/core/Azure.Core/tests/PipelineTestBase.cs +++ b/sdk/core/Azure.Core/tests/PipelineTestBase.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Azure.Core.Pipeline; @@ -63,6 +65,17 @@ protected async Task ExecuteRequest(HttpMessage message, HttpPipeline return message.Response; } + protected X509Certificate2 GetCertificate(string pfx) + { + byte[] cer = Convert.FromBase64String(pfx); + +#if NET9_0_OR_GREATER + return X509CertificateLoader.LoadPkcs12(cer, null); +#else + return new X509Certificate2(cer); +#endif + } + protected const string Pfx = @" MIIQ5gIBAzCCEKIGCSqGSIb3DQEHAaCCEJMEghCPMIIQizCCCowGCSqGSIb3DQEHAaCCCn0Eggp5 MIIKdTCCCnEGCyqGSIb3DQEMCgECoIIJfjCCCXowHAYKKoZIhvcNAQwBAzAOBAg6i6mCUbwdbAIC @@ -141,5 +154,8 @@ protected async Task ExecuteRequest(HttpMessage message, HttpPipeline SFOZJoMiZA9U17D0bxXn3LUImJn9MhBs5m3G9/A7C/M6y0PAdsnzHro/LpC17zbnJ1zOMDswHzAH BgUrDgMCGgQUwVWF1Axq6WtJTF05+H4d6s1JX64EFBqoVUD5ZqKahaRAXLZ6WX9DqGmhAgIH0AAA AAAAAAAA"; + + protected const string Pfx2 = @" +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"; } } diff --git a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs index 1864fd1c3729..154b93bb7a5a 100644 --- a/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs +++ b/sdk/core/Azure.Core/tests/TransportFunctionalTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Security; using System.Runtime.ConstrainedExecution; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; @@ -32,6 +33,8 @@ public TransportFunctionalTests(bool isAsync) : base(isAsync) { } + internal static RemoteCertificateValidationCallback certCallback = (_, _, _, _) => true; + protected abstract HttpPipelineTransport GetTransport(bool https = false, HttpPipelineTransportOptions options = null); public static object[] ContentWithLength => @@ -227,9 +230,9 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod Assert.AreEqual(expectedMethod, httpMethod); } - public static object[] HeadersWithValues => - new object[] - { + public static object[] HeadersWithValues => + new object[] + { // Name, value, is content, supports multiple new object[] { "Allow", "adcde", true, true }, new object[] { "Accept", "adcde", true, true }, @@ -256,50 +259,50 @@ public async Task CanGetAndSetMethod(RequestMethod method, string expectedMethod new object[] { "Range", "bytes=0-100", false, false }, new object[] { "Content-Length", "16", true, false }, new object[] { "Date", "Tue, 12 Nov 2019 08:00:00 GMT", false, false }, - }; + }; - [TestCaseSource(nameof(HeadersWithValues))] - public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) - { + [TestCaseSource(nameof(HeadersWithValues))] + public async Task CanGetAndAddRequestHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) + { StringValues httpHeaderValues = default; - using TestServer testServer = new TestServer( - context => - { - Assert.True(context.Request.Headers.TryGetValue(headerName, out httpHeaderValues)); - }); + using TestServer testServer = new TestServer( + context => + { + Assert.True(context.Request.Headers.TryGetValue(headerName, out httpHeaderValues)); + }); - var transport = GetTransport(); - Request request = CreateRequest(transport, testServer); + var transport = GetTransport(); + Request request = CreateRequest(transport, testServer); - request.Headers.Add(headerName, headerValue); + request.Headers.Add(headerName, headerValue); - if (contentHeader) - { - request.Content = RequestContent.Create(new byte[16]); - } + if (contentHeader) + { + request.Content = RequestContent.Create(new byte[16]); + } - Assert.True(request.Headers.TryGetValue(headerName, out var value)); - Assert.AreEqual(headerValue, value); + Assert.True(request.Headers.TryGetValue(headerName, out var value)); + Assert.AreEqual(headerValue, value); - Assert.True(request.Headers.TryGetValue(headerName.ToUpper(), out value)); - Assert.AreEqual(headerValue, value); + Assert.True(request.Headers.TryGetValue(headerName.ToUpper(), out value)); + Assert.AreEqual(headerValue, value); - CollectionAssert.AreEqual(new[] - { + CollectionAssert.AreEqual(new[] + { new HttpHeader(headerName, headerValue), }, request.Headers); - await ExecuteRequest(request, transport); + await ExecuteRequest(request, transport); - Assert.AreEqual(headerValue, string.Join(",", (string[])httpHeaderValues)); - } + Assert.AreEqual(headerValue, string.Join(",", (string[])httpHeaderValues)); + } - [TestCaseSource(nameof(HeadersWithValues))] - public async Task CanGetAndAddRequestHeadersUppercase(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) - { - await CanGetAndAddRequestHeaders(headerName.ToUpperInvariant(), headerValue, contentHeader, supportsMultiple); - } + [TestCaseSource(nameof(HeadersWithValues))] + public async Task CanGetAndAddRequestHeadersUppercase(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) + { + await CanGetAndAddRequestHeaders(headerName.ToUpperInvariant(), headerValue, contentHeader, supportsMultiple); + } [TestCaseSource(nameof(HeadersWithValues))] public void TryGetReturnsCorrectValuesWhenNotFound(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) @@ -317,7 +320,8 @@ public void TryGetReturnsCorrectValuesWhenNotFound(string headerName, string hea [TestCaseSource(nameof(HeadersWithValues))] public async Task CanAddMultipleValuesToRequestHeader(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) { - if (!supportsMultiple) return; + if (!supportsMultiple) + return; var anotherHeaderValue = headerValue + "1"; var joinedHeaderValues = headerValue + "," + anotherHeaderValue; @@ -384,7 +388,8 @@ public async Task CanGetAndSetResponseHeaders(string headerName, string headerVa [TestCaseSource(nameof(HeadersWithValues))] public async Task CanGetAndSetMultiValueResponseHeaders(string headerName, string headerValue, bool contentHeader, bool supportsMultiple) { - if (!supportsMultiple) return; + if (!supportsMultiple) + return; var anotherHeaderValue = headerValue + "1"; var joinedHeaderValues = headerValue + "," + anotherHeaderValue; @@ -523,7 +528,7 @@ public async Task CanGetAndSetContentStream() }); var stream = new MemoryStream(); - stream.SetLength(10*1024); + stream.SetLength(10 * 1024); var content = RequestContent.Create(stream); var transport = GetTransport(); @@ -537,7 +542,7 @@ public async Task CanGetAndSetContentStream() await ExecuteRequest(request, transport); CollectionAssert.AreEqual(stream.ToArray(), requestBytes); - Assert.AreEqual(10*1024, requestBytes.Length); + Assert.AreEqual(10 * 1024, requestBytes.Length); } public static object[][] RequestMethods => new[] @@ -689,13 +694,13 @@ public async Task StreamRequestContentCanBeSentMultipleTimes() await ExecuteRequest(request, transport); await ExecuteRequest(request, transport); - Assert.AreEqual(new long [] {3, 3}, requests); + Assert.AreEqual(new long[] { 3, 3 }, requests); } [Test] public async Task RequestContentIsNotDisposedOnSend() { - using TestServer testServer = new TestServer(context =>{}); + using TestServer testServer = new TestServer(context => { }); DisposeTrackingContent disposeTrackingContent = new DisposeTrackingContent(); var transport = GetTransport(); @@ -1089,14 +1094,7 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien // This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset. ServicePointManager.ServerCertificateValidationCallback = null; #endif - byte[] cer = Convert.FromBase64String(Pfx); - X509Certificate2 clientCert; - -#if NET9_0_OR_GREATER - clientCert = X509CertificateLoader.LoadPkcs12(cer, null); -#else - clientCert = new X509Certificate2(cer); -#endif + X509Certificate2 clientCert = GetCertificate(Pfx); using (TestServer testServer = new TestServer( async context => @@ -1105,6 +1103,7 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien if (setClientCertificate) { Assert.NotNull(cert); + Assert.AreEqual(clientCert, cert); } else { @@ -1130,6 +1129,130 @@ public async Task ClientCertificateIsHonored([Values(true, false)] bool setClien } } + [Test] + public async Task ClientCertificateCanBeRotatedFromEmpty() + { +#if NETFRAMEWORK // ServicePointManager is obsolete and doesn't affect HttpClient + // This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset. + ServicePointManager.ServerCertificateValidationCallback = null; +#endif + X509Certificate2 clientCert = GetCertificate(Pfx); + bool setClientCertificate = false; + bool validatedCert = false; + + using (TestServer testServer = new TestServer( + async context => + { + var cert = context.Connection.ClientCertificate; + if (setClientCertificate) + { + Assert.NotNull(cert); + Assert.AreEqual(clientCert, cert); + validatedCert = true; + } + else + { + Assert.Null(cert); + } + byte[] buffer = Encoding.UTF8.GetBytes("Hello"); + await context.Response.Body.WriteAsync(buffer, 0, buffer.Length); + }, + true)) + { + var options = new HttpPipelineTransportOptions(); + options.ServerCertificateCustomValidationCallback = args => true; + + var transport = GetTransport(true, options); + + // Initially no client certificate + Request request = transport.CreateRequest(); + request.Uri.Reset(testServer.Address); + + await ExecuteRequest(request, transport); + + // Now set the client certificate and update the transport + options.ClientCertificates.Add(clientCert); + + transport.Update(options); + setClientCertificate = true; + + request = transport.CreateRequest(); + request.Uri.Reset(testServer.Address); + + await ExecuteRequest(request, transport); + Assert.IsTrue(validatedCert, "Client certificate was not validated after transport update."); + } + } + + [Test] + public async Task ClientCertificateCanBeRotatedFromExistingCert() + { + try + { +#if NETFRAMEWORK // ServicePointManager is obsolete and doesn't affect HttpClient + // This test assumes ServicePointManager.ServerCertificateValidationCallback will be unset. + ServicePointManager.ServerCertificateValidationCallback -= certCallback; +#endif + X509Certificate2 clientCert = GetCertificate(Pfx); + X509Certificate2 anotherCert = GetCertificate(Pfx2); + bool setClientCertificate = false; + bool validatedCert = false; + + using (TestServer testServer = new TestServer( + async context => + { + var cert = context.Connection.ClientCertificate; + if (setClientCertificate) + { + Assert.NotNull(cert); + Assert.AreEqual(anotherCert, cert); + validatedCert = true; + } + else + { + Assert.NotNull(cert); + Assert.AreEqual(clientCert, cert); + validatedCert = true; + } + byte[] buffer = Encoding.UTF8.GetBytes("Hello"); + await context.Response.Body.WriteAsync(buffer, 0, buffer.Length); + }, + true)) + { + var options = new HttpPipelineTransportOptions(); + options.ServerCertificateCustomValidationCallback = args => true; + options.ClientCertificates.Add(clientCert); + + var transport = GetTransport(true, options); + + // Initially the first client certificate + Request request = transport.CreateRequest(); + request.Uri.Reset(testServer.Address); + + await ExecuteRequest(request, transport); + + // Now set the client certificate and update the transport + options.ClientCertificates.Clear(); + options.ClientCertificates.Add(anotherCert); + + transport.Update(options); + setClientCertificate = true; + + request = transport.CreateRequest(); + request.Uri.Reset(testServer.Address); + + await ExecuteRequest(request, transport); + Assert.IsTrue(validatedCert, "Client certificate was not validated after transport update."); + } + } + finally + { +#if NETFRAMEWORK + ServicePointManager.ServerCertificateValidationCallback += certCallback; +#endif + } + } + [Test] public async Task No100ContinueSentByDefault() {