diff --git a/.gitignore b/.gitignore
index b555be3..b07ff25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -362,3 +362,22 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/src/TlsClient.Core/runtimes/win-x64/native/tls-client-latest.dll
+
+# TlsClient.NET — generated artifacts that must never be committed:
+# src/native/TlsClient.Native.{os}-{arch}/ — platform package directories
+# rendered from src/native/template/
+# by build/native-builder/prepare.js
+# build/temp/ — staging directory for native DLLs
+# downloaded by the prepare-libraries
+# pipeline before they are placed into
+# the rendered platform packages
+# local-nuget/ — output of local `dotnet pack` runs
+# used to consume the wrapper from
+# another solution as a local feed
+# /runtimes/ — ad-hoc DLL staging at the repo root
+# (e.g. for NativeLoader.GetLibraryPath()
+# default lookup during local debugging)
+/src/native/TlsClient.Native.*/
+/build/temp/
+/local-nuget/
+/runtimes/
diff --git a/build/native-builder/prepare.js b/build/native-builder/prepare.js
index 188b3e7..e4ff85f 100644
--- a/build/native-builder/prepare.js
+++ b/build/native-builder/prepare.js
@@ -5,6 +5,10 @@ const fsPromises = fs.promises;
// Define paths based on environment variables
const basePath = process.env.GITHUB_WORKSPACE;
const tlsVersion = process.env.TLS_CLIENT_VERSION?.replace('v', '') || '';
+// PACKAGE_VERSION is the NuGet package version stamped into the platform-specific
+// project file. Defaults to TLS_CLIENT_VERSION when not set so existing CI runs
+// (which only set TLS_CLIENT_VERSION) keep working unchanged.
+const packageVersion = process.env.PACKAGE_VERSION || tlsVersion;
const tlsClientRegex = new RegExp(`tls-client-(.*)-(.*)-${tlsVersion}\\.(.*)`);
// Set up paths
@@ -111,6 +115,7 @@ async function processLibraries() {
os: lib.os,
arch: lib.arch,
version: tlsVersion,
+ packageVersion: packageVersion,
ext: lib.ext,
runtimeIdentifier: lib.runtimeIdentifier,
};
diff --git a/docs/streaming.md b/docs/streaming.md
new file mode 100644
index 0000000..ece6d54
--- /dev/null
+++ b/docs/streaming.md
@@ -0,0 +1,214 @@
+# 🔀 Streaming Responses (SSE / chunked)
+
+`TlsClient.Native` can consume long-lived streaming responses — Server-Sent
+Events (`text/event-stream`), NDJSON firehoses, or any chunked HTTP/1.1 / HTTP/2
+body — without buffering the entire body in memory first.
+
+This is **native-only**. `TlsClient.Api` doesn't expose it because the
+upstream `tls-client` HTTP service doesn't forward chunked bodies in a
+streaming fashion.
+
+---
+
+## 🧱 The streaming API
+
+A streaming response is consumed through four short, synchronous calls on
+`NativeTlsClient`. Each maps 1:1 to a native entry point in the underlying
+`tls-client` library:
+
+| `NativeTlsClient` method | Native export | What it does |
+| ------------------------------------------------- | --------------- | --------------------------------------------------------------------------- |
+| `RequestStream(Request)` | `requestStream` | Issues the request and returns once **headers** are available; body stays open and is owned by the native side. |
+| `ReadStream(Guid streamId, int timeoutMs)` | `readStream` | Returns the next chunk (base64-encoded bytes), or one of `EOF` / `Timeout` / `Error`. |
+| `ReadStreamAll(Guid streamId)` | `readStreamAll` | Drains the rest of the body in one call. Returns the same `Response` shape as the non-streaming `Request()`. |
+| `CancelStream(Guid streamId)` | `cancelStream` | Closes the underlying connection and releases native resources. Idempotent — safe to call after EOF, error, or on an unknown id. |
+
+`async` variants (`RequestStreamAsync`, `ReadStreamAsync`, `ReadStreamAllAsync`,
+`CancelStreamAsync`) follow the same contract.
+
+The static `NativeTlsClient.IsStreamingSupported` reports whether the loaded
+native library exposes the four entry points. Older `tls-client` builds may
+not, in which case the streaming methods throw `EntryPointNotFoundException`
+on first use:
+
+```csharp
+if (!NativeTlsClient.IsStreamingSupported)
+ throw new NotSupportedException("Loaded native library does not expose the streaming entry points.");
+```
+
+---
+
+## 🚀 Quick Start
+
+### Server-Sent Events
+
+```csharp
+using TlsClient.Native;
+using TlsClient.Core.Models.Entities;
+using TlsClient.Core.Models.Requests;
+using System.Text;
+
+NativeTlsClient.Initialize("{PATH_TO_NATIVE_LIBRARY}");
+
+using var client = new NativeTlsClient(new TlsClientOptions(
+ TlsClientIdentifier.Chrome133,
+ "Mozilla/5.0 ... Chrome/133")
+{
+ // ⚠️ A negative duration disables the native http.Client.Timeout entirely.
+ // Required for long-lived SSE streams. TimeSpan.Zero does NOT work — it
+ // is treated as "use the 30 s default" by the native side.
+ Timeout = System.Threading.Timeout.InfiniteTimeSpan,
+});
+
+var start = client.RequestStream(new Request
+{
+ RequestUrl = "https://example.com/sse",
+ RequestMethod = HttpMethod.Get,
+ Headers = new() { ["Accept"] = "text/event-stream" },
+});
+
+try
+{
+ var sb = new StringBuilder();
+ while (true)
+ {
+ var chunk = client.ReadStream(start.StreamId, timeoutMs: 1000);
+
+ if (!string.IsNullOrEmpty(chunk.Error))
+ throw new IOException($"stream error: {chunk.Error}");
+
+ if (chunk.Timeout)
+ {
+ // No data for 1s. Check your CancellationToken here, then continue.
+ ct.ThrowIfCancellationRequested();
+ continue;
+ }
+
+ sb.Append(Encoding.UTF8.GetString(chunk.GetChunkBytes()));
+
+ // Pull complete SSE events (`\n\n`-separated) out of the buffer:
+ int sep;
+ while ((sep = sb.ToString().IndexOf("\n\n", StringComparison.Ordinal)) >= 0)
+ {
+ string ev = sb.ToString(0, sep);
+ sb.Remove(0, sep + 2);
+ ProcessEvent(ev);
+ }
+
+ if (chunk.EOF) break;
+ }
+}
+finally
+{
+ client.CancelStream(start.StreamId); // idempotent
+}
+```
+
+### "I don't know upfront if it's streaming"
+
+When the same code path may receive either an SSE response *or* a regular JSON
+body, branch on `Content-Type` from the headers `RequestStream` returns:
+
+```csharp
+var start = client.RequestStream(req);
+
+string ct = start.Headers?.GetValueOrDefault("Content-Type")?.FirstOrDefault() ?? "";
+if (ct.Contains("text/event-stream") || ct.Contains("application/x-ndjson"))
+{
+ // Streaming — loop ReadStream as in the SSE example above.
+}
+else
+{
+ // Not streaming — drain the body in one extra FFI call.
+ Response full = client.ReadStreamAll(start.StreamId);
+ Console.WriteLine(full.Body);
+}
+```
+
+The non-streaming branch costs **one extra FFI call** vs. plain `Request()` —
+negligible compared to the network round-trip and avoids the "predict ahead of
+time" problem.
+
+---
+
+## 🔁 The `timeoutMs` parameter on `ReadStream`
+
+| Value | Behaviour |
+| --------- | ------------------------------------------------------------------------------------------ |
+| `< 0` | Block until the next chunk, EOF, or error. |
+| `= 0` | Non-blocking poll — returns `Timeout=true` immediately if no chunk is buffered. |
+| `> 0` | Block up to `timeoutMs` ms, then return `Timeout=true` if no chunk arrived (heartbeat). |
+
+`Timeout=true` is **not** an error — it just means "no data yet." Use it to
+periodically check a `CancellationToken` from your read loop without blocking
+the underlying P/Invoke thread indefinitely.
+
+---
+
+## 🧩 Single-thread tunneling
+
+A common pattern with `TlsClient.Native` is to serialize every native call
+through a single dedicated thread (to keep the cgo boundary simple). The
+streaming API fits that model:
+
+* `RequestStream`, `ReadStream`, `ReadStreamAll`, and `CancelStream` are all
+ short-lived calls bounded by `timeoutMs`.
+* The Go side keeps a per-stream goroutine that pumps body bytes into a
+ buffered channel — `ReadStream` just pops the next chunk.
+* You can safely interleave streaming reads with normal `Request()` calls on
+ the same dispatcher.
+
+---
+
+## ⚙️ Tuning
+
+| Knob | Where | Effect |
+| ---- | ----- | ------ |
+| `Request.StreamOutputBlockSize` | per request | Per-Read buffer size on the Go side. Defaults to **4096 bytes**. SSE rarely benefits from making it smaller; large binary streams may benefit from `64 * 1024`. |
+| `ReadStream(streamId, timeoutMs)` | per call | How often the read loop wakes up to check cancellation. Typical: **250–1000 ms**. |
+| `Request.TimeoutMilliseconds` / `TlsClientOptions.Timeout` | per client/request | ⚠️ **Set to `Timeout.InfiniteTimeSpan` (or any negative duration) for long-lived streams.** Go's `http.Client.Timeout` covers the entire request *including* body reads, so a 30 s timeout will tear down a long-lived SSE stream. `TimeSpan.Zero` / `TimeoutMilliseconds = 0` is treated as "use the 30 s default" — only a negative value disables the deadline. |
+
+---
+
+## 📖 Reference (returned types)
+
+### `StreamStartResponse`
+Extends `Response` with one extra field: `Guid StreamId`. `Body` is always empty.
+
+### `StreamChunkResponse`
+| Field | Meaning |
+| ------------ | ------------------------------------------------------------------------- |
+| `StreamId` | Echo of the input id. |
+| `Chunk` | Base64-encoded raw bytes (decompressed). Empty on EOF / Timeout / Error. |
+| `EOF` | `true` once. Stream is closed; `StreamId` is invalid after this. |
+| `Timeout` | `true` when no data arrived within `timeoutMs`. Stream still live. |
+| `Error` | Non-empty on a fatal read error. Stream is closed. |
+| `IsTerminal` | Convenience: `EOF || !string.IsNullOrEmpty(Error)`. |
+| `GetChunkBytes()` | `Convert.FromBase64String(Chunk)` with a null-safe fallback. |
+
+`CancelStream` returns the existing `DestroyResponse` (`{ Id, Body, Success }`).
+
+---
+
+## 🚧 Limitations
+
+* **No callbacks across the FFI boundary** — the model is poll-based, by
+ design. Reverse P/Invoke into managed code from arbitrary Go goroutine
+ threads is fragile and would break the single-thread tunneling pattern.
+* **Backpressure timeout** — if you stop calling `ReadStream` for >60 s, the
+ Go-side pump goroutine cancels itself to avoid leaking goroutines. Subsequent
+ `ReadStream` calls will return `EOF=true`.
+* **One reader per stream** — concurrent `ReadStream` calls on the same
+ `StreamId` are serialized internally but produce undefined chunk ordering.
+ Don't do it.
+
+---
+
+## 🧯 Troubleshooting
+
+| Symptom | Likely cause |
+| -------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| `EntryPointNotFoundException: requestStream` | The loaded native library predates the streaming exports. Use a v1.11.2-stream or later DLL. |
+| Stream ends after exactly 30 s | The native side enforces its default 30 s deadline. Set `TlsClientOptions.Timeout = Timeout.InfiniteTimeSpan` (or `Request.TimeoutMilliseconds = -1`) to disable it. Note: `TimeSpan.Zero` / `0` is interpreted as "use the default", not "no timeout". |
+| Endless `Timeout=true` heartbeats | The remote side hasn't flushed any bytes yet. Verify the endpoint actually streams. |
+| `Error: unknown streamId` | You called `ReadStream` after `EOF` / `Error` / `CancelStream`. Stop the loop on those. |
diff --git a/readme.md b/readme.md
index 6ec5f59..6e96646 100644
--- a/readme.md
+++ b/readme.md
@@ -113,6 +113,17 @@ Console.WriteLine(res.Status);
---
+## 🔀 Streaming Responses
+
+`TlsClient.Native` supports consuming streaming responses (Server-Sent Events,
+NDJSON, chunked bodies) without buffering the whole body — through a
+`RequestStream` / `ReadStream` / `CancelStream` API on `NativeTlsClient`.
+
+See [docs/streaming.md](./docs/streaming.md) for the full guide, including SSE
+examples, `Content-Type`-based branching, tuning knobs, and limitations.
+
+---
+
## 🧯 Support & Issues
* Wrapper/packaging issues → open an issue **here**.
diff --git a/src/Providers/TlsClient.Provider.HttpClient/README.md b/src/Providers/TlsClient.Provider.HttpClient/README.md
index 74da184..e203a43 100644
--- a/src/Providers/TlsClient.Provider.HttpClient/README.md
+++ b/src/Providers/TlsClient.Provider.HttpClient/README.md
@@ -31,7 +31,7 @@ TlsClient.Initialize("{LIBRARY_PATH}");
// create a TlsClient instance
var tlsClient = new TlsClientBuilder()
- .WithIdentifier(TlsClientIdentifier.Chrome132)
+ .WithIdentifier(TlsClientIdentifier.Chrome133)
.WithUserAgent("TestClient 1.0")
.Build();
diff --git a/src/TlsClient.Api/ApiTlsClient.cs b/src/TlsClient.Api/ApiTlsClient.cs
index 271a39b..9f25e4a 100644
--- a/src/TlsClient.Api/ApiTlsClient.cs
+++ b/src/TlsClient.Api/ApiTlsClient.cs
@@ -27,7 +27,7 @@ public ApiTlsClient(ApiTlsClientOptions options) : base(options) {
HttpClient.DefaultRequestHeaders.Add("x-api-key", options.ApiKey);
}
- public ApiTlsClient(Uri apiBaseUri, string apiKey) : this(new ApiTlsClientOptions(TlsClientIdentifier.Chrome133, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0", apiBaseUri, apiKey)){ }
+ public ApiTlsClient(Uri apiBaseUri, string apiKey) : this(new ApiTlsClientOptions(TlsClientIdentifier.Chrome133, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0", apiBaseUri, apiKey)){ }
#region Sync Methods
public override Response Request(Request request) => AsyncHelpers.RunSync(() => RequestAsync(request, CancellationToken.None));
diff --git a/src/TlsClient.Api/Models/Entities/ApiTlsClientOptions.cs b/src/TlsClient.Api/Models/Entities/ApiTlsClientOptions.cs
index 8dbf071..bc39979 100644
--- a/src/TlsClient.Api/Models/Entities/ApiTlsClientOptions.cs
+++ b/src/TlsClient.Api/Models/Entities/ApiTlsClientOptions.cs
@@ -8,7 +8,7 @@ public class ApiTlsClientOptions : TlsClientOptions
public Uri ApiBaseUri { get; internal set; }
public string ApiKey { get; internal set; }
- public ApiTlsClientOptions(Uri apiBaseUri, string apiKey) : base(TlsClientIdentifier.Chrome133, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0")
+ public ApiTlsClientOptions(Uri apiBaseUri, string apiKey) : base(TlsClientIdentifier.Chrome133, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0")
{
ApiBaseUri = ValidateBaseUri(apiBaseUri, nameof(apiBaseUri));
ApiKey = ValidateApiKey(apiKey, nameof(apiKey));
diff --git a/src/TlsClient.Api/README.md b/src/TlsClient.Api/README.md
index 7ad3ef9..4f2b05d 100644
--- a/src/TlsClient.Api/README.md
+++ b/src/TlsClient.Api/README.md
@@ -44,7 +44,7 @@ using TlsClient.Core.Models.Requests;
var options = new ApiTlsClientOptions(
TlsClientIdentifier.Chrome133,
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0",
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0",
new Uri("http://127.0.0.1:8080"),
"my-auth-key-1"
);
diff --git a/src/TlsClient.Core/BaseTlsClient.cs b/src/TlsClient.Core/BaseTlsClient.cs
index 1692602..df8bbbf 100644
--- a/src/TlsClient.Core/BaseTlsClient.cs
+++ b/src/TlsClient.Core/BaseTlsClient.cs
@@ -19,7 +19,7 @@ protected BaseTlsClient(TlsClientOptions options)
Options = options ?? throw new ArgumentNullException(nameof(options));
}
- protected BaseTlsClient(): this(new TlsClientOptions(TlsClientIdentifier.Chrome132, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0")){}
+ protected BaseTlsClient(): this(new TlsClientOptions(TlsClientIdentifier.Chrome133, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0")){}
public abstract Response Request(Request request);
public abstract GetCookiesFromSessionResponse GetCookies(string url);
diff --git a/src/TlsClient.Core/Models/Entities/TlsClientIdentifier.cs b/src/TlsClient.Core/Models/Entities/TlsClientIdentifier.cs
index c43444d..a242a9c 100644
--- a/src/TlsClient.Core/Models/Entities/TlsClientIdentifier.cs
+++ b/src/TlsClient.Core/Models/Entities/TlsClientIdentifier.cs
@@ -21,12 +21,18 @@ public sealed class TlsClientIdentifier
public static readonly TlsClientIdentifier Chrome124 = new TlsClientIdentifier("chrome_124");
public static readonly TlsClientIdentifier Chrome130Psk = new TlsClientIdentifier("chrome_130_PSK");
public static readonly TlsClientIdentifier Chrome131 = new TlsClientIdentifier("chrome_131");
- public static readonly TlsClientIdentifier Chrome132 = new TlsClientIdentifier("chrome_132");
public static readonly TlsClientIdentifier Chrome131Psk = new TlsClientIdentifier("chrome_131_PSK");
public static readonly TlsClientIdentifier Chrome133 = new TlsClientIdentifier("chrome_133");
public static readonly TlsClientIdentifier Chrome133Psk = new TlsClientIdentifier("chrome_133_PSK");
public static readonly TlsClientIdentifier Chrome144 = new TlsClientIdentifier("chrome_144");
public static readonly TlsClientIdentifier Chrome144Psk = new TlsClientIdentifier("chrome_144_PSK");
+ public static readonly TlsClientIdentifier Chrome146 = new TlsClientIdentifier("chrome_146");
+ public static readonly TlsClientIdentifier Chrome146Psk = new TlsClientIdentifier("chrome_146_PSK");
+ #endregion
+
+ #region Brave Profiles
+ public static readonly TlsClientIdentifier Brave146 = new TlsClientIdentifier("brave_146");
+ public static readonly TlsClientIdentifier Brave146Psk = new TlsClientIdentifier("brave_146_PSK");
#endregion
#region Safari Profiles
@@ -39,6 +45,7 @@ public sealed class TlsClientIdentifier
public static readonly TlsClientIdentifier SafariIos170 = new TlsClientIdentifier("safari_ios_17_0");
public static readonly TlsClientIdentifier SafariIos180 = new TlsClientIdentifier("safari_ios_18_0");
public static readonly TlsClientIdentifier SafariIos185 = new TlsClientIdentifier("safari_ios_18_5");
+ public static readonly TlsClientIdentifier SafariIos260 = new TlsClientIdentifier("safari_ios_26_0");
#endregion
#region Firefox Profiles
@@ -57,6 +64,7 @@ public sealed class TlsClientIdentifier
public static readonly TlsClientIdentifier Firefox146Psk = new TlsClientIdentifier("firefox_146_PSK");
public static readonly TlsClientIdentifier Firefox147 = new TlsClientIdentifier("firefox_147");
public static readonly TlsClientIdentifier Firefox147Psk = new TlsClientIdentifier("firefox_147_PSK");
+ public static readonly TlsClientIdentifier Firefox148 = new TlsClientIdentifier("firefox_148");
#endregion
#region Opera Profiles
diff --git a/src/TlsClient.Core/Models/Entities/TlsClientOptions.cs b/src/TlsClient.Core/Models/Entities/TlsClientOptions.cs
index e2cdd06..31b2824 100644
--- a/src/TlsClient.Core/Models/Entities/TlsClientOptions.cs
+++ b/src/TlsClient.Core/Models/Entities/TlsClientOptions.cs
@@ -12,6 +12,16 @@ public class TlsClientOptions
public TlsClientIdentifier? TlsClientIdentifier { get; set; } = null;
public string? ProxyURL { get; set; }
public bool IsRotatingProxy { get; set; } = false;
+ ///
+ /// Per-request timeout applied to the whole HTTP exchange — including
+ /// body reads — by the underlying native client. Forwarded to the
+ /// native side as .
+ ///
+ /// - Default: 60 s.
+ /// - — interpreted by the native side as "use the 30 s default", NOT "no timeout".
+ /// - (or any negative duration) — disables the deadline. Required for long-lived SSE / NDJSON / streaming responses, otherwise the connection will be torn down at the deadline.
+ ///
+ ///
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(60);
public string? ServerNameOverwrite { get; set; }
public bool FollowRedirects { get; set; } = false;
diff --git a/src/TlsClient.Core/Models/Requests/CancelStreamRequest.cs b/src/TlsClient.Core/Models/Requests/CancelStreamRequest.cs
new file mode 100644
index 0000000..46e9f1e
--- /dev/null
+++ b/src/TlsClient.Core/Models/Requests/CancelStreamRequest.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace TlsClient.Core.Models.Requests
+{
+ // Reference: https://github.com/bogdanfinn/tls-client/blob/master/cffi_src/types.go (CancelStreamInput)
+ public class CancelStreamRequest
+ {
+ public Guid StreamId { get; set; }
+ }
+}
diff --git a/src/TlsClient.Core/Models/Requests/ReadStreamAllRequest.cs b/src/TlsClient.Core/Models/Requests/ReadStreamAllRequest.cs
new file mode 100644
index 0000000..1c155e9
--- /dev/null
+++ b/src/TlsClient.Core/Models/Requests/ReadStreamAllRequest.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace TlsClient.Core.Models.Requests
+{
+ // Reference: https://github.com/bogdanfinn/tls-client/blob/master/cffi_src/types.go (ReadStreamAllInput)
+ public class ReadStreamAllRequest
+ {
+ public Guid StreamId { get; set; }
+ }
+}
diff --git a/src/TlsClient.Core/Models/Requests/ReadStreamRequest.cs b/src/TlsClient.Core/Models/Requests/ReadStreamRequest.cs
new file mode 100644
index 0000000..4a439cd
--- /dev/null
+++ b/src/TlsClient.Core/Models/Requests/ReadStreamRequest.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace TlsClient.Core.Models.Requests
+{
+ // Reference: https://github.com/bogdanfinn/tls-client/blob/master/cffi_src/types.go (ReadStreamInput)
+ public class ReadStreamRequest
+ {
+ public Guid StreamId { get; set; }
+
+ ///
+ /// How long the native side should block waiting for the next chunk:
+ ///
+ /// - < 0: block until the next chunk, EOF, or error.
+ /// - = 0: non-blocking poll. Returns Timeout=true immediately when no chunk is buffered.
+ /// - > 0: block up to TimeoutMs. Returns Timeout=true if no chunk arrived in time.
+ ///
+ ///
+ public int TimeoutMs { get; set; }
+ }
+}
diff --git a/src/TlsClient.Core/Models/Requests/Request.cs b/src/TlsClient.Core/Models/Requests/Request.cs
index dfd8ef2..34844c2 100644
--- a/src/TlsClient.Core/Models/Requests/Request.cs
+++ b/src/TlsClient.Core/Models/Requests/Request.cs
@@ -26,15 +26,66 @@ public class Request
public string? RequestBody { get; set; } = null;
public string? RequestHostOverride { get; set; } = null;
public Guid? SessionId { get; set; }
+
+ ///
+ /// Per-Read buffer size used by the native side when writing to
+ /// or when chunking the body for the
+ /// streaming RequestStream / ReadStream API. Defaults to
+ /// 1024 bytes for stream-to-file and 4096 bytes for the streaming API.
+ ///
public int? StreamOutputBlockSize { get; set; } = null;
+
+ ///
+ /// Optional sentinel string appended to
+ /// after the body has been fully written, so consumers tailing the file
+ /// can detect completion without polling for size. Ignored when
+ /// is null.
+ ///
public string? StreamOutputEOFSymbol { get; set; } = null;
+
+ ///
+ /// When set, the response body is streamed to this file path on the
+ /// native side instead of being returned in
+ /// ;
+ /// is empty when this is non-null.
+ /// Bytes are written as the body arrives (after any automatic
+ /// decompression by the underlying HTTP transport), preserving the
+ /// original byte stream — binary content such as images is byte-exact.
+ ///
+ ///
+ /// The file is opened with O_APPEND | O_WRONLY | O_CREATE; if
+ /// the target already exists the new bytes are appended to it. The
+ /// caller is responsible for truncating or removing a stale file
+ /// before the request. This is independent of the streaming
+ /// RequestStream / ReadStream API on
+ /// NativeTlsClient; both can produce a file, but only the
+ /// streaming API hands chunks back to the .NET caller incrementally.
+ ///
public string? StreamOutputPath { get; set; } = null;
[JsonConverter(typeof(JsonStringConverter))]
public HttpMethod RequestMethod { get; set; } = HttpMethod.Get;
public string RequestUrl { get; set; } = string.Empty;
public List? HeaderOrder { get; set; } = new List();
public List? RequestCookies { get; set; } = null;
+ ///
+ /// Per-request timeout in milliseconds. Takes precedence over
+ /// when both are set. Semantics:
+ ///
+ /// - null or 0 — fall through to the native default (30 s).
+ /// - > 0 — explicit deadline applied to the whole request, including body reads.
+ /// - < 0 — disables the deadline entirely. Required for long-lived SSE / streaming responses.
+ ///
+ /// When forwarded from ,
+ /// serializes to -1 here.
+ ///
public int? TimeoutMilliseconds { get; set; } = null;
+
+ ///
+ /// Per-request timeout in seconds. Yields to
+ /// when both are non-zero. Same three-way semantics as
+ /// : null/0 = native default, positive = deadline,
+ /// negative = disabled.
+ ///
public int? TimeoutSeconds { get; set; } = null;
public bool? CatchPanics { get; set; } = null;
public bool? FollowRedirects { get; set; } = null;
diff --git a/src/TlsClient.Core/Models/Responses/StreamChunkResponse.cs b/src/TlsClient.Core/Models/Responses/StreamChunkResponse.cs
new file mode 100644
index 0000000..848e5a7
--- /dev/null
+++ b/src/TlsClient.Core/Models/Responses/StreamChunkResponse.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace TlsClient.Core.Models.Responses
+{
+ // Reference: https://github.com/bogdanfinn/tls-client/blob/master/cffi_src/types.go (StreamChunkResponse)
+ ///
+ /// Returned by readStream. Exactly one of , ,
+ /// , or a non-empty indicates the meaningful state:
+ ///
+ /// - (base64) holds the next slice of decompressed body bytes.
+ /// - = true means the stream completed naturally; the
+ /// is invalid after this call.
+ /// - = true means no data was available within the
+ /// caller-provided timeout; the stream is still live and the caller may retry.
+ /// - holds a non-empty message when the underlying read failed;
+ /// the is invalid after this call.
+ ///
+ ///
+ public class StreamChunkResponse : BaseResponse
+ {
+ public Guid StreamId { get; set; }
+
+ /// Base64-encoded raw bytes. Empty when , , or
+ /// is set. Use for the decoded payload.
+ public string? Chunk { get; set; }
+
+ [JsonPropertyName("eof")]
+ public bool EOF { get; set; }
+
+ public bool Timeout { get; set; }
+
+ public string? Error { get; set; }
+
+ /// True when the chunk carries no payload (EOF/Timeout/Error or empty data).
+ [JsonIgnore]
+ public bool IsTerminal => EOF || !string.IsNullOrEmpty(Error);
+
+ /// Decode the base64 into raw bytes. Returns an empty array if
+ /// the chunk is missing.
+ public byte[] GetChunkBytes()
+ {
+ if (string.IsNullOrEmpty(Chunk))
+ return Array.Empty();
+ return Convert.FromBase64String(Chunk);
+ }
+ }
+}
diff --git a/src/TlsClient.Core/Models/Responses/StreamStartResponse.cs b/src/TlsClient.Core/Models/Responses/StreamStartResponse.cs
new file mode 100644
index 0000000..a4d2537
--- /dev/null
+++ b/src/TlsClient.Core/Models/Responses/StreamStartResponse.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace TlsClient.Core.Models.Responses
+{
+ // Reference: https://github.com/bogdanfinn/tls-client/blob/master/cffi_src/types.go (StreamStartResponse)
+ ///
+ /// Returned by requestStream. Carries the same fields as
+ /// (status, headers, cookies, target, used protocol) plus a
+ /// that identifies the open stream for subsequent ReadStream / ReadStreamAll /
+ /// CancelStream calls. is always empty here.
+ ///
+ public class StreamStartResponse : Response
+ {
+ public Guid StreamId { get; set; }
+ }
+}
diff --git a/src/TlsClient.Core/TlsClient.Core.csproj b/src/TlsClient.Core/TlsClient.Core.csproj
index 08ac512..90c4baf 100644
--- a/src/TlsClient.Core/TlsClient.Core.csproj
+++ b/src/TlsClient.Core/TlsClient.Core.csproj
@@ -2,6 +2,10 @@
netstandard2.1
enable
+ 1.2.0-stream.1
+ ErenKrt
+ https://github.com/ErenKrt/TlsClient.NET
+ Shared models and helpers for TlsClient.NET.
diff --git a/src/TlsClient.Native/NativeTlsClient.cs b/src/TlsClient.Native/NativeTlsClient.cs
index dcaac8c..b4435d7 100644
--- a/src/TlsClient.Native/NativeTlsClient.cs
+++ b/src/TlsClient.Native/NativeTlsClient.cs
@@ -88,5 +88,167 @@ public override DestroyResponse DestroyAll()
public override Task DestroyAllAsync(CancellationToken ct = default) => AsyncHelpers.RunAsync(() => DestroyAll(), ct);
public override Task AddCookiesAsync(string url, List cookies, CancellationToken ct = default) => AsyncHelpers.RunAsync(() => AddCookies(url, cookies), ct);
#endregion
+
+ #region Streaming
+ ///
+ /// Returns true when the loaded native library exposes the streaming exports.
+ /// Older builds may not — in which case the streaming methods on this client throw
+ /// on first use.
+ ///
+ public static bool IsStreamingSupported => TlsClientWrapper.IsStreamingSupported;
+
+ ///
+ /// Issues a request and returns once the response headers are available, without
+ /// reading the body. Use the returned
+ /// to drive subsequent / /
+ /// calls.
+ ///
+ ///
+ /// For long-lived streaming responses (server-sent events, NDJSON, etc.)
+ /// you must disable the native side's deadline by setting
+ /// to
+ /// — or by setting
+ /// /
+ /// to a negative value.
+ /// applies to the whole request including body reads, and the value 0
+ /// is interpreted as "use the 30 s default", not "no timeout".
+ ///
+ public StreamStartResponse RequestStream(Request request)
+ {
+ request = PrepareRequest(request);
+
+ StreamStartResponse response;
+
+ try
+ {
+ var payload = RequestHelpers.Prepare(request);
+ var rawResponse = TlsClientWrapper.RequestStream(payload);
+ response = rawResponse.FromJson() ?? throw new Exception("Response is null, can't convert object from json.");
+ }
+ catch (Exception err)
+ {
+ response = new StreamStartResponse
+ {
+ Body = err.Message,
+ Status = 0,
+ };
+ }
+
+ if (!string.IsNullOrEmpty(response.Id))
+ TlsClientWrapper.FreeMemory(response.Id);
+
+ return response;
+ }
+
+ ///
+ /// Polls for the next chunk of an in-flight stream. Returns immediately on data,
+ /// EOF, error, or after when no chunk is available
+ /// (heartbeat poll). Inspect ,
+ /// , and
+ /// to drive the read loop.
+ ///
+ ///
+ /// < 0: block until the next chunk, EOF, or error.
+ /// = 0: non-blocking poll.
+ /// > 0: block up to TimeoutMs, then return Timeout=true if no chunk arrived.
+ ///
+ public StreamChunkResponse ReadStream(Guid streamId, int timeoutMs = 1000)
+ {
+ StreamChunkResponse response;
+
+ try
+ {
+ var payload = RequestHelpers.Prepare(new ReadStreamRequest
+ {
+ StreamId = streamId,
+ TimeoutMs = timeoutMs,
+ });
+ var rawResponse = TlsClientWrapper.ReadStream(payload);
+ response = rawResponse.FromJson() ?? throw new Exception("Response is null, can't convert object from json.");
+ }
+ catch (Exception err)
+ {
+ response = new StreamChunkResponse
+ {
+ StreamId = streamId,
+ Error = err.Message,
+ };
+ }
+
+ if (!string.IsNullOrEmpty(response.Id))
+ TlsClientWrapper.FreeMemory(response.Id);
+
+ return response;
+ }
+
+ ///
+ /// Drains the rest of the stream's body in one call and returns it as a normal
+ /// (charset-decoded for text bodies, base64+MIME prefix for
+ /// byte responses). Use this when the response from
+ /// turns out to NOT be a streaming response (e.g. Content-Type isn't
+ /// text/event-stream) and you just want the full body in one shot.
+ /// After this returns, is invalid.
+ ///
+ public Response ReadStreamAll(Guid streamId)
+ {
+ Response response;
+
+ try
+ {
+ var payload = RequestHelpers.Prepare(new ReadStreamAllRequest { StreamId = streamId });
+ var rawResponse = TlsClientWrapper.ReadStreamAll(payload);
+ response = rawResponse.FromJson() ?? throw new Exception("Response is null, can't convert object from json.");
+ }
+ catch (Exception err)
+ {
+ response = new Response
+ {
+ Body = err.Message,
+ Status = 0,
+ };
+ }
+
+ if (!string.IsNullOrEmpty(response.Id))
+ TlsClientWrapper.FreeMemory(response.Id);
+
+ return response;
+ }
+
+ ///
+ /// Cancels an in-flight stream and releases the underlying connection. Idempotent —
+ /// safe to call after a natural EOF, after an error, or with an unknown
+ /// . Always call this in a finally block when consuming
+ /// chunks via .
+ ///
+ public DestroyResponse CancelStream(Guid streamId)
+ {
+ DestroyResponse response;
+
+ try
+ {
+ var payload = RequestHelpers.Prepare(new CancelStreamRequest { StreamId = streamId });
+ var rawResponse = TlsClientWrapper.CancelStream(payload);
+ response = rawResponse.FromJson() ?? throw new Exception("Response is null, can't convert object from json.");
+ }
+ catch (Exception err)
+ {
+ response = new DestroyResponse
+ {
+ Body = err.Message,
+ Success = false,
+ };
+ }
+
+ if (!string.IsNullOrEmpty(response.Id))
+ TlsClientWrapper.FreeMemory(response.Id);
+
+ return response;
+ }
+
+ public Task RequestStreamAsync(Request request, CancellationToken ct = default) => AsyncHelpers.RunAsync(() => RequestStream(request), ct);
+ public Task ReadStreamAsync(Guid streamId, int timeoutMs = 1000, CancellationToken ct = default) => AsyncHelpers.RunAsync(() => ReadStream(streamId, timeoutMs), ct);
+ public Task ReadStreamAllAsync(Guid streamId, CancellationToken ct = default) => AsyncHelpers.RunAsync(() => ReadStreamAll(streamId), ct);
+ public Task CancelStreamAsync(Guid streamId, CancellationToken ct = default) => AsyncHelpers.RunAsync(() => CancelStream(streamId), ct);
+ #endregion
}
}
diff --git a/src/TlsClient.Native/TlsClient.Native.csproj b/src/TlsClient.Native/TlsClient.Native.csproj
index 6ccecfe..3dd45a4 100644
--- a/src/TlsClient.Native/TlsClient.Native.csproj
+++ b/src/TlsClient.Native/TlsClient.Native.csproj
@@ -5,8 +5,8 @@
enable
TlsClient.Native
ErenKrt
+ 1.2.0-stream.1
https://github.com/ErenKrt/TlsClient.NET
- True
TlsClient.Native is a .NET Standard library that provides direct/native bindings to the TlsClient Go library.
README.md
LICENSE
diff --git a/src/TlsClient.Native/Wrappers/TlsClientWrapper.cs b/src/TlsClient.Native/Wrappers/TlsClientWrapper.cs
index c1c08ab..8ab8014 100644
--- a/src/TlsClient.Native/Wrappers/TlsClientWrapper.cs
+++ b/src/TlsClient.Native/Wrappers/TlsClientWrapper.cs
@@ -24,6 +24,19 @@ public static class TlsClientWrapper
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr DestroyAllDelegate();
+ // Streaming exports (added in tls-client v1.11.2-stream)
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate IntPtr RequestStreamDelegate(byte[] payload);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate IntPtr ReadStreamDelegate(byte[] payload);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate IntPtr ReadStreamAllDelegate(byte[] payload);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate IntPtr CancelStreamDelegate(byte[] payload);
+
private static bool _isInitialized;
private static IntPtr _module;
@@ -33,6 +46,12 @@ public static class TlsClientWrapper
private static AddCookiesToSessionDelegate _addCookiesToSessionDelegate = null!;
private static DestroySessionDelegate _destroySessionDelegate = null!;
private static DestroyAllDelegate _destroyAllDelegate = null!;
+ // Nullable: may remain null after Initialize when the loaded native library predates
+ // the streaming exports. RequireStreamingDelegate enforces non-null on first use.
+ private static RequestStreamDelegate? _requestStreamDelegate;
+ private static ReadStreamDelegate? _readStreamDelegate;
+ private static ReadStreamAllDelegate? _readStreamAllDelegate;
+ private static CancelStreamDelegate? _cancelStreamDelegate;
public static void Initialize(string? libraryPath = null)
{
@@ -52,6 +71,13 @@ public static void Initialize(string? libraryPath = null)
_destroySessionDelegate = GetDelegate("destroySession");
_destroyAllDelegate = GetDelegate("destroyAll");
+ // Streaming exports — optional. Older native libraries don't expose these,
+ // so the lookup is allowed to fail silently. The streaming wrapper methods
+ // re-check and throw a clearer error when actually invoked.
+ _requestStreamDelegate = TryGetDelegate("requestStream");
+ _readStreamDelegate = TryGetDelegate("readStream");
+ _readStreamAllDelegate = TryGetDelegate("readStreamAll");
+ _cancelStreamDelegate = TryGetDelegate("cancelStream");
_isInitialized = true;
}
@@ -72,6 +98,25 @@ private static T GetDelegate(string functionName) where T : Delegate
return Marshal.GetDelegateForFunctionPointer(functionPtr);
}
+ private static T? TryGetDelegate(string functionName) where T : Delegate
+ {
+ var functionPtr = NativeLoader.GetProcAddress(_module, functionName);
+ if (functionPtr == IntPtr.Zero)
+ return null;
+
+ return Marshal.GetDelegateForFunctionPointer(functionPtr);
+ }
+
+ private static T RequireStreamingDelegate(T? del, string functionName) where T : Delegate
+ {
+ if (del is null)
+ throw new EntryPointNotFoundException(
+ $"Native function '{functionName}' is not exported by the loaded tls-client library. " +
+ "Streaming requires a native library built with the streaming exports (requestStream, " +
+ "readStream, readStreamAll, cancelStream). Update your native library.");
+ return del;
+ }
+
private static string ExecuteNative(Func nativeCall)
{
var ptr = nativeCall();
@@ -123,6 +168,56 @@ public static string DestroyAll()
EnsureInitialized();
return ExecuteNative(() => _destroyAllDelegate());
}
+
+ public static string RequestStream(byte[] payload)
+ {
+ EnsureInitialized();
+ if (payload is null) throw new ArgumentNullException(nameof(payload));
+ var del = RequireStreamingDelegate(_requestStreamDelegate, "requestStream");
+ return ExecuteNative(() => del(payload));
+ }
+
+ public static string ReadStream(byte[] payload)
+ {
+ EnsureInitialized();
+ if (payload is null) throw new ArgumentNullException(nameof(payload));
+ var del = RequireStreamingDelegate(_readStreamDelegate, "readStream");
+ return ExecuteNative(() => del(payload));
+ }
+
+ public static string ReadStreamAll(byte[] payload)
+ {
+ EnsureInitialized();
+ if (payload is null) throw new ArgumentNullException(nameof(payload));
+ var del = RequireStreamingDelegate(_readStreamAllDelegate, "readStreamAll");
+ return ExecuteNative(() => del(payload));
+ }
+
+ public static string CancelStream(byte[] payload)
+ {
+ EnsureInitialized();
+ if (payload is null) throw new ArgumentNullException(nameof(payload));
+ var del = RequireStreamingDelegate(_cancelStreamDelegate, "cancelStream");
+ return ExecuteNative(() => del(payload));
+ }
+
+ ///
+ /// True when the loaded native library exposes the streaming exports
+ /// (requestStream, readStream, readStreamAll, cancelStream).
+ /// Older builds of the tls-client library may not expose these, in which case the streaming
+ /// methods on will throw on first use.
+ ///
+ public static bool IsStreamingSupported
+ {
+ get
+ {
+ EnsureInitialized();
+ return _requestStreamDelegate != null
+ && _readStreamDelegate != null
+ && _readStreamAllDelegate != null
+ && _cancelStreamDelegate != null;
+ }
+ }
public static void Destroy()
{
if (!_isInitialized)
@@ -146,6 +241,10 @@ public static void Destroy()
_addCookiesToSessionDelegate = null!;
_destroySessionDelegate = null!;
_destroyAllDelegate = null!;
+ _requestStreamDelegate = null;
+ _readStreamDelegate = null;
+ _readStreamAllDelegate = null;
+ _cancelStreamDelegate = null;
_module = IntPtr.Zero;
diff --git a/src/native/template/{title}.csproj b/src/native/template/{title}.csproj
index 00e08e8..d69738f 100644
--- a/src/native/template/{title}.csproj
+++ b/src/native/template/{title}.csproj
@@ -2,9 +2,9 @@
netstandard2.1
false
- true
{title}
ErenKrt
+ {packageVersion}
https://github.com/ErenKrt/TlsClient.NET
Native client of TLSClient for {os} {arch}
diff --git a/tests/TlsClient.Api.Tests/HeaderTests.cs b/tests/TlsClient.Api.Tests/HeaderTests.cs
index 9e9e00c..0e205b3 100644
--- a/tests/TlsClient.Api.Tests/HeaderTests.cs
+++ b/tests/TlsClient.Api.Tests/HeaderTests.cs
@@ -51,7 +51,7 @@ public void Should_Add_UserAgent_Header_Options()
[Fact]
public void Should_Add_UserAgent_Header_Default()
{
- var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0";
+ var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0";
using var tlsClient = new ApiTlsClient(new Uri("http://127.0.0.1:8080"), "my-auth-key-1");
var request = new Request()
diff --git a/tests/TlsClient.Native.RestSharp.Tests/BodyTests.cs b/tests/TlsClient.Native.RestSharp.Tests/BodyTests.cs
index f44c1d6..405fa2d 100644
--- a/tests/TlsClient.Native.RestSharp.Tests/BodyTests.cs
+++ b/tests/TlsClient.Native.RestSharp.Tests/BodyTests.cs
@@ -15,7 +15,7 @@ public class BodyTests
{
static BodyTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.RestSharp.Tests/CookieTests.cs b/tests/TlsClient.Native.RestSharp.Tests/CookieTests.cs
index 019a0da..74bfdd7 100644
--- a/tests/TlsClient.Native.RestSharp.Tests/CookieTests.cs
+++ b/tests/TlsClient.Native.RestSharp.Tests/CookieTests.cs
@@ -17,7 +17,7 @@ public class CookieTests
{
static CookieTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.RestSharp.Tests/MethodTests.cs b/tests/TlsClient.Native.RestSharp.Tests/MethodTests.cs
index 414691e..188b64e 100644
--- a/tests/TlsClient.Native.RestSharp.Tests/MethodTests.cs
+++ b/tests/TlsClient.Native.RestSharp.Tests/MethodTests.cs
@@ -15,7 +15,7 @@ public class MethodTests
{
static MethodTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.RestSharp.Tests/NativeTestSetup.cs b/tests/TlsClient.Native.RestSharp.Tests/NativeTestSetup.cs
new file mode 100644
index 0000000..2f917fc
--- /dev/null
+++ b/tests/TlsClient.Native.RestSharp.Tests/NativeTestSetup.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace TlsClient.RestSharp.Tests
+{
+ ///
+ /// Resolves the path to the native tls-client library used by the Native
+ /// RestSharp test fixtures. Reads the TLS_CLIENT_NATIVE_DLL
+ /// environment variable when set; otherwise falls back to a developer-machine
+ /// default that the upstream maintainer uses locally. CI / contributors
+ /// should set the env var to point at the DLL produced for their platform.
+ ///
+ internal static class NativeTestSetup
+ {
+ public const string EnvVar = "TLS_CLIENT_NATIVE_DLL";
+ public const string DefaultDllPath = "D:\\Tools\\tls-client-windows-64-1.13.1.dll";
+
+ public static string DllPath =>
+ Environment.GetEnvironmentVariable(EnvVar) ?? DefaultDllPath;
+ }
+}
diff --git a/tests/TlsClient.Native.RestSharp.Tests/StatusTests.cs b/tests/TlsClient.Native.RestSharp.Tests/StatusTests.cs
index ac0d63b..0f72321 100644
--- a/tests/TlsClient.Native.RestSharp.Tests/StatusTests.cs
+++ b/tests/TlsClient.Native.RestSharp.Tests/StatusTests.cs
@@ -16,7 +16,7 @@ public class StatusTests
{
static StatusTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/BodyTests.cs b/tests/TlsClient.Native.Tests/BodyTests.cs
index e168a85..96ab3f1 100644
--- a/tests/TlsClient.Native.Tests/BodyTests.cs
+++ b/tests/TlsClient.Native.Tests/BodyTests.cs
@@ -16,7 +16,7 @@ public class BodyTests
{
static BodyTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/BuilderTests.cs b/tests/TlsClient.Native.Tests/BuilderTests.cs
index f805cf1..33aa22b 100644
--- a/tests/TlsClient.Native.Tests/BuilderTests.cs
+++ b/tests/TlsClient.Native.Tests/BuilderTests.cs
@@ -17,7 +17,7 @@ public class BuilderTests
{
static BuilderTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
diff --git a/tests/TlsClient.Native.Tests/ClientTests.cs b/tests/TlsClient.Native.Tests/ClientTests.cs
index 38d53a0..b0a883d 100644
--- a/tests/TlsClient.Native.Tests/ClientTests.cs
+++ b/tests/TlsClient.Native.Tests/ClientTests.cs
@@ -10,7 +10,7 @@ public class ClientTests
{
static ClientTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/CookieTests.cs b/tests/TlsClient.Native.Tests/CookieTests.cs
index c756a34..85a058d 100644
--- a/tests/TlsClient.Native.Tests/CookieTests.cs
+++ b/tests/TlsClient.Native.Tests/CookieTests.cs
@@ -17,7 +17,7 @@ public class CookieTests
static CookieTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/HeaderTests.cs b/tests/TlsClient.Native.Tests/HeaderTests.cs
index 13e5c1b..405c6d4 100644
--- a/tests/TlsClient.Native.Tests/HeaderTests.cs
+++ b/tests/TlsClient.Native.Tests/HeaderTests.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
+using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using TlsClient.Core.Models.Entities;
@@ -14,7 +16,7 @@ public class HeaderTests
public static readonly string BaseURL = "https://httpbin.io";
static HeaderTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
@@ -54,7 +56,7 @@ public void Should_Add_UserAgent_Header_Options()
[Fact]
public void Should_Add_UserAgent_Header_Default()
{
- var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 OPR/117.0.0.0";
+ var userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 OPR/118.0.0.0";
using var tlsClient = new NativeTlsClient();
var request = new Request()
@@ -84,18 +86,23 @@ public void Should_Add_UserAgent_After_Ctor()
[Fact]
public void Should_Override_Host()
{
- var baseHost = "httpbin.org";
- var realIp = "http://35.169.229.34";
+ // Connect to the literal IP so DNS does not produce the Host header
+ // for us — that's what makes this an actual test of RequestHostOverride.
+ // Resolve the IP at runtime so the test self-heals when the upstream
+ // CDN / load balancer rotates addresses.
+ const string baseHost = "httpbin.org";
+ var ip = Dns.GetHostAddresses(baseHost)
+ .First(a => a.AddressFamily == AddressFamily.InterNetwork);
using var tlsClient = new NativeTlsClient();
var request = new Request()
{
- RequestUrl = realIp,
- RequestHostOverride= baseHost,
- InsecureSkipVerify= true
+ RequestUrl = $"http://{ip}",
+ RequestHostOverride = baseHost,
+ InsecureSkipVerify = true,
};
var response = tlsClient.Request(request);
- Assert.Contains($"httpbin", response.Body);
+ Assert.Contains("httpbin", response.Body);
}
}
}
\ No newline at end of file
diff --git a/tests/TlsClient.Native.Tests/MethodTests.cs b/tests/TlsClient.Native.Tests/MethodTests.cs
index af2ef1e..dde2379 100644
--- a/tests/TlsClient.Native.Tests/MethodTests.cs
+++ b/tests/TlsClient.Native.Tests/MethodTests.cs
@@ -9,7 +9,7 @@ public class MethodTests
public static readonly string BaseURL = "https://httpbin.io";
static MethodTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/NativeTestSetup.cs b/tests/TlsClient.Native.Tests/NativeTestSetup.cs
new file mode 100644
index 0000000..6d35092
--- /dev/null
+++ b/tests/TlsClient.Native.Tests/NativeTestSetup.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace TlsClient.Core.Tests
+{
+ ///
+ /// Resolves the path to the native tls-client library used by the Native
+ /// test fixtures. Reads the TLS_CLIENT_NATIVE_DLL environment variable
+ /// when set; otherwise falls back to a developer-machine default that the
+ /// upstream maintainer uses locally. CI / contributors should set the env
+ /// var to point at the DLL produced for their platform.
+ ///
+ internal static class NativeTestSetup
+ {
+ public const string EnvVar = "TLS_CLIENT_NATIVE_DLL";
+ public const string DefaultDllPath = "D:\\Tools\\tls-client-windows-64-1.13.1.dll";
+
+ public static string DllPath =>
+ Environment.GetEnvironmentVariable(EnvVar) ?? DefaultDllPath;
+ }
+}
diff --git a/tests/TlsClient.Native.Tests/PerformanceTests.cs b/tests/TlsClient.Native.Tests/PerformanceTests.cs
index 2e25ded..4edae8c 100644
--- a/tests/TlsClient.Native.Tests/PerformanceTests.cs
+++ b/tests/TlsClient.Native.Tests/PerformanceTests.cs
@@ -17,7 +17,7 @@ public class PerformanceTests
{
static PerformanceTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
@@ -36,7 +36,7 @@ public async Task Should_Perform_Multiple_Requests()
{
using var tlsClient = new NativeTlsClient(new TlsClientOptions
{
- TlsClientIdentifier = TlsClientIdentifier.Chrome132,
+ TlsClientIdentifier = TlsClientIdentifier.Chrome133,
Timeout = TimeSpan.FromSeconds(10)
});
@@ -80,7 +80,7 @@ public async Task Should_Not_Leak_Memory()
{
using var tlsClient = new NativeTlsClient(new TlsClientOptions
{
- TlsClientIdentifier = TlsClientIdentifier.Chrome132,
+ TlsClientIdentifier = TlsClientIdentifier.Chrome133,
Timeout = TimeSpan.FromSeconds(10)
});
diff --git a/tests/TlsClient.Native.Tests/StatusTests.cs b/tests/TlsClient.Native.Tests/StatusTests.cs
index b43abe4..7b0ab44 100644
--- a/tests/TlsClient.Native.Tests/StatusTests.cs
+++ b/tests/TlsClient.Native.Tests/StatusTests.cs
@@ -10,7 +10,7 @@ public class StatusTests
public static readonly string BaseURL = "https://httpbin.io";
static StatusTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
diff --git a/tests/TlsClient.Native.Tests/TlsTests.cs b/tests/TlsClient.Native.Tests/TlsTests.cs
index db94620..4fa12af 100644
--- a/tests/TlsClient.Native.Tests/TlsTests.cs
+++ b/tests/TlsClient.Native.Tests/TlsTests.cs
@@ -15,7 +15,7 @@ public class TlsTests
{
static TlsTests()
{
- NativeTlsClient.Initialize("D:\\Tools\\tls-client-windows-64-1.13.1.dll");
+ NativeTlsClient.Initialize(NativeTestSetup.DllPath);
}
[Fact]
@@ -37,7 +37,7 @@ public void Should_Skip_Verify_Ssl()
using var tlsClient = new NativeTlsClient(new TlsClientOptions()
{
InsecureSkipVerify = true,
- TlsClientIdentifier = TlsClientIdentifier.Chrome132,
+ TlsClientIdentifier = TlsClientIdentifier.Chrome133,
});
var request = new Request()
{
@@ -53,7 +53,7 @@ public void Should_Force_Http1()
using var tlsClient = new NativeTlsClient(new TlsClientOptions()
{
ForceHttp1 = true,
- TlsClientIdentifier = TlsClientIdentifier.Chrome132,
+ TlsClientIdentifier = TlsClientIdentifier.Chrome133,
});
var request = new Request()
{
@@ -69,7 +69,7 @@ public void Should_Not_Force_Http1()
{
using var tlsClient = new NativeTlsClient(new TlsClientOptions()
{
- TlsClientIdentifier = TlsClientIdentifier.Chrome132,
+ TlsClientIdentifier = TlsClientIdentifier.Chrome133,
});
var request = new Request()
{
diff --git a/tls-client-version.txt b/tls-client-version.txt
index 07fb54b..08e9010 100644
--- a/tls-client-version.txt
+++ b/tls-client-version.txt
@@ -1 +1 @@
-v1.11.2
+v1.14.0-stream
\ No newline at end of file