Skip to content

Commit

Permalink
Use SocketsHttpHandler & Implement property access with reflection fo…
Browse files Browse the repository at this point in the history
…r resiliency (fix #99)

Change HttpClientHandler to SocketsHttpHandler to match ArchiSteamFarm upstream code
This commit addresses the issue raised in #99 by using reflection to set properties on `SocketsHttpHandler` and `HttpClient`.
This ensures that our code continues to function even if the property names are changed in a future trimmed binary.

**Changes:**

* Modified `SimpleHttpClient` constructor to use reflection-based property setting for:
    * `AutomaticDecompression`
    * `MaxConnectionsPerServer`
    * `EnableMultipleHttp2Connections`
* Added a new helper method `SetPropertyValue` for generic property access with logging.
* Updated `SetExpectContinueProperty` to use reflection as well.
* Introduced a new method `SetPropertyWithLogging` to handle potential exceptions and log warnings if property access fails.
* Updated `Directory.Build.props` to increment the version to `1.7.1.0`.

**Additional Notes:**

* Reflection can be slightly slower than direct property access. However, this approach offers greater flexibility and resilience to potential changes in the underlying libraries.

**This commit is related to issue #99.**
  • Loading branch information
maxisoft committed Oct 11, 2024
1 parent 1df9843 commit 1783e53
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 40 deletions.
96 changes: 57 additions & 39 deletions ASFFreeGames/HttpClientSimple/SimpleHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -15,32 +14,31 @@ namespace Maxisoft.ASF.HttpClientSimple;
#nullable enable

public sealed class SimpleHttpClient : IDisposable {
private readonly HttpClientHandler HttpClientHandler;
private readonly HttpMessageHandler HttpMessageHandler;
private readonly HttpClient HttpClient;

public SimpleHttpClient(IWebProxy? proxy = null, long timeout = 25_000) {
HttpClientHandler = new HttpClientHandler {
AutomaticDecompression = DecompressionMethods.All,
MaxConnectionsPerServer = 5
};
SocketsHttpHandler handler = new();

SetCheckCertificateRevocationList(HttpClientHandler, true);
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.AutomaticDecompression), DecompressionMethods.All);
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.MaxConnectionsPerServer), 5, debugLogLevel: true);
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.EnableMultipleHttp2Connections), true);

if (proxy is not null) {
HttpClientHandler.Proxy = proxy;
HttpClientHandler.UseProxy = true;
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.Proxy), proxy);
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.UseProxy), true);

if (proxy.Credentials is not null) {
HttpClientHandler.PreAuthenticate = true;
SetPropertyWithLogging(handler, nameof(SocketsHttpHandler.PreAuthenticate), true);
}
}

HttpMessageHandler = handler;
#pragma warning disable CA5399
HttpClient = new HttpClient(HttpClientHandler, false) {
DefaultRequestVersion = HttpVersion.Version30,
Timeout = TimeSpan.FromMilliseconds(timeout)
};
HttpClient = new HttpClient(handler, false);
#pragma warning restore CA5399
SetPropertyWithLogging(HttpClient, nameof(HttpClient.DefaultRequestVersion), HttpVersion.Version30);
SetPropertyWithLogging(HttpClient, nameof(HttpClient.Timeout), TimeSpan.FromMilliseconds(timeout));

SetExpectContinueProperty(HttpClient, false);

