From e2cd7fdab0f74a088ac54fd87deed2ae648dcb06 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 05:53:22 +0530 Subject: [PATCH 01/11] chore(packaging): point the Homebrew formula at v7.0.17-beta. --- tools/packaging/homebrew/titanium.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/packaging/homebrew/titanium.rb b/tools/packaging/homebrew/titanium.rb index 12f1803d4..2e7822e9a 100644 --- a/tools/packaging/homebrew/titanium.rb +++ b/tools/packaging/homebrew/titanium.rb @@ -10,17 +10,17 @@ class Titanium < Formula desc "Titanium Web Proxy CLI (MITM / reverse proxy)" homepage "https://github.com/justcoding121/titanium-web-proxy" - version "7.0.16" + version "7.0.17-beta" license "MIT" on_macos do on_arm do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-arm64.zip" - sha256 "0eebb0a3cff372b004496cd44830edfb546670c42fd18d7abf79b56b15ce30e8" + sha256 "7fcd4931f57bfca08cb8c0d9c604240189d343d8f397b8df2e7d1e947fded968" end on_intel do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-x64.zip" - sha256 "786f97316afedcd82b3c32e66970984f0f084b52a6a4b2a66b1320834f3d5d07" + sha256 "e00c1b99ca8a06bf8fafa463cb437243a79e085151eab96c94c502c72d421d38" end end From 82f413bc31d2bff41a876b62f0d4a77c0365c882 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 07:11:59 +0530 Subject: [PATCH 02/11] chore(packaging): point the Homebrew formula at v7.0.17. --- tools/packaging/homebrew/titanium.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/packaging/homebrew/titanium.rb b/tools/packaging/homebrew/titanium.rb index 2e7822e9a..c3528f8d5 100644 --- a/tools/packaging/homebrew/titanium.rb +++ b/tools/packaging/homebrew/titanium.rb @@ -10,17 +10,17 @@ class Titanium < Formula desc "Titanium Web Proxy CLI (MITM / reverse proxy)" homepage "https://github.com/justcoding121/titanium-web-proxy" - version "7.0.17-beta" + version "7.0.17" license "MIT" on_macos do on_arm do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-arm64.zip" - sha256 "7fcd4931f57bfca08cb8c0d9c604240189d343d8f397b8df2e7d1e947fded968" + sha256 "720e7a47b6dd107b9939560312834e649cd0ee6fc4f8957f1d50be496e1551ab" end on_intel do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-x64.zip" - sha256 "e00c1b99ca8a06bf8fafa463cb437243a79e085151eab96c94c502c72d421d38" + sha256 "d1bf49f46877ab755da9d818298943824d7e4cd96c73fcd49689431484b30ac9" end end From 128fcaabf510d7b236734da3b584cfda4cc8d9c8 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 13:51:04 +0530 Subject: [PATCH 03/11] fix(inspector): never buffer unknown-length bodies against the abort budget Inspector buffered any body without a Content-Length (chunked, H2 without CL) up to ProxyServer.MaxBufferedBodyBytes (32 MiB), which is a hard abort limit: larger transfers (e.g. git clone packs) were RST/closed mid-stream. Root cause is the sizing policy, not any content type or host. - ShouldBufferBody: buffer only when the length is known and within the budget (or the budget is non-positive); unknown length is relayed. - Unknown-length responses and requests are marked Streaming and previewed through the existing body-write hook into a bounded 2 MiB tee; add the missing request-side tee. - Decode the tee preview per Content-Encoding (gzip/deflate/br, tolerant of truncation) so chunked compressed JSON stays readable. - Re-derive gRPC/protobuf/multipart views when a streamed preview is final. - Remove the content-type/URL special cases (git pack helper, SSE buffering denylist); SSE detection remains only for UI flags. - Tests for policy, request/response tee, decoding, truncation, gRPC views. --- .../Services/InspectorBodyLimits.cs | 93 ++++++- .../Services/InterceptionService.cs | 189 ++++++++++++--- .../Services/SessionSnapshot.cs | 19 +- .../MainWindowViewModel.Sessions.cs | 4 +- .../InspectorBodyHonestyTests.cs | 65 +++++ ...ptionBodyCaptureAndTrustBgCoverageTests.cs | 2 - .../InterceptionCaptureCoverageTests.cs | 25 +- ...InterceptionUnknownLengthStreamingTests.cs | 229 ++++++++++++++++++ website/docs/inspector.md | 2 +- 9 files changed, 580 insertions(+), 48 deletions(-) create mode 100644 tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs diff --git a/src/Titanium.Inspector/Services/InspectorBodyLimits.cs b/src/Titanium.Inspector/Services/InspectorBodyLimits.cs index 538d61e41..20acb01b3 100644 --- a/src/Titanium.Inspector/Services/InspectorBodyLimits.cs +++ b/src/Titanium.Inspector/Services/InspectorBodyLimits.cs @@ -19,7 +19,7 @@ public enum BodyCaptureState /// Known huge Content-Length — not buffered (download must not stall). NotCaptured = 3, - /// Endless / SSE-style stream — relayed; preview may fill asynchronously. + /// Unknown-length body (chunked / close-delimited / SSE) — relayed; bounded preview fills asynchronously. Streaming = 4, } @@ -84,6 +84,97 @@ public static bool LooksLikeSseContentType(string? contentType) => !string.IsNullOrEmpty(contentType) && contentType.Contains("text/event-stream", StringComparison.OrdinalIgnoreCase); + /// + /// Best-effort decode of a captured wire prefix per Content-Encoding (gzip, deflate, br). + /// Used for streamed (unknown-length) bodies, which are relayed without the library's + /// decompressing buffer. Truncated input is tolerated: whatever decodes is returned, capped + /// at . Unknown or undecodable encodings return the input unchanged. + /// + public static byte[] DecodeCapturedPrefix(byte[] wire, string? contentEncoding) + { + if (wire.Length == 0 || string.IsNullOrWhiteSpace(contentEncoding)) + { + return wire; + } + + // Content-Encoding lists encodings in the order they were applied; undo in reverse. + var tokens = contentEncoding.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var current = wire; + for (var i = tokens.Length - 1; i >= 0; i--) + { + var next = TryDecodeOne(current, tokens[i]); + if (next is null) + { + return current; + } + + current = next; + } + + return current; + } + + private static byte[]? TryDecodeOne(byte[] input, string encoding) + { + if (encoding.Equals("identity", StringComparison.OrdinalIgnoreCase)) + { + return input; + } + + if (encoding.Equals("gzip", StringComparison.OrdinalIgnoreCase) + || encoding.Equals("x-gzip", StringComparison.OrdinalIgnoreCase)) + { + return ReadDecoded(() => new System.IO.Compression.GZipStream( + new MemoryStream(input), System.IO.Compression.CompressionMode.Decompress)); + } + + if (encoding.Equals("br", StringComparison.OrdinalIgnoreCase)) + { + return ReadDecoded(() => new System.IO.Compression.BrotliStream( + new MemoryStream(input), System.IO.Compression.CompressionMode.Decompress)); + } + + if (encoding.Equals("deflate", StringComparison.OrdinalIgnoreCase)) + { + // RFC 9110 says zlib-wrapped, but some servers send raw deflate. + return ReadDecoded(() => new System.IO.Compression.ZLibStream( + new MemoryStream(input), System.IO.Compression.CompressionMode.Decompress)) + ?? ReadDecoded(() => new System.IO.Compression.DeflateStream( + new MemoryStream(input), System.IO.Compression.CompressionMode.Decompress)); + } + + return null; + } + + private static byte[]? ReadDecoded(Func open) + { + try + { + using var stream = open(); + using var output = new MemoryStream(); + var buffer = new byte[16 * 1024]; + try + { + int n; + while (output.Length < MaxBodyBytes + && (n = stream.Read(buffer, 0, (int)Math.Min(buffer.Length, MaxBodyBytes - output.Length))) > 0) + { + output.Write(buffer, 0, n); + } + } + catch (Exception ex) when (ex is InvalidDataException or IOException) + { + // Truncated tee prefix (or corrupt data): keep what decoded so far. + } + + return output.Length > 0 ? output.ToArray() : null; + } + catch (Exception ex) when (ex is InvalidDataException or IOException) + { + return null; + } + } + public static string FormatCaptureBanner( BodyCaptureState state, long? originalSize, diff --git a/src/Titanium.Inspector/Services/InterceptionService.cs b/src/Titanium.Inspector/Services/InterceptionService.cs index 680b9ba20..633d2cf15 100644 --- a/src/Titanium.Inspector/Services/InterceptionService.cs +++ b/src/Titanium.Inspector/Services/InterceptionService.cs @@ -1806,6 +1806,7 @@ private Task OnAfterResponse(object sender, SessionEventArgs e) { if (_live.TryGetValue(e.HttpClient, out var snap)) { + FinalizeStreamingRequestBody(snap); FinalizeStreamingBody(snap); ApplyTiming(snap, e.Timing, snap.StartedUtc); SessionUpdated?.Invoke(this, snap); @@ -2072,18 +2073,33 @@ private static void FillResponse(SessionSnapshot snap, SessionEventArgs e) // NO snap.SseEvents = SseEventParser.Parse(snap.ResponseBodyText); } - if (snap.IsGrpc && !snap.IsTranscoded && bodyBytes is { Length: > 0 }) - { - snap.GrpcFrames = ProtocolFrameInspectors.ParseGrpcFrames(bodyBytes); - snap.ProtobufDecodedText = ProtobufMessageDecoder.DecodeWireFormat(bodyBytes); - } + ApplyResponseBodyViews(snap, bodyBytes); if (snap.IsTranscoded && snap.UpstreamResponseBodyBytes is { Length: > 0 }) { snap.ProtobufDecodedText = ProtobufMessageDecoder.DecodeWireFormat(snap.UpstreamResponseBodyBytes); } + } + + /// + /// Structured views (gRPC frames, protobuf, multipart) derived from the response body preview. + /// Runs when the body was buffered, and again once a streamed body's preview is final, + /// so unknown-length gRPC / multipart responses keep their decoded views. + /// + private static void ApplyResponseBodyViews(SessionSnapshot snap, byte[]? bodyBytes) + { + if (bodyBytes is not { Length: > 0 }) + { + return; + } + + if (snap.IsGrpc && !snap.IsTranscoded) + { + snap.GrpcFrames = ProtocolFrameInspectors.ParseGrpcFrames(bodyBytes); + snap.ProtobufDecodedText = ProtobufMessageDecoder.DecodeWireFormat(bodyBytes); + } - if (snap.IsMultipart && bodyBytes is { Length: > 0 }) + if (snap.IsMultipart) { snap.MultipartParts = ProtocolFrameInspectors.ParseMultipart(snap.ContentType, bodyBytes); } @@ -2123,6 +2139,16 @@ private async Task OnRequestBodyWriteThrottle(object sender, BeforeBodyWriteEven await Task.Delay(delay, _processResolveCts?.Token ?? CancellationToken.None).ConfigureAwait(false); } } + + // Preview tee for unknown-length uploads that were relayed instead of buffered. + if (e.Session.HttpClient.Request.IsBodyRead + || !_live.TryGetValue(e.Session.HttpClient, out var snap) + || snap.RequestBodyCapture != BodyCaptureState.Streaming) + { + return; + } + + TeeRequestChunk(snap, e); } private async Task OnResponseBodyWriteThrottle(object sender, BeforeBodyWriteEventArgs e) @@ -2137,7 +2163,7 @@ private async Task OnResponseBodyWriteThrottle(object sender, BeforeBodyWriteEve } } - // Preview tee only for streamed SSE (not buffered). Do not tee NotCaptured huge downloads. + // Preview tee only for relayed unknown-length bodies (not buffered). Do not tee NotCaptured huge downloads. if (e.Session.HttpClient.Response.IsBodyRead) { return; @@ -2208,7 +2234,7 @@ private static void PublishTeePreview(SessionSnapshot snap) return; } - var bytes = tee.ToArray(); + var bytes = InspectorBodyLimits.DecodeCapturedPrefix(tee.ToArray(), snap.ResponseContentEncoding); snap.ResponseBodyBytes = bytes; snap.ResponseBodyText = InspectorBodyLimits.TruncateText(Encoding.UTF8.GetString(bytes)); if (snap.IsServerSentEvents) @@ -2229,14 +2255,23 @@ private static void FinalizeStreamingBody(SessionSnapshot snap) PublishTeePreview(snap); snap.ResponseBodyStreamOpen = false; var captured = snap.ResponseBodyBytes?.LongLength ?? 0; - if (snap.ResponseBytesSeen > captured && captured >= MaxBodyBytes) + var seen = snap.ResponseBytesSeen; + // Seen counts wire bytes; captured is the (possibly decoded) preview. Either the wire + // exceeded the tee cap, or decoding hit the preview cap. + if (seen > MaxBodyBytes || (captured >= MaxBodyBytes && captured != seen)) { snap.ResponseBodyCapture = BodyCaptureState.Truncated; } - else if (captured > 0 && snap.ResponseBytesSeen <= MaxBodyBytes) + else if (captured > 0) { snap.ResponseBodyCapture = BodyCaptureState.Complete; } + else if (seen == 0 && !snap.IsServerSentEvents) + { + snap.ResponseBodyCapture = BodyCaptureState.None; + } + + ApplyResponseBodyViews(snap, snap.ResponseBodyBytes); snap.ResponseBodyOriginalSize = snap.ResponseBytesSeen > 0 ? snap.ResponseBytesSeen @@ -2252,6 +2287,92 @@ private static void FinalizeStreamingBody(SessionSnapshot snap) snap.ResponseTeeStream = null; } + private void TeeRequestChunk(SessionSnapshot snap, BeforeBodyWriteEventArgs e) + { + var chunk = e.BodyBytes; + var len = chunk?.Length ?? 0; + if (len > 0) + { + snap.RequestBytesSeen += len; + snap.RequestBodyOriginalSize = snap.RequestBytesSeen; + + var tee = snap.RequestTeeStream; + if (tee is null) + { + tee = new MemoryStream(Math.Min(MaxBodyBytes, Math.Max(len, 4096))); + snap.RequestTeeStream = tee; + } + + if (tee.Length < MaxBodyBytes) + { + var toWrite = (int)Math.Min(len, MaxBodyBytes - tee.Length); + tee.Write(chunk!, 0, toWrite); + } + } + + if (e.IsLastChunk) + { + FinalizeStreamingRequestBody(snap); + SyncHttpBodySize(snap); + SessionUpdated?.Invoke(this, snap); + return; + } + + var now = DateTime.UtcNow.Ticks; + var last = snap.LastRequestTeeUiUtcTicks; + if (last != 0 && (now - last) < TimeSpan.FromMilliseconds(InspectorBodyLimits.TeeUiCoalesceMs).Ticks) + { + return; + } + + snap.LastRequestTeeUiUtcTicks = now; + PublishRequestTeePreview(snap); + SessionUpdated?.Invoke(this, snap); + } + + private static void PublishRequestTeePreview(SessionSnapshot snap) + { + var tee = snap.RequestTeeStream; + if (tee is null || tee.Length == 0) + { + return; + } + + var bytes = InspectorBodyLimits.DecodeCapturedPrefix(tee.ToArray(), snap.RequestContentEncoding); + snap.RequestBodyBytes = bytes; + snap.RequestBodyText = InspectorBodyLimits.TruncateText(Encoding.UTF8.GetString(bytes)); + } + + private static void FinalizeStreamingRequestBody(SessionSnapshot snap) + { + if (snap.RequestBodyCapture == BodyCaptureState.Streaming) + { + PublishRequestTeePreview(snap); + var captured = snap.RequestBodyBytes?.LongLength ?? 0; + var seen = snap.RequestBytesSeen; + if (seen > MaxBodyBytes || (captured >= MaxBodyBytes && captured != seen)) + { + snap.RequestBodyCapture = BodyCaptureState.Truncated; + } + else if (captured > 0) + { + snap.RequestBodyCapture = BodyCaptureState.Complete; + } + else + { + snap.RequestBodyCapture = BodyCaptureState.None; + } + + if (seen > 0) + { + snap.RequestBodyOriginalSize = seen; + } + } + + snap.RequestTeeStream?.Dispose(); + snap.RequestTeeStream = null; + } + private static void ApplyRequestBodyCapture(SessionSnapshot snap, Request req, byte[]? originalBody) { if (originalBody is not null && req.IsBodyRead) @@ -2269,6 +2390,15 @@ private static void ApplyRequestBodyCapture(SessionSnapshot snap, Request req, b return; } + if (!req.IsBodyRead && req.ContentLength < 0) + { + // Unknown length (chunked / H2 without Content-Length) is relayed, not buffered; + // a bounded preview is teed from the body-write hook. + snap.RequestBodyCapture = BodyCaptureState.Streaming; + snap.RequestContentEncoding = req.ContentEncoding; + return; + } + var limit = InspectorBodyLimits.MaxMapLocalFileBytes; if (req.ContentLength > limit) { @@ -2340,9 +2470,10 @@ private static void ApplyResponseBodyCapture( return; } - if (isSse) + // Unknown length is never buffered (see ShouldBufferBody): relay + bounded preview tee. + if (isSse || (resp.HasBody && !resp.IsBodyRead && resp.ContentLength < 0)) { - ApplyStreamingResponseBody(snap); + ApplyStreamingResponseBody(snap, resp); return; } @@ -2377,8 +2508,9 @@ private static void ApplyBufferedResponseBody(SessionSnapshot snap, byte[] origi snap.ResponseBodyStreamOpen = false; } - private static void ApplyStreamingResponseBody(SessionSnapshot snap) + private static void ApplyStreamingResponseBody(SessionSnapshot snap, Response resp) { + snap.ResponseContentEncoding = resp.ContentEncoding; snap.ResponseBodyCapture = BodyCaptureState.Streaming; snap.ResponseBodyStreamOpen = true; snap.BodySize = snap.ResponseBytesSeen > 0 ? snap.ResponseBytesSeen : null; @@ -2459,15 +2591,17 @@ private static string FormatHeaders(HeaderCollection headers) } /// - /// Whole-body buffering for the session grid must not run when Content-Length already - /// exceeds - that path RSTs HTTP/2 streams - /// with ENHANCE_YOUR_CALM and breaks the browser download. SSE and WebSocket upgrades are - /// never buffered (relay + optional 2 MiB tee). Finite unknown-length (chunked) bodies still - /// buffer up to the limit so gzip JSON can be inspected. + /// Whole-body buffering for the session grid is only safe when the size is known up front. + /// is a hard abort budget: exceeding it RSTs + /// HTTP/2 streams (ENHANCE_YOUR_CALM) or drops the connection. A body with no declared length + /// (chunked, H2 without Content-Length, close-delimited, SSE) can exceed it at any point + /// regardless of what it is, so it is relayed untouched and previewed through the bounded + /// body-write tee instead. Known lengths within the budget are buffered (decompressed) as before; + /// a non-positive budget means "unbounded" and always buffers. WebSocket upgrades are never buffered. /// private bool ShouldBufferBody(RequestResponseBase message, SessionEventArgs session, bool isRequest) { - if (LooksLikeEndlessStream(message, session, isRequest)) + if (session.HttpClient.Request.UpgradeToWebSocket) { return false; } @@ -2479,22 +2613,7 @@ private bool ShouldBufferBody(RequestResponseBase message, SessionEventArgs sess } var contentLength = message.ContentLength; - return contentLength < 0 || contentLength <= limit; - } - - private static bool LooksLikeEndlessStream(RequestResponseBase message, SessionEventArgs session, bool isRequest) - { - if (session.HttpClient.Request.UpgradeToWebSocket) - { - return true; - } - - if (!isRequest && InspectorBodyLimits.LooksLikeSseContentType(message.ContentType)) - { - return true; - } - - return false; + return contentLength >= 0 && contentLength <= limit; } public void Dispose() => EnsureShutdown(); diff --git a/src/Titanium.Inspector/Services/SessionSnapshot.cs b/src/Titanium.Inspector/Services/SessionSnapshot.cs index b7f177ca0..495107f9f 100644 --- a/src/Titanium.Inspector/Services/SessionSnapshot.cs +++ b/src/Titanium.Inspector/Services/SessionSnapshot.cs @@ -203,14 +203,14 @@ public long? ResponseBodyOriginalSize set => SetField(ref _responseBodyOriginalSize, value); } - /// True while an SSE-style response stream is still open. + /// True while an unknown-length response stream (SSE, chunked) is still open. public bool ResponseBodyStreamOpen { get => _responseBodyStreamOpen; set => SetField(ref _responseBodyStreamOpen, value); } - /// In-flight SSE tee buffer (not spilled; cleared when finalized). + /// In-flight response tee buffer of wire bytes (not spilled; cleared when finalized). internal MemoryStream? ResponseTeeStream { get; set; } /// Bytes seen on the response wire while teeing (may exceed preview). @@ -219,6 +219,21 @@ public bool ResponseBodyStreamOpen /// UTC ticks of last coalesced SessionUpdated from the tee. internal long LastTeeUiUtcTicks { get; set; } + /// Response Content-Encoding, used to decode the tee preview. + internal string? ResponseContentEncoding { get; set; } + + /// In-flight request tee buffer of wire bytes (unknown-length uploads). + internal MemoryStream? RequestTeeStream { get; set; } + + /// Bytes seen on the request wire while teeing (may exceed preview). + internal long RequestBytesSeen { get; set; } + + /// UTC ticks of last coalesced SessionUpdated from the request tee. + internal long LastRequestTeeUiUtcTicks { get; set; } + + /// Request Content-Encoding, used to decode the tee preview. + internal string? RequestContentEncoding { get; set; } + public int ProcessId { get => _processId; diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs index 1495290dd..4391b9892 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs @@ -116,7 +116,7 @@ await MarshalToUiAsync(() => ComposerHeaders = selected.RequestHeadersText ?? ""; ComposerBody = selected.RequestBodyText ?? ""; ComposerBodyFilePath = null; - StatusText = selected.RequestBodyCapture is BodyCaptureState.Truncated or BodyCaptureState.NotCaptured + StatusText = selected.RequestBodyCapture is BodyCaptureState.Truncated or BodyCaptureState.NotCaptured or BodyCaptureState.Streaming ? "Composer loaded (request body was truncated or not fully captured)" : "Composer loaded from selected session"; }, _statusRevertCts?.Token ?? CancellationToken.None).ConfigureAwait(false); @@ -180,7 +180,7 @@ await MarshalToUiAsync(() => ComposerHeaders = selected.RequestHeadersText ?? ""; ComposerBody = selected.RequestBodyText ?? ""; ComposerBodyFilePath = null; - StatusText = selected.RequestBodyCapture is BodyCaptureState.Truncated or BodyCaptureState.NotCaptured + StatusText = selected.RequestBodyCapture is BodyCaptureState.Truncated or BodyCaptureState.NotCaptured or BodyCaptureState.Streaming ? "Composer loaded (request body was truncated or not fully captured)" : "Composer loaded from selected session"; }, StatusCancelToken).ConfigureAwait(false); diff --git a/tests/Titanium.Inspector.Tests/InspectorBodyHonestyTests.cs b/tests/Titanium.Inspector.Tests/InspectorBodyHonestyTests.cs index cdbe81b45..a84454a08 100644 --- a/tests/Titanium.Inspector.Tests/InspectorBodyHonestyTests.cs +++ b/tests/Titanium.Inspector.Tests/InspectorBodyHonestyTests.cs @@ -242,6 +242,71 @@ public void IsImageAndPrettyContentType_Helpers() Assert.IsTrue(InspectorBodyLimits.LooksLikeSseContentType("text/event-stream; charset=utf-8")); } + private static byte[] Compress(byte[] data, string encoding) + { + using var ms = new MemoryStream(); + using (Stream z = encoding switch + { + "gzip" => new System.IO.Compression.GZipStream(ms, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true), + "br" => new System.IO.Compression.BrotliStream(ms, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true), + "deflate" => new System.IO.Compression.ZLibStream(ms, System.IO.Compression.CompressionLevel.Fastest, leaveOpen: true), + _ => throw new ArgumentException(encoding), + }) + { + z.Write(data); + } + + return ms.ToArray(); + } + + [TestMethod] + [DataRow("gzip")] + [DataRow("br")] + [DataRow("deflate")] + public void DecodeCapturedPrefix_DecodesKnownEncodings(string encoding) + { + var plain = Encoding.UTF8.GetBytes("{\"hello\":\"world\"}"); + var decoded = InspectorBodyLimits.DecodeCapturedPrefix(Compress(plain, encoding), encoding); + CollectionAssert.AreEqual(plain, decoded); + } + + [TestMethod] + public void DecodeCapturedPrefix_PassThroughAndStacked() + { + var plain = "abc"u8.ToArray(); + Assert.AreSame(plain, InspectorBodyLimits.DecodeCapturedPrefix(plain, null)); + Assert.AreSame(plain, InspectorBodyLimits.DecodeCapturedPrefix(plain, "identity")); + + // Unknown encoding: bytes are returned as received. + Assert.AreSame(plain, InspectorBodyLimits.DecodeCapturedPrefix(plain, "x-custom")); + + // Applied in order "deflate, gzip" => undo gzip first, then deflate. + var stacked = Compress(Compress(plain, "deflate"), "gzip"); + CollectionAssert.AreEqual(plain, InspectorBodyLimits.DecodeCapturedPrefix(stacked, "deflate, gzip")); + } + + [TestMethod] + public void DecodeCapturedPrefix_TruncatedWireKeepsDecodedPrefix_AndCapsOutput() + { + var plain = new byte[InspectorBodyLimits.MaxBodyBytes + 4096]; + new Random(7).NextBytes(plain.AsSpan(0, plain.Length / 2)); + var gz = Compress(plain, "gzip"); + + // Cut the stream mid-way (tee reached its cap): decoded prefix must still surface. + var cut = gz.AsSpan(0, gz.Length / 2).ToArray(); + var partial = InspectorBodyLimits.DecodeCapturedPrefix(cut, "gzip"); + Assert.IsTrue(partial.Length > 0); + Assert.IsTrue(partial.AsSpan().SequenceEqual(plain.AsSpan(0, partial.Length))); + + // Decompressed size above the preview cap is capped. + var full = InspectorBodyLimits.DecodeCapturedPrefix(gz, "gzip"); + Assert.AreEqual(InspectorBodyLimits.MaxBodyBytes, full.Length); + + // Garbage that claims gzip: returned unchanged rather than lost. + var junk = "not gzip at all"u8.ToArray(); + Assert.AreSame(junk, InspectorBodyLimits.DecodeCapturedPrefix(junk, "gzip")); + } + [TestMethod] public void TryPrettyPrint_Xml_DisablesExternalEntities() { diff --git a/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs b/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs index 7a46b6c67..d048d2bba 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs @@ -339,7 +339,6 @@ public void TeeThrottleAndEndlessStream_CoverEarlyReturnsAndCoalesce() var throttle = typeof(InterceptionService).GetMethod("OnResponseBodyWriteThrottle", PrivateInstance)!; var tee = typeof(InterceptionService).GetMethod("TeeResponseChunk", PrivateInstance)!; - var looks = typeof(InterceptionService).GetMethod("LooksLikeEndlessStream", PrivateStatic)!; var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", PrivateInstance)!; // Body already read → early return (no tee). @@ -376,7 +375,6 @@ public void TeeThrottleAndEndlessStream_CoverEarlyReturnsAndCoalesce() session.HttpClient.Request.Headers.AddHeader("Upgrade", "websocket"); Assert.IsTrue(session.HttpClient.Request.UpgradeToWebSocket); - Assert.IsTrue((bool)looks.Invoke(null, [session.HttpClient.Request, session, true])!); Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); } } diff --git a/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs b/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs index 09a518bf9..206323098 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs @@ -347,15 +347,30 @@ public void FillResponse_CoversSseGrpcTranscodeMultipartAndWebsocket() session.MaxBufferedBodyBytes = 0; Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); session.MaxBufferedBodyBytes = 1024; + // Unknown length can exceed the abort budget at any point: never buffered, whatever it is. session.HttpClient.Request.ContentLength = -1; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); - // SSE responses must not buffer; finite chunked JSON still does. - session.HttpClient.Response.ContentType = "text/event-stream"; - session.HttpClient.Response.ContentLength = -1; + foreach (var contentType in new[] { "text/event-stream", "application/json", "application/octet-stream", "application/x-anything" }) + { + session.HttpClient.Response.ContentType = contentType; + session.HttpClient.Response.ContentLength = -1; + Assert.IsFalse( + (bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!, + contentType); + } + + // Known length within the budget is buffered; above it is not. + session.HttpClient.Response.ContentLength = 1024; + Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + session.HttpClient.Response.ContentLength = 1025; Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); - session.HttpClient.Response.ContentType = "application/json"; + + // No budget (<= 0): nothing can abort, so unknown length buffers too. + session.MaxBufferedBodyBytes = 0; + session.HttpClient.Response.ContentLength = -1; Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + session.MaxBufferedBodyBytes = 1024; var throttleReq = typeof(InterceptionService).GetMethod("OnRequestBodyWriteThrottle", flags)!; var throttleResp = typeof(InterceptionService).GetMethod("OnResponseBodyWriteThrottle", flags)!; diff --git a/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs new file mode 100644 index 000000000..45bfe50dc --- /dev/null +++ b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs @@ -0,0 +1,229 @@ +using System.IO.Compression; +using System.Net; +using System.Reflection; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Inspector.Services; +using Titanium.Web.Proxy; +using Titanium.Web.Proxy.EventArguments; +using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Http; +using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Network; +using Titanium.Web.Proxy.Network.Tcp; + +namespace Titanium.Inspector.Tests; + +/// +/// Unknown-length (chunked / H2 without Content-Length) bodies must never be buffered against the +/// proxy's hard abort budget. They are relayed and previewed through a bounded, decoded tee, +/// independent of content type or host. +/// +[TestClass] +public class InterceptionUnknownLengthStreamingTests +{ + private static readonly BindingFlags PrivateStatic = BindingFlags.NonPublic | BindingFlags.Static; + private static readonly BindingFlags PrivateInstance = BindingFlags.NonPublic | BindingFlags.Instance; + + private static byte[] Gzip(byte[] data) + { + using var ms = new MemoryStream(); + using (var z = new GZipStream(ms, CompressionLevel.Fastest, leaveOpen: true)) + { + z.Write(data); + } + + return ms.ToArray(); + } + + private static (ProxyServer Proxy, SessionEventArgs Session, CancellationTokenSource Cts) CreateSession() + { + var proxy = new ProxyServer(userTrustRootCertificate: false); + var endPoint = new ExplicitProxyEndPoint(IPAddress.Loopback, 0, false); + var connection = new QuicClientConnection( + proxy, new IPEndPoint(IPAddress.Loopback, 4433), new IPEndPoint(IPAddress.Loopback, 12345)); + var cts = new CancellationTokenSource(); + var clientStream = new HttpClientStream(proxy, connection, Stream.Null, proxy.BufferPool, cts.Token); + var session = new SessionEventArgs(proxy, endPoint, clientStream, null, cts); + session.HttpClient.Request.Method = "POST"; + return (proxy, session, cts); + } + + [TestMethod] + public void ApplyResponseBodyCapture_UnknownLengthUnread_IsStreamingRegardlessOfContentType() + { + var apply = typeof(InterceptionService).GetMethod("ApplyResponseBodyCapture", PrivateStatic)!; + var req = new Request { Method = "GET" }; + + foreach (var contentType in new[] { "application/json", "application/octet-stream", "application/x-any-binary", "text/event-stream" }) + { + var snap = new SessionSnapshot(); + var resp = new Response { StatusCode = 200, ContentType = contentType }; + resp.Headers.AddHeader("Content-Encoding", "gzip"); + resp.IsChunked = true; + apply.Invoke(null, [snap, resp, req, null]); + + Assert.AreEqual(BodyCaptureState.Streaming, snap.ResponseBodyCapture, contentType); + Assert.IsTrue(snap.ResponseBodyStreamOpen, contentType); + Assert.AreEqual("gzip", snap.ResponseContentEncoding, contentType); + } + } + + [TestMethod] + public void ApplyRequestBodyCapture_UnknownLengthUnread_IsStreaming_KnownOrEmptyAreNot() + { + var apply = typeof(InterceptionService).GetMethod("ApplyRequestBodyCapture", PrivateStatic)!; + + var snap = new SessionSnapshot(); + var chunked = new Request { Method = "POST" }; + chunked.Headers.AddHeader("Content-Encoding", "br"); + chunked.IsChunked = true; + apply.Invoke(null, [snap, chunked, null]); + Assert.AreEqual(BodyCaptureState.Streaming, snap.RequestBodyCapture); + Assert.AreEqual("br", snap.RequestContentEncoding); + + snap = new SessionSnapshot(); + var empty = new Request { Method = "POST" }; + empty.ContentLength = 0; + apply.Invoke(null, [snap, empty, null]); + Assert.AreEqual(BodyCaptureState.None, snap.RequestBodyCapture); + + snap = new SessionSnapshot(); + var known = new Request { Method = "POST" }; + known.ContentLength = 128; + apply.Invoke(null, [snap, known, null]); + Assert.AreNotEqual(BodyCaptureState.Streaming, snap.RequestBodyCapture); + } + + [TestMethod] + public void ResponseTee_GzipChunkedJson_IsDecodedAndComplete() + { + var (proxy, session, cts) = CreateSession(); + using var _p = proxy; + using var _c = cts; + using var _s = session; + using var interception = new InterceptionService(new RecordingSystemProxyController()) { UseInMemoryTrustState = true }; + var tee = typeof(InterceptionService).GetMethod("TeeResponseChunk", PrivateInstance)!; + + const string json = "{\"items\":[1,2,3],\"ok\":true}"; + var wire = Gzip(Encoding.UTF8.GetBytes(json)); + var split = wire.Length / 2; + + var snap = new SessionSnapshot + { + ResponseBodyCapture = BodyCaptureState.Streaming, + ResponseBodyStreamOpen = true, + ResponseContentEncoding = "gzip", + }; + tee.Invoke(interception, [snap, new BeforeBodyWriteEventArgs(session, wire[..split], isChunked: true, isLastChunk: false)]); + tee.Invoke(interception, [snap, new BeforeBodyWriteEventArgs(session, wire[split..], isChunked: true, isLastChunk: true)]); + + Assert.AreEqual(BodyCaptureState.Complete, snap.ResponseBodyCapture); + Assert.IsFalse(snap.ResponseBodyStreamOpen); + Assert.AreEqual(json, snap.ResponseBodyText); + Assert.AreEqual((long)wire.Length, snap.ResponseBodyOriginalSize); + Assert.IsNull(snap.ResponseTeeStream); + } + + [TestMethod] + public void ResponseTee_LargeUnknownLengthBody_IsBoundedAndTruncated() + { + var (proxy, session, cts) = CreateSession(); + using var _p = proxy; + using var _c = cts; + using var _s = session; + using var interception = new InterceptionService(new RecordingSystemProxyController()) { UseInMemoryTrustState = true }; + var tee = typeof(InterceptionService).GetMethod("TeeResponseChunk", PrivateInstance)!; + + var snap = new SessionSnapshot + { + ResponseBodyCapture = BodyCaptureState.Streaming, + ResponseBodyStreamOpen = true, + }; + var chunk = new byte[1024 * 1024]; + const int chunks = 40; // 40 MiB: above the 32 MiB abort budget Inspector configures + for (var i = 0; i < chunks; i++) + { + var args = new BeforeBodyWriteEventArgs(session, chunk, isChunked: true, isLastChunk: i == chunks - 1); + tee.Invoke(interception, [snap, args]); + if (i < chunks - 1) + { + Assert.IsTrue((snap.ResponseTeeStream?.Length ?? 0) <= InspectorBodyLimits.MaxBodyBytes); + } + } + + Assert.AreEqual(BodyCaptureState.Truncated, snap.ResponseBodyCapture); + Assert.AreEqual(chunks * (long)chunk.Length, snap.ResponseBodyOriginalSize); + Assert.AreEqual(InspectorBodyLimits.MaxBodyBytes, snap.ResponseBodyBytes!.Length); + } + + [TestMethod] + public void RequestTee_ChunkedUpload_IsTeedViaThrottleHook_AndFinalized() + { + var (proxy, session, cts) = CreateSession(); + using var _p = proxy; + using var _c = cts; + using var _s = session; + using var interception = new InterceptionService(new RecordingSystemProxyController()) { UseInMemoryTrustState = true }; + var throttle = typeof(InterceptionService).GetMethod("OnRequestBodyWriteThrottle", PrivateInstance)!; + var live = (System.Collections.IDictionary)typeof(InterceptionService).GetField("_live", PrivateInstance)!.GetValue(interception)!; + + var snap = new SessionSnapshot { RequestBodyCapture = BodyCaptureState.Streaming }; + live[session.HttpClient] = snap; + session.HttpClient.Request.IsBodyRead = false; + + var first = new BeforeBodyWriteEventArgs(session, "hello "u8.ToArray(), isChunked: true, isLastChunk: false); + ((Task)throttle.Invoke(interception, [interception, first])!).GetAwaiter().GetResult(); + Assert.IsNotNull(snap.RequestTeeStream); + + var last = new BeforeBodyWriteEventArgs(session, "world"u8.ToArray(), isChunked: true, isLastChunk: true); + ((Task)throttle.Invoke(interception, [interception, last])!).GetAwaiter().GetResult(); + + Assert.AreEqual(BodyCaptureState.Complete, snap.RequestBodyCapture); + Assert.AreEqual("hello world", snap.RequestBodyText); + Assert.AreEqual(11L, snap.RequestBodyOriginalSize); + Assert.IsNull(snap.RequestTeeStream); + + // A request that is not in Streaming state (buffered / known length) is never teed. + var buffered = new SessionSnapshot { RequestBodyCapture = BodyCaptureState.Complete }; + live[session.HttpClient] = buffered; + ((Task)throttle.Invoke(interception, [interception, first])!).GetAwaiter().GetResult(); + Assert.IsNull(buffered.RequestTeeStream); + } + + [TestMethod] + public void FinalizeStreamingRequestBody_NoBytes_ResetsToNone_AndDisposesTee() + { + var finalize = typeof(InterceptionService).GetMethod("FinalizeStreamingRequestBody", PrivateStatic)!; + var snap = new SessionSnapshot + { + RequestBodyCapture = BodyCaptureState.Streaming, + RequestTeeStream = new MemoryStream(), + }; + finalize.Invoke(null, [snap]); + Assert.AreEqual(BodyCaptureState.None, snap.RequestBodyCapture); + Assert.IsNull(snap.RequestTeeStream); + } + + [TestMethod] + public void FinalizeStreamingBody_DerivesGrpcViewsFromStreamedPreview() + { + var finalize = typeof(InterceptionService).GetMethod("FinalizeStreamingBody", PrivateStatic)!; + // gRPC frame: flag 0, length 2, payload 0x08 0x01 (protobuf field 1 = 1). + byte[] frame = [0, 0, 0, 0, 2, 0x08, 0x01]; + var snap = new SessionSnapshot + { + IsGrpc = true, + ResponseBodyCapture = BodyCaptureState.Streaming, + ResponseBytesSeen = frame.Length, + ResponseTeeStream = new MemoryStream(frame), + }; + + finalize.Invoke(null, [snap]); + + Assert.AreEqual(BodyCaptureState.Complete, snap.ResponseBodyCapture); + Assert.IsNotNull(snap.GrpcFrames); + Assert.AreEqual(1, snap.GrpcFrames!.Count); + Assert.IsFalse(string.IsNullOrEmpty(snap.ProtobufDecodedText)); + } +} diff --git a/website/docs/inspector.md b/website/docs/inspector.md index d86288604..44389f849 100644 --- a/website/docs/inspector.md +++ b/website/docs/inspector.md @@ -90,7 +90,7 @@ Use **Tools → Composer / Breakpoints / AutoResponder / Scripts…** to open th - **SSE** — shown for `text/event-stream` responses; Inspector does **not** buffer SSE in `BeforeResponse` (events stream to the client; a 2 MiB preview may fill while open) - **Protobuf** — wire-format field dump for gRPC and gRPC-JSON-transcoded upstream frames (field number, wire type, value). MVP does **not** require a `.protoset` / descriptor set; the optional settings field `ProtobufDescriptorSetPath` is stored for a future typed decode. Until then, the Protobuf tab always shows the JSON wire dump. -**Body capture limits:** finite bodies with known `Content-Length` up to 32 MiB are buffered then previewed at 2 MiB (text view capped at 256 KiB). Larger known-length bodies are **not captured** so downloads are not stalled. Chunked/finite unknown-length bodies still buffer (capped). Size column shows the original length when known. +**Body capture limits:** finite bodies with known `Content-Length` up to 32 MiB are buffered then previewed at 2 MiB (text view capped at 256 KiB). Larger known-length bodies are **not captured** so downloads are not stalled. Bodies with **no declared length** (chunked, HTTP/2 without `Content-Length`, SSE) are never buffered, whatever their content type: they are relayed untouched and the first 2 MiB is previewed live (decoded for gzip/deflate/br), so large downloads/uploads of unknown size (for example a `git clone`) are not aborted by the buffer limit. Size column shows the original length when known. Search for WebSocket traffic with `is:ws`. Search for gRPC with `is:grpc`, and for gRPC-JSON transcoded sessions with `is:transcoded` (client REST/JSON vs upstream gRPC faces appear in the Headers/Body inspect panes). Quick filters on the toolbar toggle `hide:tunnel`, `hide:image`, and `is:error` into the same search box. Status classes (`status:2xx` … `status:5xx`), `process:`, and `content-type:` are also supported. The status strip shows **Sessions: N** with no filter, and **visible / total** when a search or quick filter is active. From 07fe21ac18d18596f4765739ffcb1008ad7484bc Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 17:37:37 +0530 Subject: [PATCH 04/11] fix: stream bridge bodies instead of aborting them against the whole-body budget HTTP/2 origin pipes, HTTP/3 frame reads, and oversize WebSocket frames were still bounded by MaxBufferedBodyBytes even when the body was never buffered. --- .../Config/ServerConfigApplier.cs | 3 +- .../Services/InterceptionService.cs | 26 +-- .../Services/ProtocolFrameInspectors.cs | 8 + .../Models/ServerConfig.cs | 2 + .../PublicAPI.Unshipped.txt | 2 + .../TwpConfigValidator.cs | 1 + .../EventArguments/SessionEventArgs.cs | 35 ++++ .../WebSocketFrameInterceptEventArgs.cs | 29 +++ .../Handlers/Http11ToHttp2BridgeHandler.cs | 110 +++++++--- .../Http2/Http2DeferredOutboundData.cs | 45 ++++- .../Http2/Http2OriginConnection.cs | 150 +++++++++++--- src/Titanium.Web.Proxy/Http3/Http3Frame.cs | 15 +- .../Http3/Http3OriginBridge.Http2.cs | 14 +- .../Http3/Http3OriginBridge.Quic.cs | 4 +- src/Titanium.Web.Proxy/Logging/ProxyLog.cs | 3 +- .../Network/Streams/BoundedBodyPipe.cs | 84 +++++++- .../Network/Streams/BoundedWriteStream.cs | 4 +- .../Options/PolicyFamily.cs | 33 ++- .../Options/ProxyPolicyModes.cs | 9 +- src/Titanium.Web.Proxy/ProxyLimits.cs | 2 +- src/Titanium.Web.Proxy/ProxyServer.cs | 17 +- .../PublicAPI.Unshipped.txt | 11 + .../WebSocket/WebSocketDecoder.cs | 65 +++++- .../WebSocket/WebSocketFrame.cs | 11 + .../WebSocket/WebSocketInterceptRelay.cs | 52 ++++- ...InterceptionUnknownLengthStreamingTests.cs | 45 +++++ .../BridgeBodyRelayTests.cs | 188 ++++++++++++++++++ .../Http2TranslationBridgeAcceptanceTests.cs | 37 ++++ .../BoundedBodyPipeTests.cs | 18 ++ .../Http2DeferredOutboundDataTests.cs | 31 ++- .../Http3ApiSurfaceTests.cs | 11 + .../Http3FrameTests.cs | 16 ++ .../WebSocketDecoderTests.cs | 26 +++ website/docs/inspector.md | 2 +- 34 files changed, 989 insertions(+), 120 deletions(-) create mode 100644 tests/Titanium.Web.Proxy.IntegrationTests/BridgeBodyRelayTests.cs diff --git a/src/Titanium.Cli/Config/ServerConfigApplier.cs b/src/Titanium.Cli/Config/ServerConfigApplier.cs index e3102bff5..f0bba73e9 100644 --- a/src/Titanium.Cli/Config/ServerConfigApplier.cs +++ b/src/Titanium.Cli/Config/ServerConfigApplier.cs @@ -345,7 +345,8 @@ private static void ApplyPolicyModes(ProxyServer proxy, PolicyModesConfig? polic ParsePolicyMode(policy.HeaderLimits, current[PolicyFamily.HeaderLimits]), ParsePolicyMode(policy.AdmissionControl, current[PolicyFamily.AdmissionControl]), ParsePolicyMode(policy.Http2AbuseBudget, current[PolicyFamily.Http2AbuseBudget]), - ParsePolicyMode(policy.Http2RelayValidation, current[PolicyFamily.Http2RelayValidation])); + ParsePolicyMode(policy.Http2RelayValidation, current[PolicyFamily.Http2RelayValidation]), + webSocketFrameBudget: ParsePolicyMode(policy.WebSocketFrameBudget, current[PolicyFamily.WebSocketFrameBudget])); if (policy.AllowAmbiguousFraming == true) { diff --git a/src/Titanium.Inspector/Services/InterceptionService.cs b/src/Titanium.Inspector/Services/InterceptionService.cs index 633d2cf15..fb6f13f92 100644 --- a/src/Titanium.Inspector/Services/InterceptionService.cs +++ b/src/Titanium.Inspector/Services/InterceptionService.cs @@ -255,6 +255,10 @@ public async Task StartAsync(IPAddress address, int port, CancellationToken canc // normal browsing (images, JS bundles) and RST'd the H2 stream. 32 MiB still bounds // memory while covering typical inspected payloads. _proxy.MaxBufferedBodyBytes = 32 * 1024 * 1024; + // Observing must not close an application that sends one large WebSocket frame. + _proxy.PolicyModes = _proxy.PolicyModes.With( + Titanium.Web.Proxy.Options.PolicyFamily.WebSocketFrameBudget, + Titanium.Web.Proxy.Options.PolicyMode.Observe); ApplyHttpProtocols(); _proxy.BeforeRequest += OnBeforeRequest; _proxy.BeforeResponse += OnBeforeResponse; @@ -2126,6 +2130,13 @@ private void AttachLiveWebSocketFrames(SessionEventArgs e, SessionSnapshot snap) SessionUpdated?.Invoke(this, snap); return Task.CompletedTask; }; + e.WebSocketFrameElided += (_, args) => + { + var direction = args.Direction == WebSocketFrameDirection.ClientToServer ? "Client" : "Server"; + live.Append(snap, ProtocolFrameInspectors.ElidedFrame(direction, args.OpCode.ToString(), args.DeclaredPayloadLength)); + SessionUpdated?.Invoke(this, snap); + return Task.CompletedTask; + }; } private async Task OnRequestBodyWriteThrottle(object sender, BeforeBodyWriteEventArgs e) @@ -2601,19 +2612,8 @@ private static string FormatHeaders(HeaderCollection headers) /// private bool ShouldBufferBody(RequestResponseBase message, SessionEventArgs session, bool isRequest) { - if (session.HttpClient.Request.UpgradeToWebSocket) - { - return false; - } - - var limit = session.MaxBufferedBodyBytes ?? _proxy?.MaxBufferedBodyBytes ?? (4 * 1024 * 1024); - if (limit <= 0) - { - return true; - } - - var contentLength = message.ContentLength; - return contentLength >= 0 && contentLength <= limit; + _ = isRequest; + return session.CanBufferBody(message); } public void Dispose() => EnsureShutdown(); diff --git a/src/Titanium.Inspector/Services/ProtocolFrameInspectors.cs b/src/Titanium.Inspector/Services/ProtocolFrameInspectors.cs index 5e051b2c1..1fa8b9d98 100644 --- a/src/Titanium.Inspector/Services/ProtocolFrameInspectors.cs +++ b/src/Titanium.Inspector/Services/ProtocolFrameInspectors.cs @@ -90,6 +90,14 @@ public static WebSocketFrameSnapshot FromLiveFrame(string direction, string opco PayloadPreview = Preview(data, opcode.Equals("Binary", StringComparison.OrdinalIgnoreCase) ? 2 : 1), }; + public static WebSocketFrameSnapshot ElidedFrame(string direction, string opcode, long declaredLength) => + new() + { + Direction = direction, + Opcode = opcode, + PayloadPreview = $"{opcode} frame, {declaredLength:N0} bytes, payload not captured", + }; + private static string OpcodeName(int opcode) => opcode switch { 0 => "Continuation", diff --git a/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs b/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs index d4c97e1d6..5a5561486 100644 --- a/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs +++ b/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs @@ -213,6 +213,8 @@ public sealed class PolicyModesConfig public string? Http2RelayValidation { get; set; } + public string? WebSocketFrameBudget { get; set; } + public bool? AllowAmbiguousFraming { get; set; } } diff --git a/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt index ac6570db5..35ed7997e 100644 --- a/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt @@ -202,6 +202,8 @@ Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http2AbuseBudget.get - Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http2AbuseBudget.set -> void Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http2RelayValidation.get -> string? Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http2RelayValidation.set -> void +Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.WebSocketFrameBudget.get -> string? +Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.WebSocketFrameBudget.set -> void Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.PolicyModesConfig() -> void Titanium.Web.Proxy.Configuration.Models.PoolingConfig Titanium.Web.Proxy.Configuration.Models.PoolingConfig.EnableConnectionPool.get -> bool? diff --git a/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs b/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs index a2006b6f6..40bef3079 100644 --- a/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs +++ b/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs @@ -268,6 +268,7 @@ private static void ValidatePolicyModes(PolicyModesConfig? policy, List RequireKnownPolicy(policy.AdmissionControl, "server.policyModes.admissionControl", errors); RequireKnownPolicy(policy.Http2AbuseBudget, "server.policyModes.http2AbuseBudget", errors); RequireKnownPolicy(policy.Http2RelayValidation, "server.policyModes.http2RelayValidation", errors); + RequireKnownPolicy(policy.WebSocketFrameBudget, "server.policyModes.webSocketFrameBudget", errors); } private static void ValidateUpstream(UpstreamConfig? upstream, List errors) diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index 8c9a23d23..c9c8debaf 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -228,6 +228,14 @@ public bool ReRequest /// public event AsyncEventHandler? BeforeWebSocketFrame; // NOSONAR S3264 -- Public extension event invoked by the WebSocket relay. + /// + /// Fired when is + /// and a frame exceeds + /// MaxWebSocketFramePayloadBytes. The payload is relayed unbuffered and is not passed + /// to . + /// + public event AsyncEventHandler? WebSocketFrameElided; // NOSONAR S3264 -- Invoked by the WebSocket relay in Observe mode. + /// /// Inject frames toward the remote server (client→server direction, masked). /// Available only while an intercepted WebSocket relay is active. @@ -242,6 +250,13 @@ public bool ReRequest internal bool HasWebSocketFrameInterceptHandler => BeforeWebSocketFrame != null; + internal bool HasWebSocketFrameElidedHandler => WebSocketFrameElided != null; + + internal Task InvokeWebSocketFrameElided(WebSocketFrameElidedEventArgs args) => + WebSocketFrameElided != null + ? WebSocketFrameElided.InvokeAsync(Server, args, Logger) + : Task.CompletedTask; + internal Task InvokeBeforeWebSocketFrame(WebSocketFrameInterceptEventArgs args) { return BeforeWebSocketFrame != null @@ -579,6 +594,26 @@ private async Task CopyResponseBodyAsync(IHttpStreamWriter writer, Transformatio } } + /// + /// Whether the proxy can buffer without exceeding the effective + /// budget. Unknown-length bodies (chunked, HTTP/2 or HTTP/3 + /// without Content-Length, server-sent events) return : they must be + /// relayed, not accumulated. A non-positive budget means unbounded and returns + /// . WebSocket upgrades are never buffered. + /// + public bool CanBufferBody(RequestResponseBase message) + { + if (HttpClient.Request.UpgradeToWebSocket) + return false; + + var limit = MaxBufferedBodyBytes ?? Server.MaxBufferedBodyBytes; + if (limit <= 0) + return true; + + var contentLength = message.ContentLength; + return contentLength >= 0 && contentLength <= limit; + } + /// /// Gets the request body as bytes. /// diff --git a/src/Titanium.Web.Proxy/EventArguments/WebSocketFrameInterceptEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/WebSocketFrameInterceptEventArgs.cs index c6605fe78..9247fc5ca 100644 --- a/src/Titanium.Web.Proxy/EventArguments/WebSocketFrameInterceptEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/WebSocketFrameInterceptEventArgs.cs @@ -97,3 +97,32 @@ public void Replace(byte[] newData, WebsocketOpCode? opCode = null) Action = WebSocketFrameInterceptAction.Replace; } } + +/// +/// Metadata for a WebSocket frame that exceeded the payload limit under +/// . The payload itself is not included. +/// +public sealed class WebSocketFrameElidedEventArgs : ProxyEventArgsBase +{ + internal WebSocketFrameElidedEventArgs(ProxyServer server, TcpClientConnection clientConnection, + SessionEventArgs session, WebSocketFrameDirection direction, WebsocketOpCode opCode, bool isFinal, + long declaredPayloadLength) + : base(server, clientConnection) + { + Session = session; + Direction = direction; + OpCode = opCode; + IsFinal = isFinal; + DeclaredPayloadLength = declaredPayloadLength; + } + + public SessionEventArgs Session { get; } + + public WebSocketFrameDirection Direction { get; } + + public WebsocketOpCode OpCode { get; } + + public bool IsFinal { get; } + + public long DeclaredPayloadLength { get; } +} diff --git a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs index cdd3b9612..1e74f2ba6 100644 --- a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs @@ -15,7 +15,9 @@ using Titanium.Web.Proxy.Http2; using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Network.Streams; using Titanium.Web.Proxy.Network.Tcp; +using Titanium.Web.Proxy.Options; namespace Titanium.Web.Proxy; @@ -82,8 +84,7 @@ public partial class ProxyServer { var tcp = await EstablishHttp2OriginTcpConnectionAsync(openSession, remoteHostName, remotePort, connectHost, connectPort, ct); - return await Http2OriginConnection.CreateAsync(tcp, logger, - openSession.MaxBufferedBodyBytes ?? MaxBufferedBodyBytes, ct, ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, ResourceLimits); }; try @@ -391,7 +392,7 @@ private async Task OfferRetainedHttp2OriginSeedAsync(SessionEventArgs args, stri try { var created = await Http2OriginConnection.CreateAsync(seedConnection, logger, - args.MaxBufferedBodyBytes ?? MaxBufferedBodyBytes, cancellationToken, ResourceLimits); + cancellationToken, ResourceLimits); Http2OriginConnectionPool.Offer(poolKey, created); } catch (Exception ex) @@ -691,16 +692,34 @@ private async Task EstablishHttp2OriginTcpConnectionAsync(S } else if (response.ContentLength < 0 && !response.IsChunked) { - var buffered = new MemoryStream(); - var streamBody = response.StreamBodyWriter; - response.StreamBodyWriter = null; - await streamBody(buffered, cancellationToken); - fastBody = buffered.ToArray(); - response.ContentLength = fastBody.Length; - await clientStream.WriteResponseWithWireBodyAsync(response, fastBody, cancellationToken); - response.IsBodyReceived = true; - response.IsBodySent = true; - return; + if (ClientCannotReceiveChunked(args)) + { + var streamBody = response.StreamBodyWriter!; + response.StreamBodyWriter = null; + fastBody = await BufferUnknownLengthForHttp10Async(args, streamBody, cancellationToken); + response.ContentLength = fastBody.Length; + await clientStream.WriteResponseWithWireBodyAsync(response, fastBody, cancellationToken); + response.IsBodyReceived = true; + response.IsBodySent = true; + return; + } + + if (exchange.OriginBodyComplete) + { + var buffered = new MemoryStream(); + var streamBody = response.StreamBodyWriter; + response.StreamBodyWriter = null; + await streamBody!(buffered, cancellationToken); + fastBody = buffered.ToArray(); + response.ContentLength = fastBody.Length; + await clientStream.WriteResponseWithWireBodyAsync(response, fastBody, cancellationToken); + response.IsBodyReceived = true; + response.IsBodySent = true; + return; + } + + // Still open (SSE, chunked upload result, any long-lived body). Send headers now. + response.IsChunked = true; } await clientStream.WriteResponseAsync(response, cancellationToken); @@ -800,18 +819,31 @@ await bodyWriter.CompleteAsync(response.HasTrailingHeaders ? response.TrailingHe } else if (response.ContentLength < 0 && !response.IsChunked) { - // The h2 origin sent no content-length (END_STREAM delimits its body). Buffer it here - - // the BoundedBodyPipe behind StreamBodyWriter already enforces MaxBufferedBodyBytes - so - // the HTTP/1.1 client keeps the Content-Length framing this bridge has always produced. - // Origin responses that do carry a content-length still stream straight through below. - var buffered = new MemoryStream(); - var streamBody = response.StreamBodyWriter; - response.StreamBodyWriter = null; - await streamBody(buffered, cancellationToken); - body = buffered.ToArray(); - response.ContentLength = body.Length; - response.Body = body; - response.BodyIsWireEncoded = true; + if (ClientCannotReceiveChunked(args)) + { + var streamBody = response.StreamBodyWriter!; + response.StreamBodyWriter = null; + body = await BufferUnknownLengthForHttp10Async(args, streamBody, cancellationToken); + response.ContentLength = body.Length; + response.Body = body; + response.BodyIsWireEncoded = true; + } + else if (exchange.OriginBodyComplete) + { + // Origin already finished. Drain (bounded by the stream window) and keep Content-Length. + var buffered = new MemoryStream(); + var streamBody = response.StreamBodyWriter; + response.StreamBodyWriter = null; + await streamBody!(buffered, cancellationToken); + body = buffered.ToArray(); + response.ContentLength = body.Length; + response.Body = body; + response.BodyIsWireEncoded = true; + } + else + { + response.IsChunked = true; + } } await clientStream.WriteResponseAsync(response, cancellationToken); @@ -834,6 +866,34 @@ await clientStream.WriteBodyAsync(body, response.IsChunked, response.IsBodySent = true; } + private static bool ClientCannotReceiveChunked(SessionEventArgs args) => + args.HttpClient.Request.HttpVersion == HttpHeader.Version10; + + /// + /// HTTP/1.0 has no chunked framing, so an unknown-length origin body has to be buffered. + /// The buffer uses the session body budget and , same as + /// every other whole-body path. + /// + private async Task BufferUnknownLengthForHttp10Async(SessionEventArgs args, + Func streamBody, CancellationToken cancellationToken) + { + var limit = args.MaxBufferedBodyBytes ?? MaxBufferedBodyBytes; + var mode = PolicyModes[PolicyFamily.BodyBudget]; + using var buffered = new MemoryStream(); + Stream target = limit > 0 ? new BoundedWriteStream(buffered, limit, mode) : buffered; + try + { + await streamBody(target, cancellationToken); + } + finally + { + if (!ReferenceEquals(target, buffered)) + await target.DisposeAsync(); + } + + return buffered.ToArray(); + } + private async Task RunHttp11ToHttp2WebSocketTunnelAsync(SessionEventArgs args, Http2OriginConnection originConnection, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken) diff --git a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs index 16ccbdbf0..4b9eaba59 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs @@ -18,11 +18,22 @@ internal sealed class Http2DeferredOutboundData internal const int MaxFramesPerStream = 4; /// - /// Hard cap: RST if more DATA arrives while already deferred. Sized for a 256 KiB body under - /// tight connection-window pressure (peers may emit ~1 KiB DATA frames, not only 16 KiB), - /// plus END_STREAM / burst margin for early-response duplex and slow-consumer backpressure. + /// Node backstop. The memory bound is . Tiny frames + /// are coalesced so a peer that sends 1-byte DATA cannot hit this by frame count alone. /// - internal const int HardMaxFramesPerStream = 256; + internal const int HardMaxFramesPerStream = 4096; + + /// + /// Bytes of parked DATA allowed per stream before RST_STREAM(ENHANCE_YOUR_CALM). + /// Matches the previous worst case of 256 full-size (16 KiB) frames, plus one frame of margin, + /// so a slow client is not failed sooner than before. Receive credit stays granted on receipt: + /// the queue is drained on the peer's write leg, and reclaiming stream credit from that leg + /// would take the other direction's write lock. Flow control on the send window is what stops + /// a healthy peer; this cap is the bound if that peer keeps sending anyway. + /// + internal const int HardMaxDeferredBytesPerStream = (4 * 1024 * 1024) + (16 * 1024); + + private const int CoalesceTarget = 16 * 1024; private readonly object gate = new(); // LinkedList so a partial drain can AddFirst the remainder without reordering later frames. @@ -96,7 +107,31 @@ public bool TryEnqueue(int streamId, byte[] rented, int offset, int payloadLengt roundRobinOrder.Add(streamId); } - if (q.Count >= HardMaxFramesPerStream) + if (!endStream && q.Count > 0) + { + var lastNode = q.Last!; + var last = lastNode.Value; + if (!last.EndStream + && last.PayloadLength > 0 + && last.PayloadLength < CoalesceTarget + && payloadLength > 0 + && last.PayloadLength + payloadLength <= CoalesceTarget) + { + var combined = last.PayloadLength + payloadLength; + var merged = ArrayPool.Shared.Rent(combined); + last.Rented.AsSpan(last.Offset, last.PayloadLength).CopyTo(merged); + rented.AsSpan(offset, payloadLength).CopyTo(merged.AsSpan(last.PayloadLength)); + ArrayPool.Shared.Return(last.Rented); + ArrayPool.Shared.Return(rented); + lastNode.Value = new PendingFrame(merged, 0, combined, endStream: false); + return true; + } + } + + var queuedBytes = 0; + foreach (var pending in q) + queuedBytes += pending.PayloadLength; + if (queuedBytes + payloadLength > HardMaxDeferredBytesPerStream || q.Count >= HardMaxFramesPerStream) return false; q.AddLast(new PendingFrame(rented, offset, payloadLength, endStream)); diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs index 5b5c8f792..44a5bfa0c 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs @@ -65,7 +65,6 @@ internal sealed class Http2OriginConnection : IDisposable private Stream stream; private Http2FrameWriter? frameWriter; private readonly ILogger logger; - private readonly long maxBufferedBodyBytes; private readonly ProxyResourceLimits resourceLimits; private readonly SemaphoreSlim writeLock = new(1, 1); private readonly Http2FlowController sendFlow = new(); @@ -99,14 +98,13 @@ internal sealed class Http2OriginConnection : IDisposable private Http2FrameHeader? encodeFrameHeader; private readonly byte[] encodeFrameHeaderBuffer = new byte[9]; - private Http2OriginConnection(TcpServerConnection connection, ILogger logger, long maxBufferedBodyBytes, + private Http2OriginConnection(TcpServerConnection connection, ILogger logger, ProxyResourceLimits resourceLimits) { this.connection = connection; socket = connection.Stream; stream = socket; this.logger = logger; - this.maxBufferedBodyBytes = maxBufferedBodyBytes; this.resourceLimits = resourceLimits; } @@ -312,10 +310,12 @@ private void TryDisposeIfRetiredAndIdle() /// always has a real MAX_CONCURRENT_STREAMS/frame-size budget to honor. /// internal static async Task CreateAsync(TcpServerConnection connection, - ILogger logger, long maxBufferedBodyBytes, CancellationToken cancellationToken, + ILogger logger, CancellationToken cancellationToken, ProxyResourceLimits? resourceLimits = null) { - var instance = new Http2OriginConnection(connection, logger, maxBufferedBodyBytes, + // Streaming bodies are bounded by HTTP/2 flow control (stream window × concurrent streams), + // not by MaxBufferedBodyBytes. That budget applies only where the library buffers a whole body. + var instance = new Http2OriginConnection(connection, logger, resourceLimits ?? ProxyResourceLimits.Default); var preface = Http2Helper.ConnectionPreface; @@ -370,7 +370,7 @@ internal static async Task CreateAsync(TcpServerConnectio // Allocate InterimChannel only when the caller will drain 1xx (on1xx != null). Passthrough // bridges wait on HeadersReceived instead — avoids per-request Channel/segment Gen0. - var pending = new PendingStream(maxBufferedBodyBytes, createInterimChannel: on1xx != null); + var pending = new PendingStream(createInterimChannel: on1xx != null); var streamId = 0; var streamOpened = false; var bodyHandedOff = false; @@ -404,6 +404,9 @@ internal static async Task CreateAsync(TcpServerConnectio throw new Http2OriginGoAwayException( $"The origin sent GOAWAY before stream {streamId} could be opened; it was never processed."); + var creditStreamId = streamId; + pending.OnBodyBytesConsumed = bytes => OnOriginBodyConsumed(creditStreamId, pending, bytes); + pending.OnBodyReadComplete = () => OnOriginBodyReadComplete(creditStreamId, pending); RegisterOpenedStream(streamId, pending); sendFlow.RegisterStream(streamId); streamOpened = true; @@ -635,7 +638,7 @@ await EnqueueDataWithFlowAsync(streamId, ReadOnlyMemory.Empty, endStream: response.TrailingHeaders.AddHeader(header); } - return new Http2OriginExchange(response, Array.Empty(), trailers); + return new Http2OriginExchange(response, Array.Empty(), trailers, pending.IsInboundComplete); } finally { @@ -903,6 +906,68 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap return Task.CompletedTask; } + /// + /// Connection-level credit only. Stream credit for piped bodies is granted from the consumer + /// () so a slow client cannot grow the pipe past the stream window. + /// + private Task FlushConnectionReceiveCreditAsync() + { + if (pendingConnectionReceiveCredit <= 0) + return Task.CompletedTask; + + var connectionBytes = pendingConnectionReceiveCredit; + pendingConnectionReceiveCredit = 0; + Http2Helper.EnqueueWindowUpdate(Writer, 0, connectionBytes); + return Task.CompletedTask; + } + + /// + /// Returns stream-window credit for body bytes the consumer has copied out. Single consumer per + /// stream; the batch threshold matches . + /// Worst-case buffered bytes are MaxConcurrentStreamsPerConnection (default 1000) times the + /// 768 KiB stream window. + /// + private void OnOriginBodyConsumed(int streamId, PendingStream pending, int bytes) + { + if (bytes <= 0) return; + + var owedAfter = Interlocked.Add(ref pending.StreamCreditOwed, -bytes); + var grant = bytes; + if (owedAfter < 0) + { + grant = bytes + owedAfter; + Interlocked.Add(ref pending.StreamCreditOwed, -owedAfter); + } + + if (grant <= 0) return; + var pendingCredit = Interlocked.Add(ref pending.UnflushedStreamCredit, grant); + if (pendingCredit >= Http2Helper.ReceiveCreditBatchThreshold) + FlushOriginStreamCredit(streamId, pending, force: false); + } + + private void OnOriginBodyReadComplete(int streamId, PendingStream pending) + { + var rest = Interlocked.Exchange(ref pending.StreamCreditOwed, 0); + if (rest > 0) + Interlocked.Add(ref pending.UnflushedStreamCredit, rest); + FlushOriginStreamCredit(streamId, pending, force: true); + } + + private void FlushOriginStreamCredit(int streamId, PendingStream pending, bool force) + { + while (true) + { + var amount = Volatile.Read(ref pending.UnflushedStreamCredit); + if (amount <= 0) return; + if (!force && amount < Http2Helper.ReceiveCreditBatchThreshold) return; + if (Interlocked.CompareExchange(ref pending.UnflushedStreamCredit, 0, amount) != amount) + continue; + if (StreamTableContains(streamId)) + Http2Helper.EnqueueWindowUpdate(Writer, streamId, amount); + return; + } + } + private async Task ReadLoopAsync(CancellationToken cancellationToken) // NOSONAR S3776 -- This protocol/state-machine path shares mutable parsing or transport state; splitting it further would create disproportionate regression risk. { var frameHeaderBuffer = new byte[9]; @@ -1116,6 +1181,7 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap case Http2FrameType.Data: { byte[]? rented = null; + var withholdStreamCredit = false; try { if (TryGetStream(streamId, out var pendingData)) @@ -1163,12 +1229,15 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap } else { - // Copy out of intake before Advance so BodyPipe may hold the memory - // if a rare backpressured write yields. + // Piped bodies: never block this loop (pause threshold is 0) and do not + // return stream credit until the consumer reads. Connection credit stays + // on receipt so one slow stream cannot shrink the connection window. + withholdStreamCredit = true; rented = ArrayPool.Shared.Rent(length); payloadSpan.CopyTo(rented); intake.Advance(length); var bodyData = StripDataFramingMemory(rented, length, flags); + var accepted = bodyData.IsEmpty; if (!bodyData.IsEmpty) { try @@ -1181,9 +1250,12 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap } else { - // Preserve per-stream DATA order; unlimited pipes complete sync. + // Known-CL overrun is the only reason this awaits; unknown-length + // pipes have no pause threshold and complete synchronously. await writeVt.ConfigureAwait(false); } + + accepted = true; } catch (BodySizeLimitExceededException) { @@ -1195,6 +1267,9 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap // Writer already completed (cancelled or stream failed); ignore stale frames. } } + + if (accepted && length > 0) + Interlocked.Add(ref pendingData.StreamCreditOwed, length); } } else @@ -1208,7 +1283,16 @@ private void ReleaseTunnelBookkeeping(int streamId, PendingStream pending, Semap ArrayPool.Shared.Return(rented); } - if ((flags & Http2FrameFlag.EndStream) != 0) + if (withholdStreamCredit) + { + if (length > 0) + pendingConnectionReceiveCredit += length; + if (pendingConnectionReceiveCredit >= Http2Helper.ReceiveCreditBatchThreshold) + await FlushConnectionReceiveCreditAsync().ConfigureAwait(false); + if ((flags & Http2FrameFlag.EndStream) != 0) + CompleteStream(streamId); + } + else if ((flags & Http2FrameFlag.EndStream) != 0) { // Tiny-GET hot path: END_STREAM closes the stream — skip stream // WINDOW_UPDATE and do not force-flush connection credit (was one @@ -1807,7 +1891,6 @@ private sealed class PendingStream : IDisposable internal const int InlineBodyThresholdBytes = 64 * 1024; private BoundedBodyPipe? bodyPipe; - private readonly long maxBodyBytes; private byte[]? inlineBody; private int inlineWritten; private int inboundComplete; @@ -1852,21 +1935,28 @@ private sealed class PendingStream : IDisposable internal Response? Response; internal HeaderCollection? TrailingHeaders; - internal PendingStream(long maxBodyBytes) - : this(maxBodyBytes, createInterimChannel: true) + /// On-wire DATA bytes read but not yet returned as stream WINDOW_UPDATE. + internal int StreamCreditOwed; + + /// Stream credit consumed by the reader and not yet flushed. + internal int UnflushedStreamCredit; + + internal Action? OnBodyBytesConsumed; + internal Action? OnBodyReadComplete; + + internal PendingStream() + : this(createInterimChannel: true) { } - /// Max buffered body bytes for . /// /// When , allocate the 1xx relay channel. Tests and interception paths /// that expect 1xx use this; passes when /// on1xx is null. /// - internal PendingStream(long maxBodyBytes, bool createInterimChannel) + internal PendingStream(bool createInterimChannel) { IsTunnel = false; - this.maxBodyBytes = maxBodyBytes; // BodyPipe is lazy: probe tiny-GET uses InlineBody; streaming / unknown CL creates on demand. if (createInterimChannel) { @@ -1875,10 +1965,9 @@ internal PendingStream(long maxBodyBytes, bool createInterimChannel) } } - private PendingStream(bool isTunnel) + private PendingStream(TunnelMarker _) { - IsTunnel = isTunnel; - maxBodyBytes = 0; + IsTunnel = true; // Tunnel streams never buffer a finite HTTP body; BodyPipe unused. TunnelDataChannel = Channel.CreateBounded(new BoundedChannelOptions(256) { @@ -1888,7 +1977,9 @@ private PendingStream(bool isTunnel) }); } - internal static PendingStream CreateTunnel() => new(true); + internal static PendingStream CreateTunnel() => new(new TunnelMarker()); + + private readonly struct TunnelMarker; /// /// When Content-Length is known and ≤ , allocate a @@ -1938,7 +2029,12 @@ internal BoundedBodyPipe EnsureBodyPipe() var existing = bodyPipe; if (existing != null) return existing; - var created = new BoundedBodyPipe(maxBodyBytes); + var contentLength = Response?.ContentLength ?? -1; + // Known length: overrun is a protocol error. Unknown length: no cumulative cap. + // Backpressure is off so ReadLoop never waits on the consumer. + var created = new BoundedBodyPipe(contentLength > 0 ? contentLength : 0, applyBackpressure: false); + created.OnBytesConsumed = OnBodyBytesConsumed; + created.OnReadCompleted = OnBodyReadComplete; var prior = Interlocked.CompareExchange(ref bodyPipe, created, null); if (prior != null) { @@ -2034,11 +2130,13 @@ public void AddHeader(ByteString name, ByteString value, bool sensitive) /// The fully materialized result of one exchange. internal sealed class Http2OriginExchange { - internal Http2OriginExchange(Response response, byte[] body, HeaderCollection? trailingHeaders) + internal Http2OriginExchange(Response response, byte[] body, HeaderCollection? trailingHeaders, + bool originBodyComplete = true) { Response = response; Body = body; TrailingHeaders = trailingHeaders; + OriginBodyComplete = originBodyComplete; } internal Response Response { get; } @@ -2046,6 +2144,12 @@ internal Http2OriginExchange(Response response, byte[] body, HeaderCollection? t internal byte[] Body { get; } internal HeaderCollection? TrailingHeaders { get; } + + /// + /// True when the origin had already finished the body before the caller started delivering it. + /// Unknown-length responses that are still open must be framed as chunked instead of buffered. + /// + internal bool OriginBodyComplete { get; } } /// diff --git a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs index abe86afe5..ef7f235b3 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs @@ -18,6 +18,14 @@ internal sealed class Http3Frame { private byte[]? rentedPayload; + /// + /// Default per-frame payload cap used when a caller passes maxPayloadBytes: 0. + /// Independent of ProxyServer.MaxBufferedBodyBytes: that budget bounds whole-body + /// buffering, not a single HTTP/3 DATA frame. 4 MiB covers legitimate frames without + /// renting an attacker-chosen length. + /// + internal const long DefaultMaxPayloadBytes = 4 * 1024 * 1024; + public ulong Type { get; } public ReadOnlyMemory Payload { get; } @@ -55,9 +63,12 @@ public void ReturnPayload() var payloadLength = await Http3VarInt.ReadAsync(stream, cancellationToken) ?? throw new Http3ConnectionException(Http3ErrorCode.FrameError, "Unexpected end of stream reading frame length."); - if (maxPayloadBytes > 0 && (long)payloadLength > maxPayloadBytes) + // 0 means "use the dedicated frame limit", not unlimited. A hostile peer picks + // payloadLength; checking before the int cast is what stops the rent. + var limit = maxPayloadBytes > 0 ? maxPayloadBytes : DefaultMaxPayloadBytes; + if (payloadLength > (ulong)limit || payloadLength > int.MaxValue) throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad, - $"HTTP/3 frame payload {payloadLength} bytes exceeds limit {maxPayloadBytes}."); + $"HTTP/3 frame payload {payloadLength} bytes exceeds limit {limit}."); if (payloadLength == 0) return new Http3Frame(frameType.Value, ReadOnlyMemory.Empty, null); diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs index 4340b927e..c60f3284b 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs @@ -218,8 +218,7 @@ private static async Task SendHttp2OriginFastWithGoAwayRetr null, sessionArgs); } - return await Http2OriginConnection.CreateAsync(tcp, logger, - fwd.MaxBufferedBodyBytes, ct, server.ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits); } finally { @@ -346,6 +345,13 @@ private static async Task SendHttp2OriginFastWithGoAwayRetr } } + /// + /// Buffers an H3 request body only when a reader was installed and the streaming pump was not. + /// Normal interception installs in + /// Http3RequestStream, and the TCP bridge prefers that pump, so this fallback is the + /// leftover whole-body path. It goes through GetRequestBody, which is bounded by + /// MaxBufferedBodyBytes. + /// private static async Task EnsureHttp3BufferedBodyAsync( SessionEventArgs sessionArgs, CancellationToken cancellationToken) { @@ -479,9 +485,7 @@ private readonly record struct Http2OriginTarget( null, sessionArgs); } - return await Http2OriginConnection.CreateAsync(tcp, logger, - sessionArgs.MaxBufferedBodyBytes ?? server.MaxBufferedBodyBytes, ct, - server.ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits); }, cancellationToken); } diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs index d6ccabd12..a8b353e4d 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs @@ -254,7 +254,7 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio } } - var maxPayload = sessionArgs.MaxBufferedBodyBytes ?? server.MaxBufferedBodyBytes; + var maxPayload = Http3Frame.DefaultMaxPayloadBytes; // Stream large / unknown-length bodies as DATA arrives (TTFB on big HTML). Tiny known-CL // must materialize first: H1 WriteResponseAsync + StreamBodyWriter emits a header-only @@ -666,7 +666,7 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Headers, encodedHeaders // Verbatim origin→client frame copy, or MITM capture when clientStream is null. // Tiny GET: coalesce HEADERS+DATA into one Quic write (origin probe sends both). const int relayCoalesceMaxBytes = 16 * 1024; - var maxPayload = Math.Max(fwd.MaxBufferedBodyBytes, server.MaxDecodedHeaderListBytes); + var maxPayload = Math.Max(Http3Frame.DefaultMaxPayloadBytes, server.MaxDecodedHeaderListBytes); var captureForMitm = clientStream is null; var sawFinalHeaders = false; while (true) diff --git a/src/Titanium.Web.Proxy/Logging/ProxyLog.cs b/src/Titanium.Web.Proxy/Logging/ProxyLog.cs index 1c6bd36de..ee1469c22 100644 --- a/src/Titanium.Web.Proxy/Logging/ProxyLog.cs +++ b/src/Titanium.Web.Proxy/Logging/ProxyLog.cs @@ -125,7 +125,7 @@ internal static void EffectiveProfileAtStartup(ILogger logger, Options.ProxyProf { if (!logger.IsEnabled(LogLevel.Information)) return; logger.LogInformation( - "Starting with profile {Profile} (body={Body}, decompressionRatio={DecompressionRatio}, headerLimits={HeaderLimits}, admission={Admission}, http2AbuseBudget={Http2AbuseBudget}, http2RelayValidation={Http2RelayValidation}, allowAmbiguousFraming={AllowAmbiguousFraming}).", + "Starting with profile {Profile} (body={Body}, decompressionRatio={DecompressionRatio}, headerLimits={HeaderLimits}, admission={Admission}, http2AbuseBudget={Http2AbuseBudget}, http2RelayValidation={Http2RelayValidation}, webSocketFrameBudget={WebSocketFrameBudget}, allowAmbiguousFraming={AllowAmbiguousFraming}).", profile, policyModes[Options.PolicyFamily.BodyBudget], policyModes[Options.PolicyFamily.DecompressionRatio], @@ -133,6 +133,7 @@ internal static void EffectiveProfileAtStartup(ILogger logger, Options.ProxyProf policyModes[Options.PolicyFamily.AdmissionControl], policyModes[Options.PolicyFamily.Http2AbuseBudget], policyModes[Options.PolicyFamily.Http2RelayValidation], + policyModes[Options.PolicyFamily.WebSocketFrameBudget], policyModes.AllowAmbiguousFraming); } diff --git a/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs b/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs index 48475bbcc..0f513a237 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs @@ -20,19 +20,55 @@ internal sealed class BoundedBodyPipe : IDisposable private long totalWritten; private bool disposed; + /// + /// Invoked on the consumer thread with the number of body bytes just copied out of the pipe. + /// Used to return HTTP/2 stream flow-control credit as the client actually reads. + /// + internal Action? OnBytesConsumed { get; set; } + + /// + /// Invoked once after the reader is completed (success or failure), so the caller can flush + /// any credit still owed for padding or a short final read. + /// + internal Action? OnReadCompleted { get; set; } + /// /// Initializes a new with the given byte limit. /// of 0 means unlimited. /// - internal BoundedBodyPipe(long maxBytes = 0) + /// + /// When , WriteAsync never waits for a reader. Origin response + /// pipes use this so the shared HTTP/2 read loop cannot stall sibling streams. Memory is then + /// bounded by receive flow control, not by a pause threshold. + /// + internal BoundedBodyPipe(long maxBytes = 0, bool applyBackpressure = true) { this.maxBytes = maxBytes; - // For unlimited pipes, disable backpressure (pauseWriterThreshold: 0) so that WriteAsync - // never blocks waiting for a reader. For bounded pipes, cap backpressure at 512 KB or the - // byte limit, whichever is smaller, to give callers early cancellation feedback. + // pauseWriterThreshold 0 still pauses once the pipe holds data (unconsumed >= 0). Origin + // response pipes pass applyBackpressure: false and use a threshold the stream window cannot + // reach, so ReadLoop's WriteAsync completes synchronously. + long pause; + long resume; + if (!applyBackpressure) + { + pause = long.MaxValue; + resume = long.MaxValue - 1; + } + else if (maxBytes > 0) + { + pause = Math.Min(maxBytes, 512 * 1024); + resume = Math.Min(maxBytes / 2, 256 * 1024); + if (resume >= pause) resume = Math.Max(0, pause - 1); + } + else + { + pause = 0; + resume = 0; + } + pipe = new Pipe(new PipeOptions( - pauseWriterThreshold: maxBytes > 0 ? Math.Min(maxBytes, 512 * 1024) : 0, - resumeWriterThreshold: maxBytes > 0 ? Math.Min(maxBytes, 256 * 1024) : 0, + pauseWriterThreshold: pause, + resumeWriterThreshold: resume, useSynchronizationContext: false)); } @@ -109,8 +145,37 @@ private async ValueTask WriteAsyncSlow(ValueTask flushVt, int byteC /// internal async Task CopyToAsync(Stream destination, CancellationToken cancellationToken = default) { - await pipe.Reader.CopyToAsync(destination, cancellationToken); - await pipe.Reader.CompleteAsync(); + try + { + while (true) + { + var result = await pipe.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); + var buffer = result.Buffer; + if (buffer.Length > 0) + { + foreach (var segment in buffer) + { + await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false); + if (segment.Length > 0) + OnBytesConsumed?.Invoke(segment.Length); + } + + pipe.Reader.AdvanceTo(buffer.End); + } + else + { + pipe.Reader.AdvanceTo(buffer.Start); + } + + if (result.IsCompleted || result.IsCanceled) + break; + } + } + finally + { + await pipe.Reader.CompleteAsync().ConfigureAwait(false); + OnReadCompleted?.Invoke(); + } } /// @@ -138,12 +203,15 @@ internal async Task ReadExactAsync(Memory destination, CancellationTo var toCopy = (int)Math.Min(buffer.Length, destination.Length - offset); buffer.Slice(0, toCopy).CopyTo(destination.Span.Slice(offset, toCopy)); offset += toCopy; + if (toCopy > 0) + OnBytesConsumed?.Invoke(toCopy); pipe.Reader.AdvanceTo(buffer.GetPosition(toCopy)); if (result.IsCompleted && offset < destination.Length) break; } await pipe.Reader.CompleteAsync().ConfigureAwait(false); + OnReadCompleted?.Invoke(); return offset; } diff --git a/src/Titanium.Web.Proxy/Network/Streams/BoundedWriteStream.cs b/src/Titanium.Web.Proxy/Network/Streams/BoundedWriteStream.cs index d29ac1b95..14254f6db 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/BoundedWriteStream.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/BoundedWriteStream.cs @@ -19,8 +19,8 @@ namespace Titanium.Web.Proxy.Network.Streams; /// and native HTTP/2 client-facing body interception in Http2Helper. Both only bound an /// individual chunk or DATA frame - per the hardening plan, "per-frame limits are not cumulative /// limits" - so an attacker sending many small chunks/frames could otherwise accumulate an -/// unbounded in-memory body. already solves the -/// same problem for the HTTP/2-to-origin body-streaming path; this type gives the whole-body +/// unbounded in-memory body. Origin response pipes bound a known Content-Length and use HTTP/2 +/// stream flow control for unknown-length bodies; this type gives the whole-body /// MemoryStream paths the same cumulative guarantee without adopting a full pipe. /// /// diff --git a/src/Titanium.Web.Proxy/Options/PolicyFamily.cs b/src/Titanium.Web.Proxy/Options/PolicyFamily.cs index d5d74d1d9..45b5511f3 100644 --- a/src/Titanium.Web.Proxy/Options/PolicyFamily.cs +++ b/src/Titanium.Web.Proxy/Options/PolicyFamily.cs @@ -12,9 +12,15 @@ namespace Titanium.Web.Proxy.Options; public enum PolicyFamily { /// - /// Cumulative whole-body buffering limits (MaxBufferedBodyBytes and the - /// / - /// pair) enforced via across H1, H2 and H3. + /// Cumulative whole-body buffering limits (MaxBufferedBodyBytes) enforced via + /// when a caller materializes a body. + /// Streaming relays do not consult this family. + /// + /// and + /// are reserved. They are accepted + /// by option plumbing and are not enforced on a live read path; the enforced whole-body + /// budget is MaxBufferedBodyBytes. + /// /// BodyBudget, @@ -98,5 +104,24 @@ public enum PolicyFamily /// refuses to replay non-idempotent methods and rejects bare CR/LF/NUL in header values. /// /// - Http1ReplaySafety + Http1ReplaySafety, + + /// + /// Per-frame WebSocket payload limit (MaxWebSocketFramePayloadBytes) applied only when + /// BeforeWebSocketFrame is subscribed. Raw relay is unaffected. + /// + /// (default on every profile) closes the connection with + /// status 1009 before the payload is buffered. + /// + /// + /// relays an oversize frame without materializing it and + /// raises SessionEventArgs.WebSocketFrameElided. Frame hooks are not invoked for + /// that payload, so a content filter must stay on . + /// + /// + /// skips the check and will buffer the frame, up to the + /// decoder's structural bound. + /// + /// + WebSocketFrameBudget } diff --git a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs index 2558e3fad..40cebf6e5 100644 --- a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs +++ b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs @@ -49,7 +49,8 @@ private ProxyPolicyModes(Dictionary modes, bool allowA admissionControl: PolicyMode.Enforce, http2AbuseBudget: PolicyMode.Enforce, http2RelayValidation: PolicyMode.Enforce, - http1ReplaySafety: PolicyMode.Enforce); + http1ReplaySafety: PolicyMode.Enforce, + webSocketFrameBudget: PolicyMode.Enforce); /// Returns the mode selected for . public PolicyMode this[PolicyFamily family] => modes[family]; @@ -62,7 +63,8 @@ public static ProxyPolicyModes Create( PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, - PolicyMode http1ReplaySafety = PolicyMode.Observe) + PolicyMode http1ReplaySafety = PolicyMode.Observe, + PolicyMode webSocketFrameBudget = PolicyMode.Enforce) { var dict = new Dictionary { @@ -72,7 +74,8 @@ public static ProxyPolicyModes Create( [PolicyFamily.AdmissionControl] = admissionControl, [PolicyFamily.Http2AbuseBudget] = http2AbuseBudget, [PolicyFamily.Http2RelayValidation] = http2RelayValidation, - [PolicyFamily.Http1ReplaySafety] = http1ReplaySafety + [PolicyFamily.Http1ReplaySafety] = http1ReplaySafety, + [PolicyFamily.WebSocketFrameBudget] = webSocketFrameBudget }; return new ProxyPolicyModes(dict, false); } diff --git a/src/Titanium.Web.Proxy/ProxyLimits.cs b/src/Titanium.Web.Proxy/ProxyLimits.cs index 8bd15816f..a17ae9045 100644 --- a/src/Titanium.Web.Proxy/ProxyLimits.cs +++ b/src/Titanium.Web.Proxy/ProxyLimits.cs @@ -40,7 +40,7 @@ public static class ProxyLimits /// Reference value only - see the type-level remarks. Matches the current default of /// ProxyServer.MaxBufferedBodyBytes: maximum buffered request or response body size for /// proxied exchanges where full buffering is required (body mutation, authentication retry, - /// etc.). + /// etc.). Streaming relays are not counted against this value. /// Default: 4 MiB. /// public static readonly long DefaultMaxBufferedBodyBytes = 4L * 1024 * 1024; diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 0afeb2355..4e56afb8d 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -755,18 +755,23 @@ internal Http3.Dns.IHttpsSvcbResolver HttpsSvcbResolver /// /// Maximum bytes the proxy will buffer for a single request or response body when - /// body buffering is required (body-read hooks, authentication retry, etc.). Bodies - /// larger than this limit are rejected with 413 (upstream request) or connection teardown - /// (upstream response). Set to 0 to disable the limit (not recommended). - /// Default: 4,194,304 (4 MiB). + /// whole-body buffering is required (GetRequestBody / GetResponseBody, + /// authentication retry, and other paths that materialize the body). Bodies larger than + /// this limit are rejected with 413 (upstream request) or connection teardown (upstream + /// response). Streaming relays (H1, H2, and H3, including bridges) are not counted against + /// this budget: they are bounded by protocol flow control. Set to 0 to disable the limit + /// (not recommended). Default: 4,194,304 (4 MiB). /// public int MaxBufferedBodyBytes { get; set; } = 4 * 1024 * 1024; /// /// Maximum WebSocket frame payload size in bytes that the proxy will accept during /// frame-level interception (i.e. when BeforeWebSocketFrame has at least one - /// subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket - /// connection to be closed with Close code 1009 (Message Too Big). + /// subscriber). Frames whose decoded payload exceeds this limit are handled by + /// . + /// (the default) closes the connection with Close code 1009 (Message Too Big) before the + /// payload is buffered. relays the frame unbuffered + /// and raises WebSocketFrameElided instead of invoking BeforeWebSocketFrame. /// Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check /// entirely and pass all frames through unvalidated. /// Default: 16,777,216 (16 MiB). diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt index 9fd2277e6..bf55eb710 100644 --- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt @@ -272,3 +272,14 @@ Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor.get -> System.Action void Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(string? host) -> bool static Titanium.Web.Proxy.Network.FirefoxCertificateTrust.LastEnterpriseRootsStep.get -> string? +*REMOVED*static Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode bodyBudget, Titanium.Web.Proxy.Options.PolicyMode decompressionRatio, Titanium.Web.Proxy.Options.PolicyMode headerLimits, Titanium.Web.Proxy.Options.PolicyMode admissionControl, Titanium.Web.Proxy.Options.PolicyMode http2AbuseBudget, Titanium.Web.Proxy.Options.PolicyMode http2RelayValidation = Titanium.Web.Proxy.Options.PolicyMode.Disabled, Titanium.Web.Proxy.Options.PolicyMode http1ReplaySafety = Titanium.Web.Proxy.Options.PolicyMode.Observe) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! +static Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode bodyBudget, Titanium.Web.Proxy.Options.PolicyMode decompressionRatio, Titanium.Web.Proxy.Options.PolicyMode headerLimits, Titanium.Web.Proxy.Options.PolicyMode admissionControl, Titanium.Web.Proxy.Options.PolicyMode http2AbuseBudget, Titanium.Web.Proxy.Options.PolicyMode http2RelayValidation = Titanium.Web.Proxy.Options.PolicyMode.Disabled, Titanium.Web.Proxy.Options.PolicyMode http1ReplaySafety = Titanium.Web.Proxy.Options.PolicyMode.Observe, Titanium.Web.Proxy.Options.PolicyMode webSocketFrameBudget = Titanium.Web.Proxy.Options.PolicyMode.Enforce) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! +Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody(Titanium.Web.Proxy.Http.RequestResponseBase! message) -> bool +Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketFrameElided -> Titanium.Web.Proxy.EventArguments.AsyncEventHandler? +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength.get -> long +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction.get -> Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal.get -> bool +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode.get -> Titanium.Web.Proxy.WebsocketOpCode +Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session.get -> Titanium.Web.Proxy.EventArguments.SessionEventArgs! +Titanium.Web.Proxy.Options.PolicyFamily.WebSocketFrameBudget = 7 -> Titanium.Web.Proxy.Options.PolicyFamily diff --git a/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs b/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs index cc15534ab..0567bcf8a 100644 --- a/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs +++ b/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs @@ -20,6 +20,11 @@ public class WebSocketDecoder private long bufferLength; private readonly long maxFramePayloadBytes; + private readonly bool relayOversizeFrames; + private long rawForwardRemaining; + private WebsocketOpCode rawOpCode; + private bool rawIsFinal; + private long rawDeclaredLength; /// Sizes the initial internal reassembly buffer. /// @@ -28,10 +33,16 @@ public class WebSocketDecoder /// structural bound below still applies) so existing call sites that /// do not have a policy limit to enforce keep their previous behavior. /// - internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = long.MaxValue) + /// + /// When (Observe mode), a frame over the limit is yielded as raw wire + /// slices instead of throwing. Enforce mode leaves this . + /// + internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = long.MaxValue, + bool relayOversizeFrames = false) { buffer = new byte[bufferPool.BufferSize]; this.maxFramePayloadBytes = maxFramePayloadBytes; + this.relayOversizeFrames = relayOversizeFrames; } /// @@ -57,6 +68,24 @@ internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = lo while (true) { + if (rawForwardRemaining > 0) + { + if (decodeBuffer.Length == 0) break; + var rawTake = (int)Math.Min(decodeBuffer.Length, rawForwardRemaining); + var rawSlice = decodeBuffer.Slice(0, rawTake).ToArray(); + rawForwardRemaining -= rawTake; + decodeBuffer = decodeBuffer.Slice(rawTake); + yield return new WebSocketFrame + { + RelayRaw = true, + OpCode = rawOpCode, + IsFinal = rawIsFinal, + DeclaredPayloadLength = rawDeclaredLength, + Data = rawSlice + }; + continue; + } + var data1 = decodeBuffer.Span; if (!IsDataEnough(data1)) break; @@ -93,7 +122,7 @@ internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = lo // WebSocketFrame.Data is ultimately sliced with a 32-bit length (below), so a // structurally valid but oversized 64-bit length can never be honored regardless of // any configured limit. - if (size > int.MaxValue) + if (size > int.MaxValue && !relayOversizeFrames) throw new WebSocketProtocolException( $"WebSocket frame declared a payload length of {size:N0} bytes, which exceeds " + $"the maximum of {int.MaxValue:N0} bytes this decoder can buffer.", @@ -107,11 +136,33 @@ internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = lo // without an upper bound to accumulate a frame that has not fully arrived yet. Checking here, // rather than after the frame is fully reassembled, is what keeps an attacker who declares an // oversized length and trickles bytes in slowly from forcing unbounded allocation. - if (size > maxFramePayloadBytes) - throw new WebSocketProtocolException( - $"WebSocket frame payload of {size:N0} bytes exceeds the configured limit of " + - $"{maxFramePayloadBytes:N0} bytes.", - 1009); + if (size > maxFramePayloadBytes || size > int.MaxValue) + { + if (!relayOversizeFrames) + throw new WebSocketProtocolException( + $"WebSocket frame payload of {size:N0} bytes exceeds the configured limit of " + + $"{maxFramePayloadBytes:N0} bytes.", + size > int.MaxValue ? (ushort)1002 : (ushort)1009); + + var frameWire = (long)idx + (masked ? 4 : 0) + size; + var takeNow = (int)Math.Min(data1.Length, frameWire); + var rawNow = decodeBuffer.Slice(0, takeNow).ToArray(); + rawForwardRemaining = frameWire - takeNow; + rawOpCode = opCode; + rawIsFinal = isFinal; + rawDeclaredLength = size; + decodeBuffer = decodeBuffer.Slice(takeNow); + yield return new WebSocketFrame + { + RelayRaw = true, + IsOversizeNotice = true, + OpCode = opCode, + IsFinal = isFinal, + DeclaredPayloadLength = size, + Data = rawNow + }; + continue; + } // The completeness check must also account for the 4-byte masking key (present right before // the payload whenever the mask bit is set) - otherwise, once just enough bytes have arrived diff --git a/src/Titanium.Web.Proxy/WebSocket/WebSocketFrame.cs b/src/Titanium.Web.Proxy/WebSocket/WebSocketFrame.cs index ba159fb65..3c95122aa 100644 --- a/src/Titanium.Web.Proxy/WebSocket/WebSocketFrame.cs +++ b/src/Titanium.Web.Proxy/WebSocket/WebSocketFrame.cs @@ -18,9 +18,20 @@ public class WebSocketFrame /// /// The unmasked frame payload (owned copy from ). + /// When is set, this is a slice of the original wire bytes to forward + /// unchanged, not a decoded payload. /// public ReadOnlyMemory Data { get; internal set; } + /// Forward as wire bytes. Do not re-encode or hand to frame hooks. + internal bool RelayRaw { get; set; } + + /// First raw slice of an oversize frame. Later slices of the same frame leave this false. + internal bool IsOversizeNotice { get; set; } + + /// Declared payload length of an oversize frame, not including the header or mask key. + internal long DeclaredPayloadLength { get; set; } + public string GetText() { return GetText(Encoding.UTF8); diff --git a/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs b/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs index af45900a2..aabb5c3d0 100644 --- a/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs +++ b/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs @@ -6,6 +6,7 @@ using Titanium.Web.Proxy.Diagnostics; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Logging; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.StreamExtended.BufferPool; namespace Titanium.Web.Proxy; @@ -28,15 +29,19 @@ internal static async Task RelayAsync(Stream clientStream, Stream serverStream, (b, o, c) => session.OnDataReceived(b, o, c)); var maxFramePayloadBytes = session.MaxWebSocketFramePayloadBytes ?? session.Server.MaxWebSocketFramePayloadBytes; + var frameBudget = session.Server.PolicyModes[PolicyFamily.WebSocketFrameBudget]; + var relayOversize = frameBudget == PolicyMode.Observe; + if (frameBudget == PolicyMode.Disabled) + maxFramePayloadBytes = int.MaxValue; var clientToServer = RelayDirectionAsync(clientStream, serverStream, bufferPool, - WebSocketFrameDirection.ClientToServer, maxFramePayloadBytes, session, + WebSocketFrameDirection.ClientToServer, maxFramePayloadBytes, relayOversize, session, serverWriteLock, cancellationTokenSource, onRead: (b, o, c) => { /* observational reads are implied by frames; wire bytes fire on write */ }, onWrite: (b, o, c) => session.OnDataSent(b, o, c)); var serverToClient = RelayDirectionAsync(serverStream, clientStream, bufferPool, - WebSocketFrameDirection.ServerToClient, maxFramePayloadBytes, session, + WebSocketFrameDirection.ServerToClient, maxFramePayloadBytes, relayOversize, session, clientWriteLock, cancellationTokenSource, onRead: (_, _, _) => { }, onWrite: (b, o, c) => session.OnDataReceived(b, o, c)); @@ -128,12 +133,12 @@ private static async Task TrySendCloseFrameAsync(Stream stream, SemaphoreSlim wr /// conformant Close for the frame/fragmentation violation that ended it. /// private static async Task RelayDirectionAsync(Stream source, Stream destination, IBufferPool bufferPool, // NOSONAR S3776 -- This protocol/state-machine path shares mutable parsing or transport state; splitting it further would create disproportionate regression risk. - WebSocketFrameDirection direction, long maxFramePayloadBytes, SessionEventArgs session, + WebSocketFrameDirection direction, long maxFramePayloadBytes, bool relayOversizeFrames, SessionEventArgs session, SemaphoreSlim writeLock, CancellationTokenSource cancellationTokenSource, Action onRead, Action onWrite) { var cancellationToken = cancellationTokenSource.Token; - var decoder = new WebSocketDecoder(bufferPool, maxFramePayloadBytes); + var decoder = new WebSocketDecoder(bufferPool, maxFramePayloadBytes, relayOversizeFrames); var messageTracker = new WebSocketMessageTracker(); var buffer = bufferPool.GetBuffer(); try @@ -150,6 +155,45 @@ private static async Task TrySendCloseFrameAsync(Stream stream, SemaphoreSlim wr { foreach (var frame in decoder.Decode(buffer, 0, read)) { + if (frame.RelayRaw) + { + if (frame.IsOversizeNotice) + { + ProxyMetrics.PolicyBreach(PolicyFamily.WebSocketFrameBudget, PolicyMode.Observe); + messageTracker.OnFrame(frame, out _, out var oversizeFragmentationError); + if (oversizeFragmentationError) + { + messageTracker.Reset(); + return 1002; + } + + if (session.HasWebSocketFrameElidedHandler) + { + await session.InvokeWebSocketFrameElided(new WebSocketFrameElidedEventArgs( + session.Server, session.ClientConnection, session, direction, frame.OpCode, + frame.IsFinal, frame.DeclaredPayloadLength)).ConfigureAwait(false); + } + } + + await writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + await destination.WriteAsync(frame.Data, cancellationToken).ConfigureAwait(false); + await destination.FlushAsync(cancellationToken).ConfigureAwait(false); + if (frame.Data.Length > 0) + { + var raw = frame.Data.ToArray(); + onWrite(raw, 0, raw.Length); + } + } + finally + { + writeLock.Release(); + } + + continue; + } + if (!ValidateWebSocketFrame(frame, out var closeCode)) { // RFC 6455 §7.2: a protocol error requires closing the connection. diff --git a/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs index 45bfe50dc..dbeffd680 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs @@ -49,6 +49,51 @@ private static (ProxyServer Proxy, SessionEventArgs Session, CancellationTokenSo return (proxy, session, cts); } + [TestMethod] + public void ShouldBufferBody_BuffersOnlyKnownLengthWithinTheAbortBudget() + { + var (proxy, session, cts) = CreateSession(); + using var _p = proxy; + using var _c = cts; + using var _s = session; + using var interception = new InterceptionService(new RecordingSystemProxyController()) { UseInMemoryTrustState = true }; + var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", PrivateInstance)!; + + const int abortBudget = 32 * 1024 * 1024; + session.MaxBufferedBodyBytes = abortBudget; + + // Known length within the budget is buffered, request and JSON response alike. + session.HttpClient.Request.ContentLength = 100; + Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + session.HttpClient.Response.ContentType = "application/json"; + session.HttpClient.Response.ContentLength = 100; + Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + + // Known length past the abort budget is relayed, not buffered. + session.HttpClient.Request.ContentLength = abortBudget + 1L; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + session.HttpClient.Response.ContentLength = abortBudget + 1L; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + + // Unknown length is never buffered, including a git push pack and a git clone pack result. + session.HttpClient.Request.RequestUriString = "https://github.com/org/repo.git/git-receive-pack"; + session.HttpClient.Request.ContentType = "application/x-git-receive-pack-request"; + session.HttpClient.Request.ContentLength = -1; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + + session.HttpClient.Response.ContentType = "application/x-git-upload-pack-result"; + session.HttpClient.Response.ContentLength = -1; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + + session.HttpClient.Response.ContentType = "text/event-stream"; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + + // Chunked JSON is the same rule: no declared length, so it is streamed. + session.HttpClient.Response.ContentType = "application/json"; + session.HttpClient.Response.IsChunked = true; + Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + } + [TestMethod] public void ApplyResponseBodyCapture_UnknownLengthUnread_IsStreamingRegardlessOfContentType() { diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/BridgeBodyRelayTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/BridgeBodyRelayTests.cs new file mode 100644 index 000000000..73c3d9941 --- /dev/null +++ b/tests/Titanium.Web.Proxy.IntegrationTests/BridgeBodyRelayTests.cs @@ -0,0 +1,188 @@ +using System; +using System.Net.Http; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.IntegrationTests.Setup; +using Titanium.Web.Proxy.Models; + +namespace Titanium.Web.Proxy.IntegrationTests; + +/// +/// Streamed bodies must not be aborted by the whole-body buffering budget, and a slow stream +/// must not stall its siblings on a pooled HTTP/2 origin connection. +/// +[DoNotParallelize] +[TestClass] +public class BridgeBodyRelayTests +{ + private static TestServer sharedServer = null!; + + [ClassInitialize] + public static void ClassSetup(TestContext _) + { + sharedServer = new TestServer(TestCertificateAuthority.ServerCertificate, requireMutualTls: false); + } + + [ClassCleanup(ClassCleanupBehavior.EndOfClass)] + public static void ClassCleanup() + { + sharedServer?.Dispose(); + } + + [TestMethod] + [Timeout(60_000)] + public async Task H1ToH2_KnownLengthAboveBodyBudget_IsRelayed() + { + var body = new byte[16 * 1024 * 1024]; + body.AsSpan().Fill(0x5A); + var expected = SHA256.HashData(body); + + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(async context => + { + context.Response.ContentLength = body.Length; + await context.Response.Body.WriteAsync(body); + }); + + var proxy = testSuite.GetProxy(); + proxy.EnableHttp2 = true; + proxy.MaxBufferedBodyBytes = 1024; + ForceHttp2Origin(proxy); + + var client = testSuite.GetClient(proxy); + var received = await client.GetByteArrayAsync(new Uri(server.ListeningHttpsUrl)); + CollectionAssert.AreEqual(expected, SHA256.HashData(received)); + } + + [TestMethod] + [Timeout(60_000)] + public async Task H1ToH2_UnknownLengthAboveBodyBudget_IsRelayed() + { + const int length = 16 * 1024 * 1024; + var chunk = new byte[64 * 1024]; + chunk.AsSpan().Fill(0x3C); + + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(async context => + { + for (var sent = 0; sent < length; sent += chunk.Length) + await context.Response.Body.WriteAsync(chunk); + }); + + var proxy = testSuite.GetProxy(); + proxy.EnableHttp2 = true; + proxy.MaxBufferedBodyBytes = 1024; + ForceHttp2Origin(proxy); + + var client = testSuite.GetClient(proxy); + var received = await client.GetByteArrayAsync(new Uri(server.ListeningHttpsUrl)); + Assert.AreEqual(length, received.Length); + } + + [TestMethod] + [Timeout(60_000)] + public async Task H1ToH1_KnownLengthAboveBodyBudget_IsRelayed() + { + var body = new byte[1024 * 1024]; + body.AsSpan().Fill(0x11); + + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(async context => + { + context.Response.ContentLength = body.Length; + await context.Response.Body.WriteAsync(body); + }); + + var proxy = testSuite.GetProxy(); + proxy.MaxBufferedBodyBytes = 1024; + + var client = testSuite.GetClient(proxy); + var received = await client.GetByteArrayAsync(new Uri(server.ListeningHttpsUrl)); + Assert.AreEqual(body.Length, received.Length); + } + + [TestMethod] + [Timeout(30_000)] + public async Task H1ToH2_ServerSentEvent_ArrivesBeforeTheOriginFinishes() + { + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(async context => + { + context.Response.ContentType = "text/event-stream"; + await context.Response.WriteAsync("data: hello\n\n"); + await context.Response.Body.FlushAsync(); + await Task.Delay(TimeSpan.FromSeconds(2), context.RequestAborted); + }); + + var proxy = testSuite.GetProxy(); + proxy.EnableHttp2 = true; + ForceHttp2Origin(proxy); + + var client = testSuite.GetClient(proxy); + using var response = await client.SendAsync( + new HttpRequestMessage(HttpMethod.Get, server.ListeningHttpsUrl), + HttpCompletionOption.ResponseHeadersRead); + var started = Environment.TickCount64; + using var reader = new System.IO.StreamReader(await response.Content.ReadAsStreamAsync()); + var line = await reader.ReadLineAsync(); + var elapsed = Environment.TickCount64 - started; + Assert.AreEqual("data: hello", line); + Assert.IsTrue(elapsed < 1500, $"first event took {elapsed} ms; the bridge buffered until the origin ended"); + } + + [TestMethod] + [Timeout(30_000)] + public async Task H1ToH2_SlowStream_DoesNotBlockASiblingOnTheSameOrigin() + { + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + var slowEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + server.HandleRequest(async context => + { + if (context.Request.Path.Value?.Contains("slow", StringComparison.Ordinal) == true) + { + slowEntered.TrySetResult(); + var buf = new byte[64 * 1024]; + for (var i = 0; i < 32; i++) + await context.Response.Body.WriteAsync(buf); + await Task.Delay(TimeSpan.FromSeconds(8), context.RequestAborted); + return; + } + + await context.Response.WriteAsync("fast"); + }); + + var proxy = testSuite.GetProxy(); + proxy.EnableHttp2 = true; + ForceHttp2Origin(proxy); + var client = testSuite.GetClient(proxy); + + var slow = client.SendAsync( + new HttpRequestMessage(HttpMethod.Get, server.ListeningHttpsUrl + "/slow"), + HttpCompletionOption.ResponseHeadersRead); + await slowEntered.Task.WaitAsync(TimeSpan.FromSeconds(10)); + + using var fastCts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var fast = await client.GetStringAsync(server.ListeningHttpsUrl + "/fast", fastCts.Token); + Assert.AreEqual("fast", fast); + _ = slow; + } + + private static void ForceHttp2Origin(ProxyServer proxy) + { + var endpoint = (ExplicitProxyEndPoint)proxy.ProxyEndPoints[0]; + endpoint.BeforeTunnelConnectRequest += (_, e) => + { + e.UpstreamHttpProtocol = UpstreamHttpProtocol.Http2; + e.AllowHttpProtocolTranslation = true; + return Task.CompletedTask; + }; + } +} diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs index bd9694d19..8ee28128a 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -268,8 +269,10 @@ public async Task H11ToH2Bridge_Large_Request_Body_Is_Relayed_Correctly() $"Expected an HTTP/1.1 200 response, got: '{statusLine}'."); var sawReceivedLengthHeader = false; + var chunked = false; string? line; while (!string.IsNullOrEmpty(line = await reader.ReadLineAsync())) + { if (line.StartsWith("X-Received-Length:", StringComparison.OrdinalIgnoreCase)) { sawReceivedLengthHeader = true; @@ -277,9 +280,18 @@ public async Task H11ToH2Bridge_Large_Request_Body_Is_Relayed_Correctly() $"The origin must have received the full {bodyBytes.Length}-byte body: '{line}'."); } + if (line.StartsWith("Transfer-Encoding:", StringComparison.OrdinalIgnoreCase) + && line.Contains("chunked", StringComparison.OrdinalIgnoreCase)) + chunked = true; + } + Assert.IsTrue(sawReceivedLengthHeader, "Expected to see the X-Received-Length response header."); + // A short unknown-length origin body may already be complete (Content-Length) or still open + // (chunked) depending on scheduling. Either framing is correct; the body bytes are the check. var responseBody = await reader.ReadToEndAsync(); + if (chunked) + responseBody = DecodeChunkedAscii(responseBody); Assert.AreEqual("large-body-received", responseBody); Assert.IsNull(exceptionCapture.LastException, $"No exception should be raised: {exceptionCapture.LastException}"); } @@ -663,4 +675,29 @@ await rawClient.Connection.WriteFrameAsync(Http2FrameType.WindowUpdate, 0, 0, return (body.ToArray(), sawShort, dataFrames); } + + private static string DecodeChunkedAscii(string raw) + { + var result = new StringBuilder(); + var i = 0; + while (i < raw.Length) + { + var lineEnd = raw.IndexOf("\r\n", i, StringComparison.Ordinal); + if (lineEnd < 0) break; + var sizeText = raw.Substring(i, lineEnd - i); + var semi = sizeText.IndexOf(';'); + if (semi >= 0) sizeText = sizeText.Substring(0, semi); + if (!int.TryParse(sizeText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var size) || size < 0) + break; + if (size == 0) break; + i = lineEnd + 2; + if (i + size > raw.Length) break; + result.Append(raw.Substring(i, size)); + i += size; + if (i + 2 <= raw.Length && raw.Substring(i, 2) == "\r\n") + i += 2; + } + + return result.ToString(); + } } diff --git a/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs b/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs index bf6fdb56e..7473e1722 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs @@ -11,6 +11,24 @@ namespace Titanium.Web.Proxy.UnitTests; [TestClass] public class BoundedBodyPipeTests { + [TestMethod] + [Timeout(5_000)] + public async Task WriteAsync_WithoutBackpressure_DoesNotWaitForAReader() + { + using var pipe = new BoundedBodyPipe(maxBytes: 2 * 1024 * 1024, applyBackpressure: false); + var chunk = new byte[1024 * 1024]; + await pipe.WriteAsync(chunk); + pipe.CompleteWriter(); + Assert.AreEqual(chunk.Length, pipe.TotalWritten); + + var consumed = 0; + pipe.OnBytesConsumed = n => consumed += n; + using var destination = new MemoryStream(); + await pipe.CopyToAsync(destination); + Assert.AreEqual(chunk.Length, consumed); + Assert.AreEqual(chunk.Length, destination.Length); + } + [TestMethod] public void Properties_ExposeReaderWriterAndTotalWritten() { diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http2DeferredOutboundDataTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http2DeferredOutboundDataTests.cs index 326fe73ea..ca7954b5c 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http2DeferredOutboundDataTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http2DeferredOutboundDataTests.cs @@ -147,14 +147,16 @@ public async Task DeferredQueue_PartialEndStream_ThenSecondStream_ThenWindowUpda public void TryEnqueue_HardCap_Rejects() { var deferred = new Http2DeferredOutboundData(); - for (var i = 0; i < Http2DeferredOutboundData.HardMaxFramesPerStream; i++) + const int frame = 16 * 1024; + var fit = Http2DeferredOutboundData.HardMaxDeferredBytesPerStream / frame; + for (var i = 0; i < fit; i++) { - var buf = ArrayPool.Shared.Rent(8); - Assert.IsTrue(deferred.TryEnqueue(7, buf, 0, 8, endStream: false)); + var buf = ArrayPool.Shared.Rent(frame); + Assert.IsTrue(deferred.TryEnqueue(7, buf, 0, frame, endStream: false), $"frame {i} should fit"); } - var overflow = ArrayPool.Shared.Rent(8); - Assert.IsFalse(deferred.TryEnqueue(7, overflow, 0, 8, endStream: false)); + var overflow = ArrayPool.Shared.Rent(frame); + Assert.IsFalse(deferred.TryEnqueue(7, overflow, 0, frame, endStream: false)); ArrayPool.Shared.Return(overflow); deferred.CancelStream(7); } @@ -288,8 +290,8 @@ public void ShouldThrottle_AndUnknownStreamHelpers() for (var i = 0; i < Http2DeferredOutboundData.MaxFramesPerStream; i++) { - var buf = ArrayPool.Shared.Rent(1); - Assert.IsTrue(deferred.TryEnqueue(3, buf, 0, 1, endStream: false)); + var buf = ArrayPool.Shared.Rent(16 * 1024); + Assert.IsTrue(deferred.TryEnqueue(3, buf, 0, 16 * 1024, endStream: false)); } Assert.IsTrue(deferred.ShouldThrottleReceiveCredit(3)); @@ -356,6 +358,21 @@ public async Task TryDrain_RoundRobinSkipsBlockedStream_AndCancelResetsIndex() Assert.AreEqual(3, ReadStreamId(bytes, 5)); } + [TestMethod] + public void TryEnqueue_TinyFrames_CoalesceUnderTheByteBudget() + { + var deferred = new Http2DeferredOutboundData(); + for (var i = 0; i < 300; i++) + { + var buf = ArrayPool.Shared.Rent(1); + buf[0] = 0x7; + Assert.IsTrue(deferred.TryEnqueue(1, buf, 0, 1, endStream: false)); + } + + Assert.AreEqual(1, deferred.PendingCount(1)); + deferred.CancelStream(1); + } + private static int ReadStreamId(byte[] buf, int offset) => ((buf[offset] & 0x7f) << 24) | (buf[offset + 1] << 16) | (buf[offset + 2] << 8) | buf[offset + 3]; } diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http3ApiSurfaceTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http3ApiSurfaceTests.cs index 99d36a604..418ab6975 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http3ApiSurfaceTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http3ApiSurfaceTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; #pragma warning disable TWP001 // Experimental HTTP/3 API — test intentionally exercises this surface @@ -20,6 +21,16 @@ public void ProxyServer_EnableHttp3_DefaultsFalse() Assert.IsFalse(proxy.EnableHttp3); } + [TestMethod] + public void ProxyServer_BodyBudgetAndWebSocketFrameBudget_KeepShippedDefaults() + { + using var proxy = new ProxyServer(); + Assert.AreEqual(4 * 1024 * 1024, proxy.MaxBufferedBodyBytes); + Assert.AreEqual(16 * 1024 * 1024, proxy.MaxWebSocketFramePayloadBytes); + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[PolicyFamily.WebSocketFrameBudget]); + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[PolicyFamily.BodyBudget]); + } + [TestMethod] public void ProxyServer_TryEnableHttp3IfSupported_MatchesOsQuic() { diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs index 851905c5c..78b32e67a 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs @@ -52,6 +52,22 @@ public async Task ReadAsync_EmptyStream_ReturnsNull() Assert.IsNull(frame); } + [TestMethod] + public async Task ReadAsync_DefaultLimit_RejectsHostileDeclaredLengthBeforeAllocating() + { + await using var ms = new MemoryStream(); + var header = new byte[16]; + var written = Http3VarInt.Write(header, (ulong)Http3FrameType.Data); + written += Http3VarInt.Write(header.AsSpan(written), (ulong)Http3Frame.DefaultMaxPayloadBytes + 1); + ms.Write(header, 0, written); + ms.Position = 0; + + var ex = await Assert.ThrowsExactlyAsync( + () => Http3Frame.ReadAsync(ms, maxPayloadBytes: 0, CancellationToken.None).AsTask()); + + Assert.AreEqual(Http3ErrorCode.ExcessiveLoad, ex.ErrorCode); + } + [TestMethod] public async Task ReadAsync_OversizedPayload_ThrowsExcessiveLoad() { diff --git a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs index 897b0adcf..ce10f296c 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs @@ -303,6 +303,32 @@ public void Decode_DeclaredLengthWithinConfiguredLimit_StillDecodesNormally() /// Hand-builds a raw WebSocket frame (RFC 6455 section 5.2) with the given opcode/payload, choosing /// the 7-bit/16-bit/64-bit length encoding automatically based on the payload size. /// + [TestMethod] + public void Decode_ObserveOversize_YieldsRawWireAndDoesNotThrow() + { + var decoder = new WebSocketDecoder(new FakeBufferPool(8192), maxFramePayloadBytes: 8, relayOversizeFrames: true); + var payload = new byte[20]; + for (var i = 0; i < payload.Length; i++) payload[i] = (byte)i; + var raw = BuildFrame(WebsocketOpCode.Binary, payload); + + var frames = decoder.Decode(raw, 0, raw.Length).ToList(); + + Assert.AreEqual(1, frames.Count); + Assert.IsTrue(frames[0].RelayRaw); + Assert.IsTrue(frames[0].IsOversizeNotice); + Assert.AreEqual(20, frames[0].DeclaredPayloadLength); + CollectionAssert.AreEqual(raw, frames[0].Data.ToArray()); + } + + [TestMethod] + public void Decode_EnforceOversize_StillThrows1009() + { + var decoder = CreateDecoder(maxFramePayloadBytes: 8); + var raw = BuildFrame(WebsocketOpCode.Binary, new byte[20]); + var ex = Assert.ThrowsExactly(() => decoder.Decode(raw, 0, raw.Length).ToList()); + Assert.AreEqual((ushort)1009, ex.CloseCode); + } + private static byte[] BuildFrame(WebsocketOpCode opCode, byte[] payload, bool mask = false, bool fin = true, uint maskKey = 0x11223344) { diff --git a/website/docs/inspector.md b/website/docs/inspector.md index 44389f849..6b3861fba 100644 --- a/website/docs/inspector.md +++ b/website/docs/inspector.md @@ -86,7 +86,7 @@ Use **Tools → Composer / Breakpoints / AutoResponder / Scripts…** to open th - **Headers** — request/response headers, cookies, query (labeled sections). **Copy headers** copies the dump. - **Body** — request and response as `=== Request ===` / `=== Response ===`. **Pretty** / **Raw** toggles JSON, XML, and HTML source indent (Pretty runs when the Body tab is selected). Images show a bitmap preview instead of mojibake. Banners explain truncated, not-captured, or streaming bodies. **Save request…** / **Save response…** write the **captured** bytes (incomplete when truncated). - **Hex** — labeled hex dump (first 4 KB of the captured preview). -- **WS Frames** — shown for WebSocket sessions; live frames when available (direction, opcode, payload preview) +- **WS Frames** — shown for WebSocket sessions; live frames when available (direction, opcode, payload preview). A single frame larger than 16 MiB is relayed and listed as "payload not captured" instead of closing the connection. - **SSE** — shown for `text/event-stream` responses; Inspector does **not** buffer SSE in `BeforeResponse` (events stream to the client; a 2 MiB preview may fill while open) - **Protobuf** — wire-format field dump for gRPC and gRPC-JSON-transcoded upstream frames (field number, wire type, value). MVP does **not** require a `.protoset` / descriptor set; the optional settings field `ProtobufDescriptorSetPath` is stored for a future typed decode. Until then, the Protobuf tab always shows the JSON wire dump. From d9f2602cb7182eaaea7cd6c539e2746cc0a8c082 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 18:44:08 +0530 Subject: [PATCH 05/11] fix: keep tiny-GET allocation-free, return H2 padding credit, validate oversize WS opcodes Review follow-up to the bridge body streaming change: - Http2OriginConnection no longer allocates a closure and two delegates per request to wire up stream credit. PendingStream implements IBodyConsumptionSink, so the InlineBody tiny-GET path attaches credit with two field stores. - DATA padding and the pad-length byte are returned to the stream window on receipt. Only real body bytes wait on the consumer, so a padded stream cannot stall at the stream window. - Observe-mode oversize WebSocket relay is limited to data opcodes. A control frame or reserved opcode with an oversize length closes with 1002, as the buffered path does. - Unit tests that reflect on the private Http2OriginConnection and PendingStream constructors now match the new signatures. --- .../Http2/Http2OriginConnection.cs | 49 +++++++++++++++---- .../Network/Streams/BoundedBodyPipe.cs | 36 +++++++++----- .../WebSocket/WebSocketDecoder.cs | 9 ++++ .../BoundedBodyPipeTests.cs | 17 +++++-- .../Http2BodyWriterAndOriginSettingsTests.cs | 10 ++-- .../Http2OriginActiveStreamCountTests.cs | 8 +-- .../SonarNewCodeCoverageTests.cs | 8 +-- .../WebSocketDecoderTests.cs | 38 ++++++++++++++ 8 files changed, 137 insertions(+), 38 deletions(-) diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs index 44a5bfa0c..4e4f5adbc 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs @@ -404,9 +404,7 @@ internal static async Task CreateAsync(TcpServerConnectio throw new Http2OriginGoAwayException( $"The origin sent GOAWAY before stream {streamId} could be opened; it was never processed."); - var creditStreamId = streamId; - pending.OnBodyBytesConsumed = bytes => OnOriginBodyConsumed(creditStreamId, pending, bytes); - pending.OnBodyReadComplete = () => OnOriginBodyReadComplete(creditStreamId, pending); + pending.AttachCredit(this, streamId); RegisterOpenedStream(streamId, pending); sendFlow.RegisterStream(streamId); streamOpened = true; @@ -945,6 +943,13 @@ private void OnOriginBodyConsumed(int streamId, PendingStream pending, int bytes FlushOriginStreamCredit(streamId, pending, force: false); } + private void ReturnOriginStreamCredit(int streamId, PendingStream pending, int bytes) + { + var pendingCredit = Interlocked.Add(ref pending.UnflushedStreamCredit, bytes); + if (pendingCredit >= Http2Helper.ReceiveCreditBatchThreshold) + FlushOriginStreamCredit(streamId, pending, force: false); + } + private void OnOriginBodyReadComplete(int streamId, PendingStream pending) { var rest = Interlocked.Exchange(ref pending.StreamCreditOwed, 0); @@ -1269,7 +1274,17 @@ private void FlushOriginStreamCredit(int streamId, PendingStream pending, bool f } if (accepted && length > 0) - Interlocked.Add(ref pendingData.StreamCreditOwed, length); + { + // Only real body bytes wait on the consumer. Padding (and the pad + // length byte) never reach the pipe, so a padded or all-padding + // stream must get that credit back now or it would stall at the + // stream window with nothing for the consumer to read. + var dataBytes = bodyData.Length; + if (dataBytes > 0) + Interlocked.Add(ref pendingData.StreamCreditOwed, dataBytes); + if (length > dataBytes) + ReturnOriginStreamCredit(streamId, pendingData, length - dataBytes); + } } } else @@ -1882,7 +1897,7 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo "Origin socket writes must go through Http2FrameWriter; mixed writes corrupt the HTTP/2 byte stream."); } - private sealed class PendingStream : IDisposable + private sealed class PendingStream : IDisposable, IBodyConsumptionSink { /// /// Known Content-Length bodies ≤ 64 KiB are filled here on the ReadLoop (no ). @@ -1941,8 +1956,24 @@ private sealed class PendingStream : IDisposable /// Stream credit consumed by the reader and not yet flushed. internal int UnflushedStreamCredit; - internal Action? OnBodyBytesConsumed; - internal Action? OnBodyReadComplete; + private Http2OriginConnection? creditOwner; + private int creditStreamId; + + /// + /// Two field stores, no allocation: this runs for every request, including the tiny-GET path + /// that never creates a body pipe. + /// + internal void AttachCredit(Http2OriginConnection owner, int streamId) + { + creditOwner = owner; + creditStreamId = streamId; + } + + void IBodyConsumptionSink.OnBytesConsumed(int bytes) => + creditOwner?.OnOriginBodyConsumed(creditStreamId, this, bytes); + + void IBodyConsumptionSink.OnReadCompleted() => + creditOwner?.OnOriginBodyReadComplete(creditStreamId, this); internal PendingStream() : this(createInterimChannel: true) @@ -2033,8 +2064,8 @@ internal BoundedBodyPipe EnsureBodyPipe() // Known length: overrun is a protocol error. Unknown length: no cumulative cap. // Backpressure is off so ReadLoop never waits on the consumer. var created = new BoundedBodyPipe(contentLength > 0 ? contentLength : 0, applyBackpressure: false); - created.OnBytesConsumed = OnBodyBytesConsumed; - created.OnReadCompleted = OnBodyReadComplete; + if (creditOwner != null) + created.ConsumptionSink = this; var prior = Interlocked.CompareExchange(ref bodyPipe, created, null); if (prior != null) { diff --git a/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs b/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs index 0f513a237..edb0b7aa9 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/BoundedBodyPipe.cs @@ -7,6 +7,22 @@ namespace Titanium.Web.Proxy.Network.Streams; +/// +/// Observes the consumer side of a . Used to return HTTP/2 +/// stream flow-control credit as the client actually reads. +/// +internal interface IBodyConsumptionSink +{ + /// Body bytes just copied out of the pipe, on the consumer thread. + void OnBytesConsumed(int bytes); + + /// + /// Once, after the reader is completed (success or failure), so the sink can flush any + /// credit still owed after a short final read. + /// + void OnReadCompleted(); +} + /// /// A bounded, cancellation-aware pipe wrapping /// for streaming HTTP body bytes between producer and consumer tasks. Enforces a @@ -21,16 +37,10 @@ internal sealed class BoundedBodyPipe : IDisposable private bool disposed; /// - /// Invoked on the consumer thread with the number of body bytes just copied out of the pipe. - /// Used to return HTTP/2 stream flow-control credit as the client actually reads. - /// - internal Action? OnBytesConsumed { get; set; } - - /// - /// Invoked once after the reader is completed (success or failure), so the caller can flush - /// any credit still owed for padding or a short final read. + /// Optional observer of the consumer side. An interface (not delegates) so attaching it costs no + /// per-request closure allocation on the HTTP/2 origin path. /// - internal Action? OnReadCompleted { get; set; } + internal IBodyConsumptionSink? ConsumptionSink { get; set; } /// /// Initializes a new with the given byte limit. @@ -157,7 +167,7 @@ internal async Task CopyToAsync(Stream destination, CancellationToken cancellati { await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false); if (segment.Length > 0) - OnBytesConsumed?.Invoke(segment.Length); + ConsumptionSink?.OnBytesConsumed(segment.Length); } pipe.Reader.AdvanceTo(buffer.End); @@ -174,7 +184,7 @@ internal async Task CopyToAsync(Stream destination, CancellationToken cancellati finally { await pipe.Reader.CompleteAsync().ConfigureAwait(false); - OnReadCompleted?.Invoke(); + ConsumptionSink?.OnReadCompleted(); } } @@ -204,14 +214,14 @@ internal async Task ReadExactAsync(Memory destination, CancellationTo buffer.Slice(0, toCopy).CopyTo(destination.Span.Slice(offset, toCopy)); offset += toCopy; if (toCopy > 0) - OnBytesConsumed?.Invoke(toCopy); + ConsumptionSink?.OnBytesConsumed(toCopy); pipe.Reader.AdvanceTo(buffer.GetPosition(toCopy)); if (result.IsCompleted && offset < destination.Length) break; } await pipe.Reader.CompleteAsync().ConfigureAwait(false); - OnReadCompleted?.Invoke(); + ConsumptionSink?.OnReadCompleted(); return offset; } diff --git a/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs b/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs index 0567bcf8a..ca81ab8e0 100644 --- a/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs +++ b/src/Titanium.Web.Proxy/WebSocket/WebSocketDecoder.cs @@ -144,6 +144,15 @@ internal WebSocketDecoder(IBufferPool bufferPool, long maxFramePayloadBytes = lo $"{maxFramePayloadBytes:N0} bytes.", size > int.MaxValue ? (ushort)1002 : (ushort)1009); + // Only data frames may skip buffering. A control frame (payload must be <= 125) or a + // reserved opcode with an oversize length is a protocol error in every mode, and raw + // relay would bypass the validation the buffered path applies. + if (opCode != WebsocketOpCode.Continuation && opCode != WebsocketOpCode.Text && + opCode != WebsocketOpCode.Binary) + throw new WebSocketProtocolException( + $"WebSocket frame with opcode {(int)opCode} declared a payload of {size:N0} bytes.", + 1002); + var frameWire = (long)idx + (masked ? 4 : 0) + size; var takeNow = (int)Math.Min(data1.Length, frameWire); var rawNow = decodeBuffer.Slice(0, takeNow).ToArray(); diff --git a/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs b/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs index 7473e1722..ed81c990d 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/BoundedBodyPipeTests.cs @@ -21,14 +21,25 @@ public async Task WriteAsync_WithoutBackpressure_DoesNotWaitForAReader() pipe.CompleteWriter(); Assert.AreEqual(chunk.Length, pipe.TotalWritten); - var consumed = 0; - pipe.OnBytesConsumed = n => consumed += n; + var sink = new CountingSink(); + pipe.ConsumptionSink = sink; using var destination = new MemoryStream(); await pipe.CopyToAsync(destination); - Assert.AreEqual(chunk.Length, consumed); + Assert.AreEqual(chunk.Length, sink.Consumed); + Assert.AreEqual(1, sink.Completed); Assert.AreEqual(chunk.Length, destination.Length); } + private sealed class CountingSink : IBodyConsumptionSink + { + internal int Consumed; + internal int Completed; + + public void OnBytesConsumed(int bytes) => Consumed += bytes; + + public void OnReadCompleted() => Completed++; + } + [TestMethod] public void Properties_ExposeReaderWriterAndTotalWritten() { diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs index 379eb8a5a..6671d5016 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs @@ -283,7 +283,7 @@ public async Task ProcessHeaderBlock_Status100Then200_WritesInterimAndFinal() using var origin = await CreateOriginConnectionShellAsync(); var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; var pending = Activator.CreateInstance(pendingType, BindingFlags.Instance | BindingFlags.NonPublic, - null, [1024L], null)!; + null, [], null)!; RegisterOpenedStream(origin, 1, pending); var process = GetProcessHeaderBlock(origin); @@ -312,7 +312,7 @@ public async Task ProcessHeaderBlock_PassthroughLite_Drops1xxAndSignalsHeadersRe using var origin = await CreateOriginConnectionShellAsync(); var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; var pending = Activator.CreateInstance(pendingType, BindingFlags.Instance | BindingFlags.NonPublic, - null, [1024L, false], null)!; + null, [false], null)!; Assert.IsNull(pendingType.GetField("InterimChannel", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)! .GetValue(pending)); @@ -340,7 +340,7 @@ public async Task ProcessHeaderBlock_TrailersWithoutStatus_Accumulate() using var origin = await CreateOriginConnectionShellAsync(); var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; var pending = Activator.CreateInstance(pendingType, BindingFlags.Instance | BindingFlags.NonPublic, - null, [1024L], null)!; + null, [], null)!; RegisterOpenedStream(origin, 3, pending); var process = GetProcessHeaderBlock(origin); @@ -477,9 +477,9 @@ private static async Task CreateOriginConnectionShellAsyn default, HttpHeader.Version20, null, null, "h2-origin"); var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, - [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(long), + [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(ProxyResourceLimits)], null)!; - return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, 1024L * 1024L, + return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, ProxyResourceLimits.Default])!; } } diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs index 89595d06b..63e53460e 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs @@ -32,13 +32,13 @@ public async Task ActiveStreamCount_TracksRegisterAndUnregister() var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; var pending = Activator.CreateInstance(pendingType, PrivateInstance, binder: null, - args: [0L], culture: null)!; + args: [], culture: null)!; InvokeRegister(connection, 1, pending); Assert.AreEqual(1, connection.ActiveStreamCount); InvokeRegister(connection, 3, Activator.CreateInstance(pendingType, PrivateInstance, binder: null, - args: [0L], culture: null)!); + args: [], culture: null)!); Assert.AreEqual(2, connection.ActiveStreamCount); Assert.IsTrue(InvokeTryUnregister(connection, 1)); @@ -159,9 +159,9 @@ private static async Task CreateShellAsync(ProxyServer pr default, HttpHeader.Version20, null, null, "h2-origin"); var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, - [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(long), + [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(ProxyResourceLimits)], null)!; - return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, 1024L * 1024L, + return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, ProxyResourceLimits.Default])!; } } diff --git a/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs b/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs index 249e9a119..07d355fa4 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs @@ -79,9 +79,9 @@ private static async Task CreateShellAsync(ProxyServer pr default, HttpHeader.Version20, null, null, "h2-origin"); var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, - [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(long), + [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), typeof(ProxyResourceLimits)], null)!; - return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, 1024L * 1024L, + return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, ProxyResourceLimits.Default])!; } @@ -653,7 +653,7 @@ public async Task OriginConnection_StreamTableGrowLookupAndDuplicateRegister() using var connection = await CreateShellAsync(proxy); var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; object Pending() => Activator.CreateInstance(pendingType, PrivateInstance, binder: null, - args: [0L], culture: null)!; + args: [], culture: null)!; var register = typeof(Http2OriginConnection).GetMethod("RegisterOpenedStream", PrivateInstance)!; var tryGet = typeof(Http2OriginConnection).GetMethod("TryGetStream", PrivateInstance)!; @@ -1380,7 +1380,7 @@ public async Task OriginPendingStream_InlineBodyPipeAndFailPending() using var proxy = new ProxyServer(false, false, false); using var connection = await CreateShellAsync(proxy); var pendingType = typeof(Http2OriginConnection).GetNestedType("PendingStream", BindingFlags.NonPublic)!; - var pending = Activator.CreateInstance(pendingType, PrivateInstance, binder: null, args: [0L], culture: null)!; + var pending = Activator.CreateInstance(pendingType, PrivateInstance, binder: null, args: [], culture: null)!; var tunnel = pendingType.GetMethod("CreateTunnel", BindingFlags.NonPublic | BindingFlags.Static)! .Invoke(null, null)!; diff --git a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs index ce10f296c..2b7ca27d8 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs @@ -320,6 +320,44 @@ public void Decode_ObserveOversize_YieldsRawWireAndDoesNotThrow() CollectionAssert.AreEqual(raw, frames[0].Data.ToArray()); } + [TestMethod] + public void Decode_ObserveOversizeControlFrame_StillThrows1002() + { + var decoder = new WebSocketDecoder(new FakeBufferPool(8192), maxFramePayloadBytes: 8, relayOversizeFrames: true); + var raw = BuildFrame(WebsocketOpCode.Ping, new byte[200]); + var ex = Assert.ThrowsExactly(() => decoder.Decode(raw, 0, raw.Length).ToList()); + Assert.AreEqual((ushort)1002, ex.CloseCode); + } + + [TestMethod] + public void Decode_ObserveOversize_SplitAcrossReads_RelaysEveryByteInOrderThenResumes() + { + var decoder = new WebSocketDecoder(new FakeBufferPool(8192), maxFramePayloadBytes: 8, relayOversizeFrames: true); + var payload = new byte[300]; + for (var i = 0; i < payload.Length; i++) payload[i] = (byte)i; + var big = BuildFrame(WebsocketOpCode.Binary, payload); + var small = BuildFrame(WebsocketOpCode.Text, Encoding.UTF8.GetBytes("ok")); + var wire = big.Concat(small).ToArray(); + + var relayed = new List(); + var decoded = new List(); + const int chunk = 37; + for (var offset = 0; offset < wire.Length; offset += chunk) + { + var count = Math.Min(chunk, wire.Length - offset); + var slice = wire.AsSpan(offset, count).ToArray(); + foreach (var frame in decoder.Decode(slice, 0, slice.Length)) + { + if (frame.RelayRaw) relayed.AddRange(frame.Data.ToArray()); + else decoded.Add(frame); + } + } + + CollectionAssert.AreEqual(big, relayed); + Assert.AreEqual(1, decoded.Count); + Assert.AreEqual("ok", Encoding.UTF8.GetString(decoded[0].Data.ToArray())); + } + [TestMethod] public void Decode_EnforceOversize_StillThrows1009() { From a463f994b7693ae7e38b31598255be4a61fc060f Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 19:00:57 +0530 Subject: [PATCH 06/11] chore(sonar): clear open code smells and the ones the bridge change would add - InterceptionService: drop the unused isRequest parameter from ShouldBufferBody and move the finite-unbuffered response branch into its own method (S1172, S3776). - SessionStreamBuffer: invoke SessionAdded through the captured delegate directly so Sonar sees the invocation (S3264); the per-batch snapshot of the handler is unchanged. - Http11ToHttp2BridgeHandler: remove the now-unused SessionEventArgs parameter from the seed offer and the redundant null-forgiving operators (S1172, S8969). - Http3FrameTests: remove an unnecessary ulong cast (S1905). --- .../Services/InterceptionService.cs | 20 ++++++++++++------- .../Services/SessionStreamBuffer.cs | 11 +++++----- .../Handlers/Http11ToHttp2BridgeHandler.cs | 14 ++++++------- ...ptionBodyCaptureAndTrustBgCoverageTests.cs | 4 ++-- .../InterceptionCaptureCoverageTests.cs | 18 ++++++++--------- ...InterceptionUnknownLengthStreamingTests.cs | 18 ++++++++--------- .../Http3FrameTests.cs | 2 +- 7 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/Titanium.Inspector/Services/InterceptionService.cs b/src/Titanium.Inspector/Services/InterceptionService.cs index fb6f13f92..437c82b2a 100644 --- a/src/Titanium.Inspector/Services/InterceptionService.cs +++ b/src/Titanium.Inspector/Services/InterceptionService.cs @@ -1664,7 +1664,7 @@ private SessionSnapshot CreateTunnelSnapshot(TunnelConnectSessionEventArgs e, Op { // Buffer when safe. GraphQL tools must NOT force GetRequestBody past the skip // (huge POST would RST HTTP/2 with ENHANCE_YOUR_CALM). - if (e.HttpClient.Request.HasBody && ShouldBufferBody(e.HttpClient.Request, e, isRequest: true)) + if (e.HttpClient.Request.HasBody && ShouldBufferBody(e.HttpClient.Request, e)) { e.HttpClient.Request.KeepBody = true; await e.GetRequestBody(CancellationToken.None); @@ -1759,7 +1759,7 @@ private async Task OnBeforeResponse(object sender, SessionEventArgs e) { try { - if (e.HttpClient.Response.HasBody && ShouldBufferBody(e.HttpClient.Response, e, isRequest: false)) + if (e.HttpClient.Response.HasBody && ShouldBufferBody(e.HttpClient.Response, e)) { e.HttpClient.Response.KeepBody = true; await e.GetResponseBody(CancellationToken.None); @@ -2488,6 +2488,15 @@ private static void ApplyResponseBodyCapture( return; } + ApplyFiniteUnbufferedResponseBody(snap, resp); + } + + /// + /// Known-length (or empty) response that was not buffered: either too large to capture, or + /// a body we chose to buffer but never read. Neither is previewed. + /// + private static void ApplyFiniteUnbufferedResponseBody(SessionSnapshot snap, Response resp) + { if (resp.HasBody && resp.ContentLength > InspectorBodyLimits.MaxMapLocalFileBytes) { snap.ResponseBodyCapture = BodyCaptureState.NotCaptured; @@ -2610,11 +2619,8 @@ private static string FormatHeaders(HeaderCollection headers) /// body-write tee instead. Known lengths within the budget are buffered (decompressed) as before; /// a non-positive budget means "unbounded" and always buffers. WebSocket upgrades are never buffered. /// - private bool ShouldBufferBody(RequestResponseBase message, SessionEventArgs session, bool isRequest) - { - _ = isRequest; - return session.CanBufferBody(message); - } + private static bool ShouldBufferBody(RequestResponseBase message, SessionEventArgs session) => + session.CanBufferBody(message); public void Dispose() => EnsureShutdown(); } diff --git a/src/Titanium.Inspector/Services/SessionStreamBuffer.cs b/src/Titanium.Inspector/Services/SessionStreamBuffer.cs index acb162421..80911813f 100644 --- a/src/Titanium.Inspector/Services/SessionStreamBuffer.cs +++ b/src/Titanium.Inspector/Services/SessionStreamBuffer.cs @@ -106,10 +106,11 @@ private void RaiseSessionAdded(List batch) { // One subscriber list for the whole batch. Re-reading the event per row would drop // the tail for a handler that unsubscribes on the first snapshot. - if (SessionAdded is { } handler) // NOSONAR S3264 -- Invoke the captured delegate so the batch shares one list. - { - foreach (var snapshot in batch) - handler(snapshot); - } + var handler = SessionAdded; + if (handler is null) + return; + + foreach (var snapshot in batch) + handler.Invoke(snapshot); } } diff --git a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs index 1e74f2ba6..1443aad14 100644 --- a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs @@ -254,7 +254,7 @@ public partial class ProxyServer remotePort, connectHost, connectPort); if (!seedOffered && retainedConnectionTask != null) { - await OfferRetainedHttp2OriginSeedAsync(args, poolKey, remoteHostName, remotePort, + await OfferRetainedHttp2OriginSeedAsync(poolKey, remoteHostName, remotePort, connectHost, connectPort, retainedConnectionTask, cancellationToken); retainedConnectionTask = null; seedOffered = true; @@ -289,7 +289,7 @@ await RunHttp11WebSocketHttp11FallbackAsync(args, remoteHostName, remotePort, remotePort, connectHost, connectPort); if (!seedOffered && retainedConnectionTask != null) { - await OfferRetainedHttp2OriginSeedAsync(args, poolKey, remoteHostName, remotePort, + await OfferRetainedHttp2OriginSeedAsync(poolKey, remoteHostName, remotePort, connectHost, connectPort, retainedConnectionTask, cancellationToken); retainedConnectionTask = null; seedOffered = true; @@ -352,7 +352,7 @@ await RunHttp11ToHttp2ExchangeAsync(args, poolKey, openFactory, remoteHostName, /// Offers the negotiation-retained TCP seed (when ALPN/h2c is valid) into the shared origin pool /// as an established . Releases invalid seeds. /// - private async Task OfferRetainedHttp2OriginSeedAsync(SessionEventArgs args, string poolKey, // NOSONAR S107 -- Parameters kept explicit to avoid allocating options bags on hot bridge/pool paths. + private async Task OfferRetainedHttp2OriginSeedAsync(string poolKey, // NOSONAR S107 -- Parameters kept explicit to avoid allocating options bags on hot bridge/pool paths. string remoteHostName, int remotePort, string? connectHost, int? connectPort, // NOSONAR S1172 -- retained for call-site / overload symmetry with exchange path Task retainedConnectionTask, CancellationToken cancellationToken) { @@ -694,7 +694,7 @@ private async Task EstablishHttp2OriginTcpConnectionAsync(S { if (ClientCannotReceiveChunked(args)) { - var streamBody = response.StreamBodyWriter!; + var streamBody = response.StreamBodyWriter; response.StreamBodyWriter = null; fastBody = await BufferUnknownLengthForHttp10Async(args, streamBody, cancellationToken); response.ContentLength = fastBody.Length; @@ -709,7 +709,7 @@ private async Task EstablishHttp2OriginTcpConnectionAsync(S var buffered = new MemoryStream(); var streamBody = response.StreamBodyWriter; response.StreamBodyWriter = null; - await streamBody!(buffered, cancellationToken); + await streamBody(buffered, cancellationToken); fastBody = buffered.ToArray(); response.ContentLength = fastBody.Length; await clientStream.WriteResponseWithWireBodyAsync(response, fastBody, cancellationToken); @@ -821,7 +821,7 @@ await bodyWriter.CompleteAsync(response.HasTrailingHeaders ? response.TrailingHe { if (ClientCannotReceiveChunked(args)) { - var streamBody = response.StreamBodyWriter!; + var streamBody = response.StreamBodyWriter; response.StreamBodyWriter = null; body = await BufferUnknownLengthForHttp10Async(args, streamBody, cancellationToken); response.ContentLength = body.Length; @@ -834,7 +834,7 @@ await bodyWriter.CompleteAsync(response.HasTrailingHeaders ? response.TrailingHe var buffered = new MemoryStream(); var streamBody = response.StreamBodyWriter; response.StreamBodyWriter = null; - await streamBody!(buffered, cancellationToken); + await streamBody(buffered, cancellationToken); body = buffered.ToArray(); response.ContentLength = body.Length; response.Body = body; diff --git a/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs b/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs index d048d2bba..3556816e2 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionBodyCaptureAndTrustBgCoverageTests.cs @@ -339,7 +339,7 @@ public void TeeThrottleAndEndlessStream_CoverEarlyReturnsAndCoalesce() var throttle = typeof(InterceptionService).GetMethod("OnResponseBodyWriteThrottle", PrivateInstance)!; var tee = typeof(InterceptionService).GetMethod("TeeResponseChunk", PrivateInstance)!; - var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", PrivateInstance)!; + var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; // Body already read → early return (no tee). session.HttpClient.Response.IsBodyRead = true; @@ -375,6 +375,6 @@ public void TeeThrottleAndEndlessStream_CoverEarlyReturnsAndCoalesce() session.HttpClient.Request.Headers.AddHeader("Upgrade", "websocket"); Assert.IsTrue(session.HttpClient.Request.UpgradeToWebSocket); - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); } } diff --git a/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs b/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs index 206323098..ed998d906 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionCaptureCoverageTests.cs @@ -183,7 +183,7 @@ public void HelperReflection_CoversTruncateFormatShouldBufferAndTiming() var req = new Request { RequestUriString = "https://host.test/x" }; Assert.AreEqual("host.test", (string?)tryHost.Invoke(null, [req])); - var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", flags)!; + var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; Assert.IsNotNull(shouldBuffer); var eval = typeof(InterceptionService).GetMethod("EvaluateUnixTrustSuccess", flags)!; @@ -338,38 +338,38 @@ public void FillResponse_CoversSseGrpcTranscodeMultipartAndWebsocket() Assert.IsTrue(previewSnap.IsGrpc); Assert.IsNotNull(previewSnap.WebSocketFrames); - var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", flags)!; + var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; // Clear WebSocket upgrade so buffering checks are not skipped as endless streams. session.HttpClient.Request.Headers.RemoveHeader("Upgrade"); session.MaxBufferedBodyBytes = 10; session.HttpClient.Request.ContentLength = 100; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); session.MaxBufferedBodyBytes = 0; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsTrue((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); session.MaxBufferedBodyBytes = 1024; // Unknown length can exceed the abort budget at any point: never buffered, whatever it is. session.HttpClient.Request.ContentLength = -1; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); foreach (var contentType in new[] { "text/event-stream", "application/json", "application/octet-stream", "application/x-anything" }) { session.HttpClient.Response.ContentType = contentType; session.HttpClient.Response.ContentLength = -1; Assert.IsFalse( - (bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!, + (bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!, contentType); } // Known length within the budget is buffered; above it is not. session.HttpClient.Response.ContentLength = 1024; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsTrue((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); session.HttpClient.Response.ContentLength = 1025; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); // No budget (<= 0): nothing can abort, so unknown length buffers too. session.MaxBufferedBodyBytes = 0; session.HttpClient.Response.ContentLength = -1; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsTrue((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); session.MaxBufferedBodyBytes = 1024; var throttleReq = typeof(InterceptionService).GetMethod("OnRequestBodyWriteThrottle", flags)!; diff --git a/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs index dbeffd680..97e9fdb22 100644 --- a/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs +++ b/tests/Titanium.Inspector.Tests/InterceptionUnknownLengthStreamingTests.cs @@ -57,41 +57,41 @@ public void ShouldBufferBody_BuffersOnlyKnownLengthWithinTheAbortBudget() using var _c = cts; using var _s = session; using var interception = new InterceptionService(new RecordingSystemProxyController()) { UseInMemoryTrustState = true }; - var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", PrivateInstance)!; + var shouldBuffer = typeof(InterceptionService).GetMethod("ShouldBufferBody", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!; const int abortBudget = 32 * 1024 * 1024; session.MaxBufferedBodyBytes = abortBudget; // Known length within the budget is buffered, request and JSON response alike. session.HttpClient.Request.ContentLength = 100; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsTrue((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); session.HttpClient.Response.ContentType = "application/json"; session.HttpClient.Response.ContentLength = 100; - Assert.IsTrue((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsTrue((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); // Known length past the abort budget is relayed, not buffered. session.HttpClient.Request.ContentLength = abortBudget + 1L; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); session.HttpClient.Response.ContentLength = abortBudget + 1L; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); // Unknown length is never buffered, including a git push pack and a git clone pack result. session.HttpClient.Request.RequestUriString = "https://github.com/org/repo.git/git-receive-pack"; session.HttpClient.Request.ContentType = "application/x-git-receive-pack-request"; session.HttpClient.Request.ContentLength = -1; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Request, session, true])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Request, session])!); session.HttpClient.Response.ContentType = "application/x-git-upload-pack-result"; session.HttpClient.Response.ContentLength = -1; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); session.HttpClient.Response.ContentType = "text/event-stream"; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); // Chunked JSON is the same rule: no declared length, so it is streamed. session.HttpClient.Response.ContentType = "application/json"; session.HttpClient.Response.IsChunked = true; - Assert.IsFalse((bool)shouldBuffer.Invoke(interception, [session.HttpClient.Response, session, false])!); + Assert.IsFalse((bool)shouldBuffer.Invoke(null, [session.HttpClient.Response, session])!); } [TestMethod] diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs index 78b32e67a..3d70f9fef 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameTests.cs @@ -57,7 +57,7 @@ public async Task ReadAsync_DefaultLimit_RejectsHostileDeclaredLengthBeforeAlloc { await using var ms = new MemoryStream(); var header = new byte[16]; - var written = Http3VarInt.Write(header, (ulong)Http3FrameType.Data); + var written = Http3VarInt.Write(header, Http3FrameType.Data); written += Http3VarInt.Write(header.AsSpan(written), (ulong)Http3Frame.DefaultMaxPayloadBytes + 1); ms.Write(header, 0, written); ms.Position = 0; From 1ac40e61a54f40ac81ca1d23b1a92ff762191bc1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:37:16 +0000 Subject: [PATCH 07/11] Update documentation --- ...oxy.EventArguments.ProxyEventArgsBase.html | 1 + ...Proxy.EventArguments.SessionEventArgs.html | 139 ++++++-- ...guments.WebSocketFrameElidedEventArgs.html | 323 ++++++++++++++++++ .../Titanium.Web.Proxy.EventArguments.html | 4 + ...tanium.Web.Proxy.Options.PolicyFamily.html | 31 +- ...um.Web.Proxy.Options.ProxyPolicyModes.html | 23 +- docs/api/Titanium.Web.Proxy.Options.html | 2 +- docs/api/Titanium.Web.Proxy.ProxyLimits.html | 2 +- docs/api/Titanium.Web.Proxy.ProxyServer.html | 165 ++++----- .../Titanium.Web.Proxy.WebSocketDecoder.html | 2 +- .../Titanium.Web.Proxy.WebSocketFrame.html | 10 +- docs/api/toc.html | 3 + docs/api/toc.json | 2 +- docs/index.json | 23 +- docs/xrefmap.yml | 108 +++++- 15 files changed, 696 insertions(+), 142 deletions(-) create mode 100644 docs/api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html b/docs/api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html index b3fda0fc1..8b2a0c593 100644 --- a/docs/api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html +++ b/docs/api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html @@ -98,6 +98,7 @@
Inheritance
SessionEventArgsBase
SocksAuthenticateEventArgs
TunnelConnectFailureEventArgs
+
WebSocketFrameElidedEventArgs
WebSocketFrameInterceptEventArgs
diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html index 3ece64110..0761bafa6 100644 --- a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html +++ b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html @@ -593,7 +593,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketClientWriter

@@ -716,7 +716,7 @@
Property Value
Edit this page - View Source + View Source

WebSocketServerWriter

@@ -745,12 +745,64 @@
Property Value

Methods

+ + | + Edit this page + + + View Source + + +

CanBufferBody(RequestResponseBase)

+

Whether the proxy can buffer message without exceeding the effective +MaxBufferedBodyBytes budget. Unknown-length bodies (chunked, HTTP/2 or HTTP/3 +without Content-Length, server-sent events) return false: they must be +relayed, not accumulated. A non-positive budget means unbounded and returns +true. WebSocket upgrades are never buffered.

+
+
+
Declaration
+
+
public bool CanBufferBody(RequestResponseBase message)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
RequestResponseBasemessage
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
bool
| Edit this page - View Source + View Source

Dispose(bool)

@@ -784,7 +836,7 @@
Overrides
Edit this page - View Source + View Source

DrainClientBodyAsync(CancellationToken)

@@ -843,7 +895,7 @@
Edit this page - View Source + View Source

DrainServerBodyAsync(CancellationToken)

@@ -899,7 +951,7 @@
Edit this page - View Source + View Source

GenericResponse(byte[], HttpStatusCode, IDictionary<string, HttpHeader>, bool)

@@ -952,7 +1004,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(byte[], HttpStatusCode, IEnumerable<HttpHeader>?, bool)

@@ -1005,7 +1057,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(string, HttpStatusCode, IDictionary<string, HttpHeader>?, bool)

@@ -1059,7 +1111,7 @@
Parameters
Edit this page - View Source + View Source

GenericResponse(string, HttpStatusCode, IEnumerable<HttpHeader>?, bool)

@@ -1113,7 +1165,7 @@
Parameters
Edit this page - View Source + View Source

GetRequestBody(CancellationToken)

@@ -1163,7 +1215,7 @@
Returns
Edit this page - View Source + View Source

GetRequestBodyAsString(CancellationToken)

@@ -1213,7 +1265,7 @@
Returns
Edit this page - View Source + View Source

GetResponseBody(CancellationToken)

@@ -1263,7 +1315,7 @@
Returns
Edit this page - View Source + View Source

GetResponseBodyAsString(CancellationToken)

@@ -1313,7 +1365,7 @@
Returns
Edit this page - View Source + View Source

Ok(byte[], IDictionary<string, HttpHeader>?, bool)

@@ -1360,7 +1412,7 @@
Parameters
Edit this page - View Source + View Source

Ok(byte[], IEnumerable<HttpHeader>?, bool)

@@ -1407,7 +1459,7 @@
Parameters
Edit this page - View Source + View Source

Ok(string, IDictionary<string, HttpHeader>?, bool)

@@ -1454,7 +1506,7 @@
Parameters
Edit this page - View Source + View Source

Ok(string, IEnumerable<HttpHeader>?, bool)

@@ -1501,7 +1553,7 @@
Parameters
Edit this page - View Source + View Source

Redirect(string, bool)

@@ -1541,7 +1593,7 @@
Parameters
Edit this page - View Source + View Source

Respond(Response, bool)

@@ -1588,7 +1640,7 @@
Edit this page - View Source + View Source

RespondStreaming(Response, Func<Stream, CancellationToken, Task>, bool)

@@ -1644,7 +1696,7 @@
Edit this page - View Source + View Source

RespondStreaming(StreamingProxyResult, bool)

@@ -1685,7 +1737,7 @@
Parameters
Edit this page - View Source + View Source

SetRequestBody(byte[])

@@ -1719,7 +1771,7 @@
Parameters
Edit this page - View Source + View Source

SetRequestBodyString(string)

@@ -1753,7 +1805,7 @@
Parameters
Edit this page - View Source + View Source

SetResponseBody(byte[])

@@ -1787,7 +1839,7 @@
Parameters
Edit this page - View Source + View Source

SetResponseBodyString(string)

@@ -1821,7 +1873,7 @@
Parameters
Edit this page - View Source + View Source

TerminateServerConnection()

@@ -1875,7 +1927,7 @@
Event Type
Edit this page - View Source + View Source

MultipartRequestPartSent

Occurs when multipart request part sent.

@@ -1900,6 +1952,39 @@
Event Type
+ + | + Edit this page + + + View Source + +

WebSocketFrameElided

+

Fired when WebSocketFrameBudget is +Observe and a frame exceeds +MaxWebSocketFramePayloadBytes. The payload is relayed unbuffered and is not passed +to BeforeWebSocketFrame.

+
+
+
Declaration
+
+
public event AsyncEventHandler<WebSocketFrameElidedEventArgs>? WebSocketFrameElided
+
+
Event Type
+ + + + + + + + + + + + + +
TypeDescription
AsyncEventHandler<WebSocketFrameElidedEventArgs>

Implements

IDisposable diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html b/docs/api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html new file mode 100644 index 000000000..5e0add027 --- /dev/null +++ b/docs/api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html @@ -0,0 +1,323 @@ + + + + + + + + Class WebSocketFrameElidedEventArgs | Titanium Web Proxy + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + +
+
+
+
+ +
+
+
+
+
+ +
+
+
    +
  • +
+
+
+
+
+ +
+
Search Results for
+
+

+
+
    +
    +
    +
    + +
    + Show / Hide Table of Contents +
    +
    +
    +
    +
    +
    +
    + + + +

    Class WebSocketFrameElidedEventArgs

    +

    Metadata for a WebSocket frame that exceeded the payload limit under +Observe. The payload itself is not included.

    +
    +
    +
    +
    Inheritance
    +
    object
    +
    EventArgs
    +
    ProxyEventArgsBase
    +
    WebSocketFrameElidedEventArgs
    +
    +
    +
    Inherited Members
    +
    + ProxyEventArgsBase.ClientUserData +
    +
    + EventArgs.Empty +
    +
    + object.Equals(object) +
    +
    + object.Equals(object, object) +
    +
    + object.GetHashCode() +
    +
    + object.GetType() +
    +
    + object.ReferenceEquals(object, object) +
    +
    + object.ToString() +
    +
    +
    Namespace: Titanium.Web.Proxy.EventArguments
    +
    Assembly: Titanium.Web.Proxy.dll
    +
    Syntax
    +
    +
    public sealed class WebSocketFrameElidedEventArgs : ProxyEventArgsBase
    +
    +

    Properties +

    + + | + Edit this page + + + View Source + + +

    DeclaredPayloadLength

    +
    +
    +
    Declaration
    +
    +
    public long DeclaredPayloadLength { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    long
    + + | + Edit this page + + + View Source + + +

    Direction

    +
    +
    +
    Declaration
    +
    +
    public WebSocketFrameDirection Direction { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    WebSocketFrameDirection
    + + | + Edit this page + + + View Source + + +

    IsFinal

    +
    +
    +
    Declaration
    +
    +
    public bool IsFinal { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    bool
    + + | + Edit this page + + + View Source + + +

    OpCode

    +
    +
    +
    Declaration
    +
    +
    public WebsocketOpCode OpCode { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    WebsocketOpCode
    + + | + Edit this page + + + View Source + + +

    Session

    +
    +
    +
    Declaration
    +
    +
    public SessionEventArgs Session { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    SessionEventArgs
    + +
    +
    + +
    +
    +
    +
      +
    • + Edit this page +
    • +
    • + View Source +
    • +
    +
    +
    +
    In this article
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + Back to top + + + Generated by DocFX +
    +
    +
    +
    + + + + + + diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.html b/docs/api/Titanium.Web.Proxy.EventArguments.html index 08f483f2a..c6f3204f1 100644 --- a/docs/api/Titanium.Web.Proxy.EventArguments.html +++ b/docs/api/Titanium.Web.Proxy.EventArguments.html @@ -147,6 +147,10 @@

    TunnelConnectSessionEventArgs

    A class that wraps the state when a tunnel connect event happen for Explicit endpoints.

    +
    +

    WebSocketFrameElidedEventArgs

    +

    Metadata for a WebSocket frame that exceeded the payload limit under +Observe. The payload itself is not included.

    WebSocketFrameInterceptEventArgs

    Frame-level interception context for an active WebSocket session.

    diff --git a/docs/api/Titanium.Web.Proxy.Options.PolicyFamily.html b/docs/api/Titanium.Web.Proxy.Options.PolicyFamily.html index 4c260ff61..8f24508f3 100644 --- a/docs/api/Titanium.Web.Proxy.Options.PolicyFamily.html +++ b/docs/api/Titanium.Web.Proxy.Options.PolicyFamily.html @@ -115,9 +115,15 @@

    Fields BodyBudget -

    Cumulative whole-body buffering limits (MaxBufferedBodyBytes and the -MaxEncodedBodyBytes/MaxDecodedBodyBytes -pair) enforced via Titanium.Web.Proxy.Network.Streams.BoundedWriteStream across H1, H2 and H3.

    +

    Cumulative whole-body buffering limits (MaxBufferedBodyBytes) enforced via +Titanium.Web.Proxy.Network.Streams.BoundedWriteStream when a caller materializes a body. +Streaming relays do not consult this family.

    +

    + MaxEncodedBodyBytes and + MaxDecodedBodyBytes are reserved. They are accepted + by option plumbing and are not enforced on a live read path; the enforced whole-body + budget is MaxBufferedBodyBytes. +

    @@ -194,6 +200,25 @@

    Fields and AllEnforce) decodes and sends GOAWAY(PROTOCOL_ERROR) on violations.

    + + + + WebSocketFrameBudget +

    Per-frame WebSocket payload limit (MaxWebSocketFramePayloadBytes) applied only when +BeforeWebSocketFrame is subscribed. Raw relay is unaffected.

    +

    + Enforce (default on every profile) closes the connection with + status 1009 before the payload is buffered. +

    +

    + Observe relays an oversize frame without materializing it and + raises SessionEventArgs.WebSocketFrameElided. Frame hooks are not invoked for + that payload, so a content filter must stay on Enforce. +

    +

    + Disabled skips the check and will buffer the frame, up to the + decoder's structural MaxValue bound. +

    diff --git a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html index 009caa159..7f333a531 100644 --- a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html +++ b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html @@ -96,7 +96,7 @@

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the + proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile.

    @@ -205,7 +205,7 @@

    Property Value
    Edit this page - View Source + View Source

    this[PolicyFamily]

    @@ -252,19 +252,19 @@

    Methods

    | - Edit this page + Edit this page - View Source + View Source -

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode)

    +

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode)

    Builds a snapshot with an explicit mode for every family. AllowAmbiguousFraming starts false.

    Declaration
    -
    public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe)
    +
    public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe, PolicyMode webSocketFrameBudget = PolicyMode.Enforce)
    Parameters
    @@ -311,6 +311,11 @@
    Parameters
    + + + + +
    http1ReplaySafety
    PolicyModewebSocketFrameBudget
    Returns
    @@ -333,7 +338,7 @@
    Returns
    Edit this page - View Source + View Source

    With(PolicyFamily, PolicyMode)

    @@ -386,7 +391,7 @@
    Returns
    Edit this page - View Source + View Source

    WithAllObservedExceptDisabled()

    @@ -420,7 +425,7 @@
    Returns
    Edit this page - View Source + View Source

    WithAllowAmbiguousFramingEnabled()

    diff --git a/docs/api/Titanium.Web.Proxy.Options.html b/docs/api/Titanium.Web.Proxy.Options.html index a2d3b8975..d9daac766 100644 --- a/docs/api/Titanium.Web.Proxy.Options.html +++ b/docs/api/Titanium.Web.Proxy.Options.html @@ -101,7 +101,7 @@

    Prox through" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the - proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the + proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile.

    diff --git a/docs/api/Titanium.Web.Proxy.ProxyLimits.html b/docs/api/Titanium.Web.Proxy.ProxyLimits.html index 9b44fa982..3ec25c79a 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyLimits.html +++ b/docs/api/Titanium.Web.Proxy.ProxyLimits.html @@ -188,7 +188,7 @@

    Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxBufferedBodyBytes: maximum buffered request or response body size for proxied exchanges where full buffering is required (body mutation, authentication retry, -etc.). +etc.). Streaming relays are not counted against this value. Default: 4 MiB.

    diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index ae4b25aba..757fd157c 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyServer.html +++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html @@ -259,7 +259,7 @@

    Fields Edit this page - View Source + View Source

    DefaultViaHeaderPseudonym

    Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym @@ -292,7 +292,7 @@

    Properties Edit this page - View Source + View Source

    AdmittedClientConnectionCount

    @@ -326,7 +326,7 @@
    Property Value
    Edit this page - View Source + View Source

    BlockLoopbackDestinations

    @@ -375,7 +375,7 @@
    Property Value
    Edit this page - View Source + View Source

    BlockPrivateNetworkDestinations

    @@ -427,7 +427,7 @@
    Property Value
    Edit this page - View Source + View Source

    BufferPool

    @@ -461,7 +461,7 @@
    Property Value
    Edit this page - View Source + View Source

    CertificateManager

    @@ -524,7 +524,7 @@
    Property Value
    Edit this page - View Source + View Source

    ClientConnectionCount

    @@ -556,7 +556,7 @@
    Property Value
    Edit this page - View Source + View Source

    ClientHeaderTimeoutSeconds

    @@ -634,7 +634,7 @@
    Property Value
    Edit this page - View Source + View Source

    ConnectTimeOutSeconds

    @@ -666,7 +666,7 @@
    Property Value
    Edit this page - View Source + View Source

    ConnectionTimeOutSeconds

    @@ -699,7 +699,7 @@
    Property Value
    Edit this page - View Source + View Source

    CustomUpStreamProxyFailureFunc

    @@ -899,7 +899,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableConnectionPool

    @@ -1076,7 +1076,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableHttpInterception

    @@ -1151,7 +1151,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableIpv6UnreachableSoftSkip

    @@ -1221,7 +1221,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableRequestTimingCapture

    @@ -1314,7 +1314,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableTcpKeepAlive

    @@ -1346,7 +1346,7 @@
    Property Value
    Edit this page - View Source + View Source

    EnableTcpServerConnectionPrefetch

    @@ -1417,7 +1417,7 @@
    Property Value
    Edit this page - View Source + View Source

    EndpointAdmissionRejectionCount

    @@ -1481,7 +1481,7 @@
    Property Value
    Edit this page - View Source + View Source

    GetCustomUpStreamProxyFunc

    @@ -1513,7 +1513,7 @@
    Property Value
    Edit this page - View Source + View Source

    GlobalAdmissionRejectionCount

    @@ -1545,7 +1545,7 @@
    Property Value
    Edit this page - View Source + View Source

    Http3ClientConnectionCount

    @@ -1576,7 +1576,7 @@
    Property Value
    Edit this page - View Source + View Source

    Http3ServerConnectionCount

    @@ -1608,7 +1608,7 @@
    Property Value
    Edit this page - View Source + View Source

    HttpResponseCacheFiller

    @@ -1641,7 +1641,7 @@
    Property Value
    Edit this page - View Source + View Source

    IdleReadTimeoutSeconds

    @@ -1674,7 +1674,7 @@
    Property Value
    Edit this page - View Source + View Source

    IdleWriteTimeoutSeconds

    @@ -1741,7 +1741,7 @@
    Property Value
    Edit this page - View Source + View Source

    ListenerBackLog

    @@ -1772,7 +1772,7 @@
    Property Value
    Edit this page - View Source + View Source

    Logger

    @@ -1804,7 +1804,7 @@
    Property Value
    Edit this page - View Source + View Source

    Logging

    @@ -1845,15 +1845,17 @@
    Property Value
    Edit this page - View Source + View Source

    MaxBufferedBodyBytes

    Maximum bytes the proxy will buffer for a single request or response body when -body buffering is required (body-read hooks, authentication retry, etc.). Bodies -larger than this limit are rejected with 413 (upstream request) or connection teardown -(upstream response). Set to 0 to disable the limit (not recommended). -Default: 4,194,304 (4 MiB).

    +whole-body buffering is required (GetRequestBody / GetResponseBody, +authentication retry, and other paths that materialize the body). Bodies larger than +this limit are rejected with 413 (upstream request) or connection teardown (upstream +response). Streaming relays (H1, H2, and H3, including bridges) are not counted against +this budget: they are bounded by protocol flow control. Set to 0 to disable the limit +(not recommended). Default: 4,194,304 (4 MiB).

    Declaration
    @@ -1880,7 +1882,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxCachedConnections

    @@ -1934,7 +1936,7 @@
    Exceptions
    Edit this page - View Source + View Source

    MaxConcurrentClientConnections

    @@ -1974,7 +1976,7 @@
    Property Value
    Edit this page - View Source + View Source

    MaxConcurrentHttp11HttpsOriginCreates

    @@ -2060,14 +2062,17 @@
    Property Value
    Edit this page - View Source + View Source

    MaxWebSocketFramePayloadBytes

    Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one -subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket -connection to be closed with Close code 1009 (Message Too Big). +subscriber). Frames whose decoded payload exceeds this limit are handled by +WebSocketFrameBudget. Enforce +(the default) closes the connection with Close code 1009 (Message Too Big) before the +payload is buffered. Observe relays the frame unbuffered +and raises WebSocketFrameElided instead of invoking BeforeWebSocketFrame. Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB).

    @@ -2128,7 +2133,7 @@
    Property Value
    Edit this page - View Source + View Source

    NoDelay

    @@ -2160,7 +2165,7 @@
    Property Value
    Edit this page - View Source + View Source

    OriginHttpVersionPolicy

    @@ -2199,7 +2204,7 @@
    Property Value
    Edit this page - View Source + View Source

    PolicyModes

    @@ -2245,7 +2250,7 @@
    Property Value
    Edit this page - View Source + View Source

    Profile

    @@ -2291,7 +2296,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyAuthenticationRealm

    @@ -2322,7 +2327,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyAuthenticationSchemes

    @@ -2355,7 +2360,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyBasicAuthenticateFunc

    @@ -2388,7 +2393,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxyEndPoints

    @@ -2450,7 +2455,7 @@
    Property Value
    Edit this page - View Source + View Source

    ProxySchemeAuthenticateFunc

    @@ -2484,7 +2489,7 @@
    Property Value
    Edit this page - View Source + View Source

    RequestTimeoutSeconds

    @@ -2559,7 +2564,7 @@
    Property Value
    Edit this page - View Source + View Source

    ResponseHeaderContributor

    @@ -2591,7 +2596,7 @@
    Property Value
    Edit this page - View Source + View Source

    ResponseHeaderTimeoutSeconds

    @@ -2631,7 +2636,7 @@
    Property Value
    Edit this page - View Source + View Source

    ReuseSocket

    @@ -2664,7 +2669,7 @@
    Property Value
    Edit this page - View Source + View Source

    ReverseProxy

    @@ -2696,7 +2701,7 @@
    Property Value
    Edit this page - View Source + View Source

    ServerConnectionCount

    @@ -2728,7 +2733,7 @@
    Property Value
    Edit this page - View Source + View Source

    ShouldInterceptHttp

    @@ -2762,7 +2767,7 @@
    Property Value
    Edit this page - View Source + View Source

    SupportedServerSslProtocols

    @@ -2803,7 +2808,7 @@
    Property Value
    Edit this page - View Source + View Source

    SupportedSslProtocols

    @@ -2841,7 +2846,7 @@
    Property Value
    Edit this page - View Source + View Source

    TcpTimeWaitSeconds

    @@ -2877,7 +2882,7 @@
    Property Value
    Edit this page - View Source + View Source

    ThreadPoolWorkerThread

    @@ -2910,7 +2915,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPoint

    @@ -2945,7 +2950,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPointIPv4

    @@ -2977,7 +2982,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamEndPointIPv6

    @@ -3009,7 +3014,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamHttpProxy

    @@ -3040,7 +3045,7 @@
    Property Value
    Edit this page - View Source + View Source

    UpStreamHttpsProxy

    @@ -3102,7 +3107,7 @@
    Property Value
    Edit this page - View Source + View Source

    ViaHeaderPseudonym

    @@ -3206,7 +3211,7 @@
    Parameters
    Edit this page - View Source + View Source

    ApplyLoggingConfiguration()

    @@ -4205,7 +4210,7 @@

    Events Edit this page - View Source + View Source

    AfterResponse

    Intercept after response event from server.

    @@ -4235,7 +4240,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeRequest

    Intercept request event to server.

    @@ -4265,7 +4270,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeResponse

    Intercept response event from server.

    @@ -4295,7 +4300,7 @@
    Event Type
    Edit this page - View Source + View Source

    BeforeUpStreamConnectRequest

    Intercept connect request sent to upstream proxy.

    @@ -4325,7 +4330,7 @@
    Event Type
    Edit this page - View Source + View Source

    ClientCertificateSelectionCallback

    Event to override client certificate selection during mutual SSL authentication.

    @@ -4355,7 +4360,7 @@
    Event Type
    Edit this page - View Source + View Source

    ClientConnectionCountChanged

    Event occurs when client connection count changed.

    @@ -4416,7 +4421,7 @@
    Event Type
    Edit this page - View Source + View Source

    Http3ClientConnectionCountChanged

    Event occurs when inbound HTTP/3 client connection count changed.

    @@ -4446,7 +4451,7 @@
    Event Type
    Edit this page - View Source + View Source

    Http3ServerConnectionCountChanged

    Event occurs when upstream HTTP/3 server connection count changed.

    @@ -4476,7 +4481,7 @@
    Event Type
    Edit this page - View Source + View Source

    OnClientConnectionCreate

    Customize TcpClient used for client connection upon create.

    @@ -4506,7 +4511,7 @@
    Event Type
    Edit this page - View Source + View Source

    OnRequestBodyWrite

    Intercept request body send event to server. @@ -4538,7 +4543,7 @@

    Event Type
    Edit this page - View Source + View Source

    OnResponseBodyWrite

    Intercept response body send event to client. @@ -4570,7 +4575,7 @@

    Event Type
    Edit this page - View Source + View Source

    OnServerConnectionCreate

    Customize TcpClient used for server connection upon create.

    @@ -4600,7 +4605,7 @@
    Event Type
    Edit this page - View Source + View Source

    ServerCertificateValidationCallback

    Event to override the default verification logic of remote SSL certificate received during authentication.

    @@ -4630,7 +4635,7 @@
    Event Type
    Edit this page - View Source + View Source

    ServerConnectionCountChanged

    Event occurs when server connection count changed.

    diff --git a/docs/api/Titanium.Web.Proxy.WebSocketDecoder.html b/docs/api/Titanium.Web.Proxy.WebSocketDecoder.html index 91e26027a..41ce5ea07 100644 --- a/docs/api/Titanium.Web.Proxy.WebSocketDecoder.html +++ b/docs/api/Titanium.Web.Proxy.WebSocketDecoder.html @@ -131,7 +131,7 @@

    Methods Edit this page - View Source + View Source

    Decode(byte[], int, int)

    diff --git a/docs/api/Titanium.Web.Proxy.WebSocketFrame.html b/docs/api/Titanium.Web.Proxy.WebSocketFrame.html index d1258aa5c..4e6cf4a84 100644 --- a/docs/api/Titanium.Web.Proxy.WebSocketFrame.html +++ b/docs/api/Titanium.Web.Proxy.WebSocketFrame.html @@ -130,11 +130,13 @@

    Properties Edit this page - View Source + View Source

    Data

    -

    The unmasked frame payload (owned copy from WebSocketDecoder).

    +

    The unmasked frame payload (owned copy from WebSocketDecoder). +When Titanium.Web.Proxy.WebSocketFrame.RelayRaw is set, this is a slice of the original wire bytes to forward +unchanged, not a decoded payload.

    Declaration
    @@ -223,7 +225,7 @@

    Methods Edit this page - View Source + View Source

    GetText()

    @@ -253,7 +255,7 @@
    Returns
    Edit this page - View Source + View Source

    GetText(Encoding)

    diff --git a/docs/api/toc.html b/docs/api/toc.html index 1f47c9fb2..0abf3c616 100644 --- a/docs/api/toc.html +++ b/docs/api/toc.html @@ -162,6 +162,9 @@
  • WebSocketFrameDirection
  • +
  • + WebSocketFrameElidedEventArgs +
  • WebSocketFrameInterceptAction
  • diff --git a/docs/api/toc.json b/docs/api/toc.json index 4deac3aaa..d50245d4b 100644 --- a/docs/api/toc.json +++ b/docs/api/toc.json @@ -1,2 +1,2 @@ -{"items":[{"name":"Titanium.Web.Proxy","href":"Titanium.Web.Proxy.html","topicHref":"Titanium.Web.Proxy.html","topicUid":"Titanium.Web.Proxy","type":"Namespace","items":[{"name":"ClientProcessId","href":"Titanium.Web.Proxy.ClientProcessId.html","topicHref":"Titanium.Web.Proxy.ClientProcessId.html","topicUid":"Titanium.Web.Proxy.ClientProcessId","type":"Class"},{"name":"MitmExclusionDefaults","href":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicHref":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicUid":"Titanium.Web.Proxy.MitmExclusionDefaults","type":"Class"},{"name":"MitmExclusionMode","href":"Titanium.Web.Proxy.MitmExclusionMode.html","topicHref":"Titanium.Web.Proxy.MitmExclusionMode.html","topicUid":"Titanium.Web.Proxy.MitmExclusionMode","type":"Enum"},{"name":"ProxyLimits","href":"Titanium.Web.Proxy.ProxyLimits.html","topicHref":"Titanium.Web.Proxy.ProxyLimits.html","topicUid":"Titanium.Web.Proxy.ProxyLimits","type":"Class"},{"name":"ProxyServer","href":"Titanium.Web.Proxy.ProxyServer.html","topicHref":"Titanium.Web.Proxy.ProxyServer.html","topicUid":"Titanium.Web.Proxy.ProxyServer","type":"Class"},{"name":"SystemProxyBypassRuleMode","href":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicHref":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicUid":"Titanium.Web.Proxy.SystemProxyBypassRuleMode","type":"Enum"},{"name":"SystemProxyChangeResult","href":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicHref":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicUid":"Titanium.Web.Proxy.SystemProxyChangeResult","type":"Struct"},{"name":"SystemProxyLoopbackPlacement","href":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicHref":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicUid":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement","type":"Enum"},{"name":"SystemProxySettings","href":"Titanium.Web.Proxy.SystemProxySettings.html","topicHref":"Titanium.Web.Proxy.SystemProxySettings.html","topicUid":"Titanium.Web.Proxy.SystemProxySettings","type":"Class"},{"name":"WebSocketDecoder","href":"Titanium.Web.Proxy.WebSocketDecoder.html","topicHref":"Titanium.Web.Proxy.WebSocketDecoder.html","topicUid":"Titanium.Web.Proxy.WebSocketDecoder","type":"Class"},{"name":"WebSocketFrame","href":"Titanium.Web.Proxy.WebSocketFrame.html","topicHref":"Titanium.Web.Proxy.WebSocketFrame.html","topicUid":"Titanium.Web.Proxy.WebSocketFrame","type":"Class"},{"name":"WebSocketFrameEncoder","href":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameEncoder","type":"Class"},{"name":"WebSocketFrameWriter","href":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameWriter","type":"Class"},{"name":"WebSocketProtocolException","href":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicHref":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicUid":"Titanium.Web.Proxy.WebSocketProtocolException","type":"Class"},{"name":"WebsocketOpCode","href":"Titanium.Web.Proxy.WebsocketOpCode.html","topicHref":"Titanium.Web.Proxy.WebsocketOpCode.html","topicUid":"Titanium.Web.Proxy.WebsocketOpCode","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Caching","href":"Titanium.Web.Proxy.Caching.html","topicHref":"Titanium.Web.Proxy.Caching.html","topicUid":"Titanium.Web.Proxy.Caching","type":"Namespace","items":[{"name":"HttpResponseCacheMiddleware","href":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicHref":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicUid":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware","type":"Class"},{"name":"MemoryHttpResponseCache","href":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicHref":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicUid":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache","type":"Class"}]},{"name":"Titanium.Web.Proxy.Clusters","href":"Titanium.Web.Proxy.Clusters.html","topicHref":"Titanium.Web.Proxy.Clusters.html","topicUid":"Titanium.Web.Proxy.Clusters","type":"Namespace","items":[{"name":"ClusterManager","href":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicHref":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicUid":"Titanium.Web.Proxy.Clusters.ClusterManager","type":"Class"},{"name":"DestinationHealthTracker","href":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicHref":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicUid":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker","type":"Class"},{"name":"LoadBalancer","href":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicHref":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicUid":"Titanium.Web.Proxy.Clusters.LoadBalancer","type":"Class"}]},{"name":"Titanium.Web.Proxy.Diagnostics","href":"Titanium.Web.Proxy.Diagnostics.html","topicHref":"Titanium.Web.Proxy.Diagnostics.html","topicUid":"Titanium.Web.Proxy.Diagnostics","type":"Namespace","items":[{"name":"ClientTlsTiming","href":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming","type":"Class"},{"name":"HttpRequestTiming","href":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming","type":"Class"},{"name":"TunnelConnectTiming","href":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming","type":"Class"},{"name":"UpstreamConnectionTiming","href":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming","type":"Class"}]},{"name":"Titanium.Web.Proxy.EventArguments","href":"Titanium.Web.Proxy.EventArguments.html","topicHref":"Titanium.Web.Proxy.EventArguments.html","topicUid":"Titanium.Web.Proxy.EventArguments","type":"Namespace","items":[{"name":"AsyncEventHandler","href":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicHref":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicUid":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler`1","type":"Delegate"},{"name":"BeforeBodyWriteEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs","type":"Class"},{"name":"BeforeHttpAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs","type":"Class"},{"name":"BeforeQuicAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs","type":"Class"},{"name":"BeforeSslAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs","type":"Class"},{"name":"CertificateSelectionEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs","type":"Class"},{"name":"CertificateValidationEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs","type":"Class"},{"name":"MultipartRequestPartSentEventArgs","href":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs","type":"Class"},{"name":"ProxyEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase","type":"Class"},{"name":"SessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgs","type":"Class"},{"name":"SessionEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase","type":"Class"},{"name":"SocksAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs","type":"Class"},{"name":"TunnelConnectFailureEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs","type":"Class"},{"name":"TunnelConnectSessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs","type":"Class"},{"name":"WebSocketFrameDirection","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection","type":"Enum"},{"name":"WebSocketFrameInterceptAction","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction","type":"Enum"},{"name":"WebSocketFrameInterceptEventArgs","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs","type":"Class"}]},{"name":"Titanium.Web.Proxy.Exceptions","href":"Titanium.Web.Proxy.Exceptions.html","topicHref":"Titanium.Web.Proxy.Exceptions.html","topicUid":"Titanium.Web.Proxy.Exceptions","type":"Namespace","items":[{"name":"BodyNotFoundException","href":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicHref":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicUid":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException","type":"Class"},{"name":"OutboundDestinationBlockedException","href":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicHref":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicUid":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException","type":"Class"},{"name":"ProxyAuthorizationException","href":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException","type":"Class"},{"name":"ProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyConnectException","type":"Class"},{"name":"ProxyException","href":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyException","type":"Class"},{"name":"ProxyHttpException","href":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyHttpException","type":"Class"},{"name":"ProxyTimeoutException","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException","type":"Class"},{"name":"ProxyTimeoutKind","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind","type":"Enum"},{"name":"UpstreamProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException","type":"Class"}]},{"name":"Titanium.Web.Proxy.Helpers","href":"Titanium.Web.Proxy.Helpers.html","topicHref":"Titanium.Web.Proxy.Helpers.html","topicUid":"Titanium.Web.Proxy.Helpers","type":"Namespace","items":[{"name":"UnixProxyBypassMapper","href":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicHref":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicUid":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http","href":"Titanium.Web.Proxy.Http.html","topicHref":"Titanium.Web.Proxy.Http.html","topicUid":"Titanium.Web.Proxy.Http","type":"Namespace","items":[{"name":"ConnectRequest","href":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicHref":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicUid":"Titanium.Web.Proxy.Http.ConnectRequest","type":"Class"},{"name":"ConnectResponse","href":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicHref":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicUid":"Titanium.Web.Proxy.Http.ConnectResponse","type":"Class"},{"name":"HeaderCollection","href":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection","type":"Class"},{"name":"HeaderCollection.Enumerator","href":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator","type":"Struct"},{"name":"HttpWebClient","href":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicHref":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicUid":"Titanium.Web.Proxy.Http.HttpWebClient","type":"Class"},{"name":"KnownHeader","href":"Titanium.Web.Proxy.Http.KnownHeader.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeader.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeader","type":"Class"},{"name":"KnownHeaders","href":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeaders","type":"Class"},{"name":"ProxyResults","href":"Titanium.Web.Proxy.Http.ProxyResults.html","topicHref":"Titanium.Web.Proxy.Http.ProxyResults.html","topicUid":"Titanium.Web.Proxy.Http.ProxyResults","type":"Class"},{"name":"Request","href":"Titanium.Web.Proxy.Http.Request.html","topicHref":"Titanium.Web.Proxy.Http.Request.html","topicUid":"Titanium.Web.Proxy.Http.Request","type":"Class"},{"name":"RequestResponseBase","href":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicHref":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicUid":"Titanium.Web.Proxy.Http.RequestResponseBase","type":"Class"},{"name":"Response","href":"Titanium.Web.Proxy.Http.Response.html","topicHref":"Titanium.Web.Proxy.Http.Response.html","topicUid":"Titanium.Web.Proxy.Http.Response","type":"Class"},{"name":"StreamingProxyResult","href":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicHref":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicUid":"Titanium.Web.Proxy.Http.StreamingProxyResult","type":"Struct"},{"name":"TunnelType","href":"Titanium.Web.Proxy.Http.TunnelType.html","topicHref":"Titanium.Web.Proxy.Http.TunnelType.html","topicUid":"Titanium.Web.Proxy.Http.TunnelType","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Http.Responses","href":"Titanium.Web.Proxy.Http.Responses.html","topicHref":"Titanium.Web.Proxy.Http.Responses.html","topicUid":"Titanium.Web.Proxy.Http.Responses","type":"Namespace","items":[{"name":"GenericResponse","href":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.GenericResponse","type":"Class"},{"name":"OkResponse","href":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.OkResponse","type":"Class"},{"name":"RedirectResponse","href":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.RedirectResponse","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http3","href":"Titanium.Web.Proxy.Http3.html","topicHref":"Titanium.Web.Proxy.Http3.html","topicUid":"Titanium.Web.Proxy.Http3","type":"Namespace","items":[{"name":"Http3NativeBootstrap","href":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicHref":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicUid":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap","type":"Class"}]},{"name":"Titanium.Web.Proxy.Logging","href":"Titanium.Web.Proxy.Logging.html","topicHref":"Titanium.Web.Proxy.Logging.html","topicUid":"Titanium.Web.Proxy.Logging","type":"Namespace","items":[{"name":"ProxyLoggingOptions","href":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicHref":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicUid":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions","type":"Class"}]},{"name":"Titanium.Web.Proxy.Middleware","href":"Titanium.Web.Proxy.Middleware.html","topicHref":"Titanium.Web.Proxy.Middleware.html","topicUid":"Titanium.Web.Proxy.Middleware","type":"Namespace","items":[{"name":"ProxyMiddlewarePipeline","href":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicHref":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicUid":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline","type":"Class"}]},{"name":"Titanium.Web.Proxy.Models","href":"Titanium.Web.Proxy.Models.html","topicHref":"Titanium.Web.Proxy.Models.html","topicUid":"Titanium.Web.Proxy.Models","type":"Namespace","items":[{"name":"DecryptFailureBypassEntry","href":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicHref":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicUid":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry","type":"Class"},{"name":"ExplicitProxyEndPoint","href":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint","type":"Class"},{"name":"ExternalProxy","href":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxy","type":"Class"},{"name":"ExternalProxyType","href":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxyType","type":"Enum"},{"name":"HttpHeader","href":"Titanium.Web.Proxy.Models.HttpHeader.html","topicHref":"Titanium.Web.Proxy.Models.HttpHeader.html","topicUid":"Titanium.Web.Proxy.Models.HttpHeader","type":"Class"},{"name":"HttpInterceptionContext","href":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicHref":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicUid":"Titanium.Web.Proxy.Models.HttpInterceptionContext","type":"Struct"},{"name":"IExternalProxy","href":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.IExternalProxy","type":"Interface"},{"name":"OriginHttpVersionPolicy","href":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicHref":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicUid":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy","type":"Enum"},{"name":"ProxyAuthenticationContext","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext","type":"Class"},{"name":"ProxyAuthenticationResult","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult","type":"Enum"},{"name":"ProxyEndPoint","href":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ProxyEndPoint","type":"Class"},{"name":"ProxyProtocolType","href":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicHref":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicUid":"Titanium.Web.Proxy.Models.ProxyProtocolType","type":"Enum"},{"name":"SocksProxyEndPoint","href":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.SocksProxyEndPoint","type":"Class"},{"name":"TransparentBaseProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint","type":"Class"},{"name":"TransparentProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint","type":"Class"},{"name":"TransparentQuicProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint","type":"Class"},{"name":"UpstreamHttpProtocol","href":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicHref":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicUid":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol","type":"Enum"},{"name":"WinAuthCredentials","href":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicHref":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicUid":"Titanium.Web.Proxy.Models.WinAuthCredentials","type":"Class"}]},{"name":"Titanium.Web.Proxy.Network","href":"Titanium.Web.Proxy.Network.html","topicHref":"Titanium.Web.Proxy.Network.html","topicUid":"Titanium.Web.Proxy.Network","type":"Namespace","items":[{"name":"CertificateEngine","href":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicHref":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicUid":"Titanium.Web.Proxy.Network.CertificateEngine","type":"Enum"},{"name":"CertificateKeyAlgorithm","href":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicHref":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicUid":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm","type":"Enum"},{"name":"CertificateManager","href":"Titanium.Web.Proxy.Network.CertificateManager.html","topicHref":"Titanium.Web.Proxy.Network.CertificateManager.html","topicUid":"Titanium.Web.Proxy.Network.CertificateManager","type":"Class"},{"name":"CertificateOsTrustKind","href":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustKind","type":"Enum"},{"name":"CertificateOsTrustResult","href":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustResult","type":"Class"},{"name":"DefaultCertificateDiskCache","href":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicHref":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicUid":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache","type":"Class"},{"name":"FirefoxCertificateTrust","href":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicHref":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicUid":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust","type":"Class"},{"name":"ICertificateCache","href":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicHref":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicUid":"Titanium.Web.Proxy.Network.ICertificateCache","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Network.Quic","href":"Titanium.Web.Proxy.Network.Quic.html","topicHref":"Titanium.Web.Proxy.Network.Quic.html","topicUid":"Titanium.Web.Proxy.Network.Quic","type":"Namespace","items":[{"name":"IOriginalDestinationResolver","href":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicHref":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicUid":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Options","href":"Titanium.Web.Proxy.Options.html","topicHref":"Titanium.Web.Proxy.Options.html","topicUid":"Titanium.Web.Proxy.Options","type":"Namespace","items":[{"name":"PolicyFamily","href":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicHref":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicUid":"Titanium.Web.Proxy.Options.PolicyFamily","type":"Enum"},{"name":"PolicyMode","href":"Titanium.Web.Proxy.Options.PolicyMode.html","topicHref":"Titanium.Web.Proxy.Options.PolicyMode.html","topicUid":"Titanium.Web.Proxy.Options.PolicyMode","type":"Enum"},{"name":"ProxyPolicyModes","href":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicHref":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicUid":"Titanium.Web.Proxy.Options.ProxyPolicyModes","type":"Class"},{"name":"ProxyProfile","href":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfile","type":"Enum"},{"name":"ProxyProfileSettings","href":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfileSettings","type":"Class"},{"name":"ProxyResourceLimits","href":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicHref":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicUid":"Titanium.Web.Proxy.Options.ProxyResourceLimits","type":"Class"},{"name":"ProxyTimeoutOptions","href":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicHref":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicUid":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions","type":"Class"},{"name":"ResolvedSessionPolicy","href":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicHref":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicUid":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy","type":"Class"}]},{"name":"Titanium.Web.Proxy.Routing","href":"Titanium.Web.Proxy.Routing.html","topicHref":"Titanium.Web.Proxy.Routing.html","topicUid":"Titanium.Web.Proxy.Routing","type":"Namespace","items":[{"name":"ReverseProxyFastPath","href":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicHref":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicUid":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath","type":"Class"},{"name":"RouteMatcher","href":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicHref":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicUid":"Titanium.Web.Proxy.Routing.RouteMatcher","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended","href":"Titanium.Web.Proxy.StreamExtended.html","topicHref":"Titanium.Web.Proxy.StreamExtended.html","topicUid":"Titanium.Web.Proxy.StreamExtended","type":"Namespace","items":[{"name":"ClientHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo","type":"Class"},{"name":"ServerHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.BufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool","type":"Namespace","items":[{"name":"IBufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool","type":"Interface"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Models","href":"Titanium.Web.Proxy.StreamExtended.Models.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models","type":"Namespace","items":[{"name":"SslExtension","href":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Network","href":"Titanium.Web.Proxy.StreamExtended.Network.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network","type":"Namespace","items":[{"name":"DataEventArgs","href":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs","type":"Class"},{"name":"IHttpStreamReader","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader","type":"Interface"},{"name":"IHttpStreamWriter","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter","type":"Interface"},{"name":"ILineStream","href":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream","type":"Interface"},{"name":"IPeekStream","href":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Transforms","href":"Titanium.Web.Proxy.Transforms.html","topicHref":"Titanium.Web.Proxy.Transforms.html","topicUid":"Titanium.Web.Proxy.Transforms","type":"Namespace","items":[{"name":"TransformEngine","href":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicHref":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicUid":"Titanium.Web.Proxy.Transforms.TransformEngine","type":"Class"}]}],"memberLayout":"SamePage"} +{"items":[{"name":"Titanium.Web.Proxy","href":"Titanium.Web.Proxy.html","topicHref":"Titanium.Web.Proxy.html","topicUid":"Titanium.Web.Proxy","type":"Namespace","items":[{"name":"ClientProcessId","href":"Titanium.Web.Proxy.ClientProcessId.html","topicHref":"Titanium.Web.Proxy.ClientProcessId.html","topicUid":"Titanium.Web.Proxy.ClientProcessId","type":"Class"},{"name":"MitmExclusionDefaults","href":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicHref":"Titanium.Web.Proxy.MitmExclusionDefaults.html","topicUid":"Titanium.Web.Proxy.MitmExclusionDefaults","type":"Class"},{"name":"MitmExclusionMode","href":"Titanium.Web.Proxy.MitmExclusionMode.html","topicHref":"Titanium.Web.Proxy.MitmExclusionMode.html","topicUid":"Titanium.Web.Proxy.MitmExclusionMode","type":"Enum"},{"name":"ProxyLimits","href":"Titanium.Web.Proxy.ProxyLimits.html","topicHref":"Titanium.Web.Proxy.ProxyLimits.html","topicUid":"Titanium.Web.Proxy.ProxyLimits","type":"Class"},{"name":"ProxyServer","href":"Titanium.Web.Proxy.ProxyServer.html","topicHref":"Titanium.Web.Proxy.ProxyServer.html","topicUid":"Titanium.Web.Proxy.ProxyServer","type":"Class"},{"name":"SystemProxyBypassRuleMode","href":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicHref":"Titanium.Web.Proxy.SystemProxyBypassRuleMode.html","topicUid":"Titanium.Web.Proxy.SystemProxyBypassRuleMode","type":"Enum"},{"name":"SystemProxyChangeResult","href":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicHref":"Titanium.Web.Proxy.SystemProxyChangeResult.html","topicUid":"Titanium.Web.Proxy.SystemProxyChangeResult","type":"Struct"},{"name":"SystemProxyLoopbackPlacement","href":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicHref":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement.html","topicUid":"Titanium.Web.Proxy.SystemProxyLoopbackPlacement","type":"Enum"},{"name":"SystemProxySettings","href":"Titanium.Web.Proxy.SystemProxySettings.html","topicHref":"Titanium.Web.Proxy.SystemProxySettings.html","topicUid":"Titanium.Web.Proxy.SystemProxySettings","type":"Class"},{"name":"WebSocketDecoder","href":"Titanium.Web.Proxy.WebSocketDecoder.html","topicHref":"Titanium.Web.Proxy.WebSocketDecoder.html","topicUid":"Titanium.Web.Proxy.WebSocketDecoder","type":"Class"},{"name":"WebSocketFrame","href":"Titanium.Web.Proxy.WebSocketFrame.html","topicHref":"Titanium.Web.Proxy.WebSocketFrame.html","topicUid":"Titanium.Web.Proxy.WebSocketFrame","type":"Class"},{"name":"WebSocketFrameEncoder","href":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameEncoder.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameEncoder","type":"Class"},{"name":"WebSocketFrameWriter","href":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicHref":"Titanium.Web.Proxy.WebSocketFrameWriter.html","topicUid":"Titanium.Web.Proxy.WebSocketFrameWriter","type":"Class"},{"name":"WebSocketProtocolException","href":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicHref":"Titanium.Web.Proxy.WebSocketProtocolException.html","topicUid":"Titanium.Web.Proxy.WebSocketProtocolException","type":"Class"},{"name":"WebsocketOpCode","href":"Titanium.Web.Proxy.WebsocketOpCode.html","topicHref":"Titanium.Web.Proxy.WebsocketOpCode.html","topicUid":"Titanium.Web.Proxy.WebsocketOpCode","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Caching","href":"Titanium.Web.Proxy.Caching.html","topicHref":"Titanium.Web.Proxy.Caching.html","topicUid":"Titanium.Web.Proxy.Caching","type":"Namespace","items":[{"name":"HttpResponseCacheMiddleware","href":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicHref":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware.html","topicUid":"Titanium.Web.Proxy.Caching.HttpResponseCacheMiddleware","type":"Class"},{"name":"MemoryHttpResponseCache","href":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicHref":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache.html","topicUid":"Titanium.Web.Proxy.Caching.MemoryHttpResponseCache","type":"Class"}]},{"name":"Titanium.Web.Proxy.Clusters","href":"Titanium.Web.Proxy.Clusters.html","topicHref":"Titanium.Web.Proxy.Clusters.html","topicUid":"Titanium.Web.Proxy.Clusters","type":"Namespace","items":[{"name":"ClusterManager","href":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicHref":"Titanium.Web.Proxy.Clusters.ClusterManager.html","topicUid":"Titanium.Web.Proxy.Clusters.ClusterManager","type":"Class"},{"name":"DestinationHealthTracker","href":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicHref":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker.html","topicUid":"Titanium.Web.Proxy.Clusters.DestinationHealthTracker","type":"Class"},{"name":"LoadBalancer","href":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicHref":"Titanium.Web.Proxy.Clusters.LoadBalancer.html","topicUid":"Titanium.Web.Proxy.Clusters.LoadBalancer","type":"Class"}]},{"name":"Titanium.Web.Proxy.Diagnostics","href":"Titanium.Web.Proxy.Diagnostics.html","topicHref":"Titanium.Web.Proxy.Diagnostics.html","topicUid":"Titanium.Web.Proxy.Diagnostics","type":"Namespace","items":[{"name":"ClientTlsTiming","href":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.ClientTlsTiming","type":"Class"},{"name":"HttpRequestTiming","href":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.HttpRequestTiming","type":"Class"},{"name":"TunnelConnectTiming","href":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.TunnelConnectTiming","type":"Class"},{"name":"UpstreamConnectionTiming","href":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicHref":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming.html","topicUid":"Titanium.Web.Proxy.Diagnostics.UpstreamConnectionTiming","type":"Class"}]},{"name":"Titanium.Web.Proxy.EventArguments","href":"Titanium.Web.Proxy.EventArguments.html","topicHref":"Titanium.Web.Proxy.EventArguments.html","topicUid":"Titanium.Web.Proxy.EventArguments","type":"Namespace","items":[{"name":"AsyncEventHandler","href":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicHref":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler-1.html","topicUid":"Titanium.Web.Proxy.EventArguments.AsyncEventHandler`1","type":"Delegate"},{"name":"BeforeBodyWriteEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeBodyWriteEventArgs","type":"Class"},{"name":"BeforeHttpAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeHttpAuthenticateEventArgs","type":"Class"},{"name":"BeforeQuicAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeQuicAuthenticateEventArgs","type":"Class"},{"name":"BeforeSslAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.BeforeSslAuthenticateEventArgs","type":"Class"},{"name":"CertificateSelectionEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateSelectionEventArgs","type":"Class"},{"name":"CertificateValidationEventArgs","href":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.CertificateValidationEventArgs","type":"Class"},{"name":"MultipartRequestPartSentEventArgs","href":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.MultipartRequestPartSentEventArgs","type":"Class"},{"name":"ProxyEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase","type":"Class"},{"name":"SessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgs","type":"Class"},{"name":"SessionEventArgsBase","href":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicHref":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html","topicUid":"Titanium.Web.Proxy.EventArguments.SessionEventArgsBase","type":"Class"},{"name":"SocksAuthenticateEventArgs","href":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.SocksAuthenticateEventArgs","type":"Class"},{"name":"TunnelConnectFailureEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectFailureEventArgs","type":"Class"},{"name":"TunnelConnectSessionEventArgs","href":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.TunnelConnectSessionEventArgs","type":"Class"},{"name":"WebSocketFrameDirection","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection","type":"Enum"},{"name":"WebSocketFrameElidedEventArgs","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs","type":"Class"},{"name":"WebSocketFrameInterceptAction","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction","type":"Enum"},{"name":"WebSocketFrameInterceptEventArgs","href":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicHref":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs.html","topicUid":"Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptEventArgs","type":"Class"}]},{"name":"Titanium.Web.Proxy.Exceptions","href":"Titanium.Web.Proxy.Exceptions.html","topicHref":"Titanium.Web.Proxy.Exceptions.html","topicUid":"Titanium.Web.Proxy.Exceptions","type":"Namespace","items":[{"name":"BodyNotFoundException","href":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicHref":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html","topicUid":"Titanium.Web.Proxy.Exceptions.BodyNotFoundException","type":"Class"},{"name":"OutboundDestinationBlockedException","href":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicHref":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException.html","topicUid":"Titanium.Web.Proxy.Exceptions.OutboundDestinationBlockedException","type":"Class"},{"name":"ProxyAuthorizationException","href":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyAuthorizationException","type":"Class"},{"name":"ProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyConnectException","type":"Class"},{"name":"ProxyException","href":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyException","type":"Class"},{"name":"ProxyHttpException","href":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyHttpException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyHttpException","type":"Class"},{"name":"ProxyTimeoutException","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutException","type":"Class"},{"name":"ProxyTimeoutKind","href":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicHref":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind.html","topicUid":"Titanium.Web.Proxy.Exceptions.ProxyTimeoutKind","type":"Enum"},{"name":"UpstreamProxyConnectException","href":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicHref":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException.html","topicUid":"Titanium.Web.Proxy.Exceptions.UpstreamProxyConnectException","type":"Class"}]},{"name":"Titanium.Web.Proxy.Helpers","href":"Titanium.Web.Proxy.Helpers.html","topicHref":"Titanium.Web.Proxy.Helpers.html","topicUid":"Titanium.Web.Proxy.Helpers","type":"Namespace","items":[{"name":"UnixProxyBypassMapper","href":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicHref":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper.html","topicUid":"Titanium.Web.Proxy.Helpers.UnixProxyBypassMapper","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http","href":"Titanium.Web.Proxy.Http.html","topicHref":"Titanium.Web.Proxy.Http.html","topicUid":"Titanium.Web.Proxy.Http","type":"Namespace","items":[{"name":"ConnectRequest","href":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicHref":"Titanium.Web.Proxy.Http.ConnectRequest.html","topicUid":"Titanium.Web.Proxy.Http.ConnectRequest","type":"Class"},{"name":"ConnectResponse","href":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicHref":"Titanium.Web.Proxy.Http.ConnectResponse.html","topicUid":"Titanium.Web.Proxy.Http.ConnectResponse","type":"Class"},{"name":"HeaderCollection","href":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection","type":"Class"},{"name":"HeaderCollection.Enumerator","href":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicHref":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator.html","topicUid":"Titanium.Web.Proxy.Http.HeaderCollection.Enumerator","type":"Struct"},{"name":"HttpWebClient","href":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicHref":"Titanium.Web.Proxy.Http.HttpWebClient.html","topicUid":"Titanium.Web.Proxy.Http.HttpWebClient","type":"Class"},{"name":"KnownHeader","href":"Titanium.Web.Proxy.Http.KnownHeader.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeader.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeader","type":"Class"},{"name":"KnownHeaders","href":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicHref":"Titanium.Web.Proxy.Http.KnownHeaders.html","topicUid":"Titanium.Web.Proxy.Http.KnownHeaders","type":"Class"},{"name":"ProxyResults","href":"Titanium.Web.Proxy.Http.ProxyResults.html","topicHref":"Titanium.Web.Proxy.Http.ProxyResults.html","topicUid":"Titanium.Web.Proxy.Http.ProxyResults","type":"Class"},{"name":"Request","href":"Titanium.Web.Proxy.Http.Request.html","topicHref":"Titanium.Web.Proxy.Http.Request.html","topicUid":"Titanium.Web.Proxy.Http.Request","type":"Class"},{"name":"RequestResponseBase","href":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicHref":"Titanium.Web.Proxy.Http.RequestResponseBase.html","topicUid":"Titanium.Web.Proxy.Http.RequestResponseBase","type":"Class"},{"name":"Response","href":"Titanium.Web.Proxy.Http.Response.html","topicHref":"Titanium.Web.Proxy.Http.Response.html","topicUid":"Titanium.Web.Proxy.Http.Response","type":"Class"},{"name":"StreamingProxyResult","href":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicHref":"Titanium.Web.Proxy.Http.StreamingProxyResult.html","topicUid":"Titanium.Web.Proxy.Http.StreamingProxyResult","type":"Struct"},{"name":"TunnelType","href":"Titanium.Web.Proxy.Http.TunnelType.html","topicHref":"Titanium.Web.Proxy.Http.TunnelType.html","topicUid":"Titanium.Web.Proxy.Http.TunnelType","type":"Enum"}]},{"name":"Titanium.Web.Proxy.Http.Responses","href":"Titanium.Web.Proxy.Http.Responses.html","topicHref":"Titanium.Web.Proxy.Http.Responses.html","topicUid":"Titanium.Web.Proxy.Http.Responses","type":"Namespace","items":[{"name":"GenericResponse","href":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.GenericResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.GenericResponse","type":"Class"},{"name":"OkResponse","href":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.OkResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.OkResponse","type":"Class"},{"name":"RedirectResponse","href":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicHref":"Titanium.Web.Proxy.Http.Responses.RedirectResponse.html","topicUid":"Titanium.Web.Proxy.Http.Responses.RedirectResponse","type":"Class"}]},{"name":"Titanium.Web.Proxy.Http3","href":"Titanium.Web.Proxy.Http3.html","topicHref":"Titanium.Web.Proxy.Http3.html","topicUid":"Titanium.Web.Proxy.Http3","type":"Namespace","items":[{"name":"Http3NativeBootstrap","href":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicHref":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html","topicUid":"Titanium.Web.Proxy.Http3.Http3NativeBootstrap","type":"Class"}]},{"name":"Titanium.Web.Proxy.Logging","href":"Titanium.Web.Proxy.Logging.html","topicHref":"Titanium.Web.Proxy.Logging.html","topicUid":"Titanium.Web.Proxy.Logging","type":"Namespace","items":[{"name":"ProxyLoggingOptions","href":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicHref":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions.html","topicUid":"Titanium.Web.Proxy.Logging.ProxyLoggingOptions","type":"Class"}]},{"name":"Titanium.Web.Proxy.Middleware","href":"Titanium.Web.Proxy.Middleware.html","topicHref":"Titanium.Web.Proxy.Middleware.html","topicUid":"Titanium.Web.Proxy.Middleware","type":"Namespace","items":[{"name":"ProxyMiddlewarePipeline","href":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicHref":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline.html","topicUid":"Titanium.Web.Proxy.Middleware.ProxyMiddlewarePipeline","type":"Class"}]},{"name":"Titanium.Web.Proxy.Models","href":"Titanium.Web.Proxy.Models.html","topicHref":"Titanium.Web.Proxy.Models.html","topicUid":"Titanium.Web.Proxy.Models","type":"Namespace","items":[{"name":"DecryptFailureBypassEntry","href":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicHref":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry.html","topicUid":"Titanium.Web.Proxy.Models.DecryptFailureBypassEntry","type":"Class"},{"name":"ExplicitProxyEndPoint","href":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ExplicitProxyEndPoint","type":"Class"},{"name":"ExternalProxy","href":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxy","type":"Class"},{"name":"ExternalProxyType","href":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicHref":"Titanium.Web.Proxy.Models.ExternalProxyType.html","topicUid":"Titanium.Web.Proxy.Models.ExternalProxyType","type":"Enum"},{"name":"HttpHeader","href":"Titanium.Web.Proxy.Models.HttpHeader.html","topicHref":"Titanium.Web.Proxy.Models.HttpHeader.html","topicUid":"Titanium.Web.Proxy.Models.HttpHeader","type":"Class"},{"name":"HttpInterceptionContext","href":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicHref":"Titanium.Web.Proxy.Models.HttpInterceptionContext.html","topicUid":"Titanium.Web.Proxy.Models.HttpInterceptionContext","type":"Struct"},{"name":"IExternalProxy","href":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicHref":"Titanium.Web.Proxy.Models.IExternalProxy.html","topicUid":"Titanium.Web.Proxy.Models.IExternalProxy","type":"Interface"},{"name":"OriginHttpVersionPolicy","href":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicHref":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy.html","topicUid":"Titanium.Web.Proxy.Models.OriginHttpVersionPolicy","type":"Enum"},{"name":"ProxyAuthenticationContext","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationContext","type":"Class"},{"name":"ProxyAuthenticationResult","href":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicHref":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult.html","topicUid":"Titanium.Web.Proxy.Models.ProxyAuthenticationResult","type":"Enum"},{"name":"ProxyEndPoint","href":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.ProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.ProxyEndPoint","type":"Class"},{"name":"ProxyProtocolType","href":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicHref":"Titanium.Web.Proxy.Models.ProxyProtocolType.html","topicUid":"Titanium.Web.Proxy.Models.ProxyProtocolType","type":"Enum"},{"name":"SocksProxyEndPoint","href":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.SocksProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.SocksProxyEndPoint","type":"Class"},{"name":"TransparentBaseProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentBaseProxyEndPoint","type":"Class"},{"name":"TransparentProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentProxyEndPoint","type":"Class"},{"name":"TransparentQuicProxyEndPoint","href":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicHref":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint.html","topicUid":"Titanium.Web.Proxy.Models.TransparentQuicProxyEndPoint","type":"Class"},{"name":"UpstreamHttpProtocol","href":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicHref":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol.html","topicUid":"Titanium.Web.Proxy.Models.UpstreamHttpProtocol","type":"Enum"},{"name":"WinAuthCredentials","href":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicHref":"Titanium.Web.Proxy.Models.WinAuthCredentials.html","topicUid":"Titanium.Web.Proxy.Models.WinAuthCredentials","type":"Class"}]},{"name":"Titanium.Web.Proxy.Network","href":"Titanium.Web.Proxy.Network.html","topicHref":"Titanium.Web.Proxy.Network.html","topicUid":"Titanium.Web.Proxy.Network","type":"Namespace","items":[{"name":"CertificateEngine","href":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicHref":"Titanium.Web.Proxy.Network.CertificateEngine.html","topicUid":"Titanium.Web.Proxy.Network.CertificateEngine","type":"Enum"},{"name":"CertificateKeyAlgorithm","href":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicHref":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm.html","topicUid":"Titanium.Web.Proxy.Network.CertificateKeyAlgorithm","type":"Enum"},{"name":"CertificateManager","href":"Titanium.Web.Proxy.Network.CertificateManager.html","topicHref":"Titanium.Web.Proxy.Network.CertificateManager.html","topicUid":"Titanium.Web.Proxy.Network.CertificateManager","type":"Class"},{"name":"CertificateOsTrustKind","href":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustKind.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustKind","type":"Enum"},{"name":"CertificateOsTrustResult","href":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicHref":"Titanium.Web.Proxy.Network.CertificateOsTrustResult.html","topicUid":"Titanium.Web.Proxy.Network.CertificateOsTrustResult","type":"Class"},{"name":"DefaultCertificateDiskCache","href":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicHref":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache.html","topicUid":"Titanium.Web.Proxy.Network.DefaultCertificateDiskCache","type":"Class"},{"name":"FirefoxCertificateTrust","href":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicHref":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust.html","topicUid":"Titanium.Web.Proxy.Network.FirefoxCertificateTrust","type":"Class"},{"name":"ICertificateCache","href":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicHref":"Titanium.Web.Proxy.Network.ICertificateCache.html","topicUid":"Titanium.Web.Proxy.Network.ICertificateCache","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Network.Quic","href":"Titanium.Web.Proxy.Network.Quic.html","topicHref":"Titanium.Web.Proxy.Network.Quic.html","topicUid":"Titanium.Web.Proxy.Network.Quic","type":"Namespace","items":[{"name":"IOriginalDestinationResolver","href":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicHref":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver.html","topicUid":"Titanium.Web.Proxy.Network.Quic.IOriginalDestinationResolver","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Options","href":"Titanium.Web.Proxy.Options.html","topicHref":"Titanium.Web.Proxy.Options.html","topicUid":"Titanium.Web.Proxy.Options","type":"Namespace","items":[{"name":"PolicyFamily","href":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicHref":"Titanium.Web.Proxy.Options.PolicyFamily.html","topicUid":"Titanium.Web.Proxy.Options.PolicyFamily","type":"Enum"},{"name":"PolicyMode","href":"Titanium.Web.Proxy.Options.PolicyMode.html","topicHref":"Titanium.Web.Proxy.Options.PolicyMode.html","topicUid":"Titanium.Web.Proxy.Options.PolicyMode","type":"Enum"},{"name":"ProxyPolicyModes","href":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicHref":"Titanium.Web.Proxy.Options.ProxyPolicyModes.html","topicUid":"Titanium.Web.Proxy.Options.ProxyPolicyModes","type":"Class"},{"name":"ProxyProfile","href":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfile.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfile","type":"Enum"},{"name":"ProxyProfileSettings","href":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicHref":"Titanium.Web.Proxy.Options.ProxyProfileSettings.html","topicUid":"Titanium.Web.Proxy.Options.ProxyProfileSettings","type":"Class"},{"name":"ProxyResourceLimits","href":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicHref":"Titanium.Web.Proxy.Options.ProxyResourceLimits.html","topicUid":"Titanium.Web.Proxy.Options.ProxyResourceLimits","type":"Class"},{"name":"ProxyTimeoutOptions","href":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicHref":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html","topicUid":"Titanium.Web.Proxy.Options.ProxyTimeoutOptions","type":"Class"},{"name":"ResolvedSessionPolicy","href":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicHref":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy.html","topicUid":"Titanium.Web.Proxy.Options.ResolvedSessionPolicy","type":"Class"}]},{"name":"Titanium.Web.Proxy.Routing","href":"Titanium.Web.Proxy.Routing.html","topicHref":"Titanium.Web.Proxy.Routing.html","topicUid":"Titanium.Web.Proxy.Routing","type":"Namespace","items":[{"name":"ReverseProxyFastPath","href":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicHref":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html","topicUid":"Titanium.Web.Proxy.Routing.ReverseProxyFastPath","type":"Class"},{"name":"RouteMatcher","href":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicHref":"Titanium.Web.Proxy.Routing.RouteMatcher.html","topicUid":"Titanium.Web.Proxy.Routing.RouteMatcher","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended","href":"Titanium.Web.Proxy.StreamExtended.html","topicHref":"Titanium.Web.Proxy.StreamExtended.html","topicUid":"Titanium.Web.Proxy.StreamExtended","type":"Namespace","items":[{"name":"ClientHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ClientHelloInfo","type":"Class"},{"name":"ServerHelloInfo","href":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicHref":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo.html","topicUid":"Titanium.Web.Proxy.StreamExtended.ServerHelloInfo","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.BufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool","type":"Namespace","items":[{"name":"IBufferPool","href":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicHref":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool.html","topicUid":"Titanium.Web.Proxy.StreamExtended.BufferPool.IBufferPool","type":"Interface"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Models","href":"Titanium.Web.Proxy.StreamExtended.Models.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models","type":"Namespace","items":[{"name":"SslExtension","href":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Models.SslExtension","type":"Class"}]},{"name":"Titanium.Web.Proxy.StreamExtended.Network","href":"Titanium.Web.Proxy.StreamExtended.Network.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network","type":"Namespace","items":[{"name":"DataEventArgs","href":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.DataEventArgs","type":"Class"},{"name":"IHttpStreamReader","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamReader","type":"Interface"},{"name":"IHttpStreamWriter","href":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IHttpStreamWriter","type":"Interface"},{"name":"ILineStream","href":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.ILineStream","type":"Interface"},{"name":"IPeekStream","href":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicHref":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream.html","topicUid":"Titanium.Web.Proxy.StreamExtended.Network.IPeekStream","type":"Interface"}]},{"name":"Titanium.Web.Proxy.Transforms","href":"Titanium.Web.Proxy.Transforms.html","topicHref":"Titanium.Web.Proxy.Transforms.html","topicUid":"Titanium.Web.Proxy.Transforms","type":"Namespace","items":[{"name":"TransformEngine","href":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicHref":"Titanium.Web.Proxy.Transforms.TransformEngine.html","topicUid":"Titanium.Web.Proxy.Transforms.TransformEngine","type":"Class"}]}],"memberLayout":"SamePage"} diff --git a/docs/index.json b/docs/index.json index 6f8f40e84..e5d1c2db6 100644 --- a/docs/index.json +++ b/docs/index.json @@ -112,12 +112,12 @@ "api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html": { "href": "api/Titanium.Web.Proxy.EventArguments.ProxyEventArgsBase.html", "title": "Class ProxyEventArgsBase | Titanium Web Proxy", - "summary": "Class ProxyEventArgsBase The base event arguments Inheritance object EventArgs ProxyEventArgsBase BeforeBodyWriteEventArgs BeforeHttpAuthenticateEventArgs BeforeSslAuthenticateEventArgs CertificateSelectionEventArgs CertificateValidationEventArgs MultipartRequestPartSentEventArgs SessionEventArgsBase SocksAuthenticateEventArgs TunnelConnectFailureEventArgs WebSocketFrameInterceptEventArgs Inherited Members EventArgs.Empty object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public abstract class ProxyEventArgsBase : EventArgs Properties | Edit this page View Source ClientUserData Declaration public object? ClientUserData { get; set; } Property Value Type Description object See Also EventArgs" + "summary": "Class ProxyEventArgsBase The base event arguments Inheritance object EventArgs ProxyEventArgsBase BeforeBodyWriteEventArgs BeforeHttpAuthenticateEventArgs BeforeSslAuthenticateEventArgs CertificateSelectionEventArgs CertificateValidationEventArgs MultipartRequestPartSentEventArgs SessionEventArgsBase SocksAuthenticateEventArgs TunnelConnectFailureEventArgs WebSocketFrameElidedEventArgs WebSocketFrameInterceptEventArgs Inherited Members EventArgs.Empty object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public abstract class ProxyEventArgsBase : EventArgs Properties | Edit this page View Source ClientUserData Declaration public object? ClientUserData { get; set; } Property Value Type Description object See Also EventArgs" }, "api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html": { "href": "api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html", "title": "Class SessionEventArgs | Titanium Web Proxy", - "summary": "Class SessionEventArgs Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. Inheritance object EventArgs ProxyEventArgsBase SessionEventArgsBase SessionEventArgs Implements IDisposable Inherited Members SessionEventArgsBase.BufferPool SessionEventArgsBase.ClientConnectionId SessionEventArgsBase.ServerConnectionId SessionEventArgsBase.Timing SessionEventArgsBase.UpstreamConnectionTiming SessionEventArgsBase.UserData SessionEventArgsBase.ConnectTimeout SessionEventArgsBase.EnableWinAuth SessionEventArgsBase.IsHttps SessionEventArgsBase.ClientLocalEndPoint SessionEventArgsBase.ClientRemoteEndPoint SessionEventArgsBase.ClientEndPoint SessionEventArgsBase.ServerRemoteEndPoint SessionEventArgsBase.ServerIpAddress SessionEventArgsBase.HttpClient SessionEventArgsBase.WebSession SessionEventArgsBase.CustomUpStreamProxy SessionEventArgsBase.CustomUpStreamProxyUsed SessionEventArgsBase.UpstreamDestinationId SessionEventArgsBase.ProxyEndPoint SessionEventArgsBase.LocalEndPoint SessionEventArgsBase.IsTransparent SessionEventArgsBase.IsSocks SessionEventArgsBase.Exception SessionEventArgsBase.Logger SessionEventArgsBase.Dispose() SessionEventArgsBase.OnException(Exception) SessionEventArgsBase.DataSent SessionEventArgsBase.DataReceived SessionEventArgsBase.TerminateSession() ProxyEventArgsBase.ClientUserData EventArgs.Empty object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public class SessionEventArgs : SessionEventArgsBase, IDisposable Properties | Edit this page View Source IdleReadTimeout Per-session override for IdleReadTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? IdleReadTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source IdleWriteTimeout Per-session override for IdleWriteTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? IdleWriteTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source IsPromise Is this session a HTTP/2 promise? Declaration public bool IsPromise { get; } Property Value Type Description bool | Edit this page View Source MaxBufferedBodyBytes Per-session override for MaxBufferedBodyBytes. null uses the server default. Set in BeforeRequest to increase the limit for large uploads/downloads without relaxing the global limit for all requests. Declaration public int? MaxBufferedBodyBytes { get; set; } Property Value Type Description int? | Edit this page View Source MaxWebSocketFramePayloadBytes Per-session override for MaxWebSocketFramePayloadBytes. null uses the server default. Set in BeforeRequest before the WebSocket upgrade completes. Declaration public int? MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int? | Edit this page View Source NetworkFailureRetryAttempts Per-session override for NetworkFailureRetryAttempts. null uses the server default. Set to 0 in BeforeRequest for non-idempotent methods (POST, PATCH) to prevent unsafe retries. Declaration public int? NetworkFailureRetryAttempts { get; set; } Property Value Type Description int? | Edit this page View Source OriginHttpVersionPolicy Per-session override for OriginHttpVersionPolicy. null uses the server default (PreserveClientVersion unless the server property was changed). Set in BeforeRequest. Declaration public OriginHttpVersionPolicy? OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy? | Edit this page View Source ReRequest Should we send the request again ? Declaration public bool ReRequest { get; set; } Property Value Type Description bool | Edit this page View Source RequestTimeout Per-session override for RequestTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? RequestTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source ResponseHeaderTimeout Per-session override for ResponseHeaderTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? ResponseHeaderTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source UpstreamHttpProtocol Per-request outbound protocol version policy. Overrides the connection-level UpstreamHttpProtocol value set during BeforeSslAuthenticate / BeforeQuicAuthenticate for this single request stream only. null uses the connection-level policy (or Auto if none was set). Evaluated in BeforeRequest; changes after that have no effect. On an H3 inbound connection (one client QUIC connection serving many concurrent streams), each stream resolves its outbound protocol independently after BeforeRequest fires, making per-stream protocol overrides possible even though the inbound leg is already QUIC. Declaration public UpstreamHttpProtocol? UpstreamHttpProtocol { get; set; } Property Value Type Description UpstreamHttpProtocol? | Edit this page View Source WebSocketClientWriter Inject frames toward the local client (server→client direction, unmasked). Available only while an intercepted WebSocket relay is active. Declaration public WebSocketFrameWriter? WebSocketClientWriter { get; } Property Value Type Description WebSocketFrameWriter | Edit this page View Source WebSocketDecoder Declaration [Obsolete(\"Use [WebSocketDecoderReceive] instead\")] public WebSocketDecoder WebSocketDecoder { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketDecoderReceive Declaration public WebSocketDecoder WebSocketDecoderReceive { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketDecoderSend Declaration public WebSocketDecoder WebSocketDecoderSend { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketServerWriter Inject frames toward the remote server (client→server direction, masked). Available only while an intercepted WebSocket relay is active. Declaration public WebSocketFrameWriter? WebSocketServerWriter { get; } Property Value Type Description WebSocketFrameWriter Methods | Edit this page View Source Dispose(bool) Declaration protected override void Dispose(bool disposing) Parameters Type Name Description bool disposing Overrides SessionEventArgsBase.Dispose(bool) | Edit this page View Source DrainClientBodyAsync(CancellationToken) Drains (reads and discards) any unread client request body from the underlying stream or connection so the client's keep-alive / multiplexed connection can be reused. This reads the bytes off the wire without buffering them in memory. It is a no-op if the body was already received or the request has no body. Declaration public Task DrainClientBodyAsync(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Returns Type Description Task Remarks Useful when short-circuiting a request (e.g. Respond(Response, bool), RespondStreaming(StreamingProxyResult, bool), or blocking) while the client is uploading a body: draining leaves the client connection in a reusable state. Note the proxy already drains the client body automatically on the normal synthetic-response path, so this is only needed for advanced/manual control. Warning: for an endless chunked request (one that never sends its terminating zero chunk) this will block until the passed cancellationToken is cancelled or the connection closes. | Edit this page View Source DrainServerBodyAsync(CancellationToken) Drains (reads and discards) any unread server response body from the underlying client/server stream or connection so it can be reused. This reads the bytes off the wire without buffering them in memory. It is a no-op if the body was already received or the response has no body. Declaration public Task DrainServerBodyAsync(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Returns Type Description Task Remarks Warning: for an endless chunked response (one that never sends its terminating zero chunk) this will block until the passed cancellationToken is cancelled or the connection closes. In that case prefer closing the connection (e.g. TerminateServerConnection()) instead. | Edit this page View Source GenericResponse(byte[], HttpStatusCode, IDictionary, bool) Before request is made to server respond with the specified byte[], the specified status to client. And then ignore the request. Declaration public void GenericResponse(byte[] result, HttpStatusCode status, IDictionary headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The bytes to sent. HttpStatusCode status The HTTP status code. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(byte[], HttpStatusCode, IEnumerable?, bool) Before request is made to server respond with the specified byte[], the specified status to client. And then ignore the request. Declaration public void GenericResponse(byte[] result, HttpStatusCode status, IEnumerable? headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The bytes to sent. HttpStatusCode status The HTTP status code. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(string, HttpStatusCode, IDictionary?, bool) Before request is made to server respond with the specified HTML string and the specified status to client. And then ignore the request. Declaration public void GenericResponse(string html, HttpStatusCode status, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description string html The html content. HttpStatusCode status The HTTP status code. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(string, HttpStatusCode, IEnumerable?, bool) Before request is made to server respond with the specified HTML string and the specified status to client. And then ignore the request. Declaration public void GenericResponse(string html, HttpStatusCode status, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description string html The html content. HttpStatusCode status The HTTP status code. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GetRequestBody(CancellationToken) Gets the request body as bytes. Declaration public Task GetRequestBody(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The body as bytes. | Edit this page View Source GetRequestBodyAsString(CancellationToken) Gets the request body as string. Declaration public Task GetRequestBodyAsString(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The body as string. | Edit this page View Source GetResponseBody(CancellationToken) Gets the response body as bytes. Declaration public Task GetResponseBody(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The resulting bytes. | Edit this page View Source GetResponseBodyAsString(CancellationToken) Gets the response body as string. Declaration public Task GetResponseBodyAsString(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The string body. | Edit this page View Source Ok(byte[], IDictionary?, bool) Before request is made to server respond with the specified byte[] to client and ignore the request. Declaration public void Ok(byte[] result, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The html content bytes. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(byte[], IEnumerable?, bool) Before request is made to server respond with the specified byte[] to client and ignore the request. Declaration public void Ok(byte[] result, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description byte[] result The html content bytes. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(string, IDictionary?, bool) Before request is made to server respond with the specified HTML string to client and ignore the request. Declaration public void Ok(string html, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description string html HTML content to sent. IDictionary headers HTTP response headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(string, IEnumerable?, bool) Before request is made to server respond with the specified HTML string to client and ignore the request. Declaration public void Ok(string html, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description string html HTML content to sent. IEnumerable headers HTTP response headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Redirect(string, bool) Redirect to provided URL. Declaration public void Redirect(string url, bool closeServerConnection = false) Parameters Type Name Description string url The URL to redirect. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Respond(Response, bool) Respond with given response object to client. Declaration public void Respond(Response response, bool closeServerConnection = false) Parameters Type Name Description Response response The response object. bool closeServerConnection Close the server connection used by request if any? Remarks If the server response was already received, the original server body (if any) is drained (syphoned) so the server connection stays reusable. To avoid reading a large or endless server body, pass closeServerConnection = true (or call TerminateServerConnection()), which closes the connection instead of draining. Note that an HTTP/1.1 connection cannot be both reused and have its body skipped. | Edit this page View Source RespondStreaming(Response, Func, bool) Respond to the client with a streamed body produced on the fly, without buffering the whole body in memory. Use this to serve large or endless bodies (e.g. a multi-gigabyte file or a synthetic server-sent-events stream) from scratch. Declaration public void RespondStreaming(Response response, Func writeBody, bool closeServerConnection = false) Parameters Type Name Description Response response The response object (status and headers). Func writeBody Delegate that writes the body to the provided stream. bool closeServerConnection Close the server connection used by request if any? Remarks Framing is chosen from the response headers: if a Content-Length is set on response the body is written raw (the delegate must write exactly that many bytes); otherwise HTTP/1.1 uses chunked transfer-encoding and HTTP/2/HTTP/3 emit DATA / stream frames. The delegate receives a write-only stream; only a single buffer is in flight at a time, so memory stays bounded regardless of the total size. See Respond(Response, bool) for the server body syphon-vs-close trade-off controlled by closeServerConnection. | Edit this page View Source RespondStreaming(StreamingProxyResult, bool) Respond to the client with a streamed body produced by Stream(HttpStatusCode, string, Func, long?) or File(string, string, HttpStatusCode). Declaration public void RespondStreaming(StreamingProxyResult result, bool closeServerConnection) Parameters Type Name Description StreamingProxyResult result Streaming response metadata and body writer. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source SetRequestBody(byte[]) Sets the request body. Declaration public void SetRequestBody(byte[] body) Parameters Type Name Description byte[] body The request body bytes. | Edit this page View Source SetRequestBodyString(string) Sets the body with the specified string. Declaration public void SetRequestBodyString(string body) Parameters Type Name Description string body The request body string to set. | Edit this page View Source SetResponseBody(byte[]) Set the response body bytes. Declaration public void SetResponseBody(byte[] body) Parameters Type Name Description byte[] body The body bytes to set. | Edit this page View Source SetResponseBodyString(string) Replace the response body with the specified string. Declaration public void SetResponseBodyString(string body) Parameters Type Name Description string body The body string to set. | Edit this page View Source TerminateServerConnection() Terminate the connection to server at the end of this HTTP request/response session. Declaration public void TerminateServerConnection() Events | Edit this page View Source BeforeWebSocketFrame Fired for each WebSocket frame after upgrade when at least one handler is subscribed. Handlers may Forward, Drop, or Replace the frame, optionally with a Delay. Observational DataSent / DataReceived still fire for bytes actually written to the peer. Declaration public event AsyncEventHandler? BeforeWebSocketFrame Event Type Type Description AsyncEventHandler | Edit this page View Source MultipartRequestPartSent Occurs when multipart request part sent. Declaration public event EventHandler? MultipartRequestPartSent Event Type Type Description EventHandler Implements IDisposable" + "summary": "Class SessionEventArgs Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. Inheritance object EventArgs ProxyEventArgsBase SessionEventArgsBase SessionEventArgs Implements IDisposable Inherited Members SessionEventArgsBase.BufferPool SessionEventArgsBase.ClientConnectionId SessionEventArgsBase.ServerConnectionId SessionEventArgsBase.Timing SessionEventArgsBase.UpstreamConnectionTiming SessionEventArgsBase.UserData SessionEventArgsBase.ConnectTimeout SessionEventArgsBase.EnableWinAuth SessionEventArgsBase.IsHttps SessionEventArgsBase.ClientLocalEndPoint SessionEventArgsBase.ClientRemoteEndPoint SessionEventArgsBase.ClientEndPoint SessionEventArgsBase.ServerRemoteEndPoint SessionEventArgsBase.ServerIpAddress SessionEventArgsBase.HttpClient SessionEventArgsBase.WebSession SessionEventArgsBase.CustomUpStreamProxy SessionEventArgsBase.CustomUpStreamProxyUsed SessionEventArgsBase.UpstreamDestinationId SessionEventArgsBase.ProxyEndPoint SessionEventArgsBase.LocalEndPoint SessionEventArgsBase.IsTransparent SessionEventArgsBase.IsSocks SessionEventArgsBase.Exception SessionEventArgsBase.Logger SessionEventArgsBase.Dispose() SessionEventArgsBase.OnException(Exception) SessionEventArgsBase.DataSent SessionEventArgsBase.DataReceived SessionEventArgsBase.TerminateSession() ProxyEventArgsBase.ClientUserData EventArgs.Empty object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public class SessionEventArgs : SessionEventArgsBase, IDisposable Properties | Edit this page View Source IdleReadTimeout Per-session override for IdleReadTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? IdleReadTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source IdleWriteTimeout Per-session override for IdleWriteTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? IdleWriteTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source IsPromise Is this session a HTTP/2 promise? Declaration public bool IsPromise { get; } Property Value Type Description bool | Edit this page View Source MaxBufferedBodyBytes Per-session override for MaxBufferedBodyBytes. null uses the server default. Set in BeforeRequest to increase the limit for large uploads/downloads without relaxing the global limit for all requests. Declaration public int? MaxBufferedBodyBytes { get; set; } Property Value Type Description int? | Edit this page View Source MaxWebSocketFramePayloadBytes Per-session override for MaxWebSocketFramePayloadBytes. null uses the server default. Set in BeforeRequest before the WebSocket upgrade completes. Declaration public int? MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int? | Edit this page View Source NetworkFailureRetryAttempts Per-session override for NetworkFailureRetryAttempts. null uses the server default. Set to 0 in BeforeRequest for non-idempotent methods (POST, PATCH) to prevent unsafe retries. Declaration public int? NetworkFailureRetryAttempts { get; set; } Property Value Type Description int? | Edit this page View Source OriginHttpVersionPolicy Per-session override for OriginHttpVersionPolicy. null uses the server default (PreserveClientVersion unless the server property was changed). Set in BeforeRequest. Declaration public OriginHttpVersionPolicy? OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy? | Edit this page View Source ReRequest Should we send the request again ? Declaration public bool ReRequest { get; set; } Property Value Type Description bool | Edit this page View Source RequestTimeout Per-session override for RequestTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? RequestTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source ResponseHeaderTimeout Per-session override for ResponseHeaderTimeoutSeconds. null uses the server default; Zero or negative disables. Declaration public TimeSpan? ResponseHeaderTimeout { get; set; } Property Value Type Description TimeSpan? | Edit this page View Source UpstreamHttpProtocol Per-request outbound protocol version policy. Overrides the connection-level UpstreamHttpProtocol value set during BeforeSslAuthenticate / BeforeQuicAuthenticate for this single request stream only. null uses the connection-level policy (or Auto if none was set). Evaluated in BeforeRequest; changes after that have no effect. On an H3 inbound connection (one client QUIC connection serving many concurrent streams), each stream resolves its outbound protocol independently after BeforeRequest fires, making per-stream protocol overrides possible even though the inbound leg is already QUIC. Declaration public UpstreamHttpProtocol? UpstreamHttpProtocol { get; set; } Property Value Type Description UpstreamHttpProtocol? | Edit this page View Source WebSocketClientWriter Inject frames toward the local client (server→client direction, unmasked). Available only while an intercepted WebSocket relay is active. Declaration public WebSocketFrameWriter? WebSocketClientWriter { get; } Property Value Type Description WebSocketFrameWriter | Edit this page View Source WebSocketDecoder Declaration [Obsolete(\"Use [WebSocketDecoderReceive] instead\")] public WebSocketDecoder WebSocketDecoder { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketDecoderReceive Declaration public WebSocketDecoder WebSocketDecoderReceive { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketDecoderSend Declaration public WebSocketDecoder WebSocketDecoderSend { get; } Property Value Type Description WebSocketDecoder | Edit this page View Source WebSocketServerWriter Inject frames toward the remote server (client→server direction, masked). Available only while an intercepted WebSocket relay is active. Declaration public WebSocketFrameWriter? WebSocketServerWriter { get; } Property Value Type Description WebSocketFrameWriter Methods | Edit this page View Source CanBufferBody(RequestResponseBase) Whether the proxy can buffer message without exceeding the effective MaxBufferedBodyBytes budget. Unknown-length bodies (chunked, HTTP/2 or HTTP/3 without Content-Length, server-sent events) return false: they must be relayed, not accumulated. A non-positive budget means unbounded and returns true. WebSocket upgrades are never buffered. Declaration public bool CanBufferBody(RequestResponseBase message) Parameters Type Name Description RequestResponseBase message Returns Type Description bool | Edit this page View Source Dispose(bool) Declaration protected override void Dispose(bool disposing) Parameters Type Name Description bool disposing Overrides SessionEventArgsBase.Dispose(bool) | Edit this page View Source DrainClientBodyAsync(CancellationToken) Drains (reads and discards) any unread client request body from the underlying stream or connection so the client's keep-alive / multiplexed connection can be reused. This reads the bytes off the wire without buffering them in memory. It is a no-op if the body was already received or the request has no body. Declaration public Task DrainClientBodyAsync(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Returns Type Description Task Remarks Useful when short-circuiting a request (e.g. Respond(Response, bool), RespondStreaming(StreamingProxyResult, bool), or blocking) while the client is uploading a body: draining leaves the client connection in a reusable state. Note the proxy already drains the client body automatically on the normal synthetic-response path, so this is only needed for advanced/manual control. Warning: for an endless chunked request (one that never sends its terminating zero chunk) this will block until the passed cancellationToken is cancelled or the connection closes. | Edit this page View Source DrainServerBodyAsync(CancellationToken) Drains (reads and discards) any unread server response body from the underlying client/server stream or connection so it can be reused. This reads the bytes off the wire without buffering them in memory. It is a no-op if the body was already received or the response has no body. Declaration public Task DrainServerBodyAsync(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Returns Type Description Task Remarks Warning: for an endless chunked response (one that never sends its terminating zero chunk) this will block until the passed cancellationToken is cancelled or the connection closes. In that case prefer closing the connection (e.g. TerminateServerConnection()) instead. | Edit this page View Source GenericResponse(byte[], HttpStatusCode, IDictionary, bool) Before request is made to server respond with the specified byte[], the specified status to client. And then ignore the request. Declaration public void GenericResponse(byte[] result, HttpStatusCode status, IDictionary headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The bytes to sent. HttpStatusCode status The HTTP status code. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(byte[], HttpStatusCode, IEnumerable?, bool) Before request is made to server respond with the specified byte[], the specified status to client. And then ignore the request. Declaration public void GenericResponse(byte[] result, HttpStatusCode status, IEnumerable? headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The bytes to sent. HttpStatusCode status The HTTP status code. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(string, HttpStatusCode, IDictionary?, bool) Before request is made to server respond with the specified HTML string and the specified status to client. And then ignore the request. Declaration public void GenericResponse(string html, HttpStatusCode status, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description string html The html content. HttpStatusCode status The HTTP status code. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GenericResponse(string, HttpStatusCode, IEnumerable?, bool) Before request is made to server respond with the specified HTML string and the specified status to client. And then ignore the request. Declaration public void GenericResponse(string html, HttpStatusCode status, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description string html The html content. HttpStatusCode status The HTTP status code. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source GetRequestBody(CancellationToken) Gets the request body as bytes. Declaration public Task GetRequestBody(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The body as bytes. | Edit this page View Source GetRequestBodyAsString(CancellationToken) Gets the request body as string. Declaration public Task GetRequestBodyAsString(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The body as string. | Edit this page View Source GetResponseBody(CancellationToken) Gets the response body as bytes. Declaration public Task GetResponseBody(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The resulting bytes. | Edit this page View Source GetResponseBodyAsString(CancellationToken) Gets the response body as string. Declaration public Task GetResponseBodyAsString(CancellationToken cancellationToken = default) Parameters Type Name Description CancellationToken cancellationToken Optional cancellation token for this async task. Returns Type Description Task The string body. | Edit this page View Source Ok(byte[], IDictionary?, bool) Before request is made to server respond with the specified byte[] to client and ignore the request. Declaration public void Ok(byte[] result, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description byte[] result The html content bytes. IDictionary headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(byte[], IEnumerable?, bool) Before request is made to server respond with the specified byte[] to client and ignore the request. Declaration public void Ok(byte[] result, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description byte[] result The html content bytes. IEnumerable headers The HTTP headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(string, IDictionary?, bool) Before request is made to server respond with the specified HTML string to client and ignore the request. Declaration public void Ok(string html, IDictionary? headers, bool closeServerConnection = false) Parameters Type Name Description string html HTML content to sent. IDictionary headers HTTP response headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Ok(string, IEnumerable?, bool) Before request is made to server respond with the specified HTML string to client and ignore the request. Declaration public void Ok(string html, IEnumerable? headers = null, bool closeServerConnection = false) Parameters Type Name Description string html HTML content to sent. IEnumerable headers HTTP response headers. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Redirect(string, bool) Redirect to provided URL. Declaration public void Redirect(string url, bool closeServerConnection = false) Parameters Type Name Description string url The URL to redirect. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source Respond(Response, bool) Respond with given response object to client. Declaration public void Respond(Response response, bool closeServerConnection = false) Parameters Type Name Description Response response The response object. bool closeServerConnection Close the server connection used by request if any? Remarks If the server response was already received, the original server body (if any) is drained (syphoned) so the server connection stays reusable. To avoid reading a large or endless server body, pass closeServerConnection = true (or call TerminateServerConnection()), which closes the connection instead of draining. Note that an HTTP/1.1 connection cannot be both reused and have its body skipped. | Edit this page View Source RespondStreaming(Response, Func, bool) Respond to the client with a streamed body produced on the fly, without buffering the whole body in memory. Use this to serve large or endless bodies (e.g. a multi-gigabyte file or a synthetic server-sent-events stream) from scratch. Declaration public void RespondStreaming(Response response, Func writeBody, bool closeServerConnection = false) Parameters Type Name Description Response response The response object (status and headers). Func writeBody Delegate that writes the body to the provided stream. bool closeServerConnection Close the server connection used by request if any? Remarks Framing is chosen from the response headers: if a Content-Length is set on response the body is written raw (the delegate must write exactly that many bytes); otherwise HTTP/1.1 uses chunked transfer-encoding and HTTP/2/HTTP/3 emit DATA / stream frames. The delegate receives a write-only stream; only a single buffer is in flight at a time, so memory stays bounded regardless of the total size. See Respond(Response, bool) for the server body syphon-vs-close trade-off controlled by closeServerConnection. | Edit this page View Source RespondStreaming(StreamingProxyResult, bool) Respond to the client with a streamed body produced by Stream(HttpStatusCode, string, Func, long?) or File(string, string, HttpStatusCode). Declaration public void RespondStreaming(StreamingProxyResult result, bool closeServerConnection) Parameters Type Name Description StreamingProxyResult result Streaming response metadata and body writer. bool closeServerConnection Close the server connection used by request if any? | Edit this page View Source SetRequestBody(byte[]) Sets the request body. Declaration public void SetRequestBody(byte[] body) Parameters Type Name Description byte[] body The request body bytes. | Edit this page View Source SetRequestBodyString(string) Sets the body with the specified string. Declaration public void SetRequestBodyString(string body) Parameters Type Name Description string body The request body string to set. | Edit this page View Source SetResponseBody(byte[]) Set the response body bytes. Declaration public void SetResponseBody(byte[] body) Parameters Type Name Description byte[] body The body bytes to set. | Edit this page View Source SetResponseBodyString(string) Replace the response body with the specified string. Declaration public void SetResponseBodyString(string body) Parameters Type Name Description string body The body string to set. | Edit this page View Source TerminateServerConnection() Terminate the connection to server at the end of this HTTP request/response session. Declaration public void TerminateServerConnection() Events | Edit this page View Source BeforeWebSocketFrame Fired for each WebSocket frame after upgrade when at least one handler is subscribed. Handlers may Forward, Drop, or Replace the frame, optionally with a Delay. Observational DataSent / DataReceived still fire for bytes actually written to the peer. Declaration public event AsyncEventHandler? BeforeWebSocketFrame Event Type Type Description AsyncEventHandler | Edit this page View Source MultipartRequestPartSent Occurs when multipart request part sent. Declaration public event EventHandler? MultipartRequestPartSent Event Type Type Description EventHandler | Edit this page View Source WebSocketFrameElided Fired when WebSocketFrameBudget is Observe and a frame exceeds MaxWebSocketFramePayloadBytes. The payload is relayed unbuffered and is not passed to BeforeWebSocketFrame. Declaration public event AsyncEventHandler? WebSocketFrameElided Event Type Type Description AsyncEventHandler Implements IDisposable" }, "api/Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html": { "href": "api/Titanium.Web.Proxy.EventArguments.SessionEventArgsBase.html", @@ -144,6 +144,11 @@ "title": "Enum WebSocketFrameDirection | Titanium Web Proxy", "summary": "Enum WebSocketFrameDirection Direction of a WebSocket frame relative to the proxy. Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public enum WebSocketFrameDirection Fields Name Description ClientToServer Client → server (must be masked on the wire). ServerToClient Server → client (must not be masked on the wire)." }, + "api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html": { + "href": "api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html", + "title": "Class WebSocketFrameElidedEventArgs | Titanium Web Proxy", + "summary": "Class WebSocketFrameElidedEventArgs Metadata for a WebSocket frame that exceeded the payload limit under Observe. The payload itself is not included. Inheritance object EventArgs ProxyEventArgsBase WebSocketFrameElidedEventArgs Inherited Members ProxyEventArgsBase.ClientUserData EventArgs.Empty object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.EventArguments Assembly: Titanium.Web.Proxy.dll Syntax public sealed class WebSocketFrameElidedEventArgs : ProxyEventArgsBase Properties | Edit this page View Source DeclaredPayloadLength Declaration public long DeclaredPayloadLength { get; } Property Value Type Description long | Edit this page View Source Direction Declaration public WebSocketFrameDirection Direction { get; } Property Value Type Description WebSocketFrameDirection | Edit this page View Source IsFinal Declaration public bool IsFinal { get; } Property Value Type Description bool | Edit this page View Source OpCode Declaration public WebsocketOpCode OpCode { get; } Property Value Type Description WebsocketOpCode | Edit this page View Source Session Declaration public SessionEventArgs Session { get; } Property Value Type Description SessionEventArgs" + }, "api/Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html": { "href": "api/Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html", "title": "Enum WebSocketFrameInterceptAction | Titanium Web Proxy", @@ -157,7 +162,7 @@ "api/Titanium.Web.Proxy.EventArguments.html": { "href": "api/Titanium.Web.Proxy.EventArguments.html", "title": "Namespace Titanium.Web.Proxy.EventArguments | Titanium Web Proxy", - "summary": "Namespace Titanium.Web.Proxy.EventArguments Classes BeforeBodyWriteEventArgs BeforeHttpAuthenticateEventArgs Fired on a transparent reverse endpoint before handling a cleartext (non-TLS) client session, including prior-knowledge HTTP/2 (h2c). Mirrors the policy knobs on BeforeSslAuthenticateEventArgs without a TLS handshake. BeforeQuicAuthenticateEventArgs Event arguments raised on a TransparentQuicProxyEndPoint before the QUIC TLS handshake completes, analogous to BeforeSslAuthenticateEventArgs for transparent TCP endpoints. Unlike its TCP counterpart, this event does not expose a DecryptSsl property: QUIC always terminates TLS 1.3 at the proxy — there is no pass-through mode. Calling Reject() is the only way to refuse the connection before the handshake finishes. Thread-safety: each inbound QUIC connection fires a dedicated instance of this event on its own accept task. Handlers that mutate shared state must synchronize themselves. Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in. BeforeSslAuthenticateEventArgs This is used in transparent endpoint before authenticating client. CertificateSelectionEventArgs An argument passed on to user for client certificate selection during mutual SSL authentication. CertificateValidationEventArgs An argument passed on to the user for validating the server certificate during SSL authentication. MultipartRequestPartSentEventArgs Class that wraps the multipart sent request arguments. ProxyEventArgsBase The base event arguments SessionEventArgs Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. SessionEventArgsBase Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. SocksAuthenticateEventArgs Context for SOCKS5 username/password authentication on a SocksProxyEndPoint. TunnelConnectFailureEventArgs Raised when optional pre-200 upstream connectivity verification fails for a CONNECT tunnel (EstablishServerConnectionBeforeResponse). Handlers may replace Response with a custom HTTP error before it is written to the client (no TLS has been started yet). TunnelConnectSessionEventArgs A class that wraps the state when a tunnel connect event happen for Explicit endpoints. WebSocketFrameInterceptEventArgs Frame-level interception context for an active WebSocket session. Enums WebSocketFrameDirection Direction of a WebSocket frame relative to the proxy. WebSocketFrameInterceptAction Action taken by a BeforeWebSocketFrame handler. Delegates AsyncEventHandler A generic asynchronous event handler used by the proxy." + "summary": "Namespace Titanium.Web.Proxy.EventArguments Classes BeforeBodyWriteEventArgs BeforeHttpAuthenticateEventArgs Fired on a transparent reverse endpoint before handling a cleartext (non-TLS) client session, including prior-knowledge HTTP/2 (h2c). Mirrors the policy knobs on BeforeSslAuthenticateEventArgs without a TLS handshake. BeforeQuicAuthenticateEventArgs Event arguments raised on a TransparentQuicProxyEndPoint before the QUIC TLS handshake completes, analogous to BeforeSslAuthenticateEventArgs for transparent TCP endpoints. Unlike its TCP counterpart, this event does not expose a DecryptSsl property: QUIC always terminates TLS 1.3 at the proxy — there is no pass-through mode. Calling Reject() is the only way to refuse the connection before the handshake finishes. Thread-safety: each inbound QUIC connection fires a dedicated instance of this event on its own accept task. Handlers that mutate shared state must synchronize themselves. Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in. BeforeSslAuthenticateEventArgs This is used in transparent endpoint before authenticating client. CertificateSelectionEventArgs An argument passed on to user for client certificate selection during mutual SSL authentication. CertificateValidationEventArgs An argument passed on to the user for validating the server certificate during SSL authentication. MultipartRequestPartSentEventArgs Class that wraps the multipart sent request arguments. ProxyEventArgsBase The base event arguments SessionEventArgs Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. SessionEventArgsBase Holds info related to a single proxy session (single request/response exchange). Under HTTP/2 and HTTP/3, many sessions share one client connection (one stream each); ending a session ends that request/response exchange, not necessarily the connection. SocksAuthenticateEventArgs Context for SOCKS5 username/password authentication on a SocksProxyEndPoint. TunnelConnectFailureEventArgs Raised when optional pre-200 upstream connectivity verification fails for a CONNECT tunnel (EstablishServerConnectionBeforeResponse). Handlers may replace Response with a custom HTTP error before it is written to the client (no TLS has been started yet). TunnelConnectSessionEventArgs A class that wraps the state when a tunnel connect event happen for Explicit endpoints. WebSocketFrameElidedEventArgs Metadata for a WebSocket frame that exceeded the payload limit under Observe. The payload itself is not included. WebSocketFrameInterceptEventArgs Frame-level interception context for an active WebSocket session. Enums WebSocketFrameDirection Direction of a WebSocket frame relative to the proxy. WebSocketFrameInterceptAction Action taken by a BeforeWebSocketFrame handler. Delegates AsyncEventHandler A generic asynchronous event handler used by the proxy." }, "api/Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html": { "href": "api/Titanium.Web.Proxy.Exceptions.BodyNotFoundException.html", @@ -502,7 +507,7 @@ "api/Titanium.Web.Proxy.Options.PolicyFamily.html": { "href": "api/Titanium.Web.Proxy.Options.PolicyFamily.html", "title": "Enum PolicyFamily | Titanium Web Proxy", - "summary": "Enum PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public enum PolicyFamily Fields Name Description AdmissionControl The global and per-endpoint admission gates that bound concurrently admitted client connections. BodyBudget Cumulative whole-body buffering limits (MaxBufferedBodyBytes and the MaxEncodedBodyBytes/MaxDecodedBodyBytes pair) enforced via Titanium.Web.Proxy.Network.Streams.BoundedWriteStream across H1, H2 and H3. DecompressionRatio The compressed-input/decompressed-output byte budgets and the expansion-ratio ceiling (MaxDecompressionRatio) applied while draining a Content-Encoding chain, so a small compressed body cannot expand unboundedly in memory before BodyBudget's own decoded-byte cap would catch it. Today, that decoded-byte cap is exactly what protects this case in practice: the decompression chain writes into the same Titanium.Web.Proxy.Network.Streams.BoundedWriteStream- wrapped target BodyBudget already bounds, so a small compressed body that expands enormously is caught the moment the decoded output crosses that limit, without needing a separately computed ratio. MaxDecompressionRatio and the encoded/decoded byte pair remain reserved for a future, more precise per-stream computation; this family's mode exists now so a profile can name it, but changing it has no additional effect while BodyBudget already covers the same paths. HeaderLimits Header line length, header count and aggregate header-byte limits (MaxHeaderLineBytes/MaxHeaderCount/ MaxHeaderAggregateBytes) intended for the request/response header-block read. Reserved, like DecompressionRatio, for numeric enforcement not yet wired to every header-reading call site; this family's mode exists so a profile can name it ahead of that work landing. The client request-line/header deadline (a different, already-enforced protection - see ProxyServer.ClientHeaderTimeoutSeconds and DeadlineRegistry) is unaffected by this family's mode. Http1ReplaySafety Whether HTTP/1 keep-alive retries (EOF-before-response, malformed status, mid-exchange IOException) and bare CR in header values follow the strict replay-safe rules. Observe (default on Balanced and LegacyCompatible) keeps today's lenient behavior and records a policy breach metric when the strict rule would have fired. Enforce (default on PublicFacing) refuses to replay non-idempotent methods and rejects bare CR/LF/NUL in header values. Http2AbuseBudget HTTP/2 abuse budgets: the open-header-block CONTINUATION frame-count/wall-clock bound and the peer-initiated incomplete-stream-reset budget. Http2RelayValidation Whether HPACK header blocks on the H2↔H2 compressed-relay path (httpInterceptionEnabled = false) are semantically validated per RFC 9113 §8.3. Unlike framing (always enforced, no Observe action), header semantics can be logged without corrupting connection state. Disabled (default on Balanced) skips HPACK decode entirely — maximum throughput when upstream peers are trusted. Observe decodes and records semantic violations without rejecting the stream. Does not mutate headers, so the compressed-relay MutationCount fast path is unaffected when the family is Disabled. Enforce (default on PublicFacing and AllEnforce) decodes and sends GOAWAY(PROTOCOL_ERROR) on violations." + "summary": "Enum PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public enum PolicyFamily Fields Name Description AdmissionControl The global and per-endpoint admission gates that bound concurrently admitted client connections. BodyBudget Cumulative whole-body buffering limits (MaxBufferedBodyBytes) enforced via Titanium.Web.Proxy.Network.Streams.BoundedWriteStream when a caller materializes a body. Streaming relays do not consult this family. MaxEncodedBodyBytes and MaxDecodedBodyBytes are reserved. They are accepted by option plumbing and are not enforced on a live read path; the enforced whole-body budget is MaxBufferedBodyBytes. DecompressionRatio The compressed-input/decompressed-output byte budgets and the expansion-ratio ceiling (MaxDecompressionRatio) applied while draining a Content-Encoding chain, so a small compressed body cannot expand unboundedly in memory before BodyBudget's own decoded-byte cap would catch it. Today, that decoded-byte cap is exactly what protects this case in practice: the decompression chain writes into the same Titanium.Web.Proxy.Network.Streams.BoundedWriteStream- wrapped target BodyBudget already bounds, so a small compressed body that expands enormously is caught the moment the decoded output crosses that limit, without needing a separately computed ratio. MaxDecompressionRatio and the encoded/decoded byte pair remain reserved for a future, more precise per-stream computation; this family's mode exists now so a profile can name it, but changing it has no additional effect while BodyBudget already covers the same paths. HeaderLimits Header line length, header count and aggregate header-byte limits (MaxHeaderLineBytes/MaxHeaderCount/ MaxHeaderAggregateBytes) intended for the request/response header-block read. Reserved, like DecompressionRatio, for numeric enforcement not yet wired to every header-reading call site; this family's mode exists so a profile can name it ahead of that work landing. The client request-line/header deadline (a different, already-enforced protection - see ProxyServer.ClientHeaderTimeoutSeconds and DeadlineRegistry) is unaffected by this family's mode. Http1ReplaySafety Whether HTTP/1 keep-alive retries (EOF-before-response, malformed status, mid-exchange IOException) and bare CR in header values follow the strict replay-safe rules. Observe (default on Balanced and LegacyCompatible) keeps today's lenient behavior and records a policy breach metric when the strict rule would have fired. Enforce (default on PublicFacing) refuses to replay non-idempotent methods and rejects bare CR/LF/NUL in header values. Http2AbuseBudget HTTP/2 abuse budgets: the open-header-block CONTINUATION frame-count/wall-clock bound and the peer-initiated incomplete-stream-reset budget. Http2RelayValidation Whether HPACK header blocks on the H2↔H2 compressed-relay path (httpInterceptionEnabled = false) are semantically validated per RFC 9113 §8.3. Unlike framing (always enforced, no Observe action), header semantics can be logged without corrupting connection state. Disabled (default on Balanced) skips HPACK decode entirely — maximum throughput when upstream peers are trusted. Observe decodes and records semantic violations without rejecting the stream. Does not mutate headers, so the compressed-relay MutationCount fast path is unaffected when the family is Disabled. Enforce (default on PublicFacing and AllEnforce) decodes and sends GOAWAY(PROTOCOL_ERROR) on violations. WebSocketFrameBudget Per-frame WebSocket payload limit (MaxWebSocketFramePayloadBytes) applied only when BeforeWebSocketFrame is subscribed. Raw relay is unaffected. Enforce (default on every profile) closes the connection with status 1009 before the payload is buffered. Observe relays an oversize frame without materializing it and raises SessionEventArgs.WebSocketFrameElided. Frame hooks are not invoked for that payload, so a content filter must stay on Enforce. Disabled skips the check and will buffer the frame, up to the decoder's structural MaxValue bound." }, "api/Titanium.Web.Proxy.Options.PolicyMode.html": { "href": "api/Titanium.Web.Proxy.Options.PolicyMode.html", @@ -512,7 +517,7 @@ "api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html", "title": "Class ProxyPolicyModes | Titanium Web Proxy", - "summary": "Class ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. Inheritance object ProxyPolicyModes Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public sealed class ProxyPolicyModes Properties | Edit this page View Source AllEnforce Every family enforced - today's shipped behavior, and the starting point every profile builds from. Declaration public static ProxyPolicyModes AllEnforce { get; } Property Value Type Description ProxyPolicyModes | Edit this page View Source AllowAmbiguousFraming Off by default and absent from every profile - see the type-level remarks. Never true unless WithAllowAmbiguousFramingEnabled() was called explicitly. Declaration public bool AllowAmbiguousFraming { get; } Property Value Type Description bool | Edit this page View Source this[PolicyFamily] Returns the mode selected for family. Declaration public PolicyMode this[PolicyFamily family] { get; } Parameters Type Name Description PolicyFamily family Property Value Type Description PolicyMode Methods | Edit this page View Source Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) Builds a snapshot with an explicit mode for every family. AllowAmbiguousFraming starts false. Declaration public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe) Parameters Type Name Description PolicyMode bodyBudget PolicyMode decompressionRatio PolicyMode headerLimits PolicyMode admissionControl PolicyMode http2AbuseBudget PolicyMode http2RelayValidation PolicyMode http1ReplaySafety Returns Type Description ProxyPolicyModes | Edit this page View Source With(PolicyFamily, PolicyMode) Returns a snapshot identical to this one but with mode for family. Declaration public ProxyPolicyModes With(PolicyFamily family, PolicyMode mode) Parameters Type Name Description PolicyFamily family PolicyMode mode Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllObservedExceptDisabled() Returns a snapshot identical to this one but with every family dropped to Observe, except families already Disabled (which stay disabled - Observe would silently turn a family back on). This is the \"drop to Observe without redeploying\" runtime switch the plan's rollout section requires. Declaration public ProxyPolicyModes WithAllObservedExceptDisabled() Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllowAmbiguousFramingEnabled() The single, explicit, isolated act of relaying ambiguous HTTP/1 framing instead of rejecting it. See the type-level remarks; never call this as a side effect of applying a profile. Declaration public ProxyPolicyModes WithAllowAmbiguousFramingEnabled() Returns Type Description ProxyPolicyModes" + "summary": "Class ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. Inheritance object ProxyPolicyModes Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public sealed class ProxyPolicyModes Properties | Edit this page View Source AllEnforce Every family enforced - today's shipped behavior, and the starting point every profile builds from. Declaration public static ProxyPolicyModes AllEnforce { get; } Property Value Type Description ProxyPolicyModes | Edit this page View Source AllowAmbiguousFraming Off by default and absent from every profile - see the type-level remarks. Never true unless WithAllowAmbiguousFramingEnabled() was called explicitly. Declaration public bool AllowAmbiguousFraming { get; } Property Value Type Description bool | Edit this page View Source this[PolicyFamily] Returns the mode selected for family. Declaration public PolicyMode this[PolicyFamily family] { get; } Parameters Type Name Description PolicyFamily family Property Value Type Description PolicyMode Methods | Edit this page View Source Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) Builds a snapshot with an explicit mode for every family. AllowAmbiguousFraming starts false. Declaration public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe, PolicyMode webSocketFrameBudget = PolicyMode.Enforce) Parameters Type Name Description PolicyMode bodyBudget PolicyMode decompressionRatio PolicyMode headerLimits PolicyMode admissionControl PolicyMode http2AbuseBudget PolicyMode http2RelayValidation PolicyMode http1ReplaySafety PolicyMode webSocketFrameBudget Returns Type Description ProxyPolicyModes | Edit this page View Source With(PolicyFamily, PolicyMode) Returns a snapshot identical to this one but with mode for family. Declaration public ProxyPolicyModes With(PolicyFamily family, PolicyMode mode) Parameters Type Name Description PolicyFamily family PolicyMode mode Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllObservedExceptDisabled() Returns a snapshot identical to this one but with every family dropped to Observe, except families already Disabled (which stay disabled - Observe would silently turn a family back on). This is the \"drop to Observe without redeploying\" runtime switch the plan's rollout section requires. Declaration public ProxyPolicyModes WithAllObservedExceptDisabled() Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllowAmbiguousFramingEnabled() The single, explicit, isolated act of relaying ambiguous HTTP/1 framing instead of rejecting it. See the type-level remarks; never call this as a side effect of applying a profile. Declaration public ProxyPolicyModes WithAllowAmbiguousFramingEnabled() Returns Type Description ProxyPolicyModes" }, "api/Titanium.Web.Proxy.Options.ProxyProfile.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyProfile.html", @@ -542,17 +547,17 @@ "api/Titanium.Web.Proxy.Options.html": { "href": "api/Titanium.Web.Proxy.Options.html", "title": "Namespace Titanium.Web.Proxy.Options | Titanium Web Proxy", - "summary": "Namespace Titanium.Web.Proxy.Options Classes ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. ProxyProfileSettings The full bundle of settings one ProxyProfile applies to a ProxyServer as a single atomic assignment via Profile. Deliberately a plain data bundle, not a type with behavior: applying it is ProxyServer's job (see Profile's setter), so this type has no back-reference and no side effects of its own, per the plan's \"Constraints on the policy layer\" section. ProxyResourceLimits Immutable, validated snapshot of the resource bounds a peer can make the proxy allocate: header shape, body/decompression budgets, concurrency and abuse-rate ceilings, and pool / certificate-cache sizing. Constructed only through Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?), which validates every field up front, so an invalid limit is a construction-time exception rather than a runtime surprise discovered mid-connection. A limit that can legitimately be turned off is typed as nullable with null meaning \"disabled\" - an explicit state - rather than overloading 0 or a negative number to mean the same thing. Limits that must always be enforced because disabling them would leave the proxy itself exploitable (the concurrent-stream cap, the open-header-block frame bound) are non-nullable and always validated to be strictly positive. This type has no back-reference to ProxyServer and no mutable state after construction: it is meant to be handed down to subsystems by value, not looked up through a service locator or an ambient static, so the dependency graph among consumers stays acyclic per the plan's \"Constraints on the policy layer\" section. ProxyTimeoutOptions Immutable, validated snapshot of every deadline the proxy enforces across a request's lifetime, expressed consistently as TimeSpan rather than the mixture of \"seconds as int\" properties this replaces (ConnectionTimeOutSeconds, ConnectTimeOutSeconds, ...). A deadline that can be legitimately unbounded is nullable, with null meaning \"no deadline\" as an explicit state rather than Zero or a magic sentinel duration. This type only holds values; it does not decide which deadline fired first when several are composed for one request. That is the responsibility of the per-request deadline registry described in the plan's \"Deadline composition\" section, introduced alongside the header-parsing deadlines in a later item so it can be exercised by a real caller instead of landing as unused scaffolding. ResolvedSessionPolicy Read-only snapshot combining ProxyResourceLimits and ProxyTimeoutOptions into the single object H1/H2/H3/WebSocket subsystems are handed, so runtime mutation of either half cannot produce inconsistent enforcement partway through a request that started under a different combination of the two. Per the plan's \"Constraints on the policy layer\" section, this type is deliberately inert: no back-pointer to ProxyServer, no service-locator lookup, no mutable fields, no static ambient accessor. Subsystems receive it as a constructor argument or method parameter only, so the dependency graph among consumers stays acyclic - the opposite of how ProxyServer itself is reached today. Per the plan's \"Two-phase policy resolution\" section, a single resolution per connection is not correct: SessionEventArgs.MaxBufferedBodyBytes is contractually settable from BeforeRequest, and HTTP/3 already reads the request body before BeforeRequest fires. This type does not itself perform either resolution phase - that is the responsibility of the call sites introduced in later hardening-plan items, once there is a real per-session override path to resolve against - but it is deliberately shaped so a caller can hold one instance at headers-complete time (ResourceLimits's framing/header-shape fields, which are never overridable) and, if a session lowers a body or streaming budget in BeforeRequest, build a second instance via Create(ProxyResourceLimits, ProxyTimeoutOptions) that shares the same Timeouts but substitutes a ProxyResourceLimits reflecting the override, rather than mutating the first instance in place. Enums PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. PolicyMode How a resource-bound policy family is applied once its numeric limit is breached, per the plan's \"Rollout, profiles and documentation\" section. Not every family supports every mode. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no Observe mode at all: an ambiguous chunk size or a conflicting length can only be forwarded (a desync) or rejected, so those call sites are unconditionally enforced and never consult a PolicyMode. ProxyPolicyModes exists for the families where a safe \"detect but let it through\" middle ground is actually possible. ProxyProfile The three shipped profiles, per the plan's \"Rollout, profiles and documentation\" section. Selecting a profile via Profile applies its ProxyProfileSettings atomically to resource limits, policy modes, TLS protocols, private-network blocking, admission caps, and deadline-second properties, so a caller can never observe a half-applied profile." + "summary": "Namespace Titanium.Web.Proxy.Options Classes ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. ProxyProfileSettings The full bundle of settings one ProxyProfile applies to a ProxyServer as a single atomic assignment via Profile. Deliberately a plain data bundle, not a type with behavior: applying it is ProxyServer's job (see Profile's setter), so this type has no back-reference and no side effects of its own, per the plan's \"Constraints on the policy layer\" section. ProxyResourceLimits Immutable, validated snapshot of the resource bounds a peer can make the proxy allocate: header shape, body/decompression budgets, concurrency and abuse-rate ceilings, and pool / certificate-cache sizing. Constructed only through Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?), which validates every field up front, so an invalid limit is a construction-time exception rather than a runtime surprise discovered mid-connection. A limit that can legitimately be turned off is typed as nullable with null meaning \"disabled\" - an explicit state - rather than overloading 0 or a negative number to mean the same thing. Limits that must always be enforced because disabling them would leave the proxy itself exploitable (the concurrent-stream cap, the open-header-block frame bound) are non-nullable and always validated to be strictly positive. This type has no back-reference to ProxyServer and no mutable state after construction: it is meant to be handed down to subsystems by value, not looked up through a service locator or an ambient static, so the dependency graph among consumers stays acyclic per the plan's \"Constraints on the policy layer\" section. ProxyTimeoutOptions Immutable, validated snapshot of every deadline the proxy enforces across a request's lifetime, expressed consistently as TimeSpan rather than the mixture of \"seconds as int\" properties this replaces (ConnectionTimeOutSeconds, ConnectTimeOutSeconds, ...). A deadline that can be legitimately unbounded is nullable, with null meaning \"no deadline\" as an explicit state rather than Zero or a magic sentinel duration. This type only holds values; it does not decide which deadline fired first when several are composed for one request. That is the responsibility of the per-request deadline registry described in the plan's \"Deadline composition\" section, introduced alongside the header-parsing deadlines in a later item so it can be exercised by a real caller instead of landing as unused scaffolding. ResolvedSessionPolicy Read-only snapshot combining ProxyResourceLimits and ProxyTimeoutOptions into the single object H1/H2/H3/WebSocket subsystems are handed, so runtime mutation of either half cannot produce inconsistent enforcement partway through a request that started under a different combination of the two. Per the plan's \"Constraints on the policy layer\" section, this type is deliberately inert: no back-pointer to ProxyServer, no service-locator lookup, no mutable fields, no static ambient accessor. Subsystems receive it as a constructor argument or method parameter only, so the dependency graph among consumers stays acyclic - the opposite of how ProxyServer itself is reached today. Per the plan's \"Two-phase policy resolution\" section, a single resolution per connection is not correct: SessionEventArgs.MaxBufferedBodyBytes is contractually settable from BeforeRequest, and HTTP/3 already reads the request body before BeforeRequest fires. This type does not itself perform either resolution phase - that is the responsibility of the call sites introduced in later hardening-plan items, once there is a real per-session override path to resolve against - but it is deliberately shaped so a caller can hold one instance at headers-complete time (ResourceLimits's framing/header-shape fields, which are never overridable) and, if a session lowers a body or streaming budget in BeforeRequest, build a second instance via Create(ProxyResourceLimits, ProxyTimeoutOptions) that shares the same Timeouts but substitutes a ProxyResourceLimits reflecting the override, rather than mutating the first instance in place. Enums PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. PolicyMode How a resource-bound policy family is applied once its numeric limit is breached, per the plan's \"Rollout, profiles and documentation\" section. Not every family supports every mode. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no Observe mode at all: an ambiguous chunk size or a conflicting length can only be forwarded (a desync) or rejected, so those call sites are unconditionally enforced and never consult a PolicyMode. ProxyPolicyModes exists for the families where a safe \"detect but let it through\" middle ground is actually possible. ProxyProfile The three shipped profiles, per the plan's \"Rollout, profiles and documentation\" section. Selecting a profile via Profile applies its ProxyProfileSettings atomically to resource limits, policy modes, TLS protocols, private-network blocking, admission caps, and deadline-second properties, so a caller can never observe a half-applied profile." }, "api/Titanium.Web.Proxy.ProxyLimits.html": { "href": "api/Titanium.Web.Proxy.ProxyLimits.html", "title": "Class ProxyLimits | Titanium Web Proxy", - "summary": "Class ProxyLimits Reference values for a handful of resource-limit defaults, kept here for documentation and cross-checking purposes. Inheritance object ProxyLimits Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public static class ProxyLimits Remarks These fields are static readonly, not const, so that if a later release changes one of these reference values, a consumer that has not recompiled still picks up the new value from the referenced Titanium.Web.Proxy.dll at load time rather than keeping a stale value inlined by the compiler into their own assembly. These fields cannot be \"overridden by assigning them before starting the proxy\": they are readonly, so external code cannot assign to them at all (that would be a compile error). Only DefaultMaxChunkSizeBytes is actually read by any live code path (by ChunkSizeParser.TryParse's call sites in HttpStream/LimitedStream); every other field below documents a value that is independently hardcoded as the real default on the corresponding ProxyServer (or, for authentication rounds, WinAuthHandler) property, and is not consulted by that property's own default or by any enforcement path. Treat the fields below other than DefaultMaxChunkSizeBytes as documentation of what each corresponding property's shipped default currently is, not as the mechanism that produces it: to change a limit at runtime, set the actual ProxyServer property (e.g. ProxyServer.MaxBufferedBodyBytes), never the field here. Fields | Edit this page View Source DefaultMaxAuthRounds Reference value only - see the type-level remarks. Matches WinAuthHandler.MaxAuthChallengeRounds: maximum number of authentication challenge rounds allowed per request (e.g. NTLM three-way handshake counts as one round; additional 401/407 responses count separately). The two values are independently maintained, not linked. Default: 3. Declaration public static readonly int DefaultMaxAuthRounds Field Value Type Description int | Edit this page View Source DefaultMaxBufferedBodyBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxBufferedBodyBytes: maximum buffered request or response body size for proxied exchanges where full buffering is required (body mutation, authentication retry, etc.). Default: 4 MiB. Declaration public static readonly long DefaultMaxBufferedBodyBytes Field Value Type Description long | Edit this page View Source DefaultMaxChunkSizeBytes Maximum accepted value of a single HTTP/1 chunk-size line (RFC 9112 §7.1), in bytes. The chunk-size grammar itself (1*HEXDIG) has no length ceiling, so this is a proxy-owned safety bound rather than a protocol requirement: it exists to reject the two's-complement chunk- size wrap (an attacker-supplied value like \"ffffffff\" must not decode to a small/negative sentinel) and to avoid ever attempting to allocate or forward a chunk of unbounded size. Set high enough that no legitimate chunk from real-world traffic should reach it. Unlike every other field on this type, this one is actually read live, by every ChunkSizeParser.TryParse call site in HttpStream and LimitedStream. Default: 1 GiB. Declaration public static readonly long DefaultMaxChunkSizeBytes Field Value Type Description long | Edit this page View Source DefaultMaxDecodedHeaderListBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxDecodedHeaderListBytes: maximum total decoded HTTP/2 header list size (sum of name + value + 32 per field). Streams with header lists exceeding that property's configured value are rejected with RST_STREAM(ENHANCE_YOUR_CALM). Default: 64 KiB. Declaration public static readonly int DefaultMaxDecodedHeaderListBytes Field Value Type Description int | Edit this page View Source DefaultMaxWebSocketFramePayloadBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxWebSocketFramePayloadBytes: maximum WebSocket frame payload size the proxy will accept during frame-level interception (validated against the declared length before any payload is buffered). Raw relay (no interception) is not subject to this limit. Default: 16 MiB. Declaration public static readonly int DefaultMaxWebSocketFramePayloadBytes Field Value Type Description int | Edit this page View Source DefaultMaxWebSocketMessageBytes Reference value only - see the type-level remarks. Unlike the other fields on this type, this one does not correspond to any enforced ProxyServer property today: there is no cumulative-reassembled-message-size cap on the WebSocket path, only the per-frame DefaultMaxWebSocketFramePayloadBytes/MaxWebSocketFramePayloadBytes limit. This value is retained as a documented target for that still-unimplemented cumulative budget. Default: 64 MiB. Declaration public static readonly long DefaultMaxWebSocketMessageBytes Field Value Type Description long | Edit this page View Source DefaultViaPseudonym Unused legacy field. ProxyServer.ViaHeaderPseudonym defaults to \"titanium-web-proxy\" (not empty) and does not read this field; nothing in the codebase references it. Retained only so removing it is not itself an undocumented breaking change; do not treat it as authoritative for the real default. Declaration public static readonly string DefaultViaPseudonym Field Value Type Description string" + "summary": "Class ProxyLimits Reference values for a handful of resource-limit defaults, kept here for documentation and cross-checking purposes. Inheritance object ProxyLimits Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public static class ProxyLimits Remarks These fields are static readonly, not const, so that if a later release changes one of these reference values, a consumer that has not recompiled still picks up the new value from the referenced Titanium.Web.Proxy.dll at load time rather than keeping a stale value inlined by the compiler into their own assembly. These fields cannot be \"overridden by assigning them before starting the proxy\": they are readonly, so external code cannot assign to them at all (that would be a compile error). Only DefaultMaxChunkSizeBytes is actually read by any live code path (by ChunkSizeParser.TryParse's call sites in HttpStream/LimitedStream); every other field below documents a value that is independently hardcoded as the real default on the corresponding ProxyServer (or, for authentication rounds, WinAuthHandler) property, and is not consulted by that property's own default or by any enforcement path. Treat the fields below other than DefaultMaxChunkSizeBytes as documentation of what each corresponding property's shipped default currently is, not as the mechanism that produces it: to change a limit at runtime, set the actual ProxyServer property (e.g. ProxyServer.MaxBufferedBodyBytes), never the field here. Fields | Edit this page View Source DefaultMaxAuthRounds Reference value only - see the type-level remarks. Matches WinAuthHandler.MaxAuthChallengeRounds: maximum number of authentication challenge rounds allowed per request (e.g. NTLM three-way handshake counts as one round; additional 401/407 responses count separately). The two values are independently maintained, not linked. Default: 3. Declaration public static readonly int DefaultMaxAuthRounds Field Value Type Description int | Edit this page View Source DefaultMaxBufferedBodyBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxBufferedBodyBytes: maximum buffered request or response body size for proxied exchanges where full buffering is required (body mutation, authentication retry, etc.). Streaming relays are not counted against this value. Default: 4 MiB. Declaration public static readonly long DefaultMaxBufferedBodyBytes Field Value Type Description long | Edit this page View Source DefaultMaxChunkSizeBytes Maximum accepted value of a single HTTP/1 chunk-size line (RFC 9112 §7.1), in bytes. The chunk-size grammar itself (1*HEXDIG) has no length ceiling, so this is a proxy-owned safety bound rather than a protocol requirement: it exists to reject the two's-complement chunk- size wrap (an attacker-supplied value like \"ffffffff\" must not decode to a small/negative sentinel) and to avoid ever attempting to allocate or forward a chunk of unbounded size. Set high enough that no legitimate chunk from real-world traffic should reach it. Unlike every other field on this type, this one is actually read live, by every ChunkSizeParser.TryParse call site in HttpStream and LimitedStream. Default: 1 GiB. Declaration public static readonly long DefaultMaxChunkSizeBytes Field Value Type Description long | Edit this page View Source DefaultMaxDecodedHeaderListBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxDecodedHeaderListBytes: maximum total decoded HTTP/2 header list size (sum of name + value + 32 per field). Streams with header lists exceeding that property's configured value are rejected with RST_STREAM(ENHANCE_YOUR_CALM). Default: 64 KiB. Declaration public static readonly int DefaultMaxDecodedHeaderListBytes Field Value Type Description int | Edit this page View Source DefaultMaxWebSocketFramePayloadBytes Reference value only - see the type-level remarks. Matches the current default of ProxyServer.MaxWebSocketFramePayloadBytes: maximum WebSocket frame payload size the proxy will accept during frame-level interception (validated against the declared length before any payload is buffered). Raw relay (no interception) is not subject to this limit. Default: 16 MiB. Declaration public static readonly int DefaultMaxWebSocketFramePayloadBytes Field Value Type Description int | Edit this page View Source DefaultMaxWebSocketMessageBytes Reference value only - see the type-level remarks. Unlike the other fields on this type, this one does not correspond to any enforced ProxyServer property today: there is no cumulative-reassembled-message-size cap on the WebSocket path, only the per-frame DefaultMaxWebSocketFramePayloadBytes/MaxWebSocketFramePayloadBytes limit. This value is retained as a documented target for that still-unimplemented cumulative budget. Default: 64 MiB. Declaration public static readonly long DefaultMaxWebSocketMessageBytes Field Value Type Description long | Edit this page View Source DefaultViaPseudonym Unused legacy field. ProxyServer.ViaHeaderPseudonym defaults to \"titanium-web-proxy\" (not empty) and does not read this field; nothing in the codebase references it. Retained only so removing it is not itself an undocumented breaking change; do not treat it as authoritative for the real default. Declaration public static readonly string DefaultViaPseudonym Field Value Type Description string" }, "api/Titanium.Web.Proxy.ProxyServer.html": { "href": "api/Titanium.Web.Proxy.ProxyServer.html", "title": "Class ProxyServer | Titanium Web Proxy", - "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockLoopbackDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked for loopback (127.0.0.0/8, ::1, including IPv4-mapped forms such as ::ffff:127.0.0.1) before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Private (RFC 1918), link-local, and cloud metadata addresses (including 169.254.169.254) remain reachable under this flag alone. Off by default. Use when the proxy must allow intranet destinations (for example 10.*) but must still stop clients from pivoting to the proxy host's own loopback. BlockPrivateNetworkDestinations already covers loopback as part of its broader set; when that flag is enabled, this property is not consulted for the reject. Assigning Profile does not read or clear this property. Enforced on TCP outbound connects only (the same call sites as BlockPrivateNetworkDestinations). Origin HTTP/3 (QUIC) dials a hostname and is not covered. An explicitly configured upstream proxy address is always exempt. Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled. Declaration public bool BlockLoopbackDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DecryptFailureBypassMaxEntries Maximum learned hosts retained (approximate LRU eviction). Default 256. Declaration public int DecryptFailureBypassMaxEntries { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassThreshold Origin TLS failure strikes required before a host is bypassed on later CONNECTs. Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. Default 2. Declaration public int DecryptFailureBypassThreshold { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassTtl How long a learned decrypt-bypass entry remains valid. Default 30 minutes. Declaration public TimeSpan DecryptFailureBypassTtl { get; set; } Property Value Type Description TimeSpan | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableDecryptFailureBypass When true, the proxy learns hosts whose origin TLS handshake fails under MITM (non-ALPN AuthenticationException, typically bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. Default false so library and RPS baselines are unchanged. Inspector enables this by default. Success-path cost when on is one dictionary lookup per CONNECT. Declaration public bool EnableDecryptFailureBypass { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability selects outbound HTTP/3 on the next CONNECT or new HTTP/1.1 request. Background QUIC warm-up starts when the cache is filled so that handshake is often already done. An already-open H2↔H2 MITM session is not upgraded mid-connection. Forced Http3 fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source HttpResponseCacheFiller Fills the GET/HEAD response cache from a session that is already on the interception path. Does not subscribe BeforeResponse or AfterResponse, so it does not turn NeedsHttpInterception(ProxyEndPoint) on. Declaration public Action? HttpResponseCacheFiller { get; set; } Property Value Type Description Action | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when body buffering is required (body-read hooks, authentication retry, etc.). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit cause the WebSocket connection to be closed with Close code 1009 (Message Too Big). Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to Balanced's modes (resource families enforced, Http2RelayValidation disabled so compressed H2 relay stays the verbatim-HPACK fast path). AllEnforce additionally enforces relay validation and is what PublicFacing applies. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderContributor Adds response headers on the way to the client without subscribing BeforeResponse. Plus CORS uses this so a listener can stay on H1 terminate-lite. Declaration public Action? ResponseHeaderContributor { get; set; } Property Value Type Description Action | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source ClearDecryptFailureBypass() Clears all learned decrypt-bypass entries. Declaration public void ClearDecryptFailureBypass() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source ForceDecryptFailureBypass(string) Marks host as actively bypassed (same as a forced learn after origin TLS failure). Raises DecryptFailureBypassChanged when bypass newly becomes active. Declaration public bool ForceDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source GetDecryptFailureBypassEntries() Snapshot of current learned decrypt-bypass entries (may include non-active strikes). Declaration public IReadOnlyList GetDecryptFailureBypassEntries() Returns Type Description IReadOnlyList | Edit this page View Source RemoveDecryptFailureBypass(string) Removes one host from the learned decrypt-bypass cache. Declaration public bool RemoveDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source ShouldBypassDecryptForLearnedHost(string?) When EnableDecryptFailureBypass is on and host is actively bypassed, returns true (decrypt should be skipped). Declaration public bool ShouldBypassDecryptForLearnedHost(string? host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source DecryptFailureBypassChanged Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). Handlers must not block; Inspector marshals to the UI thread. Declaration public event EventHandler? DecryptFailureBypassChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable" + "summary": "Class ProxyServer Translates an HTTP/1.1 client connection onto an h2-only origin (Http2 with AllowHttpProtocolTranslation enabled - see ResolveHttp2ForClientAsync(SessionEventArgsBase, bool, string, int, string, int?, UpstreamHttpProtocol, bool, bool, CancellationToken, bool)), leasing one h2 stream per HTTP/1.1 request from a shared Titanium.Web.Proxy.Http2.Http2OriginConnection via Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool rather than opening a new TCP/TLS connection for every request. Inheritance object ProxyServer Implements IDisposable Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class ProxyServer : IDisposable Remarks This re-implements the HTTP/1.1 client read loop (request line, headers, BeforeRequest, authorization, header preparation, CancelRequest/replaced-response handling) rather than reusing the private HandleHttpSessionRequest/HandleHttpSessionResponse methods, because those methods send/receive over TcpServerConnection.Stream using the raw HTTP/1.1 wire format, which an h2 origin connection cannot speak. This mirrors the precedent set by the h2-to-HTTP/1.1 bridge (Http2ToHttp11BridgeHandler), which similarly bypasses the wire-format-specific machinery for the leg that does not match it. Origin connections are multiplexed across independent HTTP/1.1 clients through Titanium.Web.Proxy.ProxyServer.Http2OriginConnectionPool (fan-in share). Response bodies are delivered via Titanium.Web.Proxy.Http2.Http2OriginConnection streaming writers where available. Constructors | Edit this page View Source ProxyServer(bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. | Edit this page View Source ProxyServer(string?, string?, bool, bool, bool) Initializes a new instance of ProxyServer class with provided parameters. Declaration public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerName, bool userTrustRootCertificate = true, bool machineTrustRootCertificate = false, bool trustRootCertificateAsAdmin = false) Parameters Type Name Description string rootCertificateName Name of the root certificate. string rootCertificateIssuerName Name of the root certificate issuer. bool userTrustRootCertificate When true (the default), EnsureRootCertificate() installs the MITM root into the current-user Personal and Trusted Root stores. Prefer user-only trust for interactive apps; pass false when trust must be fully opt-in. bool machineTrustRootCertificate When true, also trust in the local-machine stores (needs elevation). Defaults to false — machine trust is opt-in for services/admin installs, not for normal desktop use. bool trustRootCertificateAsAdmin When true, attempt elevated trust via UAC (Windows only). Defaults to false. Fields | Edit this page View Source DefaultViaHeaderPseudonym Default Via header pseudonym (RFC 9110 §7.6.3). Used by ViaHeaderPseudonym and by Inspector when Add Via header is enabled. Declaration public const string DefaultViaHeaderPseudonym = \"titanium-web-proxy\" Field Value Type Description string Properties | Edit this page View Source AdmittedClientConnectionCount Number of client connections currently admitted (accepted and past the admission gate, not yet finished being handled), across all TCP-based endpoints. Unlike ClientConnectionCount, this drops to zero as soon as the handler returns, without the trailing TIME_WAIT delay. Declaration public int AdmittedClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source BlockLoopbackDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked for loopback (127.0.0.0/8, ::1, including IPv4-mapped forms such as ::ffff:127.0.0.1) before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Private (RFC 1918), link-local, and cloud metadata addresses (including 169.254.169.254) remain reachable under this flag alone. Off by default. Use when the proxy must allow intranet destinations (for example 10.*) but must still stop clients from pivoting to the proxy host's own loopback. BlockPrivateNetworkDestinations already covers loopback as part of its broader set; when that flag is enabled, this property is not consulted for the reject. Assigning Profile does not read or clear this property. Enforced on TCP outbound connects only (the same call sites as BlockPrivateNetworkDestinations). Origin HTTP/3 (QUIC) dials a hostname and is not covered. An explicitly configured upstream proxy address is always exempt. Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled. Declaration public bool BlockLoopbackDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BlockPrivateNetworkDestinations Outbound destination policy hook: when true, every resolved destination IP address is checked against loopback, private (RFC 1918/4193), link-local (which subsumes the 169.254.169.254 cloud metadata endpoint), and other non-globally-routable ranges before connecting, and the connection attempt is rejected with an OutboundDestinationBlockedException if it matches. Off by default: blocking private destinations would break this library's most common configurations, including upstream-proxy chaining to localhost and interception of local development servers. Only enable this when the proxy accepts requests from untrusted clients (an SSRF-relevant deployment), where those same destinations become an attacker-reachable pivot into the host's private network instead of an operator's own intentional configuration. An explicitly configured upstream proxy address (UpStreamHttpProxy, UpStreamHttpsProxy, or a per-session external proxy) is always exempt - that address is operator intent, not attacker-controlled. Checked against the resolved address actually used to connect (no re-resolution afterward, which would make the check a TOCTOU no-op against DNS rebinding). Not currently enforced for a SOCKS upstream with ProxyDnsRequests enabled, since the proxy never resolves the origin itself in that mode and has no address of its own to validate. Declaration public bool BlockPrivateNetworkDestinations { get; set; } Property Value Type Description bool | Edit this page View Source BufferPool The buffer pool used throughout this proxy instance. Set custom implementations by implementing this interface. By default this uses DefaultBufferPool implementation available in StreamExtended library package. Buffer size should be at least 10 bytes. Declaration public IBufferPool BufferPool { get; set; } Property Value Type Description IBufferPool | Edit this page View Source CertificateManager Manages certificates used by this proxy. Declaration public CertificateManager CertificateManager { get; } Property Value Type Description CertificateManager | Edit this page View Source CheckCertificateRevocation Should we check for certificate revocation during SSL authentication to servers Note: If enabled can reduce performance. Defaults to false. Declaration public X509RevocationMode CheckCertificateRevocation { get; set; } Property Value Type Description X509RevocationMode | Edit this page View Source ClientConnectionCount Total number of active TCP client connections. Does not include inbound HTTP/3 (QUIC) clients; see Http3ClientConnectionCount. Declaration public int ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source ClientHeaderTimeoutSeconds Seconds to wait for a client to finish sending the request line and headers, from the moment this proxy starts reading a new request on the connection. Enforced with a linked CancellationTokenSource around the request-line and header read, not Socket.ReceiveTimeout: that property only bounds a single blocking Receive call, not the asynchronous reads this proxy actually issues, so without this deadline a client that opens a connection and trickles bytes arbitrarily slowly (or stops sending entirely) after the first byte ties up a read loop indefinitely. Default is 0 (disabled), matching every other deadline in this class - no per-session override exists because there is no SessionEventArgs for this request yet at the point this deadline applies. Declaration public int ClientHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CompatibilityMode100Continue When true, the proxy immediately responds with a synthetic 100 Continue to any client request carrying Expect: 100-continue, before forwarding the headers to the origin and without waiting for the origin to respond. This breaks the strict handshake (client → proxy 100 → client body → origin body) but prevents the deadlock that occurs with strict clients when Enable100ContinueBehaviour is false (the default). Has no effect when Enable100ContinueBehaviour is true. Default: false. Declaration public bool CompatibilityMode100Continue { get; set; } Property Value Type Description bool | Edit this page View Source ConnectTimeOutSeconds Seconds server connection are to wait for connection to be established. Default value is 20 seconds. Declaration public int ConnectTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ConnectionTimeOutSeconds Seconds client/server connection are to be kept alive when waiting for read/write to complete. This will also determine the pool eviction time when connection pool is enabled. Default value is 60 seconds. Declaration public int ConnectionTimeOutSeconds { get; set; } Property Value Type Description int | Edit this page View Source CustomUpStreamProxyFailureFunc A callback to provide a chance for an upstream proxy failure to be handled by a new upstream proxy. User should return the ExternalProxy object with valid credentials or null. Declaration public Func>? CustomUpStreamProxyFailureFunc { get; set; } Property Value Type Description Func> | Edit this page View Source DecryptFailureBypassMaxEntries Maximum learned hosts retained (approximate LRU eviction). Default 256. Declaration public int DecryptFailureBypassMaxEntries { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassThreshold Origin TLS failure strikes required before a host is bypassed on later CONNECTs. Same-CONNECT opaque fallback after an awaited H2 probe failure marks bypass immediately. Default 2. Declaration public int DecryptFailureBypassThreshold { get; set; } Property Value Type Description int | Edit this page View Source DecryptFailureBypassTtl How long a learned decrypt-bypass entry remains valid. Default 30 minutes. Declaration public TimeSpan DecryptFailureBypassTtl { get; set; } Property Value Type Description TimeSpan | Edit this page View Source DnsServerEndPoint DNS server endpoint used by Titanium.Web.Proxy.Http3.Dns.UdpSvcbDnsResolver for HTTPS/SVCB queries. Defaults to the first usable OS-configured plain-UDP DNS server discovered via NetworkInterface. This is a best-effort default and does not honor Windows NRPT, DoH, or VPN split-DNS policy. When no OS-configured DNS server can be discovered, the property reports 0.0.0.0:0 and proactive SVCB discovery is skipped (never falls back to a public third-party resolver). Assign an explicit endpoint to override discovery. Declaration [Experimental(\"TWP001\")] public IPEndPoint DnsServerEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source Enable100ContinueBehaviour Does this proxy uses the HTTP protocol 100 continue behaviour strictly? Broken 100 continue implementations on server/client may cause problems if enabled. Defaults to false. Declaration public bool Enable100ContinueBehaviour { get; set; } Property Value Type Description bool | Edit this page View Source EnableConnectionPool Should we enable the server connection pool. Defaults to true. When connection pooling is enabled, instead of creating a new TCP connection to the server for each client TCP connection, we check if an idle server connection is available in our cached pool. If a compatible connection (same destination, scheme, upstream proxy, credentials and negotiated protocol) created from an earlier request is available, we reuse it. Only connections that are safe to reuse under the HTTP protocol are pooled: the response body must be fully received and the connection must be persistent (HTTP/1.1 keep-alive, or an HTTP/1.0 connection that explicitly opted in via \"Connection: keep-alive\"). Connections whose response asked to close, that failed, or that carry connection-oriented authentication state (WinAuth NTLM/Negotiate) or a per-session client certificate are never returned to the shared pool. The ConnectionTimeOutSeconds parameter determines the eviction time for inactive server connections. This reduces TCP (and TLS) connection establishment cost, both in wall clock time and CPU cycles. Set to false to force a fresh server connection for every client connection. Declaration public bool EnableConnectionPool { get; set; } Property Value Type Description bool | Edit this page View Source EnableDecryptFailureBypass When true, the proxy learns hosts whose origin TLS handshake fails under MITM (non-ALPN AuthenticationException, typically bot / TLS-fingerprint rejection) and tunnels subsequent CONNECTs without decrypt. Default false so library and RPS baselines are unchanged. Inspector enables this by default. Success-path cost when on is one dictionary lookup per CONNECT. Declaration public bool EnableDecryptFailureBypass { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp2 Enable disable HTTP/2 support. Client-facing HTTP/2 is negotiated via TLS ALPN, or as prior-knowledge cleartext h2c on a transparent reverse endpoint (DecryptSsl: false). No Upgrade: h2c. Origin-facing HTTP/2 uses TLS ALPN h2 by default; with ForwardCleartext and Http2, the origin speaks cleartext HTTP/2 prior-knowledge (outbound h2c). A client/server that does not support HTTP/2 transparently falls back to HTTP/1.1 when policy allows. Request/response header and body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/ RespondStreaming) are all supported over HTTP/2, the same as over HTTP/1.x. Not supported: HTTP/2 server push (the wire frames are transcoded but there is no public API to originate a push) and Upgrade: h2c. Explicit-proxy inbound h2c is not implemented. See the protocol support matrix on the wiki for exact, up-to-date HTTP/1.x/HTTP/2 feature coverage. Declaration public bool EnableHttp2 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttp3 Enable HTTP/3 (QUIC) support. When true: Any TransparentQuicProxyEndPoint is started as a UDP-only QUIC listener for transparent/NAT HTTP/3 interception. Any TransparentProxyEndPoint with EnableHttp3 also listens for HTTP/3 on the same IP:port (TCP H1/H2 + UDP H3) and injects client-facing Alt-Svc. With Auto (default), a cached Alt-Svc / HTTPS/SVCB capability selects outbound HTTP/3 on the next CONNECT or new HTTP/1.1 request. Background QUIC warm-up starts when the cache is filled so that handshake is often already done. An already-open H2↔H2 MITM session is not upgraded mid-connection. Forced Http3 fails closed with no TCP fallback. Requires MsQuic native library and a supported operating-system version (IsSupported). Setting to true with no inbound HTTP/3 endpoint is fine when an explicit/SOCKS/transparent TCP endpoint is present (origin-side QUIC only). A warning is emitted only when EnableHttp3 is set with no client-facing endpoints at all. Default: false (opt-in). Experimental: HTTP/3 support has not yet completed the full interop/soak/fuzz gate process. Suppress TWP001 to opt in; the attribute is removed when the feature graduates to stable. Declaration [Experimental(\"TWP001\")] public bool EnableHttp3 { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpInterception Forces the full interception path (SessionEventArgs, BeforeRequest, etc.) even when no event handlers are subscribed. Set this when consuming SessionEventArgs for timing or metrics without subscribing to any event. Default: false. Declaration public bool EnableHttpInterception { get; set; } Property Value Type Description bool | Edit this page View Source EnableHttpsSvcbDnsDiscovery When true, the proxy queues a background HTTPS/SVCB RR (DNS type 65) lookup after an Auto-mode capability-cache miss. A positive result (ALPN h3 found) warms Titanium.Web.Proxy.Http3.Http3OriginCapabilityCache for subsequent connections; the CONNECT / request path itself never awaits DNS. Negative results are cached for 1 minute; transient failures use a short backoff. Defaults to true whenever EnableHttp3 is true. Set explicitly to false to disable discovery even when HTTP/3 is enabled — for example, when the configured DNS server is untrusted or unreachable. First-connection HTTP/3 adoption then comes from Alt-Svc. Declaration [Experimental(\"TWP001\")] public bool EnableHttpsSvcbDnsDiscovery { get; set; } Property Value Type Description bool | Edit this page View Source EnableIpv6UnreachableSoftSkip When true (default), after one IPv6 connect failure with NetworkUnreachable (or equivalent), temporarily omit IPv6 addresses from the Happy Eyeballs race for 30 seconds. Reduces first-chance SocketException noise on dual-stack hosts with a broken IPv6 path. Disable if operators require strict IPv6 preference even when the path is unreachable. Declaration public bool EnableIpv6UnreachableSoftSkip { get; set; } Property Value Type Description bool | Edit this page View Source EnableQpackDynamicTable When true, enables RFC 9204 QPACK dynamic table encoding and decoding for inbound HTTP/3 connections. Each connection gets its own Titanium.Web.Proxy.Http3.Qpack.QpackContext with two independent 4096-byte tables (one inbound, one outbound). Defaults to false (static-table-only); existing deployments are unaffected. Declaration [Experimental(\"TWP001\")] public bool EnableQpackDynamicTable { get; set; } Property Value Type Description bool | Edit this page View Source EnableRequestTimingCapture Enables structured request/connection timing capture. When false (the default) no timing objects are allocated and no UtcNow calls are made for timing purposes anywhere in the proxy, so there is zero overhead on the hot path. When enabled, every SessionEventArgsBase exposes a populated Timing (per-request phases: client header read, connection wait, request send, time-to-first-byte, response delivery, total), every upstream connection exposes a populated UpstreamConnectionTiming (reachable from a session via UpstreamConnectionTiming, describing DNS, TCP connect, optional upstream-proxy CONNECT, and TLS handshake durations), and a decrypted TunnelConnectSessionEventArgs exposes the client-facing TLS handshake duration via ClientTlsTiming. Can be toggled at any time; it only affects sessions/connections created after the change, never mutating timing objects already handed out. Defaults to false. Declaration public bool EnableRequestTimingCapture { get; set; } Property Value Type Description bool | Edit this page View Source EnableRfc8441 When true, the proxy enables RFC 8441 WebSocket-over-HTTP/2: Accepts extended CONNECT (:protocol = websocket) from h2 clients and advertises SETTINGS_ENABLE_CONNECT_PROTOCOL=1 to them. Per origin: if the origin is HTTP/2 and advertises RFC 8441 support, DATA frames are relayed directly; if the origin is HTTP/2 and does not, the stream is reset with REFUSED_STREAM; if the origin is HTTP/1.1, the h2→h1 WebSocket upgrade bridge is used. On the HTTP/1.1-client-to-h2-origin translation bridge, translates Upgrade: websocket into extended CONNECT when the origin advertises the setting; otherwise falls back to a dedicated HTTP/1.1 origin connection for that WebSocket. When this property is false, that bridge still returns synthetic 501 Not Implemented for WebSocket upgrades (historical default). Default: false (must opt-in). Declaration public bool EnableRfc8441 { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpKeepAlive Enable TCP KeepAlive on client and server sockets so NAT/firewall mappings for long-lived CONNECT tunnels are refreshed. Default: true. Declaration public bool EnableTcpKeepAlive { get; set; } Property Value Type Description bool | Edit this page View Source EnableTcpServerConnectionPrefetch Should we enable tcp server connection prefetching? When enabled, as soon as we receive a client connection we concurrently initiate corresponding server connection process using CONNECT hostname or SNI hostname on a separate task so that after parsing client request we will have the server connection immediately ready or in the process of getting ready. If a server connection is available in cache then this prefetch task will immediately return with the available connection from cache. Defaults to true. Declaration public bool EnableTcpServerConnectionPrefetch { get; set; } Property Value Type Description bool | Edit this page View Source EnableWinAuth Enable disable Windows Authentication (NTLM/Kerberos). By default SSPI uses the process identity. To authenticate as another user, set WinAuthCredentialsProvider (issue #461). Defaults to false. Declaration public bool EnableWinAuth { get; set; } Property Value Type Description bool | Edit this page View Source EndpointAdmissionRejectionCount Total number of client connections rejected by any endpoint's MaxConcurrentClients since this instance was created. Declaration public long EndpointAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source ForwardToUpstreamGateway Gets or sets a value indicating whether requests will be chained to upstream gateway. Defaults to false. Declaration public bool ForwardToUpstreamGateway { get; set; } Property Value Type Description bool | Edit this page View Source GetCustomUpStreamProxyFunc A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. User should return the ExternalProxy object with valid credentials. Declaration public Func>? GetCustomUpStreamProxyFunc { get; set; } Property Value Type Description Func> | Edit this page View Source GlobalAdmissionRejectionCount Total number of client connections rejected by MaxConcurrentClientConnections since this instance was created. Declaration public long GlobalAdmissionRejectionCount { get; } Property Value Type Description long | Edit this page View Source Http3ClientConnectionCount Total number of active inbound HTTP/3 (QUIC) client connections. Declaration public int Http3ClientConnectionCount { get; } Property Value Type Description int | Edit this page View Source Http3ServerConnectionCount Total number of active upstream HTTP/3 (QUIC) server connections. These are also included in ServerConnectionCount. Declaration public int Http3ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source HttpResponseCacheFiller Fills the GET/HEAD response cache from a session that is already on the interception path. Does not subscribe BeforeResponse or AfterResponse, so it does not turn NeedsHttpInterception(ProxyEndPoint) on. Declaration public Action? HttpResponseCacheFiller { get; set; } Property Value Type Description Action | Edit this page View Source IdleReadTimeoutSeconds Seconds of idle time allowed while reading from the origin (stalled header/body waits). Applied via CancelAfter on the active read operation. Default is 0 (disabled). Per-session override: IdleReadTimeout. Declaration public int IdleReadTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IdleWriteTimeoutSeconds Seconds of idle time allowed while writing to the origin (stalled header/body waits). Applied via CancelAfter on the active write operation. Default is 0 (disabled). Per-session override: IdleWriteTimeout. Declaration public int IdleWriteTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source IgnoreServerCertificateErrors When true, origin TLS certificates that fail OS chain validation are still accepted (MITM of loopback/self-signed/private CAs). Inspector's \"Ignore server certificate errors\" maps here. Default false. A subscribed ServerCertificateValidationCallback still wins. Declaration public bool IgnoreServerCertificateErrors { get; set; } Property Value Type Description bool | Edit this page View Source ListenerBackLog TCP listener accept backlog. Default: 1024 for burst connection handling. Declaration public int ListenerBackLog { get; set; } Property Value Type Description int | Edit this page View Source Logger The live, shared logger used throughout this proxy instance. Reflects the most recent call to ApplyLoggingConfiguration(). Declaration public ILogger Logger { get; } Property Value Type Description ILogger | Edit this page View Source Logging Configuration for this proxy instance's built-in diagnostic logging - the replacement for the removed ExceptionFunc callback. Every exception the proxy catches (even when handled internally and never surfaced to user code) is reported through this logger at an appropriate severity; see ProxyLoggingOptions for the console/file sinks, enable/disable switch, and minimum level. Mutate the returned instance (or assign a new one) at any point; each assignment/mutation you want to take effect must be followed by ApplyLoggingConfiguration() (which Start(bool) also calls automatically, so the configuration active at the moment the proxy starts running is picked up for the run even if you never call it yourself). Calling it again later - including while the proxy is already running - immediately swaps in the new configuration; this is safe because logging never blocks or otherwise affects proxy traffic. Declaration public ProxyLoggingOptions Logging { get; set; } Property Value Type Description ProxyLoggingOptions | Edit this page View Source MaxBufferedBodyBytes Maximum bytes the proxy will buffer for a single request or response body when whole-body buffering is required (GetRequestBody / GetResponseBody, authentication retry, and other paths that materialize the body). Bodies larger than this limit are rejected with 413 (upstream request) or connection teardown (upstream response). Streaming relays (H1, H2, and H3, including bridges) are not counted against this budget: they are bounded by protocol flow control. Set to 0 to disable the limit (not recommended). Default: 4,194,304 (4 MiB). Declaration public int MaxBufferedBodyBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxCachedConnections Maximum number of concurrent connections per remote host in cache. Only meaningful when EnableConnectionPool is true; to disable pooling, set EnableConnectionPool to false rather than setting this to 0 - the pool eviction loop treats a value below 1 as \"evict without limit while holding the pool-wide lock\", which spins indefinitely once the cache for that host is empty and would stall every other connection acquire/release in the process. Rejected outright at assignment so that state cannot be reached. Default value is 128. Declaration public int MaxCachedConnections { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxConcurrentClientConnections Maximum number of client connections admitted across all TCP-based endpoints at once. null (the default) disables the global admission gate, preserving today's unbounded behavior. When set, a connection beyond this limit is rejected and disposed immediately after accept, before a handler task is even started. Enforced independently of ClientConnectionCount: see Titanium.Web.Proxy.ProxyServer.admittedClientConnectionCount for why. See also MaxConcurrentClients for a per-endpoint cap layered on top of this global one. Declaration public int? MaxConcurrentClientConnections { get; set; } Property Value Type Description int? | Edit this page View Source MaxConcurrentHttp11HttpsOriginCreates Caps concurrent new HTTPS origin TCP/TLS opens on the H2→H1 bridge only (MITM / re-encrypt). Pool hits (warm keep-alive) are uncapped. Cleartext H1 origins are not gated. Default is Clamp(ProcessorCount, 4, 32). Set before the first H2→H1 HTTPS origin open (typically before Start(bool)); changing the value after the create gate has been used has no effect on the live semaphore. Declaration public int MaxConcurrentHttp11HttpsOriginCreates { get; set; } Property Value Type Description int Exceptions Type Condition ArgumentOutOfRangeException The assigned value is less than 1. | Edit this page View Source MaxDecodedHeaderListBytes Maximum decoded HTTP/2 header list size in bytes, using RFC 7541 accounting (name.Length + value.Length + 32 per field). Requests or responses with a decoded header list exceeding this limit will be refused with RST_STREAM(ENHANCE_YOUR_CALM) (code 0xb). Set to 0 to disable the limit (not recommended). Default: 65,536 (64 KiB). Advertised via SETTINGS_MAX_HEADER_LIST_SIZE. Declaration public int MaxDecodedHeaderListBytes { get; set; } Property Value Type Description int | Edit this page View Source MaxWebSocketFramePayloadBytes Maximum WebSocket frame payload size in bytes that the proxy will accept during frame-level interception (i.e. when BeforeWebSocketFrame has at least one subscriber). Frames whose decoded payload exceeds this limit are handled by WebSocketFrameBudget. Enforce (the default) closes the connection with Close code 1009 (Message Too Big) before the payload is buffered. Observe relays the frame unbuffered and raises WebSocketFrameElided instead of invoking BeforeWebSocketFrame. Raw-relay sessions (no BeforeWebSocketFrame subscriber) bypass this check entirely and pass all frames through unvalidated. Default: 16,777,216 (16 MiB). Declaration public int MaxWebSocketFramePayloadBytes { get; set; } Property Value Type Description int | Edit this page View Source NetworkFailureRetryAttempts Number of times to retry upon network failures when connection pool is enabled. Declaration public int NetworkFailureRetryAttempts { get; set; } Property Value Type Description int | Edit this page View Source NoDelay Gets or sets a Boolean value that specifies whether server and client stream Sockets are using the Nagle algorithm. Defaults to true, no nagle algorithm is used. Declaration public bool NoDelay { get; set; } Property Value Type Description bool | Edit this page View Source OriginHttpVersionPolicy Controls which HTTP version is declared to the origin server on the request line, independently of the version the client declared to the proxy. Defaults to PreserveClientVersion, which matches the proxy's historical pass-through behavior exactly. Set to NormalizeToHttp11 to let HTTP/1.0 clients share pooled, persistent origin connections the same way HTTP/1.1 clients already do. This only changes the wire version written to the origin request line - it never changes the client-facing Http.Request.HttpVersion that event handlers observe, nor the version/persistence used to write the response back to the client. Declaration public OriginHttpVersionPolicy OriginHttpVersionPolicy { get; set; } Property Value Type Description OriginHttpVersionPolicy | Edit this page View Source PolicyModes Which resource-bound PolicyFamily is enforced, observed, or disabled, per the plan's rollout section. Read live by each family's enforcement call site - not baked into a per-request snapshot at connection accept time - so assigning a new value here (a whole-object replacement, never a mutation of the previous instance) takes effect for the next check any in-flight or new request makes, without restarting the proxy. This is the \"runtime switch to drop to Observe without redeploying\" the plan requires; see WithAllObservedExceptDisabled() for the one-call way to do that. Defaults to Balanced's modes (resource families enforced, Http2RelayValidation disabled so compressed H2 relay stays the verbatim-HPACK fast path). AllEnforce additionally enforces relay validation and is what PublicFacing applies. Assigning Profile also replaces this value with that profile's bundle; assign PolicyModes afterward to deviate from the selected profile's modes without changing anything else the profile set. Declaration public ProxyPolicyModes PolicyModes { get; set; } Property Value Type Description ProxyPolicyModes | Edit this page View Source Profile The last profile applied via this property's setter, defaulting to Balanced - the profile every field on this instance already starts at, so a fresh new ProxyServer() reports Balanced without needing its setter to run once at construction time. Assigning this property applies its entire ProxyProfileSettings bundle - ResourceLimits, PolicyModes, SupportedSslProtocols, BlockPrivateNetworkDestinations, MaxConcurrentClientConnections and the deadline-seconds properties - as a single atomic assignment, so a reader can never observe a half-applied profile. Assigning any of those properties individually afterward overrides just that one, without reverting the rest of the profile's bundle. Logged once per Start(bool) call, by name only - never with hosts, URLs or secrets, per the plan's rollout section. Declaration public ProxyProfile Profile { get; set; } Property Value Type Description ProxyProfile | Edit this page View Source ProxyAuthenticationRealm Realm used during Proxy Basic Authentication. Declaration public string ProxyAuthenticationRealm { get; set; } Property Value Type Description string | Edit this page View Source ProxyAuthenticationSchemes A collection of scheme types, e.g. basic, NTLM, Kerberos, Negotiate, to return if scheme authentication is required. Works in relation with ProxySchemeAuthenticateFunc. Declaration public IEnumerable ProxyAuthenticationSchemes { get; set; } Property Value Type Description IEnumerable | Edit this page View Source ProxyBasicAuthenticateFunc A callback to authenticate proxy clients via basic authentication. Parameters are username and password as provided by client. Should return true for successful authentication. Declaration public Func>? ProxyBasicAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source ProxyEndPoints A list of IpAddress and port this proxy is listening to. Declaration public List ProxyEndPoints { get; set; } Property Value Type Description List | Edit this page View Source ProxyRunning Is the proxy currently running? Declaration public bool ProxyRunning { get; } Property Value Type Description bool | Edit this page View Source ProxySchemeAuthenticateFunc A pluggable callback to authenticate clients by scheme instead of requiring basic authentication through ProxyBasicAuthenticateFunc. Parameters are current working session, schemeType, and token as provided by a calling client. Should return success for successful authentication, continuation if the package requests, or failure. Declaration public Func>? ProxySchemeAuthenticateFunc { get; set; } Property Value Type Description Func> | Edit this page View Source RequestTimeoutSeconds Total seconds allowed for a single request/response exchange after BeforeRequest returns (connect, send, wait for headers, and body copy). Default is 0 (disabled). Per-session override: RequestTimeout. Declaration public int RequestTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ResourceLimits The shared, immutable resource-bound snapshot (concurrent-stream cap, CONTINUATION frame-count/wall-clock bounds, peer-initiated incomplete-stream-reset budget, and the other limits described in ProxyResourceLimits) consulted by the HTTP/2 relay so a single proxy-owned value governs both what is enforced and what is advertised to each peer, rather than admitting purely against whatever the origin advertised. Assign a new ProxyResourceLimits (constructed via Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)) to override the Default snapshot. There is no artificial upper clamp: high-CPU/RAM hosts may pass larger maxCachedConnectionsPerHost, maxConcurrentStreamsPerConnection, etc. as needed. The live TCP pool depth knob MaxCachedConnections remains independently settable and should usually be kept in sync with MaxCachedConnectionsPerHost. Declaration public ProxyResourceLimits ResourceLimits { get; set; } Property Value Type Description ProxyResourceLimits | Edit this page View Source ResponseHeaderContributor Adds response headers on the way to the client without subscribing BeforeResponse. Plus CORS uses this so a listener can stay on H1 terminate-lite. Declaration public Action? ResponseHeaderContributor { get; set; } Property Value Type Description Action | Edit this page View Source ResponseHeaderTimeoutSeconds Seconds to wait for the origin to send the response status line and headers after the request has been sent. Enforced with a linked CancellationTokenSource (not Socket receive timeout alone). When the deadline elapses a ProxyTimeoutException with ResponseHeader is raised (and may be converted to HTTP 504 before any response bytes have been committed to the client). Default is 0 (disabled). WebSocket upgrades, Server-Sent Events, raw tunnels, and sessions that already wrote a response status to the client are exempt; those waits use IdleReadTimeoutSeconds when configured. Per-session override: ResponseHeaderTimeout. Declaration public int ResponseHeaderTimeoutSeconds { get; set; } Property Value Type Description int | Edit this page View Source ReuseSocket When true (default), SO_REUSEADDR is requested where Titanium.Web.Proxy.Helpers.RunTime.IsSocketReuseAvailable() reports support (always on Windows; on non-Windows, .NET Core 3+ / compatible runtimes). Declaration public bool ReuseSocket { get; set; } Property Value Type Description bool | Edit this page View Source ReverseProxy Optional reverse-proxy route/cluster configuration. When null (default), Core keeps 6.x ForwardHost behavior with zero added cost on the hot path. Declaration public ReverseProxyOptions? ReverseProxy { get; set; } Property Value Type Description ReverseProxyOptions | Edit this page View Source ServerConnectionCount Total number of active server connections (TCP plus upstream QUIC). For HTTP/3-only upstreams see Http3ServerConnectionCount. Declaration public int ServerConnectionCount { get; } Property Value Type Description int | Edit this page View Source ShouldInterceptHttp Optional per-request/stream predicate consulted only when the global interception gate is active. Return true to use the full SessionEventArgs path; return false to use the fast-forward path. null (the default) intercepts every request — preserving today's behavior. Declaration public Func? ShouldInterceptHttp { get; set; } Property Value Type Description Func | Edit this page View Source SupportedServerSslProtocols Ssl versions offered on outbound HTTPS connections to origins (and upstream proxies). Default None means “use SupportedSslProtocols” (typically TLS 1.2 and 1.3). Set an explicit mask to restrict or expand outbound-only independently of inbound client TLS. Older docs described None as “same as the proxy client.” That coupling is incorrect across protocol translations (e.g. inbound QUIC is always TLS 1.3 while outbound TCP SslStream on macOS SecureTransport cannot offer TLS 1.3). Declaration public SslProtocols SupportedServerSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source SupportedSslProtocols List of supported Ssl versions. Defaults to TLS 1.2/1.3 only as of 5.0 - a breaking change from 4.x, which also enabled SSL 3.0/TLS 1.0/1.1. Those legacy, broken-by-design protocols require an explicit opt-in by assigning this property directly (e.g. SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12 | SslProtocols.Tls13) if a legacy client/server genuinely requires them. Declaration public SslProtocols SupportedSslProtocols { get; set; } Property Value Type Description SslProtocols | Edit this page View Source TcpTimeWaitSeconds SO_LINGER timeout in seconds applied to client and upstream sockets via LingerOption (enabled with this timeout). This is not the kernel TCP TIME_WAIT duration — TIME_WAIT is controlled by the OS. A positive value means Close may block up to that many seconds flushing send buffers; use 0 for an abortive close (RST). Default is 0 so high-churn proxies avoid TIME_WAIT accumulation; the 1-second connection disposal delay already prefers peer-first close. Declaration public int TcpTimeWaitSeconds { get; set; } Property Value Type Description int | Edit this page View Source ThreadPoolWorkerThread Customize the minimum ThreadPool size (increase it on a server). Defaults to max(ProcessorCount * 2, 16) so short loopback/proxy workloads are not starved while the pool is still ramping workers. Declaration public int ThreadPoolWorkerThread { get; set; } Property Value Type Description int | Edit this page View Source UpStreamEndPoint Local adapter/NIC endpoint where proxy makes request via. Defaults via any IP addresses of this machine. When the resolved destination address family does not match this endpoint, it is ignored so dual-stack destinations can still connect (see UpStreamEndPointIPv4 / UpStreamEndPointIPv6). Declaration public IPEndPoint? UpStreamEndPoint { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv4 Local bind endpoint used when the resolved upstream destination is IPv4. Takes precedence over UpStreamEndPoint for IPv4 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv4 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamEndPointIPv6 Local bind endpoint used when the resolved upstream destination is IPv6. Takes precedence over UpStreamEndPoint for IPv6 destinations. Declaration public IPEndPoint? UpStreamEndPointIPv6 { get; set; } Property Value Type Description IPEndPoint | Edit this page View Source UpStreamHttpProxy External proxy used for Http requests. Declaration public IExternalProxy? UpStreamHttpProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpStreamHttpsProxy External proxy used for Https requests. Declaration public IExternalProxy? UpStreamHttpsProxy { get; set; } Property Value Type Description IExternalProxy | Edit this page View Source UpstreamProxyConfigurationScript If set, the upstream proxy will be detected by a script that will be loaded from the provided Uri Declaration public Uri? UpstreamProxyConfigurationScript { get; set; } Property Value Type Description Uri | Edit this page View Source ViaHeaderPseudonym Pseudonym used in Via header fields appended to forwarded requests and responses (RFC 9110 §7.6.3). Defaults to DefaultViaHeaderPseudonym. Set to an empty string to disable Via header injection entirely. Loop detection uses this value: a request arriving with this pseudonym already present in Via is refused with 508 Loop Detected. Declaration public string ViaHeaderPseudonym { get; set; } Property Value Type Description string | Edit this page View Source WinAuthCredentialsProvider Optional per-session credential provider for server 401 WinAuth (NTLM/Negotiate/Kerberos). Return null to use the current process identity (legacy behavior). Do not put plaintext passwords on SessionEventArgs — use this callback instead. Windows SSPI only; ignored on non-Windows platforms. Declaration public Func>? WinAuthCredentialsProvider { get; set; } Property Value Type Description Func> Methods | Edit this page View Source AddEndPoint(ProxyEndPoint) Add a proxy end point. Declaration public void AddEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The proxy endpoint. | Edit this page View Source ApplyLoggingConfiguration() Rebuilds the active logger/logger factory from the current Logging configuration, disposing any previously owned built-in providers. Called automatically from the constructor (with the default configuration) and from Start(bool). Call this explicitly any time after changing Logging and you want the change to take effect immediately - whether the proxy is stopped (e.g. before using CertificateManager directly) or already running. Declaration public void ApplyLoggingConfiguration() | Edit this page View Source ClearDecryptFailureBypass() Clears all learned decrypt-bypass entries. Declaration public void ClearDecryptFailureBypass() | Edit this page View Source DisableAllSystemProxies() Clear all proxy settings for current machine. Declaration public void DisableAllSystemProxies() | Edit this page View Source DisableSystemHttpProxy() Clear HTTP proxy settings of current machine. Declaration public void DisableSystemHttpProxy() | Edit this page View Source DisableSystemHttpsProxy() Clear HTTPS proxy settings of current machine. Declaration public void DisableSystemHttpsProxy() | Edit this page View Source DisableSystemProxy(ProxyProtocolType) Clear the specified proxy setting for current machine. Declaration public void DisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType | Edit this page View Source Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. Declaration public void Dispose() | Edit this page View Source Dispose(bool) Declaration [SuppressMessage(\"ApiDesign\", \"RS0016:Add public types and members to the declared API\", Justification = \"Protected Dispose(bool) is required by the standard IDisposable pattern but is not public API.\")] protected virtual void Dispose(bool disposing) Parameters Type Name Description bool disposing | Edit this page View Source ForceDecryptFailureBypass(string) Marks host as actively bypassed (same as a forced learn after origin TLS failure). Raises DecryptFailureBypassChanged when bypass newly becomes active. Declaration public bool ForceDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source GetDecryptFailureBypassEntries() Snapshot of current learned decrypt-bypass entries (may include non-active strikes). Declaration public IReadOnlyList GetDecryptFailureBypassEntries() Returns Type Description IReadOnlyList | Edit this page View Source RemoveDecryptFailureBypass(string) Removes one host from the learned decrypt-bypass cache. Declaration public bool RemoveDecryptFailureBypass(string host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source RemoveEndPoint(ProxyEndPoint) Remove a proxy end point. Will throw error if the end point doesn't exist. Declaration public void RemoveEndPoint(ProxyEndPoint endPoint) Parameters Type Name Description ProxyEndPoint endPoint The existing endpoint to remove. | Edit this page View Source RestoreOriginalProxySettings() Restores the original proxy settings. Declaration public void RestoreOriginalProxySettings() | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTP proxy server for current machine. Declaration public void SetAsSystemHttpProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. | Edit this page View Source SetAsSystemHttpsProxy(ExplicitProxyEndPoint, SystemProxySettings) Set the given explicit end point as the default HTTPS proxy server for current machine. Declaration public void SetAsSystemHttpsProxy(ExplicitProxyEndPoint endPoint, SystemProxySettings settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. SystemProxySettings settings The Windows system proxy settings. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. | Edit this page View Source SetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Set the given explicit end point as the default proxy server for current machine. Declaration public void SetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings) Parameters Type Name Description ExplicitProxyEndPoint endPoint The explicit endpoint. ProxyProtocolType protocolType The proxy protocol type. SystemProxySettings settings The Windows system proxy settings, or null to preserve the current bypass list. | Edit this page View Source SetHttp3Enabled(bool) Enables or disables EnableHttp3. Enabling still requires MsQuic (IsSupported); disabling is always applied. Safe to call while the proxy is running — new origin connections pick up the change. Existing sessions keep the protocol they already negotiated. Declaration public bool SetHttp3Enabled(bool enabled) Parameters Type Name Description bool enabled Returns Type Description bool true when HTTP/3 is enabled after the call. | Edit this page View Source ShouldBypassDecryptForLearnedHost(string?) When EnableDecryptFailureBypass is on and host is actively bypassed, returns true (decrypt should be skipped). Declaration public bool ShouldBypassDecryptForLearnedHost(string? host) Parameters Type Name Description string host Returns Type Description bool | Edit this page View Source Start(bool) Start this proxy server instance. Transactional: if any endpoint fails to start, every listener this call already started is stopped, the system-upstream-proxy resolver (if this call created one) is disposed, and ProxyRunning is left false before the exception propagates. A caller that catches the exception is left with an instance in exactly the same state as before calling Start(bool), not a partially-bound proxy with some endpoints silently listening. Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings Whether or not clear any system proxy settings which is pointing to our own endpoint (causing a cycle). E.g due to ungracious proxy shutdown before. | Edit this page View Source Stop() Stop this proxy server instance. Endpoints remain registered so Start(bool) can re-listen on the same ports. In-flight sessions are cancelled; pooled upstream connections are cleared. The connection factory itself stays usable for a subsequent Start (it is only disposed with the proxy). Declaration public void Stop() | Edit this page View Source StopAsync(TimeSpan?) Asynchronously stop this proxy server, cancel in-flight sessions, and wait briefly for client connection count to drain before clearing the upstream pool. Declaration public Task StopAsync(TimeSpan? drainTimeout = null) Parameters Type Name Description TimeSpan? drainTimeout Maximum time to wait for active client handlers to exit after cancellation. Defaults to 5 seconds. Returns Type Description Task | Edit this page View Source TryDisableAllSystemProxies() Clear all OS proxy settings without throwing. Declaration public SystemProxyChangeResult TryDisableAllSystemProxies() Returns Type Description SystemProxyChangeResult | Edit this page View Source TryDisableSystemProxy(ProxyProtocolType) Clear OS proxy for the given protocols without throwing. Declaration public SystemProxyChangeResult TryDisableSystemProxy(ProxyProtocolType protocolType) Parameters Type Name Description ProxyProtocolType protocolType Returns Type Description SystemProxyChangeResult | Edit this page View Source TryEnableHttp3IfSupported() Turns on EnableHttp3 when MsQuic is available (IsSupported). Hosts (CLI, Inspector, examples) should call this instead of setting EnableHttp3 blindly. Returns true when HTTP/3 was enabled. Declaration public bool TryEnableHttp3IfSupported() Returns Type Description bool | Edit this page View Source TryRestoreOriginalProxySettings() Restore OS proxy without throwing. Declaration public SystemProxyChangeResult TryRestoreOriginalProxySettings() Returns Type Description SystemProxyChangeResult | Edit this page View Source TrySetAsSystemProxy(ExplicitProxyEndPoint, ProxyProtocolType, SystemProxySettings?) Enable OS system proxy without throwing. Failures are logged and returned so Inspector/CLI can show a status message instead of crashing. Declaration public SystemProxyChangeResult TrySetAsSystemProxy(ExplicitProxyEndPoint endPoint, ProxyProtocolType protocolType, SystemProxySettings? settings = null) Parameters Type Name Description ExplicitProxyEndPoint endPoint ProxyProtocolType protocolType SystemProxySettings settings Returns Type Description SystemProxyChangeResult Events | Edit this page View Source AfterResponse Intercept after response event from server. Declaration public event AsyncEventHandler? AfterResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeRequest Intercept request event to server. Declaration public event AsyncEventHandler? BeforeRequest Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeResponse Intercept response event from server. Declaration public event AsyncEventHandler? BeforeResponse Event Type Type Description AsyncEventHandler | Edit this page View Source BeforeUpStreamConnectRequest Intercept connect request sent to upstream proxy. Declaration public event AsyncEventHandler? BeforeUpStreamConnectRequest Event Type Type Description AsyncEventHandler | Edit this page View Source ClientCertificateSelectionCallback Event to override client certificate selection during mutual SSL authentication. Declaration public event AsyncEventHandler? ClientCertificateSelectionCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ClientConnectionCountChanged Event occurs when client connection count changed. Declaration public event EventHandler? ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source DecryptFailureBypassChanged Raised when a host becomes actively bypassed (threshold reached or same-CONNECT mark). Handlers must not block; Inspector marshals to the UI thread. Declaration public event EventHandler? DecryptFailureBypassChanged Event Type Type Description EventHandler | Edit this page View Source Http3ClientConnectionCountChanged Event occurs when inbound HTTP/3 client connection count changed. Declaration public event EventHandler? Http3ClientConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source Http3ServerConnectionCountChanged Event occurs when upstream HTTP/3 server connection count changed. Declaration public event EventHandler? Http3ServerConnectionCountChanged Event Type Type Description EventHandler | Edit this page View Source OnClientConnectionCreate Customize TcpClient used for client connection upon create. Declaration public event AsyncEventHandler? OnClientConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source OnRequestBodyWrite Intercept request body send event to server. Subscribe to inspect or modify the request body chunk-by-chunk as it streams to the server, without buffering the whole body. Do not combine with SessionEventArgs.GetRequestBody (which buffers). Declaration public event AsyncEventHandler? OnRequestBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnResponseBodyWrite Intercept response body send event to client. Subscribe to inspect or modify the response body chunk-by-chunk as it streams to the client, without buffering the whole body. Do not combine with SessionEventArgs.GetResponseBody (which buffers). Declaration public event AsyncEventHandler? OnResponseBodyWrite Event Type Type Description AsyncEventHandler | Edit this page View Source OnServerConnectionCreate Customize TcpClient used for server connection upon create. Declaration public event AsyncEventHandler? OnServerConnectionCreate Event Type Type Description AsyncEventHandler | Edit this page View Source ServerCertificateValidationCallback Event to override the default verification logic of remote SSL certificate received during authentication. Declaration public event AsyncEventHandler? ServerCertificateValidationCallback Event Type Type Description AsyncEventHandler | Edit this page View Source ServerConnectionCountChanged Event occurs when server connection count changed. Declaration public event EventHandler? ServerConnectionCountChanged Event Type Type Description EventHandler Implements IDisposable" }, "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html": { "href": "api/Titanium.Web.Proxy.Routing.ReverseProxyFastPath.html", @@ -672,7 +677,7 @@ "api/Titanium.Web.Proxy.WebSocketFrame.html": { "href": "api/Titanium.Web.Proxy.WebSocketFrame.html", "title": "Class WebSocketFrame | Titanium Web Proxy", - "summary": "Class WebSocketFrame A single decoded WebSocket frame, as produced by Decode(byte[], int, int). Inheritance object WebSocketFrame Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class WebSocketFrame Remarks Data is an owned copy of the unmasked payload produced by Decode(byte[], int, int), safe to retain after that call returns. Properties | Edit this page View Source Data The unmasked frame payload (owned copy from WebSocketDecoder). Declaration public ReadOnlyMemory Data { get; } Property Value Type Description ReadOnlyMemory | Edit this page View Source IsFinal Declaration public bool IsFinal { get; } Property Value Type Description bool | Edit this page View Source OpCode Declaration public WebsocketOpCode OpCode { get; } Property Value Type Description WebsocketOpCode Methods | Edit this page View Source GetText() Declaration public string GetText() Returns Type Description string | Edit this page View Source GetText(Encoding) Declaration public string GetText(Encoding encoding) Parameters Type Name Description Encoding encoding Returns Type Description string" + "summary": "Class WebSocketFrame A single decoded WebSocket frame, as produced by Decode(byte[], int, int). Inheritance object WebSocketFrame Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.MemberwiseClone() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy Assembly: Titanium.Web.Proxy.dll Syntax public class WebSocketFrame Remarks Data is an owned copy of the unmasked payload produced by Decode(byte[], int, int), safe to retain after that call returns. Properties | Edit this page View Source Data The unmasked frame payload (owned copy from WebSocketDecoder). When Titanium.Web.Proxy.WebSocketFrame.RelayRaw is set, this is a slice of the original wire bytes to forward unchanged, not a decoded payload. Declaration public ReadOnlyMemory Data { get; } Property Value Type Description ReadOnlyMemory | Edit this page View Source IsFinal Declaration public bool IsFinal { get; } Property Value Type Description bool | Edit this page View Source OpCode Declaration public WebsocketOpCode OpCode { get; } Property Value Type Description WebsocketOpCode Methods | Edit this page View Source GetText() Declaration public string GetText() Returns Type Description string | Edit this page View Source GetText(Encoding) Declaration public string GetText(Encoding encoding) Parameters Type Name Description Encoding encoding Returns Type Description string" }, "api/Titanium.Web.Proxy.WebSocketFrameEncoder.html": { "href": "api/Titanium.Web.Proxy.WebSocketFrameEncoder.html", diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml index 07c017c2e..44a0dd7ec 100644 --- a/docs/xrefmap.yml +++ b/docs/xrefmap.yml @@ -1685,6 +1685,19 @@ references: commentId: E:Titanium.Web.Proxy.EventArguments.SessionEventArgs.BeforeWebSocketFrame fullName: Titanium.Web.Proxy.EventArguments.SessionEventArgs.BeforeWebSocketFrame nameWithType: SessionEventArgs.BeforeWebSocketFrame +- uid: Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody(Titanium.Web.Proxy.Http.RequestResponseBase) + name: CanBufferBody(RequestResponseBase) + href: api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html#Titanium_Web_Proxy_EventArguments_SessionEventArgs_CanBufferBody_Titanium_Web_Proxy_Http_RequestResponseBase_ + commentId: M:Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody(Titanium.Web.Proxy.Http.RequestResponseBase) + fullName: Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody(Titanium.Web.Proxy.Http.RequestResponseBase) + nameWithType: SessionEventArgs.CanBufferBody(RequestResponseBase) +- uid: Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody* + name: CanBufferBody + href: api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html#Titanium_Web_Proxy_EventArguments_SessionEventArgs_CanBufferBody_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody + nameWithType: SessionEventArgs.CanBufferBody - uid: Titanium.Web.Proxy.EventArguments.SessionEventArgs.Dispose(System.Boolean) name: Dispose(bool) href: api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html#Titanium_Web_Proxy_EventArguments_SessionEventArgs_Dispose_System_Boolean_ @@ -2200,6 +2213,12 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketDecoderSend nameWithType: SessionEventArgs.WebSocketDecoderSend +- uid: Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketFrameElided + name: WebSocketFrameElided + href: api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html#Titanium_Web_Proxy_EventArguments_SessionEventArgs_WebSocketFrameElided + commentId: E:Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketFrameElided + fullName: Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketFrameElided + nameWithType: SessionEventArgs.WebSocketFrameElided - uid: Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketServerWriter name: WebSocketServerWriter href: api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html#Titanium_Web_Proxy_EventArguments_SessionEventArgs_WebSocketServerWriter @@ -2840,6 +2859,77 @@ references: commentId: F:Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.ServerToClient fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameDirection.ServerToClient nameWithType: WebSocketFrameDirection.ServerToClient +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs + name: WebSocketFrameElidedEventArgs + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html + commentId: T:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs + nameWithType: WebSocketFrameElidedEventArgs +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength + name: DeclaredPayloadLength + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_DeclaredPayloadLength + commentId: P:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength + nameWithType: WebSocketFrameElidedEventArgs.DeclaredPayloadLength +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength* + name: DeclaredPayloadLength + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_DeclaredPayloadLength_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.DeclaredPayloadLength + nameWithType: WebSocketFrameElidedEventArgs.DeclaredPayloadLength +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction + name: Direction + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_Direction + commentId: P:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction + nameWithType: WebSocketFrameElidedEventArgs.Direction +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction* + name: Direction + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_Direction_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Direction + nameWithType: WebSocketFrameElidedEventArgs.Direction +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal + name: IsFinal + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_IsFinal + commentId: P:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal + nameWithType: WebSocketFrameElidedEventArgs.IsFinal +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal* + name: IsFinal + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_IsFinal_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal + nameWithType: WebSocketFrameElidedEventArgs.IsFinal +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode + name: OpCode + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_OpCode + commentId: P:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode + nameWithType: WebSocketFrameElidedEventArgs.OpCode +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode* + name: OpCode + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_OpCode_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.OpCode + nameWithType: WebSocketFrameElidedEventArgs.OpCode +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session + name: Session + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_Session + commentId: P:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session + nameWithType: WebSocketFrameElidedEventArgs.Session +- uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session* + name: Session + href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.html#Titanium_Web_Proxy_EventArguments_WebSocketFrameElidedEventArgs_Session_ + commentId: Overload:Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session + isSpec: "True" + fullName: Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.Session + nameWithType: WebSocketFrameElidedEventArgs.Session - uid: Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction name: WebSocketFrameInterceptAction href: api/Titanium.Web.Proxy.EventArguments.WebSocketFrameInterceptAction.html @@ -8012,6 +8102,12 @@ references: commentId: F:Titanium.Web.Proxy.Options.PolicyFamily.Http2RelayValidation fullName: Titanium.Web.Proxy.Options.PolicyFamily.Http2RelayValidation nameWithType: PolicyFamily.Http2RelayValidation +- uid: Titanium.Web.Proxy.Options.PolicyFamily.WebSocketFrameBudget + name: WebSocketFrameBudget + href: api/Titanium.Web.Proxy.Options.PolicyFamily.html#Titanium_Web_Proxy_Options_PolicyFamily_WebSocketFrameBudget + commentId: F:Titanium.Web.Proxy.Options.PolicyFamily.WebSocketFrameBudget + fullName: Titanium.Web.Proxy.Options.PolicyFamily.WebSocketFrameBudget + nameWithType: PolicyFamily.WebSocketFrameBudget - uid: Titanium.Web.Proxy.Options.PolicyMode name: PolicyMode href: api/Titanium.Web.Proxy.Options.PolicyMode.html @@ -8068,12 +8164,12 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.AllowAmbiguousFraming nameWithType: ProxyPolicyModes.AllowAmbiguousFraming -- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) - name: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) - href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_ - commentId: M:Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) - fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode) - nameWithType: ProxyPolicyModes.Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) +- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) + name: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) + href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_ + commentId: M:Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) + fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode) + nameWithType: ProxyPolicyModes.Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) - uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create* name: Create href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_ From 85db6292f43aa85e078ec06a002cb0e388de1f15 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 19:34:29 +0530 Subject: [PATCH 08/11] fix(inspector): stop UI freezes on large binary bodies and heavy live/diff actions Selecting the Body tab for a 2 MiB git upload-pack response froze the window for seconds: ~256K characters of U+FFFD/control characters were laid out in a wrapped Avalonia TextBox on the UI thread (headless repro: Body 5012 ms, Hex 105-274 ms; now 105 ms / 10 ms). - InspectorDisplayText.ForTextBox: display-only cap (128K chars; 16K when the content is binary-looking), control/replacement characters shown as '.', trailing note pointing to Save body / Hex. Applied to Body, Frames, SSE, Protobuf and Diff text boxes. Captured bytes, Save, Hex and clipboard keep the full data. - SessionInspectors.TryDecompress gains a maxOutputBytes bound; Body decode is capped at MaxBodyBytes and Hex decodes only MaxHexBytes+1, so a small compressed body can no longer expand without limit on the UI thread. - SessionDiff: bound the 8-line lookahead search to its window (was a scan to the end of the body per mismatch, quadratic); output is identical. - Session Diff command computes off the UI thread when a UI exists, shows a bounded text and still copies the full report. - Live updates of the selected session (chatty WebSocket, streaming body) coalesce Inspect refreshes to 200 ms with a trailing refresh instead of rebuilding every text box per event. Inspector-only; the proxy library and RPS paths are untouched. Adds unit tests for the bounds and a headless real-window regression test. --- .../Services/InspectorDisplayText.cs | 109 +++++++++++++ .../Services/SessionDiff.cs | 21 ++- .../Services/SessionInspectors.cs | 43 ++++- .../MainWindowViewModel.BodyInspect.cs | 2 +- .../MainWindowViewModel.Sessions.cs | 13 +- .../ViewModels/MainWindowViewModel.cs | 54 ++++++- .../InspectLargeBodyHeadlessTests.cs | 70 ++++++++ .../InspectorUiBoundsTests.cs | 153 ++++++++++++++++++ 8 files changed, 447 insertions(+), 18 deletions(-) create mode 100644 src/Titanium.Inspector/Services/InspectorDisplayText.cs create mode 100644 tests/Titanium.E2E.Tests/UiHeadless/InspectLargeBodyHeadlessTests.cs create mode 100644 tests/Titanium.Inspector.Tests/InspectorUiBoundsTests.cs diff --git a/src/Titanium.Inspector/Services/InspectorDisplayText.cs b/src/Titanium.Inspector/Services/InspectorDisplayText.cs new file mode 100644 index 000000000..9b9403f3b --- /dev/null +++ b/src/Titanium.Inspector/Services/InspectorDisplayText.cs @@ -0,0 +1,109 @@ +using System.Globalization; +using System.Text; + +namespace Titanium.Inspector.Services; + +/// +/// Bounds the text handed to the Inspect panel TextBoxes. Avalonia lays the whole string +/// out on the UI thread, and the cost explodes for binary payloads decoded as UTF-8 (every +/// U+FFFD / control character needs font fallback): a 256 K-character git pack froze the window +/// for seconds when the Body tab was selected. This is display-only; captured bytes, Save body, +/// Hex and Copy are unaffected. +/// +public static class InspectorDisplayText +{ + /// Maximum characters shown for text-like content (about 0.1-0.3 s of layout). + public const int MaxTextChars = 128 * 1024; + + /// Maximum characters shown for binary-looking content (unreadable beyond the head anyway). + public const int MaxBinaryChars = 16 * 1024; + + private const int BinarySampleChars = 8 * 1024; + private const int BinaryPercentThreshold = 10; + + /// + /// Returns unchanged (same instance) when it is short and renderable; + /// otherwise a copy with control / replacement characters shown as '.' and, past the cap, + /// truncated with a trailing note. + /// + public static string ForTextBox(string? text) + { + if (string.IsNullOrEmpty(text)) + { + return text ?? ""; + } + + var binary = LooksBinary(text); + var limit = binary ? MaxBinaryChars : MaxTextChars; + var take = Math.Min(text.Length, limit); + if (take < text.Length && char.IsHighSurrogate(text[take - 1])) + { + take--; + } + + var truncated = take < text.Length; + if (!truncated && !ContainsUnrenderable(text.AsSpan(0, take))) + { + return text; + } + + var sb = new StringBuilder(take + 128); + for (var i = 0; i < take; i++) + { + var c = text[i]; + sb.Append(IsUnrenderable(c) ? '.' : c); + } + + if (truncated) + { + sb.AppendLine(); + sb.Append("… display limited to the first ") + .Append(take.ToString("N0", CultureInfo.CurrentCulture)) + .Append(" of ") + .Append(text.Length.ToString("N0", CultureInfo.CurrentCulture)) + .Append(" characters"); + if (binary) + { + sb.Append(" (binary content, non-printable bytes shown as '.')"); + } + + sb.Append(" — use Save body or the Hex tab for the full content"); + } + + return sb.ToString(); + } + + /// True when a sample from the start of is mostly non-printable. + internal static bool LooksBinary(string text) + { + var sample = Math.Min(text.Length, BinarySampleChars); + var bad = 0; + for (var i = 0; i < sample; i++) + { + if (IsUnrenderable(text[i])) + { + bad++; + } + } + + return bad * 100 >= sample * BinaryPercentThreshold && bad > 0; + } + + private static bool ContainsUnrenderable(ReadOnlySpan text) + { + foreach (var c in text) + { + if (IsUnrenderable(c)) + { + return true; + } + } + + return false; + } + + private static bool IsUnrenderable(char c) => + c == '\uFFFD' + || (c < ' ' && c != '\n' && c != '\r' && c != '\t') + || (c >= '\u007F' && c <= '\u009F'); +} diff --git a/src/Titanium.Inspector/Services/SessionDiff.cs b/src/Titanium.Inspector/Services/SessionDiff.cs index 06a3bc11f..c9eb3eccf 100644 --- a/src/Titanium.Inspector/Services/SessionDiff.cs +++ b/src/Titanium.Inspector/Services/SessionDiff.cs @@ -170,8 +170,8 @@ private static int AppendHeaderDiff(StringBuilder sb, string title, string? left continue; } - var later = IndexOf(rightLines, leftLines[i], j + 1); - if (later >= 0 && later - j <= 8) + var later = IndexOf(rightLines, leftLines[i], j + 1, MaxLookahead); + if (later >= 0) { while (j < later) { @@ -182,8 +182,8 @@ private static int AppendHeaderDiff(StringBuilder sb, string title, string? left continue; } - var laterLeft = IndexOf(leftLines, rightLines[j], i + 1); - if (laterLeft >= 0 && laterLeft - i <= 8) + var laterLeft = IndexOf(leftLines, rightLines[j], i + 1, MaxLookahead); + if (laterLeft >= 0) { while (i < laterLeft) { @@ -233,9 +233,18 @@ private static List SplitLines(string text) .ToList(); } - private static int IndexOf(List lines, string value, int start) + /// + /// Lines a match may be ahead of the cursor to still count as an insert/delete. The search is + /// bounded to this window (not the rest of the body), keeping the diff linear on large bodies + /// where it used to be quadratic and could freeze the UI thread. + /// + private const int MaxLookahead = 8; + + /// First index in [start, cursor + maxAhead] equal to , else -1. + private static int IndexOf(List lines, string value, int start, int maxAhead) { - for (var i = start; i < lines.Count; i++) + var end = Math.Min(lines.Count, start - 1 + maxAhead + 1); + for (var i = start; i < end; i++) { if (string.Equals(lines[i], value, StringComparison.Ordinal)) { diff --git a/src/Titanium.Inspector/Services/SessionInspectors.cs b/src/Titanium.Inspector/Services/SessionInspectors.cs index 1673a600f..2ffcbe6bb 100644 --- a/src/Titanium.Inspector/Services/SessionInspectors.cs +++ b/src/Titanium.Inspector/Services/SessionInspectors.cs @@ -123,7 +123,14 @@ public static string TryFormatJson(string? text) } } - public static byte[]? TryDecompress(byte[]? body, string? contentEncoding) + /// Captured (possibly compressed) bytes. + /// Content-Encoding header value. + /// + /// Upper bound on the decompressed size. The Inspect panel decodes on the UI thread, so a small + /// compressed body (decompression bomb) must not be allowed to expand without limit. + /// Output beyond the bound is dropped; the default keeps the historical unbounded behavior. + /// + public static byte[]? TryDecompress(byte[]? body, string? contentEncoding, int maxOutputBytes = int.MaxValue) { if (body is null || body.Length == 0 || string.IsNullOrEmpty(contentEncoding)) { @@ -148,7 +155,15 @@ public static string TryFormatJson(string? text) using (codec) using (var output = new MemoryStream()) { - codec.CopyTo(output); + if (maxOutputBytes == int.MaxValue) + { + codec.CopyTo(output); + } + else + { + CopyBounded(codec, output, maxOutputBytes); + } + return output.ToArray(); } } @@ -158,6 +173,23 @@ public static string TryFormatJson(string? text) } } + private static void CopyBounded(Stream source, Stream destination, int maxBytes) + { + var buffer = new byte[16 * 1024]; + var remaining = maxBytes; + while (remaining > 0) + { + var read = source.Read(buffer, 0, Math.Min(buffer.Length, remaining)); + if (read <= 0) + { + return; + } + + destination.Write(buffer, 0, read); + remaining -= read; + } + } + /// /// Labeled request + response body text (same section style as Headers). /// @@ -205,7 +237,7 @@ private static string ResolveBodyText(string? headersText, string? bodyText, byt var headers = ParseHeaderBlock(headersText); headers.TryGetValue("Content-Encoding", out var encoding); - var bytes = TryDecompress(bodyBytes, encoding); + var bytes = TryDecompress(bodyBytes, encoding, InspectorBodyLimits.MaxBodyBytes); if (bytes is { Length: > 0 }) { return Encoding.UTF8.GetString(bytes); @@ -218,8 +250,9 @@ private static string ResolveHex(string? headersText, byte[]? bodyBytes) { var headers = ParseHeaderBlock(headersText); headers.TryGetValue("Content-Encoding", out var encoding); - var bytes = TryDecompress(bodyBytes, encoding); - var hex = ToHex(bytes); + // Hex shows only the first MaxHexBytes; decode one byte more so ToHex still appends its "…". + var bytes = TryDecompress(bodyBytes, encoding, InspectorBodyLimits.MaxHexBytes + 1); + var hex = ToHex(bytes, InspectorBodyLimits.MaxHexBytes); return string.IsNullOrEmpty(hex) ? "(empty)" : hex; } } diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.BodyInspect.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.BodyInspect.cs index 13cecec3f..aa7836c45 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.BodyInspect.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.BodyInspect.cs @@ -296,7 +296,7 @@ private void RefreshBodyInspector() BodyCaptureHint = BuildBodyCaptureHint(_selected); HexCaptureHint = BuildHexCaptureHint(_selected); - SelectedBody = BuildSelectedBodyText(_selected); + SelectedBody = InspectorDisplayText.ForTextBox(BuildSelectedBodyText(_selected)); UpdateBodyPreviewImage(_selected); NotifySaveBodyCanExecute(); } diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs index 4391b9892..1977ddda0 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.Sessions.cs @@ -228,13 +228,22 @@ private async Task CopyAsFetchAsync() } private async Task DiffSessionsAsync() { - if (!TryBuildSessionDiff(out var diff)) + var selection = ResolveFilterSelection(); + if (selection.Count != 2) { StatusText = "Select exactly two sessions to diff"; return; } - SessionDiffText = diff.Text; + // Comparing two multi-MB bodies is CPU-bound: keep it off the UI thread when a UI exists. + var left = selection[0]; + var right = selection[1]; + var diff = Application.Current is null + ? SessionDiff.Compare(left, right) + : await Task.Run(() => SessionDiff.Compare(left, right), CancellationToken.None); + + // The clipboard gets the full report; the on-screen TextBox is bounded (layout cost). + SessionDiffText = InspectorDisplayText.ForTextBox(diff.Text); await CopyTextToClipboardAsync(diff.Text).ConfigureAwait(false); ShowSessionDetails = true; SelectedOuterPaneIndex = 0; diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs index 339a3530f..58fc28bce 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -732,7 +732,7 @@ private void WireSessionPipelineHandlers() if (ReferenceEquals(SelectedSession, snap)) { UpdateWsFramesVisibility(); - RefreshSelectedInspectors(); + RefreshSelectedInspectorsCoalesced(snap); } }); _interception.DecryptFailureBypassLearned += (_, entry) => @@ -2824,9 +2824,55 @@ private void RefreshSelectedInspectors() _selected.ResponseHeadersText, _selected.RequestBodyBytes, _selected.ResponseBodyBytes); - SelectedFrames = BuildSelectedFramesText(_selected); - SelectedSseEvents = BuildSelectedSseText(_selected); - SelectedProtobufDecoded = BuildSelectedProtobufText(_selected); + SelectedFrames = InspectorDisplayText.ForTextBox(BuildSelectedFramesText(_selected)); + SelectedSseEvents = InspectorDisplayText.ForTextBox(BuildSelectedSseText(_selected)); + SelectedProtobufDecoded = InspectorDisplayText.ForTextBox(BuildSelectedProtobufText(_selected)); + } + + private long _lastSelectedRefreshTicks; + private bool _selectedRefreshPending; + + /// + /// Live-update refresh of the selected session. A chatty WebSocket (one update per frame) or a + /// streaming body would otherwise rebuild every Inspect text box on the UI thread per event; + /// coalesce to with a trailing refresh so + /// the final state is always shown. Selection changes still refresh immediately, and without a + /// running Avalonia application (unit tests) the refresh stays synchronous. + /// + private void RefreshSelectedInspectorsCoalesced(SessionSnapshot snap) + { + if (Application.Current is null) + { + RefreshSelectedInspectors(); + return; + } + + var interval = TimeSpan.FromMilliseconds(InspectorBodyLimits.TeeUiCoalesceMs); + if (_lastSelectedRefreshTicks == 0 || System.Diagnostics.Stopwatch.GetElapsedTime(_lastSelectedRefreshTicks) >= interval) + { + _lastSelectedRefreshTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + RefreshSelectedInspectors(); + return; + } + + if (_selectedRefreshPending) + { + return; + } + + _selectedRefreshPending = true; + var remaining = interval - System.Diagnostics.Stopwatch.GetElapsedTime(_lastSelectedRefreshTicks); + DispatcherTimer.RunOnce( + () => + { + _selectedRefreshPending = false; + if (ReferenceEquals(SelectedSession, snap)) + { + _lastSelectedRefreshTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + RefreshSelectedInspectors(); + } + }, + remaining > TimeSpan.Zero ? remaining : TimeSpan.FromMilliseconds(1)); } private static string BuildSelectedHeadersText(SessionSnapshot selected) diff --git a/tests/Titanium.E2E.Tests/UiHeadless/InspectLargeBodyHeadlessTests.cs b/tests/Titanium.E2E.Tests/UiHeadless/InspectLargeBodyHeadlessTests.cs new file mode 100644 index 000000000..c669e8c88 --- /dev/null +++ b/tests/Titanium.E2E.Tests/UiHeadless/InspectLargeBodyHeadlessTests.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.E2E.Tests.Harness; +using Titanium.Inspector.Services; + +namespace Titanium.E2E.Tests.UiHeadless; + +/// +/// Regression: selecting Body / Hex for a large binary response (a git upload-pack stream) +/// froze the UI for seconds because ~256 K characters of U+FFFD / control characters were laid +/// out in a wrapped TextBox on the UI thread. +/// +[TestClass] +public class InspectLargeBodyHeadlessTests +{ + // Generous: the fix measures well under a second; the unfixed layout took several seconds. + private const int MaxTabSwitchMs = 3000; + + [TestMethod] + [TestCategory("E2E-UI-Headless")] + public async Task BodyAndHexTabs_LargeBinaryResponse_StayResponsiveAndBounded() + { + await using var fx = new InspectorHeadlessFixture(); + await fx.StartAsync(); + + var wire = new byte[InspectorBodyLimits.MaxBodyBytes]; + new Random(7).NextBytes(wire); + var text = InspectorBodyLimits.TruncateText(Encoding.UTF8.GetString(wire)); + + long bodyMs = 0; + long hexMs = 0; + await fx.DispatchAsync(() => + { + fx.ViewModel.Sessions.Add(new SessionSnapshot + { + Id = 21, + Method = "POST", + StatusCode = 200, + Host = "git.test", + Url = "http://git.test/repo.git/git-upload-pack", + Protocol = "HTTP/1.1", + ContentType = "application/x-git-upload-pack-result", + ResponseBodyBytes = wire, + ResponseBodyText = text, + ResponseBodyCapture = BodyCaptureState.Truncated, + }); + fx.ViewModel.SelectedSession = fx.ViewModel.Sessions[0]; + + var sw = Stopwatch.StartNew(); + fx.Robot.Click("TabBody"); + fx.Window.UpdateLayout(); + bodyMs = sw.ElapsedMilliseconds; + + sw.Restart(); + fx.Robot.Click("TabHex"); + fx.Window.UpdateLayout(); + hexMs = sw.ElapsedMilliseconds; + + var body = fx.ViewModel.SelectedBody; + Assert.IsTrue(body.Length <= InspectorDisplayText.MaxBinaryChars + 512, $"Body text length {body.Length}"); + Assert.IsFalse(body.Contains('\uFFFD'), "replacement characters must not reach the TextBox"); + StringAssert.Contains(body, "display limited"); + }); + + Console.WriteLine($"Inspect tab switch: Body={bodyMs} ms, Hex={hexMs} ms"); + Assert.IsTrue(bodyMs < MaxTabSwitchMs, $"Body tab took {bodyMs} ms"); + Assert.IsTrue(hexMs < MaxTabSwitchMs, $"Hex tab took {hexMs} ms"); + } +} diff --git a/tests/Titanium.Inspector.Tests/InspectorUiBoundsTests.cs b/tests/Titanium.Inspector.Tests/InspectorUiBoundsTests.cs new file mode 100644 index 000000000..14fdd9b3a --- /dev/null +++ b/tests/Titanium.Inspector.Tests/InspectorUiBoundsTests.cs @@ -0,0 +1,153 @@ +using System.Diagnostics; +using System.IO.Compression; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Inspector.Services; + +namespace Titanium.Inspector.Tests; + +/// +/// Bounds that keep the Inspect panel responsive: everything handed to a TextBox is capped and +/// renderable, decompression on the UI thread is bounded, and the session diff stays linear. +/// +[TestClass] +public class InspectorUiBoundsTests +{ + [TestMethod] + public void ForTextBox_ShortRenderableText_ReturnsSameInstance() + { + var text = "=== Request ===\n(empty)\n\n=== Response ===\n{\"a\":1}\tök"; + Assert.AreSame(text, InspectorDisplayText.ForTextBox(text)); + Assert.AreEqual("", InspectorDisplayText.ForTextBox(null)); + Assert.AreEqual("", InspectorDisplayText.ForTextBox("")); + } + + [TestMethod] + public void ForTextBox_ScrubsControlAndReplacementCharacters() + { + var text = "ok\uFFFD\u0000\u0001\u007F\u0085end\r\n\t"; + Assert.AreEqual("ok.....end\r\n\t", InspectorDisplayText.ForTextBox(text)); + } + + [TestMethod] + public void ForTextBox_LargeText_IsCappedWithNote() + { + var text = new string('a', InspectorDisplayText.MaxTextChars + 5000); + var shown = InspectorDisplayText.ForTextBox(text); + + Assert.IsTrue(shown.StartsWith(new string('a', InspectorDisplayText.MaxTextChars), StringComparison.Ordinal)); + StringAssert.Contains(shown, "display limited"); + Assert.IsTrue(shown.Length < InspectorDisplayText.MaxTextChars + 400); + Assert.IsFalse(shown.Contains("binary content", StringComparison.Ordinal)); + } + + [TestMethod] + public void ForTextBox_BinaryText_UsesTighterCapAndNoReplacementCharacters() + { + var wire = new byte[InspectorBodyLimits.MaxBodyBytes]; + new Random(3).NextBytes(wire); + var text = InspectorBodyLimits.TruncateText(Encoding.UTF8.GetString(wire)); + Assert.IsTrue(InspectorDisplayText.LooksBinary(text)); + + var shown = InspectorDisplayText.ForTextBox(text); + + Assert.IsTrue(shown.Length < InspectorDisplayText.MaxBinaryChars + 400, $"len={shown.Length}"); + Assert.IsFalse(shown.Contains('\uFFFD')); + StringAssert.Contains(shown, "binary content"); + } + + [TestMethod] + public void ForTextBox_FewReplacementCharsInLargeText_AreNotTreatedAsBinary() + { + var text = new string('x', 50_000) + "\uFFFD" + new string('y', 50_000); + Assert.IsFalse(InspectorDisplayText.LooksBinary(text)); + + var shown = InspectorDisplayText.ForTextBox(text); + + Assert.AreEqual(100_001, shown.Length); + Assert.AreEqual('.', shown[50_000]); + } + + [TestMethod] + public void ForTextBox_DoesNotSplitSurrogatePairAtCap() + { + var text = new string('a', InspectorDisplayText.MaxTextChars - 1) + "😀" + "tail"; + var shown = InspectorDisplayText.ForTextBox(text); + + var head = shown[..(InspectorDisplayText.MaxTextChars - 1)]; + Assert.AreEqual(new string('a', InspectorDisplayText.MaxTextChars - 1), head); + Assert.IsTrue(shown[InspectorDisplayText.MaxTextChars - 1] is '\r' or '\n'); + Assert.IsFalse(shown.Any(char.IsSurrogate)); + } + + [TestMethod] + public void TryDecompress_MaxOutput_StopsDecompressionBomb() + { + var raw = new byte[8 * 1024 * 1024]; // compresses to a few KB + using var packed = new MemoryStream(); + using (var gz = new GZipStream(packed, CompressionLevel.SmallestSize, leaveOpen: true)) + { + gz.Write(raw); + } + + var bomb = packed.ToArray(); + Assert.IsTrue(bomb.Length < 64 * 1024); + + var bounded = SessionInspectors.TryDecompress(bomb, "gzip", 1000); + + Assert.IsNotNull(bounded); + Assert.AreEqual(1000, bounded.Length); + Assert.AreEqual(raw.Length, SessionInspectors.TryDecompress(bomb, "gzip")!.Length); + } + + [TestMethod] + public void FormatLabeledHex_CompressedBody_DecodesOnlyHexWindowAndKeepsEllipsis() + { + var raw = Enumerable.Range(0, 100_000).Select(i => (byte)(i % 251)).ToArray(); + using var packed = new MemoryStream(); + using (var gz = new GZipStream(packed, CompressionLevel.Optimal, leaveOpen: true)) + { + gz.Write(raw); + } + + var hex = SessionInspectors.FormatLabeledHex( + "", "Content-Encoding: gzip\r\n", null, packed.ToArray()); + + StringAssert.EndsWith(hex, " …"); + StringAssert.Contains(hex, "00 01 02 03"); + Assert.IsTrue(hex.Length < InspectorBodyLimits.MaxHexBytes * 3 + 200); + } + + [TestMethod] + public void SessionDiff_LargeDissimilarBodies_StaysLinearAndKeepsLookaheadSemantics() + { + var left = new SessionSnapshot + { + Id = 1, + Method = "GET", + Url = "http://a.test/", + ResponseBodyText = string.Join('\n', Enumerable.Range(0, 60_000).Select(i => "L" + i)), + }; + var right = new SessionSnapshot + { + Id = 2, + Method = "GET", + Url = "http://a.test/", + ResponseBodyText = string.Join('\n', Enumerable.Range(0, 60_000).Select(i => "R" + i)), + }; + + var sw = Stopwatch.StartNew(); + var diff = SessionDiff.Compare(left, right); + sw.Stop(); + + Assert.IsTrue(diff.HasDifferences); + Assert.IsTrue(sw.ElapsedMilliseconds < 3000, $"diff took {sw.ElapsedMilliseconds} ms"); + + // A match within 8 lines is still reported as an insert, not a replace. + var near = SessionDiff.Compare( + new SessionSnapshot { Id = 3, Method = "GET", Url = "u", ResponseBodyText = "a\nb\nc\nd" }, + new SessionSnapshot { Id = 4, Method = "GET", Url = "u", ResponseBodyText = "a\nX\nb\nc\nd" }); + StringAssert.Contains(near.Text, "+ X"); + Assert.IsFalse(near.Text.Contains("- b", StringComparison.Ordinal)); + } +} From 006cf8226063e97d5cd96b7b40e05aad629d4650 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 19:38:00 +0530 Subject: [PATCH 09/11] chore(sonar): clear the last two open code smells - S107: ProxyPolicyModes.Create had grown to 8 parameters. Restore the shipped 7-parameter signature (also repairs the binary break that had been recorded as *REMOVED* in PublicAPI.Unshipped.txt); WebSocketFrameBudget still defaults to Enforce and is changed through the existing With(family, mode). The CLI config applier uses With for the WebSocket family. Config-time only, not a request-path change. - CA1846: use StringBuilder.Append(string, int, int) / CompareOrdinal instead of Substring in the chunked-body test helper. --- src/Titanium.Cli/Config/ServerConfigApplier.cs | 6 ++++-- .../Options/ProxyPolicyModes.cs | 15 +++++++++------ src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt | 2 -- .../Http2TranslationBridgeAcceptanceTests.cs | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Titanium.Cli/Config/ServerConfigApplier.cs b/src/Titanium.Cli/Config/ServerConfigApplier.cs index f0bba73e9..599840515 100644 --- a/src/Titanium.Cli/Config/ServerConfigApplier.cs +++ b/src/Titanium.Cli/Config/ServerConfigApplier.cs @@ -345,8 +345,10 @@ private static void ApplyPolicyModes(ProxyServer proxy, PolicyModesConfig? polic ParsePolicyMode(policy.HeaderLimits, current[PolicyFamily.HeaderLimits]), ParsePolicyMode(policy.AdmissionControl, current[PolicyFamily.AdmissionControl]), ParsePolicyMode(policy.Http2AbuseBudget, current[PolicyFamily.Http2AbuseBudget]), - ParsePolicyMode(policy.Http2RelayValidation, current[PolicyFamily.Http2RelayValidation]), - webSocketFrameBudget: ParsePolicyMode(policy.WebSocketFrameBudget, current[PolicyFamily.WebSocketFrameBudget])); + ParsePolicyMode(policy.Http2RelayValidation, current[PolicyFamily.Http2RelayValidation])) + .With( + PolicyFamily.WebSocketFrameBudget, + ParsePolicyMode(policy.WebSocketFrameBudget, current[PolicyFamily.WebSocketFrameBudget])); if (policy.AllowAmbiguousFraming == true) { diff --git a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs index 40cebf6e5..b3e6c61c7 100644 --- a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs +++ b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs @@ -49,13 +49,17 @@ private ProxyPolicyModes(Dictionary modes, bool allowA admissionControl: PolicyMode.Enforce, http2AbuseBudget: PolicyMode.Enforce, http2RelayValidation: PolicyMode.Enforce, - http1ReplaySafety: PolicyMode.Enforce, - webSocketFrameBudget: PolicyMode.Enforce); + http1ReplaySafety: PolicyMode.Enforce); /// Returns the mode selected for . public PolicyMode this[PolicyFamily family] => modes[family]; - /// Builds a snapshot with an explicit mode for every family. starts . + /// + /// Builds a snapshot with an explicit mode for the first seven families. + /// starts at (use + /// to change it) so this shipped signature stays binary-compatible. + /// starts . + /// public static ProxyPolicyModes Create( PolicyMode bodyBudget, PolicyMode decompressionRatio, @@ -63,8 +67,7 @@ public static ProxyPolicyModes Create( PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, - PolicyMode http1ReplaySafety = PolicyMode.Observe, - PolicyMode webSocketFrameBudget = PolicyMode.Enforce) + PolicyMode http1ReplaySafety = PolicyMode.Observe) { var dict = new Dictionary { @@ -75,7 +78,7 @@ public static ProxyPolicyModes Create( [PolicyFamily.Http2AbuseBudget] = http2AbuseBudget, [PolicyFamily.Http2RelayValidation] = http2RelayValidation, [PolicyFamily.Http1ReplaySafety] = http1ReplaySafety, - [PolicyFamily.WebSocketFrameBudget] = webSocketFrameBudget + [PolicyFamily.WebSocketFrameBudget] = PolicyMode.Enforce }; return new ProxyPolicyModes(dict, false); } diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt index bf55eb710..c8c3f66e3 100644 --- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt @@ -272,8 +272,6 @@ Titanium.Web.Proxy.ProxyServer.ResponseHeaderContributor.get -> System.Action void Titanium.Web.Proxy.ProxyServer.ShouldBypassDecryptForLearnedHost(string? host) -> bool static Titanium.Web.Proxy.Network.FirefoxCertificateTrust.LastEnterpriseRootsStep.get -> string? -*REMOVED*static Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode bodyBudget, Titanium.Web.Proxy.Options.PolicyMode decompressionRatio, Titanium.Web.Proxy.Options.PolicyMode headerLimits, Titanium.Web.Proxy.Options.PolicyMode admissionControl, Titanium.Web.Proxy.Options.PolicyMode http2AbuseBudget, Titanium.Web.Proxy.Options.PolicyMode http2RelayValidation = Titanium.Web.Proxy.Options.PolicyMode.Disabled, Titanium.Web.Proxy.Options.PolicyMode http1ReplaySafety = Titanium.Web.Proxy.Options.PolicyMode.Observe) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! -static Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode bodyBudget, Titanium.Web.Proxy.Options.PolicyMode decompressionRatio, Titanium.Web.Proxy.Options.PolicyMode headerLimits, Titanium.Web.Proxy.Options.PolicyMode admissionControl, Titanium.Web.Proxy.Options.PolicyMode http2AbuseBudget, Titanium.Web.Proxy.Options.PolicyMode http2RelayValidation = Titanium.Web.Proxy.Options.PolicyMode.Disabled, Titanium.Web.Proxy.Options.PolicyMode http1ReplaySafety = Titanium.Web.Proxy.Options.PolicyMode.Observe, Titanium.Web.Proxy.Options.PolicyMode webSocketFrameBudget = Titanium.Web.Proxy.Options.PolicyMode.Enforce) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! Titanium.Web.Proxy.EventArguments.SessionEventArgs.CanBufferBody(Titanium.Web.Proxy.Http.RequestResponseBase! message) -> bool Titanium.Web.Proxy.EventArguments.SessionEventArgs.WebSocketFrameElided -> Titanium.Web.Proxy.EventArguments.AsyncEventHandler? Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs index 8ee28128a..56e2ba2be 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/Http2TranslationBridgeAcceptanceTests.cs @@ -692,9 +692,9 @@ private static string DecodeChunkedAscii(string raw) if (size == 0) break; i = lineEnd + 2; if (i + size > raw.Length) break; - result.Append(raw.Substring(i, size)); + result.Append(raw, i, size); i += size; - if (i + 2 <= raw.Length && raw.Substring(i, 2) == "\r\n") + if (i + 2 <= raw.Length && string.CompareOrdinal(raw, i, "\r\n", 0, 2) == 0) i += 2; } From 44c68df07b8c58455a9047177f4d866eabe4d728 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:30:21 +0000 Subject: [PATCH 10/11] Update documentation --- ...um.Web.Proxy.Options.ProxyPolicyModes.html | 28 +++++++++---------- docs/api/Titanium.Web.Proxy.Options.html | 2 +- docs/index.json | 4 +-- docs/xrefmap.yml | 12 ++++---- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html index 7f333a531..66f3c2701 100644 --- a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html +++ b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html @@ -96,7 +96,7 @@

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the + proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile.

    @@ -205,7 +205,7 @@

    Property Value
    Edit this page - View Source + View Source

    this[PolicyFamily]

    @@ -252,19 +252,22 @@

    Methods

    | - Edit this page + Edit this page - View Source + View Source -

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode)

    -

    Builds a snapshot with an explicit mode for every family. AllowAmbiguousFraming starts false.

    +

    Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode)

    +

    Builds a snapshot with an explicit mode for the first seven families. +WebSocketFrameBudget starts at Enforce (use +With(PolicyFamily, PolicyMode) to change it) so this shipped signature stays binary-compatible. +AllowAmbiguousFraming starts false.

    Declaration
    -
    public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe, PolicyMode webSocketFrameBudget = PolicyMode.Enforce)
    +
    public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe)
    Parameters
    @@ -311,11 +314,6 @@
    Parameters
    - - - - -
    http1ReplaySafety
    PolicyModewebSocketFrameBudget
    Returns
    @@ -338,7 +336,7 @@
    Returns
    Edit this page - View Source + View Source

    With(PolicyFamily, PolicyMode)

    @@ -391,7 +389,7 @@
    Returns
    Edit this page - View Source + View Source

    WithAllObservedExceptDisabled()

    @@ -425,7 +423,7 @@
    Returns
    Edit this page - View Source + View Source

    WithAllowAmbiguousFramingEnabled()

    diff --git a/docs/api/Titanium.Web.Proxy.Options.html b/docs/api/Titanium.Web.Proxy.Options.html index d9daac766..a2d3b8975 100644 --- a/docs/api/Titanium.Web.Proxy.Options.html +++ b/docs/api/Titanium.Web.Proxy.Options.html @@ -101,7 +101,7 @@

    Prox through" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the - proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the + proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile.

    diff --git a/docs/index.json b/docs/index.json index e5d1c2db6..d78964710 100644 --- a/docs/index.json +++ b/docs/index.json @@ -517,7 +517,7 @@ "api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html", "title": "Class ProxyPolicyModes | Titanium Web Proxy", - "summary": "Class ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. Inheritance object ProxyPolicyModes Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public sealed class ProxyPolicyModes Properties | Edit this page View Source AllEnforce Every family enforced - today's shipped behavior, and the starting point every profile builds from. Declaration public static ProxyPolicyModes AllEnforce { get; } Property Value Type Description ProxyPolicyModes | Edit this page View Source AllowAmbiguousFraming Off by default and absent from every profile - see the type-level remarks. Never true unless WithAllowAmbiguousFramingEnabled() was called explicitly. Declaration public bool AllowAmbiguousFraming { get; } Property Value Type Description bool | Edit this page View Source this[PolicyFamily] Returns the mode selected for family. Declaration public PolicyMode this[PolicyFamily family] { get; } Parameters Type Name Description PolicyFamily family Property Value Type Description PolicyMode Methods | Edit this page View Source Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) Builds a snapshot with an explicit mode for every family. AllowAmbiguousFraming starts false. Declaration public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe, PolicyMode webSocketFrameBudget = PolicyMode.Enforce) Parameters Type Name Description PolicyMode bodyBudget PolicyMode decompressionRatio PolicyMode headerLimits PolicyMode admissionControl PolicyMode http2AbuseBudget PolicyMode http2RelayValidation PolicyMode http1ReplaySafety PolicyMode webSocketFrameBudget Returns Type Description ProxyPolicyModes | Edit this page View Source With(PolicyFamily, PolicyMode) Returns a snapshot identical to this one but with mode for family. Declaration public ProxyPolicyModes With(PolicyFamily family, PolicyMode mode) Parameters Type Name Description PolicyFamily family PolicyMode mode Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllObservedExceptDisabled() Returns a snapshot identical to this one but with every family dropped to Observe, except families already Disabled (which stay disabled - Observe would silently turn a family back on). This is the \"drop to Observe without redeploying\" runtime switch the plan's rollout section requires. Declaration public ProxyPolicyModes WithAllObservedExceptDisabled() Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllowAmbiguousFramingEnabled() The single, explicit, isolated act of relaying ambiguous HTTP/1 framing instead of rejecting it. See the type-level remarks; never call this as a side effect of applying a profile. Declaration public ProxyPolicyModes WithAllowAmbiguousFramingEnabled() Returns Type Description ProxyPolicyModes" + "summary": "Class ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. Inheritance object ProxyPolicyModes Inherited Members object.Equals(object) object.Equals(object, object) object.GetHashCode() object.GetType() object.ReferenceEquals(object, object) object.ToString() Namespace: Titanium.Web.Proxy.Options Assembly: Titanium.Web.Proxy.dll Syntax public sealed class ProxyPolicyModes Properties | Edit this page View Source AllEnforce Every family enforced - today's shipped behavior, and the starting point every profile builds from. Declaration public static ProxyPolicyModes AllEnforce { get; } Property Value Type Description ProxyPolicyModes | Edit this page View Source AllowAmbiguousFraming Off by default and absent from every profile - see the type-level remarks. Never true unless WithAllowAmbiguousFramingEnabled() was called explicitly. Declaration public bool AllowAmbiguousFraming { get; } Property Value Type Description bool | Edit this page View Source this[PolicyFamily] Returns the mode selected for family. Declaration public PolicyMode this[PolicyFamily family] { get; } Parameters Type Name Description PolicyFamily family Property Value Type Description PolicyMode Methods | Edit this page View Source Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) Builds a snapshot with an explicit mode for the first seven families. WebSocketFrameBudget starts at Enforce (use With(PolicyFamily, PolicyMode) to change it) so this shipped signature stays binary-compatible. AllowAmbiguousFraming starts false. Declaration public static ProxyPolicyModes Create(PolicyMode bodyBudget, PolicyMode decompressionRatio, PolicyMode headerLimits, PolicyMode admissionControl, PolicyMode http2AbuseBudget, PolicyMode http2RelayValidation = PolicyMode.Disabled, PolicyMode http1ReplaySafety = PolicyMode.Observe) Parameters Type Name Description PolicyMode bodyBudget PolicyMode decompressionRatio PolicyMode headerLimits PolicyMode admissionControl PolicyMode http2AbuseBudget PolicyMode http2RelayValidation PolicyMode http1ReplaySafety Returns Type Description ProxyPolicyModes | Edit this page View Source With(PolicyFamily, PolicyMode) Returns a snapshot identical to this one but with mode for family. Declaration public ProxyPolicyModes With(PolicyFamily family, PolicyMode mode) Parameters Type Name Description PolicyFamily family PolicyMode mode Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllObservedExceptDisabled() Returns a snapshot identical to this one but with every family dropped to Observe, except families already Disabled (which stay disabled - Observe would silently turn a family back on). This is the \"drop to Observe without redeploying\" runtime switch the plan's rollout section requires. Declaration public ProxyPolicyModes WithAllObservedExceptDisabled() Returns Type Description ProxyPolicyModes | Edit this page View Source WithAllowAmbiguousFramingEnabled() The single, explicit, isolated act of relaying ambiguous HTTP/1 framing instead of rejecting it. See the type-level remarks; never call this as a side effect of applying a profile. Declaration public ProxyPolicyModes WithAllowAmbiguousFramingEnabled() Returns Type Description ProxyPolicyModes" }, "api/Titanium.Web.Proxy.Options.ProxyProfile.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyProfile.html", @@ -547,7 +547,7 @@ "api/Titanium.Web.Proxy.Options.html": { "href": "api/Titanium.Web.Proxy.Options.html", "title": "Namespace Titanium.Web.Proxy.Options | Titanium Web Proxy", - "summary": "Namespace Titanium.Web.Proxy.Options Classes ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. ProxyProfileSettings The full bundle of settings one ProxyProfile applies to a ProxyServer as a single atomic assignment via Profile. Deliberately a plain data bundle, not a type with behavior: applying it is ProxyServer's job (see Profile's setter), so this type has no back-reference and no side effects of its own, per the plan's \"Constraints on the policy layer\" section. ProxyResourceLimits Immutable, validated snapshot of the resource bounds a peer can make the proxy allocate: header shape, body/decompression budgets, concurrency and abuse-rate ceilings, and pool / certificate-cache sizing. Constructed only through Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?), which validates every field up front, so an invalid limit is a construction-time exception rather than a runtime surprise discovered mid-connection. A limit that can legitimately be turned off is typed as nullable with null meaning \"disabled\" - an explicit state - rather than overloading 0 or a negative number to mean the same thing. Limits that must always be enforced because disabling them would leave the proxy itself exploitable (the concurrent-stream cap, the open-header-block frame bound) are non-nullable and always validated to be strictly positive. This type has no back-reference to ProxyServer and no mutable state after construction: it is meant to be handed down to subsystems by value, not looked up through a service locator or an ambient static, so the dependency graph among consumers stays acyclic per the plan's \"Constraints on the policy layer\" section. ProxyTimeoutOptions Immutable, validated snapshot of every deadline the proxy enforces across a request's lifetime, expressed consistently as TimeSpan rather than the mixture of \"seconds as int\" properties this replaces (ConnectionTimeOutSeconds, ConnectTimeOutSeconds, ...). A deadline that can be legitimately unbounded is nullable, with null meaning \"no deadline\" as an explicit state rather than Zero or a magic sentinel duration. This type only holds values; it does not decide which deadline fired first when several are composed for one request. That is the responsibility of the per-request deadline registry described in the plan's \"Deadline composition\" section, introduced alongside the header-parsing deadlines in a later item so it can be exercised by a real caller instead of landing as unused scaffolding. ResolvedSessionPolicy Read-only snapshot combining ProxyResourceLimits and ProxyTimeoutOptions into the single object H1/H2/H3/WebSocket subsystems are handed, so runtime mutation of either half cannot produce inconsistent enforcement partway through a request that started under a different combination of the two. Per the plan's \"Constraints on the policy layer\" section, this type is deliberately inert: no back-pointer to ProxyServer, no service-locator lookup, no mutable fields, no static ambient accessor. Subsystems receive it as a constructor argument or method parameter only, so the dependency graph among consumers stays acyclic - the opposite of how ProxyServer itself is reached today. Per the plan's \"Two-phase policy resolution\" section, a single resolution per connection is not correct: SessionEventArgs.MaxBufferedBodyBytes is contractually settable from BeforeRequest, and HTTP/3 already reads the request body before BeforeRequest fires. This type does not itself perform either resolution phase - that is the responsibility of the call sites introduced in later hardening-plan items, once there is a real per-session override path to resolve against - but it is deliberately shaped so a caller can hold one instance at headers-complete time (ResourceLimits's framing/header-shape fields, which are never overridable) and, if a session lowers a body or streaming budget in BeforeRequest, build a second instance via Create(ProxyResourceLimits, ProxyTimeoutOptions) that shares the same Timeouts but substitutes a ProxyResourceLimits reflecting the override, rather than mutating the first instance in place. Enums PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. PolicyMode How a resource-bound policy family is applied once its numeric limit is breached, per the plan's \"Rollout, profiles and documentation\" section. Not every family supports every mode. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no Observe mode at all: an ambiguous chunk size or a conflicting length can only be forwarded (a desync) or rejected, so those call sites are unconditionally enforced and never consult a PolicyMode. ProxyPolicyModes exists for the families where a safe \"detect but let it through\" middle ground is actually possible. ProxyProfile The three shipped profiles, per the plan's \"Rollout, profiles and documentation\" section. Selecting a profile via Profile applies its ProxyProfileSettings atomically to resource limits, policy modes, TLS protocols, private-network blocking, admission caps, and deadline-second properties, so a caller can never observe a half-applied profile." + "summary": "Namespace Titanium.Web.Proxy.Options Classes ProxyPolicyModes Immutable snapshot of the PolicyMode selected for each PolicyFamily, plus the one deliberately-separate AllowAmbiguousFraming escape hatch. Read live by enforcement call sites through PolicyModes, which the plan's rollout section requires to be a runtime switch: replacing the whole snapshot (see PolicyModes's setter) rather than mutating a field lets an operator drop every family to Observe without redeploying, while every in-flight request that already read the previous snapshot keeps behaving consistently with whichever snapshot it observed. AllowAmbiguousFraming is not a PolicyFamily member and has no corresponding PolicyMode: framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no safe \"detect but let it through\" middle ground, so this is a single named, binary, off-by-default flag that relays malformed framing instead of rejecting it - useful only for security research that needs to observe how a client or origin reacts to smuggling-shaped input through the proxy. No profile sets it: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) never accepts it as a parameter, and the only way to turn it on is the explicit WithAllowAmbiguousFramingEnabled() call, so enabling it can never be a side effect of selecting a profile. ProxyProfileSettings The full bundle of settings one ProxyProfile applies to a ProxyServer as a single atomic assignment via Profile. Deliberately a plain data bundle, not a type with behavior: applying it is ProxyServer's job (see Profile's setter), so this type has no back-reference and no side effects of its own, per the plan's \"Constraints on the policy layer\" section. ProxyResourceLimits Immutable, validated snapshot of the resource bounds a peer can make the proxy allocate: header shape, body/decompression budgets, concurrency and abuse-rate ceilings, and pool / certificate-cache sizing. Constructed only through Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?), which validates every field up front, so an invalid limit is a construction-time exception rather than a runtime surprise discovered mid-connection. A limit that can legitimately be turned off is typed as nullable with null meaning \"disabled\" - an explicit state - rather than overloading 0 or a negative number to mean the same thing. Limits that must always be enforced because disabling them would leave the proxy itself exploitable (the concurrent-stream cap, the open-header-block frame bound) are non-nullable and always validated to be strictly positive. This type has no back-reference to ProxyServer and no mutable state after construction: it is meant to be handed down to subsystems by value, not looked up through a service locator or an ambient static, so the dependency graph among consumers stays acyclic per the plan's \"Constraints on the policy layer\" section. ProxyTimeoutOptions Immutable, validated snapshot of every deadline the proxy enforces across a request's lifetime, expressed consistently as TimeSpan rather than the mixture of \"seconds as int\" properties this replaces (ConnectionTimeOutSeconds, ConnectTimeOutSeconds, ...). A deadline that can be legitimately unbounded is nullable, with null meaning \"no deadline\" as an explicit state rather than Zero or a magic sentinel duration. This type only holds values; it does not decide which deadline fired first when several are composed for one request. That is the responsibility of the per-request deadline registry described in the plan's \"Deadline composition\" section, introduced alongside the header-parsing deadlines in a later item so it can be exercised by a real caller instead of landing as unused scaffolding. ResolvedSessionPolicy Read-only snapshot combining ProxyResourceLimits and ProxyTimeoutOptions into the single object H1/H2/H3/WebSocket subsystems are handed, so runtime mutation of either half cannot produce inconsistent enforcement partway through a request that started under a different combination of the two. Per the plan's \"Constraints on the policy layer\" section, this type is deliberately inert: no back-pointer to ProxyServer, no service-locator lookup, no mutable fields, no static ambient accessor. Subsystems receive it as a constructor argument or method parameter only, so the dependency graph among consumers stays acyclic - the opposite of how ProxyServer itself is reached today. Per the plan's \"Two-phase policy resolution\" section, a single resolution per connection is not correct: SessionEventArgs.MaxBufferedBodyBytes is contractually settable from BeforeRequest, and HTTP/3 already reads the request body before BeforeRequest fires. This type does not itself perform either resolution phase - that is the responsibility of the call sites introduced in later hardening-plan items, once there is a real per-session override path to resolve against - but it is deliberately shaped so a caller can hold one instance at headers-complete time (ResourceLimits's framing/header-shape fields, which are never overridable) and, if a session lowers a body or streaming budget in BeforeRequest, build a second instance via Create(ProxyResourceLimits, ProxyTimeoutOptions) that shares the same Timeouts but substitutes a ProxyResourceLimits reflecting the override, rather than mutating the first instance in place. Enums PolicyFamily The resource-bound policy families that support an PolicyMode other than Enforce, per the plan's rollout section. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution are deliberately not members of this enum: they are always enforced and never consult a mode, because there is no safe Observe action for an ambiguous or malformed message - see ProxyPolicyModes and AllowAmbiguousFraming for the one explicit, isolated escape hatch from that rule. PolicyMode How a resource-bound policy family is applied once its numeric limit is breached, per the plan's \"Rollout, profiles and documentation\" section. Not every family supports every mode. Framing, chunk parsing and Content-Length/Transfer-Encoding resolution have no Observe mode at all: an ambiguous chunk size or a conflicting length can only be forwarded (a desync) or rejected, so those call sites are unconditionally enforced and never consult a PolicyMode. ProxyPolicyModes exists for the families where a safe \"detect but let it through\" middle ground is actually possible. ProxyProfile The three shipped profiles, per the plan's \"Rollout, profiles and documentation\" section. Selecting a profile via Profile applies its ProxyProfileSettings atomically to resource limits, policy modes, TLS protocols, private-network blocking, admission caps, and deadline-second properties, so a caller can never observe a half-applied profile." }, "api/Titanium.Web.Proxy.ProxyLimits.html": { "href": "api/Titanium.Web.Proxy.ProxyLimits.html", diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml index 44a0dd7ec..f6be8c6d7 100644 --- a/docs/xrefmap.yml +++ b/docs/xrefmap.yml @@ -8164,12 +8164,12 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.AllowAmbiguousFraming nameWithType: ProxyPolicyModes.AllowAmbiguousFraming -- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) - name: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) - href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_ - commentId: M:Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) - fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode) - nameWithType: ProxyPolicyModes.Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) +- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) + name: Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) + href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_Titanium_Web_Proxy_Options_PolicyMode_ + commentId: M:Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode,Titanium.Web.Proxy.Options.PolicyMode) + fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create(Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode, Titanium.Web.Proxy.Options.PolicyMode) + nameWithType: ProxyPolicyModes.Create(PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode, PolicyMode) - uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.Create* name: Create href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_Create_ From 43f4fb64bffd0c070f6569555c1ca27d9dbe638a Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 20:45:08 +0530 Subject: [PATCH 11/11] chore(release): bump product version to 7.0.18 for the next beta cut. --- src/Titanium.Cli/Titanium.Cli.csproj | 2 +- src/Titanium.Inspector/Titanium.Inspector.csproj | 2 +- src/Titanium.Plus/Titanium.Plus.csproj | 2 +- .../Titanium.Web.Proxy.Abstractions.csproj | 2 +- .../Titanium.Web.Proxy.Configuration.csproj | 2 +- src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs | 4 ++-- src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj | 2 +- tests/Titanium.E2E.Tests/CliCommandE2ETests.cs | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Titanium.Cli/Titanium.Cli.csproj b/src/Titanium.Cli/Titanium.Cli.csproj index a5b7fce27..2b7d38d57 100644 --- a/src/Titanium.Cli/Titanium.Cli.csproj +++ b/src/Titanium.Cli/Titanium.Cli.csproj @@ -7,7 +7,7 @@ latest enable false - 7.0.17 + 7.0.18 Jehonathan Thomas Titanium Web Proxy CLI (titanium / twp). MIT diff --git a/src/Titanium.Inspector/Titanium.Inspector.csproj b/src/Titanium.Inspector/Titanium.Inspector.csproj index f66d463cd..1fda90cf1 100644 --- a/src/Titanium.Inspector/Titanium.Inspector.csproj +++ b/src/Titanium.Inspector/Titanium.Inspector.csproj @@ -8,7 +8,7 @@ enable true false - 7.0.17 + 7.0.18 Jehonathan Thomas Titanium Inspector desktop traffic debugger (PolyForm Noncommercial). LICENSE diff --git a/src/Titanium.Plus/Titanium.Plus.csproj b/src/Titanium.Plus/Titanium.Plus.csproj index 95cfc2ffa..7f6796f05 100644 --- a/src/Titanium.Plus/Titanium.Plus.csproj +++ b/src/Titanium.Plus/Titanium.Plus.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.17 + 7.0.18 Jehonathan Thomas Titanium Web Proxy Plus advanced features plugin (PolyForm Noncommercial). LICENSE diff --git a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj index 40ada07a3..7612d5453 100644 --- a/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj +++ b/src/Titanium.Web.Proxy.Abstractions/Titanium.Web.Proxy.Abstractions.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.17 + 7.0.18 Jehonathan Thomas Shared contracts for Titanium Web Proxy routing, clusters, middleware, and plugins. MIT diff --git a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj index e58157f06..133a243c3 100644 --- a/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj +++ b/src/Titanium.Web.Proxy.Configuration/Titanium.Web.Proxy.Configuration.csproj @@ -7,7 +7,7 @@ enable True StrongNameKey.snk - 7.0.17 + 7.0.18 Jehonathan Thomas YAML/JSON configuration binding for Titanium Web Proxy CLI and reverse-proxy documents. MIT diff --git a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs index f63b23397..b62d50043 100644 --- a/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs +++ b/src/Titanium.Web.Proxy/Properties/AssemblyInfo.cs @@ -77,5 +77,5 @@ // file-properties version disagreed with the package it was published in. Keep both of the values // below equal to (as Major.Minor.Build.0) whenever that property changes. -[assembly: AssemblyVersion("7.0.17.0")] -[assembly: AssemblyFileVersion("7.0.17.0")] +[assembly: AssemblyVersion("7.0.18.0")] +[assembly: AssemblyFileVersion("7.0.18.0")] diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index 072e45039..9a2cce5e1 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj @@ -13,7 +13,7 @@ - 7.0.17 + 7.0.18