Skip to content

Commit

Permalink
Merge pull request #7 from matteocontrini/next
Browse files Browse the repository at this point in the history
v2.0.0 release
  • Loading branch information
matteocontrini committed Jan 24, 2024
2 parents 7d3ded6 + e1375ba commit c0b8b90
Show file tree
Hide file tree
Showing 19 changed files with 1,036 additions and 611 deletions.
23 changes: 0 additions & 23 deletions PlainHttp/AsyncExtensionMethods.cs

This file was deleted.

10 changes: 0 additions & 10 deletions PlainHttp/ContentType.cs

This file was deleted.

245 changes: 153 additions & 92 deletions PlainHttp/HttpClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,117 +1,178 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Net.Security;
using System.Security.Authentication;

namespace PlainHttp
namespace PlainHttp;

/// <summary>
/// Factory that creates and caches HttpClient.
/// Supports both proxied and non-proxied clients.
/// A similar concept is better explained here by @matteocontrini:
/// https://stackoverflow.com/a/52708837/1633924
/// </summary>
public class HttpClientFactory : IHttpClientFactory
{
/// <summary>
/// Factory that creates and caches HttpClient.
/// Supports both proxied and non-proxied clients.
/// A similar concept is better explained here by @matteocontrini:
/// https://stackoverflow.com/a/52708837/1633924
/// </summary>
public class HttpClientFactory : IHttpClientFactory
private readonly ConcurrentDictionary<string, HttpClient> clients = new();
private readonly HttpHandlerOptions handlerOptions;
private readonly HttpHandlerOptions proxyHandlerOptions;

public record HttpHandlerOptions
{
/// <summary>
/// Cache for the clients
/// The maximum lifetime of a connection in the pool. Default: 10 minutes.
/// </summary>
private readonly ConcurrentDictionary<string, HttpClient> clients =
new ConcurrentDictionary<string, HttpClient>();
public TimeSpan PooledConnectionLifetime { get; init; } = TimeSpan.FromMinutes(10);

/// <summary>
/// Gets a cached client for the host associated to the input URL
/// The maximum idle time of a connection in the pool. If a connection is idle for more than this time, it will be closed. Default: 1 minute.
/// </summary>
/// <param name="uri"><see cref="Uri"/> used as the cache key</param>
/// <returns>A cached <see cref="HttpClient"/> instance for the host</returns>
public HttpClient GetClient(Uri uri)
{
if (uri == null)
{
throw new ArgumentNullException(nameof(uri));
}
public TimeSpan PooledConnectionIdleTimeout { get; init; } = TimeSpan.FromMinutes(1);

return PerHostClientFromCache(uri);
}
/// <summary>
/// The timeout for establishing a connection to the server. Default: infinite.
/// </summary>
public TimeSpan ConnectTimeout { get; init; } = Timeout.InfiniteTimeSpan;

/// <summary>
/// The decompression methods to use for the response body. By default, all methods (gzip, DEFLATE and Brotli) are enabled.
/// </summary>
public DecompressionMethods AutomaticDecompression { get; init; } = DecompressionMethods.All;

/// <summary>
/// The SSL/TLS protocols to use. By default, the system default is used.
/// </summary>
public SslProtocols EnabledSslProtocols { get; init; } = SslProtocols.None;

/// <summary>
/// Gets a random cached client with a proxy attached to it
/// Whether to ignore certificate validation errors. Default: false.
/// </summary>
/// <param name="proxyUri"><see cref="Uri"/> of the proxy, used as the cache key</param>
/// <returns>A cached <see cref="HttpClient"/> instance with a random proxy. Returns null if no proxies are available</returns>
public HttpClient GetProxiedClient(Uri proxyUri)
public bool IgnoreCertificateValidationErrors { get; init; }
}

public HttpClientFactory()
{
this.handlerOptions = new HttpHandlerOptions();
this.proxyHandlerOptions = new HttpHandlerOptions();
}

public HttpClientFactory(HttpHandlerOptions handlerOptions)
{
this.handlerOptions = handlerOptions;
this.proxyHandlerOptions = handlerOptions;
}

public HttpClientFactory(HttpHandlerOptions handlerOptions, HttpHandlerOptions proxyHandlerOptions)
{
this.handlerOptions = handlerOptions;
this.proxyHandlerOptions = proxyHandlerOptions;
}

/// <summary>
/// Gets a cached client for the host associated to the provided request URI.
/// </summary>
/// <param name="requestUri">Request <see cref="Uri"/> used as the cache key</param>
/// <returns>A cached <see cref="HttpClient"/> instance for the host</returns>
public HttpClient GetClient(Uri requestUri)
{
return PerHostClientFromCache(requestUri);
}

/// <summary>
/// Gets a cached client with the given proxy attached to it.
/// </summary>
/// <param name="requestUri">Request <see cref="Uri"/></param>
/// <param name="proxyUri"><see cref="Uri"/> of the proxy, used as the cache key</param>
/// <returns>A cached <see cref="HttpClient"/> instance with the proxy configured</returns>
public HttpClient GetProxiedClient(Uri requestUri, Uri proxyUri)
{
return ProxiedClientFromCache(proxyUri);
}

private HttpClient PerHostClientFromCache(Uri uri)
{
return this.clients.AddOrUpdate(
key: uri.Host,
addValueFactory: u => CreateClient(),
updateValueFactory: (u, client) => client
);
}

private HttpClient ProxiedClientFromCache(Uri proxyUri)
{
return this.clients.AddOrUpdate(
key: proxyUri.ToString(),
addValueFactory: u => CreateProxiedClient(proxyUri),
updateValueFactory: (u, client) => client
);
}

protected virtual HttpClient CreateClient()
{
HttpMessageHandler handler = new SocketsHttpHandler
{
return ProxiedClientFromCache(proxyUri);
}
PooledConnectionLifetime = this.handlerOptions.PooledConnectionLifetime,
PooledConnectionIdleTimeout = this.handlerOptions.PooledConnectionIdleTimeout,
UseCookies = false,
AutomaticDecompression = this.handlerOptions.AutomaticDecompression,
SslOptions = CreateSslOptions(
this.handlerOptions.EnabledSslProtocols,
this.handlerOptions.IgnoreCertificateValidationErrors
),
ConnectTimeout = this.handlerOptions.ConnectTimeout
};

private HttpClient PerHostClientFromCache(Uri uri)
HttpClient client = new HttpClient(handler)
{
return this.clients.AddOrUpdate(
key: uri.Host,
addValueFactory: u => {
return CreateClient();
},
updateValueFactory: (u, client) => {
return client;
}
);
}
Timeout = Timeout.InfiniteTimeSpan
};