Expand Down Expand Up @@ -82,21 +80,28 @@ public async Task<HttpStreamResponse> GetStreamAsync(Uri uri, IEnumerable<KeyVal

public void Dispose() {
HttpClient.Dispose();
HttpClientHandler.Dispose();
HttpMessageHandler.Dispose();
}

# region System.MissingMethodException workaround
private static bool SetCheckCertificateRevocationList(HttpClientHandler httpClientHandler, bool value) {
# region System.MissingMethodException workarounds
private static bool SetExpectContinueProperty(HttpClient httpClient, bool value) {
try {
// Get the type of HttpClientHandler
Type httpClientHandlerType = httpClientHandler.GetType();
// Get the DefaultRequestHeaders property
PropertyInfo? defaultRequestHeadersProperty = httpClient.GetType().GetProperty(nameof(HttpClient.DefaultRequestHeaders), BindingFlags.Public | BindingFlags.Instance) ?? httpClient.GetType().GetProperty("DefaultRequestHeaders", BindingFlags.Public | BindingFlags.Instance);

// Get the property information
PropertyInfo? propertyInfo = httpClientHandlerType.GetProperty("CheckCertificateRevocationList", BindingFlags.Public | BindingFlags.Instance);
if (defaultRequestHeadersProperty == null) {
throw new InvalidOperationException("HttpClient does not have DefaultRequestHeaders property.");
}

if ((propertyInfo is not null) && propertyInfo.CanWrite) {
// Set the property value
propertyInfo.SetValue(httpClientHandler, true);
if (defaultRequestHeadersProperty.GetValue(httpClient) is not HttpRequestHeaders defaultRequestHeaders) {
throw new InvalidOperationException("DefaultRequestHeaders is null.");
}

// Get the ExpectContinue property
PropertyInfo? expectContinueProperty = defaultRequestHeaders.GetType().GetProperty(nameof(HttpRequestHeaders.ExpectContinue), BindingFlags.Public | BindingFlags.Instance) ?? defaultRequestHeaders.GetType().GetProperty("ExpectContinue", BindingFlags.Public | BindingFlags.Instance);

if ((expectContinueProperty != null) && expectContinueProperty.CanWrite) {
expectContinueProperty.SetValue(defaultRequestHeaders, value);

return true;
}
Expand All @@ -108,24 +113,17 @@ private static bool SetCheckCertificateRevocationList(HttpClientHandler httpClie
return false;
}

private static bool SetExpectContinueProperty(HttpClient httpClient, bool value) {
private static bool TrySetPropertyValue<T>(T targetObject, string propertyName, object value) where T : class {
try {
// Get the DefaultRequestHeaders property
PropertyInfo? defaultRequestHeadersProperty = httpClient.GetType().GetProperty("DefaultRequestHeaders", BindingFlags.Public | BindingFlags.Instance);

if (defaultRequestHeadersProperty == null) {
throw new InvalidOperationException("HttpClient does not have DefaultRequestHeaders property.");
}
// Get the type of the target object
Type targetType = targetObject.GetType();

if (defaultRequestHeadersProperty.GetValue(httpClient) is not HttpRequestHeaders defaultRequestHeaders) {
throw new InvalidOperationException("DefaultRequestHeaders is null.");
}

// Get the ExpectContinue property
PropertyInfo? expectContinueProperty = defaultRequestHeaders.GetType().GetProperty("ExpectContinue", BindingFlags.Public | BindingFlags.Instance);
// Get the property information
PropertyInfo? propertyInfo = targetType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);

if ((expectContinueProperty != null) && expectContinueProperty.CanWrite) {
expectContinueProperty.SetValue(defaultRequestHeaders, value);
if ((propertyInfo is not null) && propertyInfo.CanWrite) {
// Set the property value
propertyInfo.SetValue(targetObject, value);

return true;
}
Expand All @@ -136,6 +134,26 @@ private static bool SetExpectContinueProperty(HttpClient httpClient, bool value)

return false;
}

private static void SetPropertyWithLogging<T>(T targetObject, string propertyName, object value, bool debugLogLevel = false) where T : class {
try {
if (TrySetPropertyValue(targetObject, propertyName, value)) {
return;
}
}
catch (Exception) {
// ignored
}

string logMessage = $"Failed to set {targetObject.GetType().Name} property {propertyName} to {value}. Please report this issue to github.";

if (debugLogLevel) {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericDebug(logMessage);
}
else {
ArchiSteamFarm.Core.ASF.ArchiLogger.LogGenericWarning(logMessage);
}
}
#endregion
}

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PropertyGroup>
<PluginName>ASFFreeGames</PluginName>
<Version>1.7.0.0</Version>
<Version>1.7.1.0</Version>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

Expand Down

0 comments on commit 1783e53

Please sign in to comment.