// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Google.Apis.Util; using System; using System.Net.Http; namespace Google.Apis.Http { /// /// An implementation of that allows /// for the inner message handler to be injected. /// public sealed class HttpClientFromMessageHandlerFactory : IHttpClientFactory { /// /// Factory for obtaining the underlying /// of the returned by /// . /// Won't be null. /// private readonly Func _httpMessageHandlerFactory; /// /// Creates an that will use /// the given factory for creating the inner /// message handlers that will be used when creating the . /// /// /// The obtained from the factory won't be disposed /// when the created is. This allows calling code /// to control the handlers' lifetime and so they can possibly be reused. /// This may be a requirement for using System.Net.Http.IHttpMessageHandler. See /// https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests /// for information on why to use System.Net.Http.IHttpMessageHandler. /// public HttpClientFromMessageHandlerFactory(Func httpMessageHandlerFactory) => _httpMessageHandlerFactory = httpMessageHandlerFactory.ThrowIfNull(nameof(httpMessageHandlerFactory)); /// public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) { var handler = CreateHandler(args); var configurableHandler = new ConfigurableMessageHandler(handler) { ApplicationName = args.ApplicationName, GoogleApiClientHeader = args.GoogleApiClientHeader, UniverseDomain = args.UniverseDomain, }; // We always set not to dispose the inner handler, it's not created // by us so we don't control it's lifetime. var client = new ConfigurableHttpClient(configurableHandler, false); foreach (var initializer in args.Initializers) { initializer.Initialize(client); } return client; } private HttpMessageHandler CreateHandler(CreateHttpClientArgs args) { // We need to handle three situations in order to intercept uncompressed data where necessary // while using the built-in decompression where possible. // - No compression requested // - Compression requested but not supported by the injected message handler // (easy; just GzipDeflateHandler on top of an interceptor on top of HttpMessagetHandler) // - Compression requested and supported by the injected message handler // (complex: create two different handlers and decide which to use on a per-request basis) // First request a message handler that may perform decompression or not depending on whether // GZip is enabled. var effectiveOptions = args.GZipEnabled ? HttpMessageHandlerOptions.AllowDecompressionOptions : HttpMessageHandlerOptions.DisallowDecompressionOptions; var configuredMessageHandler = effectiveOptions.CheckItMatches(_httpMessageHandlerFactory(effectiveOptions)); if (!effectiveOptions.MayPerformDecompression) { // Simple: nothing will be decompressing content, because we have a configured handler that does not // perform automatic decompression, so we can just intercept the original handler. return new StreamInterceptionHandler(configuredMessageHandler.MessageHandler); } else if (configuredMessageHandler.PerformsAutomaticDecompression) { // Complex: we want to use a simple handler with no interception but with built-in decompression // for requests that wouldn't perform interception anyway, and a longer chain for interception cases. var noCompressionMessageHandler = HttpMessageHandlerOptions.DisallowDecompressionOptions.CheckItMatches( _httpMessageHandlerFactory(HttpMessageHandlerOptions.DisallowDecompressionOptions)); return new TwoWayDelegatingHandler( // Normal handler (with built-in decompression) when there's no interception. configuredMessageHandler.MessageHandler, // Alternative handler for requests that might be intercepted, and need that interception to happen // before decompression. new GzipDeflateHandler(new StreamInterceptionHandler(noCompressionMessageHandler.MessageHandler)), request => StreamInterceptionHandler.GetInterceptorProvider(request) != null); } else { // Simple: we have to create our own decompression handler anyway, so there's still just a single chain. var interceptionHandler = new StreamInterceptionHandler(configuredMessageHandler.MessageHandler); return new GzipDeflateHandler(interceptionHandler); } } /// /// Specifies the configuration options for a message handler. /// public sealed class HttpMessageHandlerOptions { internal static readonly HttpMessageHandlerOptions AllowDecompressionOptions = new HttpMessageHandlerOptions(true, false); internal static readonly HttpMessageHandlerOptions DisallowDecompressionOptions = new HttpMessageHandlerOptions(false, false); /// /// Whether the message handler built from these options /// may perform automatic decompression or not. /// If set to true, the message handler may or may not perform automatic decompression. /// If set to false, the message handler must not perform automatic decompression. /// public bool MayPerformDecompression { get; } /// /// Whether the message handler built from these options /// may handle redirects or not. Redirects that are not handled /// should bubble up the handlers chain. /// If set to true, the message handler may or may not handle redirects. /// If set to false, the message handler must not handle redirects. /// public bool MayHandleRedirects { get; } private HttpMessageHandlerOptions(bool mayPerformDecompression, bool mayHandleRedirects) { MayPerformDecompression = mayPerformDecompression; MayHandleRedirects = mayHandleRedirects; } internal ConfiguredHttpMessageHandler CheckItMatches(ConfiguredHttpMessageHandler handler) { if (handler == null) { throw new InvalidOperationException("The given HTTP message handler factory returned null."); } if (handler.MessageHandler == null) { throw new InvalidOperationException( $"The given HTTP message handler factory returned a null {nameof(ConfiguredHttpMessageHandler.MessageHandler)}."); } if (!MayPerformDecompression && handler.PerformsAutomaticDecompression) { throw new InvalidOperationException( "A handler that does not perform decompression was requested, but the HTTP message handler factory returned one that does."); } if (!MayHandleRedirects && handler.HandlesRedirects) { throw new InvalidOperationException( "A handler that does not handles redirects was requested, but the HTTP message handler factory returned one that does."); } return handler; } } /// /// Represents the already configured to be used /// when building a by the factory and /// information about the actual configuration. /// public sealed class ConfiguredHttpMessageHandler { /// /// The already configured to be used /// when building a by the factory. /// public HttpMessageHandler MessageHandler { get; } /// /// Whether is configured /// to perform automatic decompression or not. /// public bool PerformsAutomaticDecompression { get; } /// /// Whether is configured /// to handle redirects or not. /// public bool HandlesRedirects { get; } /// /// Builds a new with the given parameters. /// public ConfiguredHttpMessageHandler(HttpMessageHandler messageHandler, bool performsAutomaticDecompression, bool handlesRedirect) { MessageHandler = messageHandler; PerformsAutomaticDecompression = performsAutomaticDecompression; HandlesRedirects = handlesRedirect; } } } }