return client;
}

private HttpClient ProxiedClientFromCache(Uri proxyUri)
protected virtual HttpClient CreateProxiedClient(Uri proxyUrl)
{
WebProxy proxy = new WebProxy(proxyUrl);

if (!string.IsNullOrEmpty(proxyUrl.UserInfo))
{
return this.clients.AddOrUpdate(
key: proxyUri.ToString(),
addValueFactory: u => {
return CreateProxiedClient(proxyUri);
},
updateValueFactory: (u, client) => {
return client;
}
);
string[] parts = proxyUrl.UserInfo.Split(':', 2);
proxy.Credentials = new NetworkCredential(parts[0], parts[1]);
}

protected virtual HttpClient CreateProxiedClient(Uri proxyUrl)
HttpMessageHandler handler = new SocketsHttpHandler
{
WebProxy proxy = new WebProxy(proxyUrl);

if (!string.IsNullOrEmpty(proxyUrl.UserInfo))
{
string[] parts = proxyUrl.UserInfo.Split(':', 2);
proxy.Credentials = new NetworkCredential(parts[0], parts[1]);
}

HttpMessageHandler handler = new SocketsHttpHandler()
{
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
UseCookies = false,
AutomaticDecompression = DecompressionMethods.All
};

HttpClient client = new HttpClient(handler)
{
Timeout = System.Threading.Timeout.InfiniteTimeSpan
};

return client;
}
Proxy = proxy,
UseProxy = true,
PooledConnectionLifetime = this.proxyHandlerOptions.PooledConnectionLifetime,
PooledConnectionIdleTimeout = this.proxyHandlerOptions.PooledConnectionIdleTimeout,
UseCookies = false,
AutomaticDecompression = this.proxyHandlerOptions.AutomaticDecompression,
SslOptions = CreateSslOptions(
this.proxyHandlerOptions.EnabledSslProtocols,
this.proxyHandlerOptions.IgnoreCertificateValidationErrors
),
ConnectTimeout = this.proxyHandlerOptions.ConnectTimeout
};

protected virtual HttpClient CreateClient()
HttpClient client = new HttpClient(handler)
{
HttpMessageHandler handler = new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10),
UseCookies = false,
AutomaticDecompression = DecompressionMethods.All
};

HttpClient client = new HttpClient(handler)
{
Timeout = System.Threading.Timeout.InfiniteTimeSpan
};

return client;
}
Timeout = Timeout.InfiniteTimeSpan
};

return client;
}

protected virtual SslClientAuthenticationOptions CreateSslOptions(
SslProtocols sslProtocols,
bool ignoreCertificateValidationErrors)
{
return new SslClientAuthenticationOptions
{
EnabledSslProtocols = sslProtocols,
RemoteCertificateValidationCallback = ignoreCertificateValidationErrors
? (sender, certificate, chain, errors) => true
: null
};
}
}

0 comments on commit c0b8b90

Please sign in to comment.