diff --git a/README.md b/README.md index 57bcebd..e2df4ca 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,16 @@ Http2Client requires the native TLS library from the original [bogdanfinn/tls-cl **Installation Instructions:** 1. Download the appropriate library file for your platform from the table above -2. Place the native library in your application's output directory, or -3. Specify the custom path using the `WithLibraryPath()` method in your code +2. Place the native library in your application's output directory, or specify a custom path +3. Initialize the library once at application startup using `Http2Client.Initialize()` **Example for Windows:** ```csharp +// Initialize once at application startup +Http2Client.Initialize("tls-client-windows-64-1.11.0.dll"); + +// Create clients as needed using var client = new HttpClientBuilder() - .WithLibraryPath("tls-client-windows-64-1.11.0.dll") .Build(); ``` @@ -56,9 +59,11 @@ using Http2Client.Builders; using Http2Client.Core.Enums; using Http2Client.Core.Request; +// Initialize native library once at application startup +Http2Client.Initialize("tls-client-windows-64-1.11.0.dll"); + // Create an Http2Client instance using the builder using var client = new HttpClientBuilder() - .WithLibraryPath("tls-client-windows-64-1.11.0.dll") .WithBrowserType(BrowserType.Chrome133) .WithUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") .WithTimeout(TimeSpan.FromSeconds(30)) @@ -118,7 +123,6 @@ using var client = new HttpClientBuilder() | Method | Description | |--------|-------------| -| `WithLibraryPath(string)` | Sets the path to the native TLS library. | | `WithBrowserType(BrowserType)` | Sets the browser fingerprint to mimic. | | `WithUserAgent(string)` | Sets the User-Agent header. | | `WithTimeout(TimeSpan)` | Sets the request timeout. | @@ -142,8 +146,10 @@ using var client = new HttpClientBuilder() ### Advanced Example ```csharp +// Initialize library once at startup +Http2Client.Initialize("tls-client-windows-64-1.11.0.dll"); + using var client = new HttpClientBuilder() - .WithLibraryPath("tls-client-windows-64-1.11.0.dll") .WithBrowserType(BrowserType.Firefox132) .WithUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)") .WithProxy("http://127.0.0.1:8888", isRotating: true) @@ -228,7 +234,6 @@ var request = new HttpRequest ```csharp using var client = new HttpClientBuilder() - .WithLibraryPath("tls-client-windows-64-1.11.0.dll") .WithBrowserType(BrowserType.Chrome133) .Build(); @@ -265,7 +270,6 @@ var response = client.Send(request); ```csharp using var client = new HttpClientBuilder() - .WithLibraryPath("tls-client-windows-64-1.11.0.dll") .WithBrowserType(BrowserType.Chrome133) .WithProxy("http://proxy.example.com:8080") .Build(); diff --git a/examples/Program.cs b/examples/Program.cs index ca54176..9073dff 100644 --- a/examples/Program.cs +++ b/examples/Program.cs @@ -8,15 +8,22 @@ internal class Program { - private const string PATH_LIB = "Native\\tls-client-windows-64-1.11.0.dll"; - private static void Main() { - //BasicGetRequest(); - //PostJsonRequest(); - //CookieHandling(); - //HeadersAndProxy(); - ErrorHandlingAndTimeouts(); + Http2Client.Http2Client.Initialize("Native\\tls-client-windows-64-1.11.0.dll"); + + try + { + BasicGetRequest(); + PostJsonRequest(); + CookieHandling(); + HeadersAndProxy(); + ErrorHandlingAndTimeouts(); + } + finally + { + Http2Client.Http2Client.Cleanup(); + } Console.ReadLine(); } @@ -27,7 +34,6 @@ private static void BasicGetRequest() using var client = new HttpClientBuilder() .WithUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36") - .WithLibraryPath(PATH_LIB) .WithRandomTlsExtensions() .Build(); @@ -50,7 +56,6 @@ private static void PostJsonRequest() using var client = new HttpClientBuilder() .WithUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36") - .WithLibraryPath(PATH_LIB) .WithHeader("Content-Type", "application/json") .WithCookies() .Build(); @@ -83,7 +88,6 @@ private static void CookieHandling() using var client = new HttpClientBuilder() .WithUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36") - .WithLibraryPath(PATH_LIB) .WithCookies(true) .Build(); @@ -118,7 +122,6 @@ private static void HeadersAndProxy() using var client = new HttpClientBuilder() .WithUserAgent("Custom-Agent/1.0") - .WithLibraryPath(PATH_LIB) .WithHeader("Accept", "application/json") .WithHeader("Accept-Language", "en-US,en;q=0.9") .WithHeader("Accept-Encoding", "gzip, deflate, br") @@ -150,7 +153,6 @@ private static void ErrorHandlingAndTimeouts() using var client = new HttpClientBuilder() .WithUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36") - .WithLibraryPath(PATH_LIB) .WithTimeout(TimeSpan.FromSeconds(10)) .WithCatchPanics(true) .WithInsecureSkipVerify(false) diff --git a/src/Builders/HttpClientBuilder.cs b/src/Builders/HttpClientBuilder.cs index 22a9b40..280c218 100644 --- a/src/Builders/HttpClientBuilder.cs +++ b/src/Builders/HttpClientBuilder.cs @@ -195,17 +195,6 @@ public HttpClientBuilder WithRandomTlsExtensions(bool enable = true) return this; } - /// - /// Sets native library path. Auto-detected if not set. - /// - public HttpClientBuilder WithLibraryPath(string libraryPath) - { - ThrowException.FileNotExists(libraryPath, nameof(libraryPath)); - - _options.LibraryPath = libraryPath; - return this; - } - /// /// Sets User-Agent header. /// diff --git a/src/Http2Client.cs b/src/Http2Client.cs index cb74c69..0972aa7 100644 --- a/src/Http2Client.cs +++ b/src/Http2Client.cs @@ -15,7 +15,6 @@ namespace Http2Client; public sealed class Http2Client : IDisposable { private readonly Http2ClientOptions _options; - private readonly NativeWrapper _wrapper; private bool _disposed; /// @@ -33,6 +32,11 @@ public sealed class Http2Client : IDisposable /// public bool IsDisposed => _disposed; + /// + /// True if native library is loaded and ready to use. + /// + public static bool IsInitialized => NativeWrapper.IsInitialized; + /// /// Create client with custom options. /// @@ -41,7 +45,6 @@ public Http2Client(Http2ClientOptions options) { _options = options ?? throw new ArgumentNullException(nameof(options)); _options.Validate(); - _wrapper = NativeWrapper.Load(_options.LibraryPath); } /// @@ -51,6 +54,23 @@ public Http2Client() : this(new Http2ClientOptions()) { } + /// + /// Initialize native library once. Call this before creating any Http2Client instances. + /// + /// Path to native TLS library + public static void Initialize(string path) + { + NativeWrapper.Initialize(path); + } + + /// + /// Cleanup native library resources. Call at application shutdown. + /// + public static void Cleanup() + { + NativeWrapper.Cleanup(); + } + /// /// Send HTTP request. Main method for making requests. /// @@ -67,7 +87,7 @@ public Http2Client() : this(new Http2ClientOptions()) try { - var responseJson = _wrapper.Request(Serializer.SerializeToBytes(prepared)); + var responseJson = NativeWrapper.Request(Serializer.SerializeToBytes(prepared)); response = Serializer.Deserialize(responseJson); return response; } @@ -80,7 +100,7 @@ public Http2Client() : this(new Http2ClientOptions()) // Clean up native memory for this response if (response != null && !string.IsNullOrEmpty(response.Id)) { - _wrapper.FreeMemory(response.Id); + NativeWrapper.FreeMemory(response.Id); } } } @@ -100,7 +120,7 @@ public Http2Client() : this(new Http2ClientOptions()) SessionId = _options.SessionId, }; - var responseJson = _wrapper.GetCookiesFromSession(Serializer.SerializeToBytes(payload)); + var responseJson = NativeWrapper.GetCookiesFromSession(Serializer.SerializeToBytes(payload)); return Serializer.Deserialize(responseJson); } @@ -122,7 +142,7 @@ public Http2Client() : this(new Http2ClientOptions()) Cookies = [.. cookies] }; - var responseJson = _wrapper.AddCookiesToSession(Serializer.SerializeToBytes(payload)); + var responseJson = NativeWrapper.AddCookiesToSession(Serializer.SerializeToBytes(payload)); return Serializer.Deserialize(responseJson); } @@ -135,7 +155,7 @@ public bool DestroySession() try { var payload = new { sessionId = _options.SessionId }; - var responseJson = _wrapper.DestroySession(Serializer.SerializeToBytes(payload)); + var responseJson = NativeWrapper.DestroySession(Serializer.SerializeToBytes(payload)); return !string.IsNullOrEmpty(responseJson); } catch @@ -153,7 +173,7 @@ public bool DestroyAllSessions() { try { - var responseJson = _wrapper.DestroyAllSessions(); + var responseJson = NativeWrapper.DestroyAllSessions(); return !string.IsNullOrEmpty(responseJson); } catch @@ -286,11 +306,9 @@ private void Dispose(bool disposing) } catch { - // If session cleanup fails, we still want to dispose the wrapper + // If session cleanup fails, we still want to continue disposal // Better to leak a session than crash during disposal } - - _wrapper?.Dispose(); } _disposed = true; diff --git a/src/Http2Client.csproj b/src/Http2Client.csproj index d24c040..7307721 100644 --- a/src/Http2Client.csproj +++ b/src/Http2Client.csproj @@ -6,7 +6,7 @@ enable AnyCPU;x64 Http2Client - 1.1.3 + 1.1.4 Rckov Rckov Http2Client diff --git a/src/Http2ClientOptions.cs b/src/Http2ClientOptions.cs index 4581af5..d4779f0 100644 --- a/src/Http2ClientOptions.cs +++ b/src/Http2ClientOptions.cs @@ -103,11 +103,6 @@ public class Http2ClientOptions /// public bool WithRandomTlsExtensionOrder { get; set; } - /// - /// Path to native TLS library. Auto-detected if not specified. - /// - public string? LibraryPath { get; set; } = GetDefaultLibraryPath(); - /// /// User-Agent header value. /// @@ -132,8 +127,6 @@ public string? UserAgent /// public void Validate() { - ThrowException.FileNotExists(LibraryPath, nameof(LibraryPath)); - if (!string.IsNullOrEmpty(ProxyUrl)) { ThrowException.IsUri(ProxyUrl, nameof(ProxyUrl)); @@ -178,7 +171,6 @@ public Http2ClientOptions Clone() WithDefaultCookieJar = WithDefaultCookieJar, WithoutCookieJar = WithoutCookieJar, WithRandomTlsExtensionOrder = WithRandomTlsExtensionOrder, - LibraryPath = LibraryPath }; // Deep copy headers so changes to the clone don't mess with the original @@ -189,14 +181,4 @@ public Http2ClientOptions Clone() return clone; } - - /// - /// Gets default native library path for current platform. - /// - private static string GetDefaultLibraryPath() - { - // .dll / .so / .dylib - var extension = PlatformSupport.GetNativeLibraryExtension(); - return PlatformSupport.GetRuntimePath($"tls-client.{extension}"); - } } \ No newline at end of file diff --git a/src/Native/NativeWrapper.cs b/src/Native/NativeWrapper.cs index acf9d58..b37bcc2 100644 --- a/src/Native/NativeWrapper.cs +++ b/src/Native/NativeWrapper.cs @@ -8,7 +8,7 @@ namespace Http2Client.Native; /// /// Wrapper for native Go TLS library. /// -internal class NativeWrapper : IDisposable +internal static class NativeWrapper { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr RequestDelegate(byte[] payload); @@ -28,46 +28,45 @@ internal class NativeWrapper : IDisposable [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr DestroyAllDelegate(); - private readonly IntPtr _libraryHandle; - private readonly RequestDelegate _requestDelegate; - private readonly FreeMemoryDelegate _freeMemoryDelegate; - private readonly GetCookiesFromSessionDelegate _getCookiesDelegate; - private readonly AddCookiesToSessionDelegate _addCookiesDelegate; - private readonly DestroySessionDelegate _destroySessionDelegate; - private readonly DestroyAllDelegate _destroyAllDelegate; + private static IntPtr _libraryHandle; + private static RequestDelegate? _requestDelegate; + private static FreeMemoryDelegate? _freeMemoryDelegate; + private static GetCookiesFromSessionDelegate? _getCookiesDelegate; + private static AddCookiesToSessionDelegate? _addCookiesDelegate; + private static DestroySessionDelegate? _destroySessionDelegate; + private static DestroyAllDelegate? _destroyAllDelegate; - /// - /// Creates wrapper from loaded library. Use Load() instead. - /// - private NativeWrapper(IntPtr libraryHandle) - { - _libraryHandle = libraryHandle; - - // Find all the functions we need in the loaded library - _requestDelegate = GetDelegate("request"); - _freeMemoryDelegate = GetDelegate("freeMemory"); - _getCookiesDelegate = GetDelegate("getCookiesFromSession"); - _addCookiesDelegate = GetDelegate("addCookiesToSession"); - _destroySessionDelegate = GetDelegate("destroySession"); - _destroyAllDelegate = GetDelegate("destroyAll"); - } + public static bool IsInitialized { get; private set; } /// - /// Loads native library and creates wrapper. Cleans up on failure. + /// Initializes native library once. Call at application startup. /// - public static NativeWrapper Load(string? path) + public static void Initialize(string? path) { + if (IsInitialized) + { + return; + } + ThrowException.FileNotExists(path); - var handleLib = NativeLoader.LoadLibrary(path); + _libraryHandle = NativeLoader.LoadLibrary(path); try { - return new NativeWrapper(handleLib); + // Find all the functions we need in the loaded library + _requestDelegate = GetDelegate("request"); + _freeMemoryDelegate = GetDelegate("freeMemory"); + _getCookiesDelegate = GetDelegate("getCookiesFromSession"); + _addCookiesDelegate = GetDelegate("addCookiesToSession"); + _destroySessionDelegate = GetDelegate("destroySession"); + _destroyAllDelegate = GetDelegate("destroyAll"); + + IsInitialized = true; } catch { - // If wrapper creation fails, clean up the loaded library - NativeLoader.FreeLibrary(handleLib); + // If initialization fails, clean up the loaded library + NativeLoader.FreeLibrary(_libraryHandle); throw; } } @@ -75,55 +74,98 @@ public static NativeWrapper Load(string? path) /// /// Sends HTTP request through native library. /// - public string Request(byte[] payload) + public static string Request(byte[] payload) { - return ExecuteFunction(() => _requestDelegate(payload)); + ThrowIsNotLoaded(); + return ExecuteFunction(() => _requestDelegate!(payload)); } /// /// Gets cookies from session for URL. /// - public string GetCookiesFromSession(byte[] payload) + public static string GetCookiesFromSession(byte[] payload) { - return ExecuteFunction(() => _getCookiesDelegate(payload)); + ThrowIsNotLoaded(); + return ExecuteFunction(() => _getCookiesDelegate!(payload)); } /// /// Adds cookies to session for automatic sending. /// - public string AddCookiesToSession(byte[] payload) + public static string AddCookiesToSession(byte[] payload) { - return ExecuteFunction(() => _addCookiesDelegate(payload)); + ThrowIsNotLoaded(); + return ExecuteFunction(() => _addCookiesDelegate!(payload)); } /// /// Destroys session and frees memory. /// - public string DestroySession(byte[] payload) + public static string DestroySession(byte[] payload) { - return ExecuteFunction(() => _destroySessionDelegate(payload)); + ThrowIsNotLoaded(); + return ExecuteFunction(() => _destroySessionDelegate!(payload)); } /// /// Destroys ALL sessions. Breaks other client instances! /// - public string DestroyAllSessions() + public static string DestroyAllSessions() { - return ExecuteFunction(() => _destroyAllDelegate()); + ThrowIsNotLoaded(); + return ExecuteFunction(() => _destroyAllDelegate!()); } /// /// Frees response memory. Called automatically. /// - public void FreeMemory(string responseId) + public static void FreeMemory(string responseId) + { + ThrowIsNotLoaded(); + _freeMemoryDelegate!(responseId); + } + + /// + /// Cleanup native library resources. Call at application shutdown. + /// + public static void Cleanup() + { + if (_libraryHandle != IntPtr.Zero) + { + NativeLoader.FreeLibrary(_libraryHandle); + } + + _libraryHandle = IntPtr.Zero; + + // Clear delegates + _requestDelegate = null; + _freeMemoryDelegate = null; + _getCookiesDelegate = null; + _addCookiesDelegate = null; + _destroySessionDelegate = null; + _destroyAllDelegate = null; + + // + IsInitialized = false; + } + + /// + /// Ensures library is initialized before use. + /// + private static void ThrowIsNotLoaded() { - _freeMemoryDelegate(responseId); + if (IsInitialized) + { + return; + } + + throw new InvalidOperationException("Native library is not initialized. Call Http2Client.Initialize(libraryPath) before creating any Http2Client instances."); } /// /// Finds function in library and creates delegate. /// - private T GetDelegate(string functionName) + private static T GetDelegate(string functionName) { var functionPtr = NativeLoader.GetProcAddress(_libraryHandle, functionName); @@ -149,9 +191,4 @@ private static string ExecuteFunction(Func nativeFunction) return Marshal.PtrToStringAnsi(resultPtr)!; } - - public void Dispose() - { - NativeLoader.FreeLibrary(_libraryHandle); - } } \ No newline at end of file diff --git a/tests/Builders/HttpClientBuilderTests.cs b/tests/Builders/HttpClientBuilderTests.cs index cc0b179..6e79bcd 100644 --- a/tests/Builders/HttpClientBuilderTests.cs +++ b/tests/Builders/HttpClientBuilderTests.cs @@ -52,7 +52,6 @@ public void WithUserAgent_Null_Throws() public void Chaining_Works() { var options = new HttpClientBuilder() - .WithLibraryPath(TestConstants.LibraryPath) .WithBrowserType(BrowserType.Firefox132) .WithTimeout(TimeSpan.FromSeconds(30)) .WithProxy("http://proxy:8080", true) diff --git a/tests/Http2ClientOptionsTests.cs b/tests/Http2ClientOptionsTests.cs index ed33495..b1484b9 100644 --- a/tests/Http2ClientOptionsTests.cs +++ b/tests/Http2ClientOptionsTests.cs @@ -8,6 +8,11 @@ namespace Http2Client.Test; public class Http2ClientOptionsTests { + static Http2ClientOptionsTests() + { + Http2Client.Initialize(TestConstants.LibraryPath); + } + [Fact] public void SetsDefaults() { @@ -51,7 +56,6 @@ public void Validate_Cookies_Throws() { var options = new Http2ClientOptions { - LibraryPath = TestConstants.LibraryPath, WithDefaultCookieJar = true, WithoutCookieJar = true }; @@ -67,7 +71,6 @@ public void Validate_IP_Throws() { var options = new Http2ClientOptions { - LibraryPath = TestConstants.LibraryPath, DisableIPv4 = true, DisableIPv6 = true }; @@ -83,7 +86,6 @@ public void Validate_Timeout_Throws() { var options = new Http2ClientOptions { - LibraryPath = TestConstants.LibraryPath, Timeout = TimeSpan.Zero }; @@ -95,20 +97,12 @@ public void Validate_ProxyUrl_Throws() { var options = new Http2ClientOptions { - LibraryPath = TestConstants.LibraryPath, ProxyUrl = "invalid-url" }; options.Invoking(o => o.Validate()).Should().Throw(); } - [Fact] - public void Validate_LibraryPath_Throws() - { - var options = new Http2ClientOptions { LibraryPath = "nonexistent.dll" }; - options.Invoking(o => o.Validate()).Should().Throw(); - } - [Fact] public void Clone_Works() { diff --git a/tests/Http2ClientTests.cs b/tests/Http2ClientTests.cs index 03c99a7..2f182ec 100644 --- a/tests/Http2ClientTests.cs +++ b/tests/Http2ClientTests.cs @@ -6,6 +6,11 @@ namespace Http2Client.Test; public class Http2ClientTests { + static Http2ClientTests() + { + Http2Client.Initialize(TestConstants.LibraryPath); + } + [Fact] public void Ctor_Null_Throws() { @@ -16,7 +21,7 @@ public void Ctor_Null_Throws() [Fact] public void Ctor_ValidOptions_Works() { - var options = new Http2ClientOptions { LibraryPath = TestConstants.LibraryPath }; + var options = new Http2ClientOptions(); using var client = new Http2Client(options); @@ -28,8 +33,7 @@ public void Ctor_ValidOptions_Works() [Fact] public void Dispose_Works() { - var options = new Http2ClientOptions { LibraryPath = TestConstants.LibraryPath }; - var client = new Http2Client(options); + var client = new Http2Client(); client.Dispose(); @@ -39,8 +43,7 @@ public void Dispose_Works() [Fact] public void Send_NullRequest_Throws() { - var options = new Http2ClientOptions { LibraryPath = TestConstants.LibraryPath }; - using var client = new Http2Client(options); + using var client = new Http2Client(); var action = () => client.Send(null!); action.Should().Throw(); diff --git a/tests/Http2ClientWebTests.cs b/tests/Http2ClientWebTests.cs index 5437d75..20fac55 100644 --- a/tests/Http2ClientWebTests.cs +++ b/tests/Http2ClientWebTests.cs @@ -14,6 +14,11 @@ public class Http2ClientWebTests { private Http2Client? _client; + static Http2ClientWebTests() + { + Http2Client.Initialize(TestConstants.LibraryPath); + } + [Fact] public void TlsPeet_Works() { @@ -131,7 +136,6 @@ public void Headers_Work() private Http2Client CreateClient() { return new HttpClientBuilder() - .WithLibraryPath(TestConstants.LibraryPath) .WithBrowserType(BrowserType.Chrome133) .WithUserAgent("Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36") .WithCookies()