From 71db78825a9c9ea066accc709721e09ec9398d7b Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Mon, 5 Oct 2026 22:47:03 +0530 Subject: [PATCH 01/19] chore(packaging): point the Homebrew formula at v7.0.18-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 c3528f8d5..523e0a069 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" + version "7.0.18-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 "720e7a47b6dd107b9939560312834e649cd0ee6fc4f8957f1d50be496e1551ab" + sha256 "c3a4b345bf8e387578fc59bbfff54a61b906e839988198f32e57a893d9f1d4a2" end on_intel do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-x64.zip" - sha256 "d1bf49f46877ab755da9d818298943824d7e4cd96c73fcd49689431484b30ac9" + sha256 "53ec39884176ac1458c5ba34bc422ae98938a86cacaae16075e54ebc0c8bc437" end end From 6e9aeba3da603d97e43ebbb95a83cfc17c471651 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 04:38:43 +0530 Subject: [PATCH 02/19] chore(packaging): point the Homebrew formula at v7.0.18. --- 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 523e0a069..d6e856aa9 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.18-beta" + version "7.0.18" 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 "c3a4b345bf8e387578fc59bbfff54a61b906e839988198f32e57a893d9f1d4a2" + sha256 "8375729ba30c78ffa78e205dc1eb32b5bbc8b6ee7b8c61c5c3f0614dd42f592b" end on_intel do url "https://github.com/justcoding121/titanium-web-proxy/releases/download/v#{version}/Titanium.Cli-osx-x64.zip" - sha256 "53ec39884176ac1458c5ba34bc422ae98938a86cacaae16075e54ebc0c8bc437" + sha256 "56f23954bbf9ba424d3082e3875b3b5c7e5e1658b20368a2ddf9dc27067e3f87" end end From eb843a41320319aed9253f19676fd71740e537c8 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 10:00:23 +0530 Subject: [PATCH 03/19] feat(inspector): clearer status bar session count wording (showing X of Y, since HH:mm) --- src/Titanium.Inspector/Services/SessionSearch.cs | 11 ++++++++--- .../Titanium.Inspector.Tests/SessionPipelineTests.cs | 6 +++--- .../SessionSearchAndArchiveTests.cs | 4 ++-- website/docs/inspector.md | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Titanium.Inspector/Services/SessionSearch.cs b/src/Titanium.Inspector/Services/SessionSearch.cs index 4d2dcfac1..85224fce3 100644 --- a/src/Titanium.Inspector/Services/SessionSearch.cs +++ b/src/Titanium.Inspector/Services/SessionSearch.cs @@ -195,15 +195,20 @@ public static string BuildSessionCountText( { var searching = !string.IsNullOrWhiteSpace(searchQuery); var text = searching - ? $"Sessions: {visibleCount} / {totalCount}" - : $"Sessions: {totalCount}"; + ? $"Sessions: showing {visibleCount:N0} of {totalCount:N0}" + : $"Sessions: {totalCount:N0}"; + + var emptySearch = searching && visibleCount == 0 && totalCount > 0; if (retentionEvictedTotal > 0 && oldestStartedUtc is { } oldest) { + // Retention limit dropped older sessions: the list only covers "since ". text += $" · since {oldest.ToLocalTime():HH:mm}"; + if (!emptySearch) + text += " (older removed by retention limit)"; } - if (searching && visibleCount == 0 && totalCount > 0) + if (emptySearch) text += FormatEmptySearchRetentionHint(retentionEvictedTotal); return text; diff --git a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs index 1f1dd728b..2533d9987 100644 --- a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs @@ -96,7 +96,7 @@ public async Task SessionAdded_UpdatesSessionCount_WithoutOverwritingStatusText( Assert.IsTrue(vm.HideTunnelsFilter); vm.ErrorsOnlyFilter = true; Assert.IsTrue(vm.SearchQuery.Contains("is:error", StringComparison.Ordinal)); - Assert.AreEqual("Sessions: 0 / 1", vm.SessionCountText); + Assert.AreEqual("Sessions: showing 0 of 1", vm.SessionCountText); vm.ClearFiltersCommand.Execute(null); await Task.Delay(50); @@ -180,7 +180,7 @@ public async Task SessionCountText_BodySearchFindsSpilledBodies() vm.SearchQuery = "body:alpha-body"; Assert.AreEqual(1, vm.Sessions.Count); Assert.AreEqual(1, vm.Sessions[0].Id); - StringAssert.Contains(vm.SessionCountText, "Sessions: 1 / 2"); + StringAssert.Contains(vm.SessionCountText, "Sessions: showing 1 of 2"); Assert.IsFalse(vm.SessionCountText.Contains("in-memory only", StringComparison.Ordinal)); } finally @@ -235,7 +235,7 @@ public void SessionCountText_RetentionEviction_SurfacesCaptureWindowOnZeroMatch( Assert.IsNull(registry.TryGet(1)); vm.SearchQuery = "host:does-not-exist"; - StringAssert.Contains(vm.SessionCountText, "Sessions: 0 / 2"); + StringAssert.Contains(vm.SessionCountText, "Sessions: showing 0 of 2"); StringAssert.Contains(vm.SessionCountText, "since "); StringAssert.Contains(vm.SessionCountText, "no matches in current list"); StringAssert.Contains(vm.SessionCountText, "removed by retention"); diff --git a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs index 914a29c27..608049872 100644 --- a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs @@ -399,7 +399,7 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "body:needle", retentionEvictedTotal: 0, oldestStartedUtc: null); - Assert.AreEqual("Sessions: 0 / 10", withBody); + Assert.AreEqual("Sessions: showing 0 of 10", withBody); var oldest = new DateTimeOffset(2026, 9, 2, 19, 2, 0, TimeSpan.Zero); var withRetention = SessionSearch.BuildSessionCountText( @@ -408,7 +408,7 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "host:missing", retentionEvictedTotal: 120, oldestStartedUtc: oldest); - StringAssert.Contains(withRetention, "Sessions: 0 / 50"); + StringAssert.Contains(withRetention, "Sessions: showing 0 of 50"); StringAssert.Contains(withRetention, "since "); StringAssert.Contains(withRetention, "no matches in current list · 120 removed by retention"); } diff --git a/website/docs/inspector.md b/website/docs/inspector.md index 6b3861fba..9cf4e58d6 100644 --- a/website/docs/inspector.md +++ b/website/docs/inspector.md @@ -92,7 +92,7 @@ Use **Tools → Composer / Breakpoints / AutoResponder / Scripts…** to open th **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. +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 **Sessions: showing X of Y** when a search or quick filter is active (X matching, Y currently kept). If the session retention limit has removed older sessions, it adds **since HH:mm** — the start time of the oldest session still kept. **Network throttle:** use the toolbar **Throttle** combo (`None`, `Slow 3G`, `Fast 3G`, `LTE`) to add latency and bandwidth shaping on body writes / WebSocket frames during capture. Off by default (`None`); the hot path skips delay work when no profile is enabled. From d9f0d78704748f4478925972960010bd17730dd5 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 10:36:23 +0530 Subject: [PATCH 04/19] =?UTF-8?q?=EF=BB=BFfeat(limits):=20name=20the=20key?= =?UTF-8?q?=20that=20fixes=20a=20rejected=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every hard cap that can reject legitimate traffic is configurable, and an Enforce failure logs at Error with the limit, the observed value, and the CLI key. Origin HTTP/2 headers up to 64 KiB stay intact, and Balanced HTTP/1 header count and size are enforced. --- src/Titanium.Cli/Config/RunCommand.cs | 12 + .../Config/ServerConfigApplier.cs | 66 +- src/Titanium.Cli/Config/TestCommand.cs | 11 +- src/Titanium.Cli/Parsers/ConfigLoader.cs | 7 +- .../PublicAPI.Unshipped.txt | 12 + .../Routing/RouteConfig.cs | 14 + .../Models/ServerConfig.cs | 32 + .../PublicAPI.Unshipped.txt | 37 + .../TwpConfigSchema.cs | 102 +++ .../TwpConfigValidator.cs | 115 +++ .../UnknownConfigKeyScanner.cs | 149 ++++ .../twp.schema.json | 747 ++++++++++++++++++ .../EventArguments/SessionEventArgs.cs | 5 +- .../Handlers/ExplicitClientHandler.cs | 22 +- .../Handlers/Http11ToHttp2BridgeHandler.cs | 23 +- .../Handlers/Http2ToHttp11BridgeHandler.cs | 11 +- .../Handlers/RequestHandler.cs | 33 +- .../Handlers/ResponseHandler.cs | 14 + .../Handlers/WebSocketHandler.cs | 3 +- .../Handlers/WinAuthHandler.cs | 27 +- .../Http/ChunkedTrailerHelper.cs | 22 +- .../Http/HeaderLimitsExceededException.cs | 27 + src/Titanium.Web.Proxy/Http/HeaderParser.cs | 73 +- .../Http2/Http2ConnectionState.cs | 20 +- .../Http2/Http2DeferredOutboundData.cs | 10 +- .../Http2/Http2FlowController.cs | 16 +- .../Http2/Http2Helper.Copy.Headers.cs | 2 + .../Http2/Http2Helper.Copy.cs | 9 +- src/Titanium.Web.Proxy/Http2/Http2Helper.cs | 4 +- .../Http2/Http2OriginConnection.cs | 41 +- .../Http2/Http2OriginRelayPool.cs | 15 +- src/Titanium.Web.Proxy/Http3/Http3Frame.cs | 122 ++- .../Http3/Http3OriginBridge.Http2.cs | 4 +- .../Http3/Http3OriginBridge.Quic.cs | 29 +- .../Http3/Http3RequestStream.cs | 42 +- .../Logging/LimitCatalog.cs | 192 +++++ .../Logging/ProxyDiagnostics.cs | 4 + src/Titanium.Web.Proxy/Logging/ProxyLog.cs | 72 +- .../Network/HttpWebClient.cs | 3 +- .../Network/Streams/HttpStream.Body.cs | 12 +- .../Network/Streams/HttpStream.cs | 4 + .../Network/Streams/LimitedStream.cs | 9 +- .../TcpConnection/TcpConnectionFactory.cs | 11 +- .../WinAuth/Security/WinAuthEndPoint.cs | 8 +- .../Network/WinAuth/WinAuthHandler.cs | 22 +- .../Options/ProxyPolicyModes.cs | 7 + .../Options/ProxyResourceLimits.cs | 191 ++++- .../ProxyServer.Lifecycle.cs | 18 + src/Titanium.Web.Proxy/ProxyServer.cs | 6 + .../PublicAPI.Unshipped.txt | 20 + .../Routing/ReverseProxySessionDispatch.cs | 1 + .../Routing/StreamDestinationDispatch.cs | 20 + .../WebSocket/WebSocketInterceptRelay.cs | 3 + .../ServerConfigApplierTests.cs | 74 ++ .../UnknownKeyStrictTests.cs | 31 + .../EscapeHatchConfigTests.cs | 100 +++ .../HeaderLimitEnforcementTests.cs | 129 +++ .../HeaderParserTests.cs | 50 ++ .../HpackTruncationConsistencyTests.cs | 56 ++ .../Http2BodyWriterAndOriginSettingsTests.cs | 4 +- .../Http2OriginActiveStreamCountTests.cs | 4 +- .../Http3FrameStreamingTests.cs | 132 ++++ .../LimitCatalogConsistencyTests.cs | 41 + .../LimitExceededTests.cs | 135 ++++ .../LoggingTests.cs | 6 +- .../RouteLimitsDispatchTests.cs | 92 +++ .../SonarNewCodeCoverageTests.cs | 4 +- .../WebSocketDecoderTests.cs | 19 + website/.vitepress/config.mts | 2 + website/docs/cli.md | 2 + website/docs/configuration.md | 35 +- website/docs/getting-started.md | 2 +- website/docs/limits.md | 100 +++ website/docs/troubleshooting.md | 36 + website/docs/websocket-large-messages.md | 21 + wiki/Home.md | 4 +- wiki/Limits-and-Escape-Hatches.md | 45 ++ wiki/Protocol-Support.md | 2 +- wiki/Troubleshooting.md | 26 + 79 files changed, 3465 insertions(+), 168 deletions(-) create mode 100644 src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs create mode 100644 src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs create mode 100644 src/Titanium.Web.Proxy.Configuration/twp.schema.json create mode 100644 src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs create mode 100644 src/Titanium.Web.Proxy/Logging/LimitCatalog.cs create mode 100644 tests/Titanium.Cli.Tests/UnknownKeyStrictTests.cs create mode 100644 tests/Titanium.Web.Proxy.Configuration.Tests/EscapeHatchConfigTests.cs create mode 100644 tests/Titanium.Web.Proxy.IntegrationTests/HeaderLimitEnforcementTests.cs create mode 100644 tests/Titanium.Web.Proxy.UnitTests/HpackTruncationConsistencyTests.cs create mode 100644 tests/Titanium.Web.Proxy.UnitTests/Http3FrameStreamingTests.cs create mode 100644 tests/Titanium.Web.Proxy.UnitTests/LimitCatalogConsistencyTests.cs create mode 100644 tests/Titanium.Web.Proxy.UnitTests/LimitExceededTests.cs create mode 100644 tests/Titanium.Web.Proxy.UnitTests/RouteLimitsDispatchTests.cs create mode 100644 website/docs/limits.md create mode 100644 website/docs/troubleshooting.md create mode 100644 website/docs/websocket-large-messages.md create mode 100644 wiki/Limits-and-Escape-Hatches.md create mode 100644 wiki/Troubleshooting.md diff --git a/src/Titanium.Cli/Config/RunCommand.cs b/src/Titanium.Cli/Config/RunCommand.cs index ad1ab5e20..57fa29ed1 100644 --- a/src/Titanium.Cli/Config/RunCommand.cs +++ b/src/Titanium.Cli/Config/RunCommand.cs @@ -74,6 +74,8 @@ internal static async Task ExecuteCoreAsync( // NOSONAR S3776 -- CLI run li return 1; } + WriteConfigWarnings(loaded); + // When launched as a service, resolve relative paths against the config directory // (SCM / systemd / launchd cwd is typically System32 or /). var configDir = Path.GetDirectoryName(Path.GetFullPath(configPath)); @@ -292,6 +294,8 @@ internal static async Task ReloadConfigAsync( // NOSONAR S107 -- Reload keeps es throw new InvalidOperationException(string.Join("; ", errors)); } + WriteConfigWarnings(loaded); + ServerConfigApplier.Apply(proxy, loaded.Config.Server); if (loaded.Config.Clusters.Count > 0) { @@ -1041,4 +1045,12 @@ config.Plus.Options is not null && return config.Routes.Any(r => r.Transforms is { Count: > 0 }); } + + private static void WriteConfigWarnings(LoadedConfig loaded) + { + foreach (var warning in loaded.UnknownKeys) + AsyncConsole.WriteError("warning: " + warning); + foreach (var warning in TwpConfigValidator.CollectWarnings(loaded.Config)) + AsyncConsole.WriteError("warning: " + warning); + } } diff --git a/src/Titanium.Cli/Config/ServerConfigApplier.cs b/src/Titanium.Cli/Config/ServerConfigApplier.cs index 599840515..2e229a420 100644 --- a/src/Titanium.Cli/Config/ServerConfigApplier.cs +++ b/src/Titanium.Cli/Config/ServerConfigApplier.cs @@ -38,6 +38,21 @@ public static void Apply(ProxyServer proxy, ServerConfig? server) ApplyUpstream(proxy, server.Upstream); ApplyCertificateManager(proxy, server.CertificateManager); ApplyDecryptExclusions(proxy, server); + ApplyDecryptFailureBypass(proxy, server); + if (!string.IsNullOrWhiteSpace(server.ProxyAuthenticationRealm)) + proxy.ProxyAuthenticationRealm = server.ProxyAuthenticationRealm; + } + + private static void ApplyDecryptFailureBypass(ProxyServer proxy, ServerConfig server) + { + if (server.EnableDecryptFailureBypass is bool enabled) + proxy.EnableDecryptFailureBypass = enabled; + if (server.DecryptFailureBypassTtlMinutes is int minutes && minutes > 0) + proxy.DecryptFailureBypassTtl = TimeSpan.FromMinutes(minutes); + if (server.DecryptFailureBypassMaxEntries is int entries && entries > 0) + proxy.DecryptFailureBypassMaxEntries = entries; + if (server.DecryptFailureBypassThreshold is int threshold && threshold > 0) + proxy.DecryptFailureBypassThreshold = threshold; } /// @@ -312,6 +327,22 @@ private static void ApplyLimits(ProxyServer proxy, LimitsConfig? limits) built = built.WithCertificateCacheBounds( built.MaxCertificateCacheEntries, limits.MaxCertificateDiskCacheEntries ?? current.MaxCertificateDiskCacheEntries); + built = built.WithMaxHttp3FramePayloadBytes( + limits.MaxHttp3FramePayloadBytes ?? current.MaxHttp3FramePayloadBytes); + built = built.WithMaxDeferredOutboundBytesPerStream( + limits.MaxDeferredOutboundBytesPerStream ?? current.MaxDeferredOutboundBytesPerStream); + built = built.WithTrailerHeaderBounds( + limits.MaxTrailerHeaderCount ?? current.MaxTrailerHeaderCount, + limits.MaxTrailerHeaderBlockBytes ?? current.MaxTrailerHeaderBlockBytes); + built = built.WithMaxHttp2CompressedHeaderBlockBytes( + limits.MaxHttp2CompressedHeaderBlockBytes ?? current.MaxHttp2CompressedHeaderBlockBytes); + built = built.WithMaxInterimResponses(limits.MaxInterimResponses ?? current.MaxInterimResponses); + built = built.WithAuthenticationBounds( + limits.MaxAuthChallengeRounds ?? current.MaxAuthChallengeRounds, + limits.MaxUpstreamProxyAuthenticationAttempts ?? current.MaxUpstreamProxyAuthenticationAttempts, + limits.MaxWinAuthTokenBytes ?? current.MaxWinAuthTokenBytes); + built = built.WithHttp2WindowUpdateTimeoutSeconds( + limits.Http2WindowUpdateTimeoutSeconds ?? current.Http2WindowUpdateTimeoutSeconds); proxy.ResourceLimits = built; @@ -338,22 +369,25 @@ private static void ApplyPolicyModes(ProxyServer proxy, PolicyModesConfig? polic return; } - var current = proxy.PolicyModes; - var modes = ProxyPolicyModes.Create( - ParsePolicyMode(policy.BodyBudget, current[PolicyFamily.BodyBudget]), - ParsePolicyMode(policy.DecompressionRatio, current[PolicyFamily.DecompressionRatio]), - ParsePolicyMode(policy.HeaderLimits, current[PolicyFamily.HeaderLimits]), - ParsePolicyMode(policy.AdmissionControl, current[PolicyFamily.AdmissionControl]), - ParsePolicyMode(policy.Http2AbuseBudget, current[PolicyFamily.Http2AbuseBudget]), - ParsePolicyMode(policy.Http2RelayValidation, current[PolicyFamily.Http2RelayValidation])) - .With( - PolicyFamily.WebSocketFrameBudget, - ParsePolicyMode(policy.WebSocketFrameBudget, current[PolicyFamily.WebSocketFrameBudget])); - - if (policy.AllowAmbiguousFraming == true) - { - modes = modes.WithAllowAmbiguousFramingEnabled(); - } + var modes = proxy.PolicyModes; + if (!string.IsNullOrWhiteSpace(policy.BodyBudget)) + modes = modes.With(PolicyFamily.BodyBudget, ParsePolicyMode(policy.BodyBudget, modes[PolicyFamily.BodyBudget])); + if (!string.IsNullOrWhiteSpace(policy.DecompressionRatio)) + modes = modes.With(PolicyFamily.DecompressionRatio, ParsePolicyMode(policy.DecompressionRatio, modes[PolicyFamily.DecompressionRatio])); + if (!string.IsNullOrWhiteSpace(policy.HeaderLimits)) + modes = modes.With(PolicyFamily.HeaderLimits, ParsePolicyMode(policy.HeaderLimits, modes[PolicyFamily.HeaderLimits])); + if (!string.IsNullOrWhiteSpace(policy.AdmissionControl)) + modes = modes.With(PolicyFamily.AdmissionControl, ParsePolicyMode(policy.AdmissionControl, modes[PolicyFamily.AdmissionControl])); + if (!string.IsNullOrWhiteSpace(policy.Http2AbuseBudget)) + modes = modes.With(PolicyFamily.Http2AbuseBudget, ParsePolicyMode(policy.Http2AbuseBudget, modes[PolicyFamily.Http2AbuseBudget])); + if (!string.IsNullOrWhiteSpace(policy.Http2RelayValidation)) + modes = modes.With(PolicyFamily.Http2RelayValidation, ParsePolicyMode(policy.Http2RelayValidation, modes[PolicyFamily.Http2RelayValidation])); + if (!string.IsNullOrWhiteSpace(policy.WebSocketFrameBudget)) + modes = modes.With(PolicyFamily.WebSocketFrameBudget, ParsePolicyMode(policy.WebSocketFrameBudget, modes[PolicyFamily.WebSocketFrameBudget])); + if (!string.IsNullOrWhiteSpace(policy.Http1ReplaySafety)) + modes = modes.With(PolicyFamily.Http1ReplaySafety, ParsePolicyMode(policy.Http1ReplaySafety, modes[PolicyFamily.Http1ReplaySafety])); + if (policy.AllowAmbiguousFraming is bool framing) + modes = modes.WithAllowAmbiguousFraming(framing); proxy.PolicyModes = modes; } diff --git a/src/Titanium.Cli/Config/TestCommand.cs b/src/Titanium.Cli/Config/TestCommand.cs index 4da47f511..460528556 100644 --- a/src/Titanium.Cli/Config/TestCommand.cs +++ b/src/Titanium.Cli/Config/TestCommand.cs @@ -13,6 +13,7 @@ public static Task ExecuteAsync(string[] args) return Task.FromResult(PrintHelp()); } + var strict = args.Any(a => string.Equals(a, "--strict", StringComparison.OrdinalIgnoreCase)); var configPath = RunCommand.ParseConfigPath(args); var loaded = ConfigLoader.Load(configPath); var errors = TwpConfigValidator.Validate(loaded.Config); @@ -26,6 +27,13 @@ public static Task ExecuteAsync(string[] args) return Task.FromResult(1); } + foreach (var warning in loaded.UnknownKeys) + AsyncConsole.WriteError("warning: " + warning); + foreach (var warning in TwpConfigValidator.CollectWarnings(loaded.Config)) + AsyncConsole.WriteError("warning: " + warning); + if (strict && loaded.UnknownKeys.Count > 0) + return Task.FromResult(1); + var needsSession = RunCommand.ConfigNeedsSessionPath(loaded.Config); AsyncConsole.WriteLine($"Config OK: {configPath}"); AsyncConsole.WriteLine($"Routes: {loaded.Config.Routes.Count}, Clusters: {loaded.Config.Clusters.Count}, Listeners: {loaded.Config.Listeners.Count}"); @@ -37,9 +45,10 @@ public static Task ExecuteAsync(string[] args) internal static int PrintHelp() { AsyncConsole.WriteLine(""" - titanium test -c + titanium test -c [--strict] -c, --config Path to twp.yaml / .json / .twp / .conf (required). + --strict Exit 1 when the file contains unknown keys. Validates the config without opening listeners or serving traffic. """); diff --git a/src/Titanium.Cli/Parsers/ConfigLoader.cs b/src/Titanium.Cli/Parsers/ConfigLoader.cs index 278f52f3d..926f1ee07 100644 --- a/src/Titanium.Cli/Parsers/ConfigLoader.cs +++ b/src/Titanium.Cli/Parsers/ConfigLoader.cs @@ -10,6 +10,7 @@ internal sealed class LoadedConfig public required TwpConfig Config { get; init; } public required string Path { get; init; } public required string Dialect { get; init; } + public IReadOnlyList UnknownKeys { get; init; } = []; } internal static class ConfigLoader @@ -68,11 +69,15 @@ public static LoadedConfig Load(string path) } } + var text = File.ReadAllText(path); + var yaml = ext.Equals(".yaml", StringComparison.OrdinalIgnoreCase) || + ext.Equals(".yml", StringComparison.OrdinalIgnoreCase); return new LoadedConfig { Path = path, Dialect = "twp-native", - Config = TwpConfigLoader.LoadFile(path), + Config = yaml ? TwpConfigLoader.LoadYaml(text) : TwpConfigLoader.LoadJson(text), + UnknownKeys = UnknownConfigKeyScanner.Scan(text, yaml), }; } diff --git a/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt index 91d8e0cd5..915b7ef37 100644 --- a/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy.Abstractions/PublicAPI.Unshipped.txt @@ -261,3 +261,15 @@ Titanium.Web.Proxy.Abstractions.Routing.TransformRequestContext.ResponseHeadersT Titanium.Web.Proxy.Abstractions.Routing.TransformRequestContext.ResponseHeadersToSet.get -> System.Collections.Generic.Dictionary! Titanium.Web.Proxy.Abstractions.Routing.TransformRequestContext.TransformRequestContext() -> void virtual Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareDelegate.Invoke(Titanium.Web.Proxy.Abstractions.Middleware.ProxyMiddlewareContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Titanium.Web.Proxy.Abstractions.Routing.RouteConfig.Limits.get -> Titanium.Web.Proxy.Abstractions.Routing.RouteLimits? +Titanium.Web.Proxy.Abstractions.Routing.RouteConfig.Limits.init -> void +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.IdleTimeoutSeconds.get -> int? +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.IdleTimeoutSeconds.init -> void +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.MaxBufferedBodyBytes.get -> int? +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.MaxBufferedBodyBytes.init -> void +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.MaxWebSocketFramePayloadBytes.get -> int? +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.MaxWebSocketFramePayloadBytes.init -> void +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.RequestTimeoutSeconds.get -> int? +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.RequestTimeoutSeconds.init -> void +Titanium.Web.Proxy.Abstractions.Routing.RouteLimits.RouteLimits() -> void diff --git a/src/Titanium.Web.Proxy.Abstractions/Routing/RouteConfig.cs b/src/Titanium.Web.Proxy.Abstractions/Routing/RouteConfig.cs index 31bce4ffb..350aef162 100644 --- a/src/Titanium.Web.Proxy.Abstractions/Routing/RouteConfig.cs +++ b/src/Titanium.Web.Proxy.Abstractions/Routing/RouteConfig.cs @@ -27,6 +27,20 @@ public sealed class RouteConfig public required RouteMatch Match { get; init; } public int Order { get; init; } public IReadOnlyList? Transforms { get; init; } + + /// + /// Optional per-route overrides. Null leaves the server limits unchanged (one null check). + /// + public RouteLimits? Limits { get; init; } +} + +/// Per-route timeout and body/WebSocket caps applied to a matched session. +public sealed class RouteLimits +{ + public int? RequestTimeoutSeconds { get; init; } + public int? IdleTimeoutSeconds { get; init; } + public int? MaxBufferedBodyBytes { get; init; } + public int? MaxWebSocketFramePayloadBytes { get; init; } } /// Request/response rewrite descriptor. diff --git a/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs b/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs index 5a5561486..a6ea18357 100644 --- a/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs +++ b/src/Titanium.Web.Proxy.Configuration/Models/ServerConfig.cs @@ -25,6 +25,16 @@ public sealed class ServerConfig public bool? EnableWinAuth { get; set; } + public bool? EnableDecryptFailureBypass { get; set; } + + public int? DecryptFailureBypassTtlMinutes { get; set; } + + public int? DecryptFailureBypassMaxEntries { get; set; } + + public int? DecryptFailureBypassThreshold { get; set; } + + public string? ProxyAuthenticationRealm { get; set; } + /// PreserveClientVersion or NormalizeToHttp11. public string? OriginHttpVersionPolicy { get; set; } @@ -193,6 +203,26 @@ public sealed class LimitsConfig public int? MaxDecodedHeaderListBytes { get; set; } public int? MaxWebSocketFramePayloadBytes { get; set; } + + public long? MaxHttp3FramePayloadBytes { get; set; } + + public int? MaxDeferredOutboundBytesPerStream { get; set; } + + public int? MaxTrailerHeaderCount { get; set; } + + public int? MaxTrailerHeaderBlockBytes { get; set; } + + public int? MaxHttp2CompressedHeaderBlockBytes { get; set; } + + public int? MaxInterimResponses { get; set; } + + public int? MaxAuthChallengeRounds { get; set; } + + public int? MaxUpstreamProxyAuthenticationAttempts { get; set; } + + public int? MaxWinAuthTokenBytes { get; set; } + + public int? Http2WindowUpdateTimeoutSeconds { get; set; } } /// @@ -215,6 +245,8 @@ public sealed class PolicyModesConfig public string? WebSocketFrameBudget { get; set; } + public string? Http1ReplaySafety { 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 35ed7997e..0e801bb76 100644 --- a/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy.Configuration/PublicAPI.Unshipped.txt @@ -373,3 +373,40 @@ Titanium.Web.Proxy.Configuration.Parsers.JsonReverseProxyDocument Titanium.Web.Proxy.Configuration.Parsers.SiteFileReader Titanium.Web.Proxy.Configuration.TwpConfigLoader Titanium.Web.Proxy.Configuration.TwpConfigValidator +static Titanium.Web.Proxy.Configuration.TwpConfigSchema.Generate() -> string! +Titanium.Web.Proxy.Configuration.TwpConfigSchema +static Titanium.Web.Proxy.Configuration.TwpConfigValidator.CollectWarnings(Titanium.Web.Proxy.Configuration.Models.TwpConfig! config) -> System.Collections.Generic.IReadOnlyList! +static Titanium.Web.Proxy.Configuration.UnknownConfigKeyScanner.Scan(string! text, bool yaml) -> System.Collections.Generic.IReadOnlyList! +Titanium.Web.Proxy.Configuration.UnknownConfigKeyScanner +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.Http2WindowUpdateTimeoutSeconds.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.Http2WindowUpdateTimeoutSeconds.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxAuthChallengeRounds.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxAuthChallengeRounds.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxDeferredOutboundBytesPerStream.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxDeferredOutboundBytesPerStream.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxHttp2CompressedHeaderBlockBytes.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxHttp2CompressedHeaderBlockBytes.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxHttp3FramePayloadBytes.get -> long? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxHttp3FramePayloadBytes.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxInterimResponses.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxInterimResponses.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxTrailerHeaderBlockBytes.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxTrailerHeaderBlockBytes.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxTrailerHeaderCount.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxTrailerHeaderCount.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxUpstreamProxyAuthenticationAttempts.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxUpstreamProxyAuthenticationAttempts.set -> void +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxWinAuthTokenBytes.get -> int? +Titanium.Web.Proxy.Configuration.Models.LimitsConfig.MaxWinAuthTokenBytes.set -> void +Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http1ReplaySafety.get -> string? +Titanium.Web.Proxy.Configuration.Models.PolicyModesConfig.Http1ReplaySafety.set -> void +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassMaxEntries.get -> int? +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassMaxEntries.set -> void +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassThreshold.get -> int? +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassThreshold.set -> void +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassTtlMinutes.get -> int? +Titanium.Web.Proxy.Configuration.Models.ServerConfig.DecryptFailureBypassTtlMinutes.set -> void +Titanium.Web.Proxy.Configuration.Models.ServerConfig.EnableDecryptFailureBypass.get -> bool? +Titanium.Web.Proxy.Configuration.Models.ServerConfig.EnableDecryptFailureBypass.set -> void +Titanium.Web.Proxy.Configuration.Models.ServerConfig.ProxyAuthenticationRealm.get -> string? +Titanium.Web.Proxy.Configuration.Models.ServerConfig.ProxyAuthenticationRealm.set -> void diff --git a/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs b/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs new file mode 100644 index 000000000..a22410cdf --- /dev/null +++ b/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs @@ -0,0 +1,102 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Titanium.Web.Proxy.Configuration; + +/// +/// JSON Schema for a native twp.yaml / twp.json document, reflected from the config model. +/// +public static class TwpConfigSchema +{ + public static string Generate() + { + var root = new JsonObject + { + ["$schema"] = "https://json-schema.org/draft/2020-12/schema", + ["$id"] = "https://titaniumproxy.com/twp.schema.json", + ["title"] = "Titanium Web Proxy configuration", + ["type"] = "object", + ["additionalProperties"] = false, + ["properties"] = PropertiesOf(typeof(Models.TwpConfig), []), + }; + return root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }) + Environment.NewLine; + } + + private static JsonObject PropertiesOf(Type type, HashSet stack) + { + var props = new JsonObject(); + foreach (var property in type.GetProperties().OrderBy(p => p.Name, StringComparer.Ordinal)) + { + if (property.GetMethod is null || property.GetIndexParameters().Length > 0) + continue; + props[JsonNamingPolicy.CamelCase.ConvertName(property.Name)] = SchemaFor(property.PropertyType, stack); + } + + return props; + } + + private static JsonNode SchemaFor(Type type, HashSet stack) + { + type = Nullable.GetUnderlyingType(type) ?? type; + if (type == typeof(string)) + return new JsonObject { ["type"] = "string" }; + if (type == typeof(bool)) + return new JsonObject { ["type"] = "boolean" }; + if (type == typeof(int) || type == typeof(long) || type == typeof(short)) + return new JsonObject { ["type"] = "integer" }; + if (type == typeof(double) || type == typeof(float) || type == typeof(decimal)) + return new JsonObject { ["type"] = "number" }; + if (type.IsEnum) + return new JsonObject { ["type"] = "string" }; + + if (IsDictionary(type)) + return new JsonObject + { + ["type"] = "object", + ["additionalProperties"] = new JsonObject { ["type"] = "string" }, + }; + + var item = ItemType(type); + if (item is not null) + return new JsonObject { ["type"] = "array", ["items"] = SchemaFor(item, stack) }; + + if (!type.IsClass) + return new JsonObject { ["type"] = "string" }; + + if (!stack.Add(type)) + return new JsonObject { ["type"] = "object" }; + + try + { + return new JsonObject + { + ["type"] = "object", + ["additionalProperties"] = false, + ["properties"] = PropertiesOf(type, stack), + }; + } + finally + { + stack.Remove(type); + } + } + + private static bool IsDictionary(Type type) => + type.IsGenericType && ( + type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(Dictionary<,>)); + + private static Type? ItemType(Type type) + { + if (type.IsArray) + return type.GetElementType(); + if (!type.IsGenericType) + return null; + var def = type.GetGenericTypeDefinition(); + if (def == typeof(IList<>) || def == typeof(List<>) || def == typeof(IReadOnlyList<>) || + def == typeof(IEnumerable<>) || def == typeof(ICollection<>)) + return type.GetGenericArguments()[0]; + return null; + } +} diff --git a/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs b/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs index 40bef3079..aef5ef68c 100644 --- a/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs +++ b/src/Titanium.Web.Proxy.Configuration/TwpConfigValidator.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Security.Authentication; using Titanium.Web.Proxy.Abstractions.Clusters; using Titanium.Web.Proxy.Abstractions.Routing; using Titanium.Web.Proxy.Configuration.Models; @@ -84,6 +86,17 @@ private static void ValidateRoutes( { errors.Add($"Route '{route.Id}' is missing Match."); } + + var limits = route.Limits; + if (limits is null) + { + continue; + } + + RequirePositiveIfSet(limits.RequestTimeoutSeconds, $"routes.{route.Id}.limits.requestTimeoutSeconds", errors); + RequirePositiveIfSet(limits.IdleTimeoutSeconds, $"routes.{route.Id}.limits.idleTimeoutSeconds", errors); + RequirePositiveIfSet(limits.MaxBufferedBodyBytes, $"routes.{route.Id}.limits.maxBufferedBodyBytes", errors); + RequirePositiveIfSet(limits.MaxWebSocketFramePayloadBytes, $"routes.{route.Id}.limits.maxWebSocketFramePayloadBytes", errors); } } @@ -149,6 +162,17 @@ private static void ValidateServer(ServerConfig? server, List errors) errors.Add($"server.checkCertificateRevocation '{server.CheckCertificateRevocation}' is unknown."); } + if (!string.IsNullOrWhiteSpace(server.DnsServerEndPoint) && !TryParseIpEndPoint(server.DnsServerEndPoint)) + errors.Add($"server.dnsServerEndPoint '{server.DnsServerEndPoint}' is not a host:port address."); + + if (server.DecryptFailureBypassTtlMinutes is <= 0) + errors.Add("server.decryptFailureBypassTtlMinutes must be positive."); + if (server.DecryptFailureBypassMaxEntries is <= 0) + errors.Add("server.decryptFailureBypassMaxEntries must be positive."); + if (server.DecryptFailureBypassThreshold is <= 0) + errors.Add("server.decryptFailureBypassThreshold must be positive."); + + ValidateTls(server.Tls, errors); ValidateTimeouts(server.Timeouts, errors); ValidatePooling(server.Pooling, errors); ValidateLimits(server.Limits, errors); @@ -253,6 +277,16 @@ private static void ValidateLimits(LimitsConfig? limits, List errors) RequireNonNegative(limits.MaxBufferedBodyBytes, "server.limits.maxBufferedBodyBytes", errors); RequireNonNegative(limits.MaxDecodedHeaderListBytes, "server.limits.maxDecodedHeaderListBytes", errors); RequireNonNegative(limits.MaxWebSocketFramePayloadBytes, "server.limits.maxWebSocketFramePayloadBytes", errors); + RequireInRange(limits.MaxHttp3FramePayloadBytes, 64L * 1024 * 1024, "server.limits.maxHttp3FramePayloadBytes", errors); + RequireInRange(limits.MaxDeferredOutboundBytesPerStream, 64 * 1024 * 1024, "server.limits.maxDeferredOutboundBytesPerStream", errors); + RequireInRange(limits.MaxTrailerHeaderCount, 10_000, "server.limits.maxTrailerHeaderCount", errors); + RequireInRange(limits.MaxTrailerHeaderBlockBytes, 1024 * 1024, "server.limits.maxTrailerHeaderBlockBytes", errors); + RequireInRange(limits.MaxHttp2CompressedHeaderBlockBytes, 16 * 1024 * 1024, "server.limits.maxHttp2CompressedHeaderBlockBytes", errors); + RequireInRange(limits.MaxInterimResponses, 100, "server.limits.maxInterimResponses", errors); + RequireInRange(limits.MaxAuthChallengeRounds, 20, "server.limits.maxAuthChallengeRounds", errors); + RequireInRange(limits.MaxUpstreamProxyAuthenticationAttempts, 20, "server.limits.maxUpstreamProxyAuthenticationAttempts", errors); + RequireInRange(limits.MaxWinAuthTokenBytes, 1024 * 1024, "server.limits.maxWinAuthTokenBytes", errors); + RequireInRange(limits.Http2WindowUpdateTimeoutSeconds, 3600, "server.limits.http2WindowUpdateTimeoutSeconds", errors); } private static void ValidatePolicyModes(PolicyModesConfig? policy, List errors) @@ -269,6 +303,7 @@ private static void ValidatePolicyModes(PolicyModesConfig? policy, List RequireKnownPolicy(policy.Http2AbuseBudget, "server.policyModes.http2AbuseBudget", errors); RequireKnownPolicy(policy.Http2RelayValidation, "server.policyModes.http2RelayValidation", errors); RequireKnownPolicy(policy.WebSocketFrameBudget, "server.policyModes.webSocketFrameBudget", errors); + RequireKnownPolicy(policy.Http1ReplaySafety, "server.policyModes.http1ReplaySafety", errors); } private static void ValidateUpstream(UpstreamConfig? upstream, List errors) @@ -284,6 +319,9 @@ private static void ValidateUpstream(UpstreamConfig? upstream, List erro errors.Add("server.upstream.upstreamProxyConfigurationScript must be an absolute URI."); } + RequireEndPoint(upstream.UpStreamEndPoint, "server.upstream.upStreamEndPoint", errors); + RequireEndPoint(upstream.UpStreamEndPointIPv4, "server.upstream.upStreamEndPointIPv4", errors); + RequireEndPoint(upstream.UpStreamEndPointIPv6, "server.upstream.upStreamEndPointIPv6", errors); ValidateExternalProxy(upstream.HttpProxy, "server.upstream.httpProxy", errors); ValidateExternalProxy(upstream.HttpsProxy, "server.upstream.httpsProxy", errors); } @@ -393,4 +431,81 @@ private static void RequirePositiveIfSet(double? value, string path, List errors) + { + if (value is null) return; + if (value <= 0) + errors.Add($"{path} must be positive (use null to leave default / disable)."); + else if (value > ceiling) + errors.Add($"{path} exceeds the ceiling of {ceiling}."); + } + + private static void RequireInRange(int? value, int ceiling, string path, List errors) => + RequireInRange(value.HasValue ? (long?)value.Value : null, ceiling, path, errors); + + private static void ValidateTls(TlsConfig? tls, List errors) + { + if (tls is null) return; + RejectUnknownProtocols(tls.SupportedSslProtocols, "server.tls.supportedSslProtocols", errors); + RejectUnknownProtocols(tls.SupportedServerSslProtocols, "server.tls.supportedServerSslProtocols", errors); + } + + private static void RejectUnknownProtocols(IList? names, string path, List errors) + { + if (names is null) return; + foreach (var name in names) + { + if (string.IsNullOrWhiteSpace(name) || !Enum.TryParse(name.Trim(), ignoreCase: true, out _)) + errors.Add($"{path} '{name}' is not an SslProtocols name."); + } + } + + private static void RequireEndPoint(string? value, string path, List errors) + { + if (!string.IsNullOrWhiteSpace(value) && !TryParseIpEndPoint(value)) + errors.Add($"{path} '{value}' is not a host:port address."); + } + + private static bool TryParseIpEndPoint(string value) + { + var text = value.Trim(); + string host; + string portText; + if (text.StartsWith('[')) + { + var close = text.IndexOf(']'); + if (close < 0 || close + 2 >= text.Length || text[close + 1] != ':') return false; + host = text[1..close]; + portText = text[(close + 2)..]; + } + else + { + var colon = text.LastIndexOf(':'); + if (colon <= 0) return false; + host = text[..colon]; + portText = text[(colon + 1)..]; + } + + return IPAddress.TryParse(host, out _) && + int.TryParse(portText, out var port) && + port is > 0 and <= 65535; + } + + /// + /// Limits that are accepted by config but not enforced on a live path. Callers print these + /// without failing validation. + /// + public static IReadOnlyList CollectWarnings(TwpConfig config) + { + var warnings = new List(); + var limits = config.Server?.Limits; + if (limits?.MaxDecompressionRatio is not null) + warnings.Add("server.limits.maxDecompressionRatio is reserved and not enforced. Body expansion is bounded by maxBufferedBodyBytes."); + if (limits?.MaxEncodedBodyBytes is not null) + warnings.Add("server.limits.maxEncodedBodyBytes is reserved and not enforced. The enforced whole-body budget is maxBufferedBodyBytes."); + if (limits?.MaxDecodedBodyBytes is not null) + warnings.Add("server.limits.maxDecodedBodyBytes is reserved and not enforced. The enforced whole-body budget is maxBufferedBodyBytes."); + return warnings; + } } diff --git a/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs b/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs new file mode 100644 index 000000000..5aefcf657 --- /dev/null +++ b/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs @@ -0,0 +1,149 @@ +using System.Text.Json; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using Titanium.Web.Proxy.Configuration.Models; + +namespace Titanium.Web.Proxy.Configuration; + +/// +/// Finds keys the native config model does not declare. Config load warns; titanium test --strict fails. +/// +public static class UnknownConfigKeyScanner +{ + public static IReadOnlyList Scan(string text, bool yaml) + { + if (string.IsNullOrWhiteSpace(text)) return []; + JsonElement root; + if (yaml) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + var graph = deserializer.Deserialize(text); + if (graph is null) return []; + root = JsonDocument.Parse(ToJson(graph)).RootElement; + } + else + { + root = JsonDocument.Parse(text).RootElement; + } + + if (root.ValueKind != JsonValueKind.Object) return []; + var found = new List(); + Walk(root, typeof(TwpConfig), "", found); + return found; + } + + private static void Walk(JsonElement element, Type type, string path, List found) + { + if (element.ValueKind != JsonValueKind.Object) return; + var props = Properties(type); + foreach (var property in element.EnumerateObject()) + { + var childPath = path.Length == 0 ? property.Name : path + "." + property.Name; + if (!props.TryGetValue(property.Name, out var clr)) + { + var suggestion = Suggest(property.Name, props.Keys); + var hinted = path.Length == 0 ? suggestion : path + "." + suggestion; + found.Add(suggestion is null + ? $"Unknown key '{childPath}'." + : $"Unknown key '{childPath}'. Did you mean '{hinted}'?"); + continue; + } + + var next = Nullable.GetUnderlyingType(clr) ?? clr; + if (property.Value.ValueKind == JsonValueKind.Object && !IsDictionary(next)) + Walk(property.Value, next, childPath, found); + else if (property.Value.ValueKind == JsonValueKind.Array) + { + var item = ItemType(next); + if (item != null && !IsDictionary(item)) + { + foreach (var entry in property.Value.EnumerateArray()) + Walk(entry, item, childPath, found); + } + } + } + } + + private static Dictionary Properties(Type type) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var property in type.GetProperties()) + { + if (property.GetMethod is null || property.GetMethod.GetParameters().Length > 0) continue; + map[JsonNamingPolicy.CamelCase.ConvertName(property.Name)] = property.PropertyType; + } + + return map; + } + + private static bool IsDictionary(Type type) => + type.IsGenericType && ( + type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(Dictionary<,>)); + + private static Type? ItemType(Type type) + { + if (type.IsArray) return type.GetElementType(); + if (!type.IsGenericType) return null; + var def = type.GetGenericTypeDefinition(); + if (def == typeof(IList<>) || def == typeof(List<>) || def == typeof(IReadOnlyList<>) || + def == typeof(IEnumerable<>) || def == typeof(ICollection<>)) + return type.GetGenericArguments()[0]; + return null; + } + + private static string? Suggest(string key, IEnumerable candidates) + { + string? best = null; + var bestDistance = 3; + foreach (var candidate in candidates) + { + var distance = Distance(key, candidate); + if (distance < bestDistance) + { + bestDistance = distance; + best = candidate; + } + } + + return best; + } + + private static int Distance(string left, string right) + { + var a = left.AsSpan(); + var b = right.AsSpan(); + var prev = new int[b.Length + 1]; + var next = new int[b.Length + 1]; + for (var j = 0; j <= b.Length; j++) prev[j] = j; + for (var i = 1; i <= a.Length; i++) + { + next[0] = i; + for (var j = 1; j <= b.Length; j++) + { + var cost = char.ToLowerInvariant(a[i - 1]) == char.ToLowerInvariant(b[j - 1]) ? 0 : 1; + next[j] = Math.Min(Math.Min(next[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + + (prev, next) = (next, prev); + } + + return prev[b.Length]; + } + + private static string ToJson(object? value) => JsonSerializer.Serialize(Normalize(value)); + + private static object? Normalize(object? value) => value switch + { + null => null, + IDictionary map => map.ToDictionary( + pair => pair.Key?.ToString() ?? "", + pair => Normalize(pair.Value), + StringComparer.Ordinal), + IList list => list.Select(Normalize).ToList(), + _ => value + }; +} diff --git a/src/Titanium.Web.Proxy.Configuration/twp.schema.json b/src/Titanium.Web.Proxy.Configuration/twp.schema.json new file mode 100644 index 000000000..a558ba57d --- /dev/null +++ b/src/Titanium.Web.Proxy.Configuration/twp.schema.json @@ -0,0 +1,747 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://titaniumproxy.com/twp.schema.json", + "title": "Titanium Web Proxy configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "certificates": { + "type": "object", + "additionalProperties": false, + "properties": { + "acmeDirectory": { + "type": "string" + }, + "acmeDomain": { + "type": "string" + }, + "acmeEmail": { + "type": "string" + }, + "certificatePath": { + "type": "string" + }, + "privateKeyPath": { + "type": "string" + } + } + }, + "clusters": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "affinityCookie": { + "type": "string" + }, + "affinityHeader": { + "type": "string" + }, + "algorithm": { + "type": "string" + }, + "destinations": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "address": { + "type": "string" + }, + "id": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "useDestinationHost": { + "type": "boolean" + }, + "useHttps": { + "type": "boolean" + }, + "weight": { + "type": "integer" + } + } + } + }, + "id": { + "type": "string" + } + } + } + }, + "listeners": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "decryptSsl": { + "type": "boolean" + }, + "enableHttp2": { + "type": "boolean" + }, + "enableHttp3": { + "type": "boolean" + }, + "forwardHost": { + "type": "string" + }, + "forwardPort": { + "type": "integer" + }, + "genericCertificateName": { + "type": "string" + }, + "handshakeTimeoutSeconds": { + "type": "integer" + }, + "host": { + "type": "string" + }, + "idleTimeoutSeconds": { + "type": "integer" + }, + "maxCachedConnections": { + "type": "integer" + }, + "maxConcurrentClients": { + "type": "integer" + }, + "maxInboundBidirectionalStreams": { + "type": "integer" + }, + "maxInboundUnidirectionalStreams": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "type": { + "type": "string" + } + } + } + }, + "logging": { + "type": "object", + "additionalProperties": false, + "properties": { + "enableConsole": { + "type": "boolean" + }, + "enableConsoleColors": { + "type": "boolean" + }, + "enableFile": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "filePath": { + "type": "string" + }, + "maxFileSizeBytes": { + "type": "integer" + }, + "maxRolledFiles": { + "type": "integer" + }, + "minimumLevel": { + "type": "string" + }, + "queueCapacity": { + "type": "integer" + } + } + }, + "plus": { + "type": "object", + "additionalProperties": false, + "properties": { + "controlPlane": { + "type": "object", + "additionalProperties": false, + "properties": { + "dashboardPort": { + "type": "integer" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "sharedSecret": { + "type": "string" + } + } + }, + "enabled": { + "type": "boolean" + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "clusterId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "idleTimeoutSeconds": { + "type": "integer" + }, + "maxBufferedBodyBytes": { + "type": "integer" + }, + "maxWebSocketFramePayloadBytes": { + "type": "integer" + }, + "requestTimeoutSeconds": { + "type": "integer" + } + } + }, + "match": { + "type": "object", + "additionalProperties": false, + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "host": { + "type": "string" + }, + "method": { + "type": "string" + }, + "path": { + "type": "string" + }, + "pathKind": { + "type": "string" + }, + "query": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "order": { + "type": "integer" + }, + "transforms": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string" + }, + "parameters": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "schemaVersion": { + "type": "string" + }, + "server": { + "type": "object", + "additionalProperties": false, + "properties": { + "accessLog": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "sampleRate": { + "type": "number" + } + } + }, + "blockLoopbackDestinations": { + "type": "boolean" + }, + "blockPrivateNetworkDestinations": { + "type": "boolean" + }, + "certificateManager": { + "type": "object", + "additionalProperties": false, + "properties": { + "certificateCacheTimeOutMinutes": { + "type": "integer" + }, + "certificateEngine": { + "type": "string" + }, + "certificateGraceDays": { + "type": "integer" + }, + "certificateValidDays": { + "type": "integer" + }, + "disableWildCardCertificates": { + "type": "boolean" + }, + "leafCertificateKeyAlgorithm": { + "type": "string" + }, + "overwritePfxFile": { + "type": "boolean" + }, + "pfxFilePath": { + "type": "string" + }, + "pfxPassword": { + "type": "string" + }, + "rootCertificateIssuerName": { + "type": "string" + }, + "rootCertificateName": { + "type": "string" + }, + "saveFakeCertificates": { + "type": "boolean" + } + } + }, + "checkCertificateRevocation": { + "type": "string" + }, + "compatibilityMode100Continue": { + "type": "boolean" + }, + "decryptFailureBypassMaxEntries": { + "type": "integer" + }, + "decryptFailureBypassThreshold": { + "type": "integer" + }, + "decryptFailureBypassTtlMinutes": { + "type": "integer" + }, + "decryptOnlyHosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "decryptSkipHosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "dnsServerEndPoint": { + "type": "string" + }, + "enable100ContinueBehaviour": { + "type": "boolean" + }, + "enableDecryptFailureBypass": { + "type": "boolean" + }, + "enableHttp2": { + "type": "boolean" + }, + "enableHttp3": { + "type": "boolean" + }, + "enableHttpsSvcbDnsDiscovery": { + "type": "boolean" + }, + "enableQpackDynamicTable": { + "type": "boolean" + }, + "enableRfc8441": { + "type": "boolean" + }, + "enableWinAuth": { + "type": "boolean" + }, + "ignoreServerCertificateErrors": { + "type": "boolean" + }, + "limits": { + "type": "object", + "additionalProperties": false, + "properties": { + "connectionPoolingEnabled": { + "type": "boolean" + }, + "http2WindowUpdateTimeoutSeconds": { + "type": "integer" + }, + "maxAuthChallengeRounds": { + "type": "integer" + }, + "maxBufferedBodyBytes": { + "type": "integer" + }, + "maxCachedConnectionsPerHost": { + "type": "integer" + }, + "maxCertificateCacheEntries": { + "type": "integer" + }, + "maxCertificateDiskCacheEntries": { + "type": "integer" + }, + "maxConcurrentClients": { + "type": "integer" + }, + "maxConcurrentStreamsPerConnection": { + "type": "integer" + }, + "maxDecodedBodyBytes": { + "type": "integer" + }, + "maxDecodedHeaderListBytes": { + "type": "integer" + }, + "maxDecompressionRatio": { + "type": "number" + }, + "maxDeferredOutboundBytesPerStream": { + "type": "integer" + }, + "maxEncodedBodyBytes": { + "type": "integer" + }, + "maxHeaderAggregateBytes": { + "type": "integer" + }, + "maxHeaderCount": { + "type": "integer" + }, + "maxHeaderLineBytes": { + "type": "integer" + }, + "maxHttp2CompressedHeaderBlockBytes": { + "type": "integer" + }, + "maxHttp3FramePayloadBytes": { + "type": "integer" + }, + "maxInterimResponses": { + "type": "integer" + }, + "maxOpenHeaderBlockDurationSeconds": { + "type": "integer" + }, + "maxOpenHeaderBlockFrames": { + "type": "integer" + }, + "maxOriginHttp2ConnectionsPerAuthority": { + "type": "integer" + }, + "maxPeerInitiatedIncompleteStreamResets": { + "type": "integer" + }, + "maxTrailerHeaderBlockBytes": { + "type": "integer" + }, + "maxTrailerHeaderCount": { + "type": "integer" + }, + "maxUpstreamProxyAuthenticationAttempts": { + "type": "integer" + }, + "maxWebSocketFramePayloadBytes": { + "type": "integer" + }, + "maxWinAuthTokenBytes": { + "type": "integer" + } + } + }, + "originHttpVersionPolicy": { + "type": "string" + }, + "policyModes": { + "type": "object", + "additionalProperties": false, + "properties": { + "admissionControl": { + "type": "string" + }, + "allowAmbiguousFraming": { + "type": "boolean" + }, + "bodyBudget": { + "type": "string" + }, + "decompressionRatio": { + "type": "string" + }, + "headerLimits": { + "type": "string" + }, + "http1ReplaySafety": { + "type": "string" + }, + "http2AbuseBudget": { + "type": "string" + }, + "http2RelayValidation": { + "type": "string" + }, + "webSocketFrameBudget": { + "type": "string" + } + } + }, + "pooling": { + "type": "object", + "additionalProperties": false, + "properties": { + "enableConnectionPool": { + "type": "boolean" + }, + "enableIpv6UnreachableSoftSkip": { + "type": "boolean" + }, + "enableTcpKeepAlive": { + "type": "boolean" + }, + "enableTcpServerConnectionPrefetch": { + "type": "boolean" + }, + "listenerBackLog": { + "type": "integer" + }, + "maxCachedConnections": { + "type": "integer" + }, + "maxConcurrentClientConnections": { + "type": "integer" + }, + "maxConcurrentHttp11HttpsOriginCreates": { + "type": "integer" + }, + "noDelay": { + "type": "boolean" + }, + "reuseSocket": { + "type": "boolean" + }, + "tcpTimeWaitSeconds": { + "type": "integer" + }, + "threadPoolWorkerThread": { + "type": "integer" + } + } + }, + "profile": { + "type": "string" + }, + "proxyAuthenticationRealm": { + "type": "string" + }, + "proxyLoopback": { + "type": "boolean" + }, + "systemProxyBypassHosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "timeouts": { + "type": "object", + "additionalProperties": false, + "properties": { + "clientHeaderTimeoutSeconds": { + "type": "integer" + }, + "connectTimeOutSeconds": { + "type": "integer" + }, + "connectionTimeOutSeconds": { + "type": "integer" + }, + "idleReadTimeoutSeconds": { + "type": "integer" + }, + "idleWriteTimeoutSeconds": { + "type": "integer" + }, + "networkFailureRetryAttempts": { + "type": "integer" + }, + "requestTimeoutSeconds": { + "type": "integer" + }, + "responseHeaderTimeoutSeconds": { + "type": "integer" + } + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "properties": { + "supportedServerSslProtocols": { + "type": "array", + "items": { + "type": "string" + } + }, + "supportedSslProtocols": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "upstream": { + "type": "object", + "additionalProperties": false, + "properties": { + "forwardToUpstreamGateway": { + "type": "boolean" + }, + "httpProxy": { + "type": "object", + "additionalProperties": false, + "properties": { + "bypassLocalhost": { + "type": "boolean" + }, + "hostName": { + "type": "string" + }, + "nextHop": { + "type": "object" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "proxyDnsRequests": { + "type": "boolean" + }, + "proxyType": { + "type": "string" + }, + "useDefaultCredentials": { + "type": "boolean" + }, + "userName": { + "type": "string" + } + } + }, + "httpsProxy": { + "type": "object", + "additionalProperties": false, + "properties": { + "bypassLocalhost": { + "type": "boolean" + }, + "hostName": { + "type": "string" + }, + "nextHop": { + "type": "object" + }, + "password": { + "type": "string" + }, + "port": { + "type": "integer" + }, + "proxyDnsRequests": { + "type": "boolean" + }, + "proxyType": { + "type": "string" + }, + "useDefaultCredentials": { + "type": "boolean" + }, + "userName": { + "type": "string" + } + } + }, + "upStreamEndPoint": { + "type": "string" + }, + "upStreamEndPointIPv4": { + "type": "string" + }, + "upStreamEndPointIPv6": { + "type": "string" + }, + "upstreamProxyConfigurationScript": { + "type": "string" + } + } + }, + "viaHeaderPseudonym": { + "type": "string" + } + } + }, + "staticFiles": { + "type": "object", + "additionalProperties": false, + "properties": { + "enableBrotli": { + "type": "boolean" + }, + "enableGzip": { + "type": "boolean" + }, + "root": { + "type": "string" + } + } + } + } +} diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index c9c8debaf..d994dc95e 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -514,7 +514,10 @@ internal async Task CopyRequestBodyAsync(IHttpStreamWriter writer, Transformatio if (contentLength > copyStream.ReadBytes) { var headers = new HeaderCollection(); - await HeaderParser.ReadHeaders(copyStream, headers, cancellationToken); + await HeaderParser.ReadHeaders(copyStream, headers, cancellationToken, + new HeaderBlockBudget(Server.ResourceLimits.MaxHeaderCount, + Server.ResourceLimits.MaxHeaderAggregateBytes, + Server.PolicyModes[PolicyFamily.HeaderLimits], isRequest: true)); OnMultipartRequestPartSent(boundary.Span, headers); } } diff --git a/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs index 4eb224c96..ce21acf6c 100644 --- a/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/ExplicitClientHandler.cs @@ -85,7 +85,8 @@ public partial class ProxyServer HttpVersion = requestLine.Version }; - await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); + await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken, + clientStream.HeaderBudget(isRequest: true)); connectArgs = new TunnelConnectSessionEventArgs(this, endPoint, connectRequest, clientStream, cancellationTokenSource); @@ -722,6 +723,25 @@ await SendHttp11ToHttp2Bridge(clientStream, endPoint, connectRequest, closeServerConnection = true; ProxyDiagnostics.ReportException(logger, "Client session cancelled", e); } + catch (HeaderLimitsExceededException) + { + closeServerConnection = true; + var rejected = new Response + { + HttpVersion = HttpHeader.Version11, + StatusCode = (int)HttpStatusCode.RequestHeaderFieldsTooLarge, + StatusDescription = "Request Header Fields Too Large" + }; + rejected.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionClose); + try + { + await clientStream.WriteResponseAsync(rejected, CancellationToken.None); + } + catch (Exception writeEx) when (writeEx is IOException or OperationCanceledException) + { + // The client is already gone. The limit was logged at the parser. + } + } catch (Exception e) { closeServerConnection = true; diff --git a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs index 1443aad14..7eca5bb05 100644 --- a/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/Http11ToHttp2BridgeHandler.cs @@ -84,7 +84,7 @@ public partial class ProxyServer { var tcp = await EstablishHttp2OriginTcpConnectionAsync(openSession, remoteHostName, remotePort, connectHost, connectPort, ct); - return await Http2OriginConnection.CreateAsync(tcp, logger, ct, ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, ResourceLimits, MaxDecodedHeaderListBytes); }; try @@ -126,9 +126,26 @@ public partial class ProxyServer { try { - if (!await HeaderParser.TryReadHeadersAsync(clientStream, request.Headers, cancellationToken)) + try + { + if (!await HeaderParser.TryReadHeadersAsync(clientStream, request.Headers, cancellationToken, + clientStream.HeaderBudget(isRequest: true))) + { + closeConnection = true; + return; + } + } + catch (HeaderLimitsExceededException) { closeConnection = true; + args.HttpClient.Response = new GenericResponse(HttpStatusCode.RequestHeaderFieldsTooLarge) + { + HttpVersion = HttpHeader.Version11 + }; + args.HttpClient.Response.Headers.AddHeader(KnownHeaders.Connection, + KnownHeaders.ConnectionClose); + await clientStream.WriteResponseAsync(args.HttpClient.Response, cancellationToken); + args.IsClientResponseCommitted = true; return; } @@ -392,7 +409,7 @@ private async Task OfferRetainedHttp2OriginSeedAsync(string poolKey, // NOSONAR try { var created = await Http2OriginConnection.CreateAsync(seedConnection, logger, - cancellationToken, ResourceLimits); + cancellationToken, ResourceLimits, MaxDecodedHeaderListBytes); Http2OriginConnectionPool.Offer(poolKey, created); } catch (Exception ex) diff --git a/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs b/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs index 7d4e30922..c8a6f1823 100644 --- a/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/Http2ToHttp11BridgeHandler.cs @@ -18,6 +18,7 @@ using Titanium.Web.Proxy.Http2; using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.Network.Tcp; using Titanium.Web.Proxy.StreamExtended.Network; using SslExtensions = Titanium.Web.Proxy.Extensions.SslExtensions; @@ -918,12 +919,20 @@ await Http2Helper.EmitSyntheticResponseAsync(sessionArgs, ctx.StreamId, var upgradeResponseHeaders = new HeaderCollection(); int upgradeHeaderBytes = 0; int upgradeHeaderCount = 0; + var maxUpgradeHeaders = ResourceLimits.MaxHeaderCount; + var maxUpgradeHeaderBytes = (int)Math.Min(int.MaxValue, ResourceLimits.MaxHeaderAggregateBytes); string? headerLine; while ((headerLine = await ReadLineAsync(connection.Stream, cancellationToken)) is { Length: > 0 }) { upgradeHeaderBytes += headerLine.Length + 2; - if (++upgradeHeaderCount > 100 || upgradeHeaderBytes > 64 * 1024) + if (++upgradeHeaderCount > maxUpgradeHeaders || upgradeHeaderBytes > maxUpgradeHeaderBytes) + { + ProxyLog.LimitExceeded(logger, LimitId.UpgradeHandshakeHeaders, PolicyMode.Enforce, + upgradeHeaderCount > maxUpgradeHeaders ? upgradeHeaderCount : upgradeHeaderBytes, + upgradeHeaderCount > maxUpgradeHeaders ? maxUpgradeHeaders : maxUpgradeHeaderBytes, + "upgrade failed"); throw new InvalidDataException("WebSocket upgrade response headers exceeded safety limits."); + } int colon = headerLine.IndexOf(':'); if (colon <= 0) continue; diff --git a/src/Titanium.Web.Proxy/Handlers/RequestHandler.cs b/src/Titanium.Web.Proxy/Handlers/RequestHandler.cs index 394c49d2f..c549e4b68 100644 --- a/src/Titanium.Web.Proxy/Handlers/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/RequestHandler.cs @@ -15,6 +15,7 @@ using Titanium.Web.Proxy.Http.Responses; using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Network.Streams; using Titanium.Web.Proxy.Network.Tcp; @@ -132,7 +133,7 @@ public partial class ProxyServer preparedRequest.IsHttps = true; if (!await HeaderParser.TryReadHeadersAsync(clientStream, preparedRequest.Headers, - headerDeadline.Token)) + headerDeadline.Token, clientStream.HeaderBudget(isRequest: true))) { ThrowIfHeaderDeadlineTimedOut(headerDeadline); return; @@ -218,7 +219,7 @@ public partial class ProxyServer // (skipped when preparedRequest already filled them above). if (!headersAlreadyRead && !await HeaderParser.TryReadHeadersAsync(clientStream, args.HttpClient.Request.Headers, - headerDeadline.Token)) + headerDeadline.Token, clientStream.HeaderBudget(isRequest: true))) { args.Dispose(); args = null; @@ -226,6 +227,17 @@ public partial class ProxyServer return; } } + catch (HeaderLimitsExceededException) + { + var rejected = new GenericResponse(HttpStatusCode.RequestHeaderFieldsTooLarge) + { + HttpVersion = HttpHeader.Version11 + }; + rejected.Headers.AddHeader(KnownHeaders.Connection, KnownHeaders.ConnectionClose); + await clientStream.WriteResponseAsync(rejected, cancellationToken); + args?.Dispose(); + return; + } catch (OperationCanceledException ex) { // No response was ever attempted (there may be no request line at all yet, or no @@ -358,6 +370,8 @@ public partial class ProxyServer // breach (which can only close the connection - see the catch-all further // down and DowngradeChunkedFramingForHttp10OriginIfNeeded's caller) this one // can still produce a normal 413 to the client. + ProxyLog.LimitExceeded(logger, LimitId.BufferedBody, PolicyMode.Enforce, + MaxBufferedBodyBytes, MaxBufferedBodyBytes, "413"); ProxyDiagnostics.ReportCaught(logger, "Request body size limit exceeded in BeforeRequest; returning 413", bodyLimitEx); args.HttpClient.Response = new GenericResponse(System.Net.HttpStatusCode.RequestEntityTooLarge) @@ -440,6 +454,8 @@ public partial class ProxyServer { // Still request-side and still nothing sent to the origin yet, same // as the BeforeRequest breach above. + ProxyLog.LimitExceeded(logger, LimitId.BufferedBody, PolicyMode.Enforce, + MaxBufferedBodyBytes, MaxBufferedBodyBytes, "413"); ProxyDiagnostics.ReportCaught(logger, "Request body size limit exceeded buffering for WinAuth; returning 413", bodyLimitEx); @@ -635,6 +651,19 @@ public partial class ProxyServer connection = null; } } + catch (HeaderLimitsExceededException) + { + closeServerConnection = true; + if (!args.IsClientResponseCommitted && !args.HttpClient.Response.Locked) + { + args.GenericResponse("Bad Gateway", HttpStatusCode.BadGateway, + closeServerConnection: true); + await args.ClientStream.WriteResponseAsync(args.HttpClient.Response, cancellationToken); + args.IsClientResponseCommitted = true; + } + + return; + } catch (ProxyTimeoutException timeoutEx) { await HandleProxyTimeoutAsync(args, timeoutEx, cancellationToken); diff --git a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs index b59eaaab1..3be6ade12 100644 --- a/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/ResponseHandler.cs @@ -10,6 +10,7 @@ using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.Network.WinAuth.Security; using Titanium.Web.Proxy.Routing; @@ -278,6 +279,8 @@ private async Task HandleProxyTimeoutAsync(SessionEventArgs args, ProxyTimeoutEx { args.Exception = ex; args.HttpClient.CloseServerConnection = true; + ProxyLog.LimitExceeded(logger, TimeoutLimitId(ex.Kind), PolicyMode.Enforce, + 0, ex.Kind.ToString(), "504"); ProxyDiagnostics.ReportBenign(logger, $"Proxy {ex.Kind} timeout", ex); // 504 only before any response bytes have been committed; afterward terminate without injecting HTTP. @@ -301,6 +304,17 @@ private async Task HandleProxyTimeoutAsync(SessionEventArgs args, ProxyTimeoutEx await args.CancellationTokenSource.CancelAsync(); } + private static LimitId TimeoutLimitId(ProxyTimeoutKind kind) => kind switch + { + ProxyTimeoutKind.Connect => LimitId.TimeoutConnect, + ProxyTimeoutKind.ResponseHeader => LimitId.TimeoutResponseHeader, + ProxyTimeoutKind.IdleRead => LimitId.TimeoutIdleRead, + ProxyTimeoutKind.IdleWrite => LimitId.TimeoutIdleWrite, + ProxyTimeoutKind.Request => LimitId.TimeoutRequest, + ProxyTimeoutKind.ClientHeader => LimitId.TimeoutClientHeader, + _ => LimitId.TimeoutRequest + }; + /// /// Decides whether a re-request must reuse the same server connection. /// Connection-oriented authentication handshakes (proxy 407, or a server 401 handled by diff --git a/src/Titanium.Web.Proxy/Handlers/WebSocketHandler.cs b/src/Titanium.Web.Proxy/Handlers/WebSocketHandler.cs index e75c3d538..772a65a93 100644 --- a/src/Titanium.Web.Proxy/Handlers/WebSocketHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/WebSocketHandler.cs @@ -48,7 +48,8 @@ private async Task HandleWebSocketUpgrade(SessionEventArgs args, upgradeResponse.StatusCode = httpStatus.StatusCode; upgradeResponse.StatusDescription = httpStatus.Description; - await HeaderParser.ReadHeaders(serverConnection.Stream, upgradeResponse.Headers, idleDeadline.Token); + await HeaderParser.ReadHeaders(serverConnection.Stream, upgradeResponse.Headers, idleDeadline.Token, + serverConnection.Stream.HeaderBudget(isRequest: false)); } catch (OperationCanceledException ex) { diff --git a/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs b/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs index e75d9180d..adef7438f 100644 --- a/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/Handlers/WinAuthHandler.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Titanium.Web.Proxy.Diagnostics; +using Titanium.Web.Proxy.Logging; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Http; @@ -15,15 +17,6 @@ namespace Titanium.Web.Proxy; public partial class ProxyServer { - /// - /// Maximum number of NTLM/Kerberos/Negotiate challenge-response rounds the proxy will perform for a - /// single request. The standard NTLM handshake completes in two rounds (initial token + challenge - /// response). A cap of three rounds provides one extra margin for servers that re-challenge after - /// the connection is marked authenticated (RFC-conformant but still bounded) while preventing an - /// infinite retry loop when credentials are persistently rejected. - /// - private const int MaxAuthChallengeRounds = 3; - /// Key used to store the per-request auth challenge round counter in . private const string WinAuthRoundCountKey = "WinAuthRoundCount"; @@ -108,8 +101,12 @@ public partial class ProxyServer var currentRound = args.HttpClient.Data.TryGetValueAs(WinAuthRoundCountKey, out int storedRound) ? storedRound : 0; - if (currentRound >= MaxAuthChallengeRounds) + var maxAuthRounds = ResourceLimits.MaxAuthChallengeRounds; + var maxTokenBytes = ResourceLimits.MaxWinAuthTokenBytes; + if (currentRound >= maxAuthRounds) { + ProxyLog.LimitExceeded(logger, LimitId.AuthChallengeRounds, PolicyMode.Enforce, + currentRound, maxAuthRounds, "401"); await RewriteUnauthorizedResponse(args); return; } @@ -143,7 +140,7 @@ public partial class ProxyServer credentials = await WinAuthCredentialsProvider(args); var clientToken = WinAuthHandler.GetInitialAuthToken(request.Host!, scheme, args.HttpClient.Data, - credentials); + credentials, maxTokenBytes); var auth = string.Concat(scheme, clientToken); @@ -161,7 +158,8 @@ public partial class ProxyServer authHeader.Value.Length > x.Length + 1); var serverToken = authHeader.Value.Substring(scheme.Length + 1); - var clientToken = WinAuthHandler.GetFinalAuthToken(request.Host!, serverToken, args.HttpClient.Data); + var clientToken = WinAuthHandler.GetFinalAuthToken(request.Host!, serverToken, args.HttpClient.Data, + maxTokenBytes); var auth = string.Concat(scheme, clientToken); @@ -204,8 +202,11 @@ public partial class ProxyServer var currentRound407 = args.HttpClient.Data.TryGetValueAs(WinAuthRoundCountKey, out int stored407) ? stored407 : 0; - if (currentRound407 >= MaxAuthChallengeRounds) + var maxAuthRounds = ResourceLimits.MaxAuthChallengeRounds; + if (currentRound407 >= maxAuthRounds) { + ProxyLog.LimitExceeded(logger, LimitId.AuthChallengeRounds, PolicyMode.Enforce, + currentRound407, maxAuthRounds, "407"); await RewriteUnauthorizedResponse(args); return; } diff --git a/src/Titanium.Web.Proxy/Http/ChunkedTrailerHelper.cs b/src/Titanium.Web.Proxy/Http/ChunkedTrailerHelper.cs index 677c42561..96b5ee5ea 100644 --- a/src/Titanium.Web.Proxy/Http/ChunkedTrailerHelper.cs +++ b/src/Titanium.Web.Proxy/Http/ChunkedTrailerHelper.cs @@ -3,6 +3,8 @@ using System.Threading; using System.Threading.Tasks; using Titanium.Web.Proxy.Exceptions; +using Titanium.Web.Proxy.Logging; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.StreamExtended.Network; namespace Titanium.Web.Proxy.Http; @@ -56,7 +58,9 @@ internal static class ChunkedTrailerHelper /// Cancellation token. /// The trailer block is malformed, or exceeds the count/size bounds above. internal static async ValueTask ReadTrailingHeaders(ILineStream reader, HeaderCollection into, - List? rawLines, CancellationToken cancellationToken = default) + List? rawLines, CancellationToken cancellationToken = default, + int maxTrailerHeaderCount = MaxTrailerHeaderCount, + int maxTrailerHeaderBlockBytes = MaxTrailerHeaderBlockSize) { var count = 0; var totalSize = 0; @@ -66,15 +70,23 @@ internal static async ValueTask ReadTrailingHeaders(ILineStream reader, HeaderCo var line = await reader.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(line)) break; count++; - if (count > MaxTrailerHeaderCount) + if (count > maxTrailerHeaderCount) + { + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, LimitId.TrailerHeaderCount, PolicyMode.Enforce, + count, maxTrailerHeaderCount, "connection closed"); throw new ProxyHttpException( - $"Chunked trailer has too many header lines (> {MaxTrailerHeaderCount}).", null, null); + $"Chunked trailer has too many header lines (> {maxTrailerHeaderCount}).", null, null); + } totalSize += line.Length; - if (totalSize > MaxTrailerHeaderBlockSize) + if (totalSize > maxTrailerHeaderBlockBytes) + { + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, LimitId.TrailerHeaderBlock, PolicyMode.Enforce, + totalSize, maxTrailerHeaderBlockBytes, "connection closed"); throw new ProxyHttpException( - $"Chunked trailer exceeds the maximum allowed size of {MaxTrailerHeaderBlockSize} bytes.", + $"Chunked trailer exceeds the maximum allowed size of {maxTrailerHeaderBlockBytes} bytes.", null, null); + } var colonIndex = line.IndexOf(':'); if (colonIndex == -1) diff --git a/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs b/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs new file mode 100644 index 000000000..9e0ba969d --- /dev/null +++ b/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs @@ -0,0 +1,27 @@ +using System; + +namespace Titanium.Web.Proxy.Http; + +/// +/// An HTTP/1 header block exceeded MaxHeaderCount or MaxHeaderAggregateBytes +/// while HeaderLimits was Enforce. Not an , +/// so it is not treated as a client disconnect. +/// +internal sealed class HeaderLimitsExceededException : Exception +{ + internal HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest) + : base(isCount + ? $"Header count {observed} exceeded limit {limit}." + : $"Header block size {observed} exceeded limit {limit}.") + { + IsCount = isCount; + Observed = observed; + Limit = limit; + IsRequest = isRequest; + } + + internal bool IsCount { get; } + internal long Observed { get; } + internal long Limit { get; } + internal bool IsRequest { get; } +} diff --git a/src/Titanium.Web.Proxy/Http/HeaderParser.cs b/src/Titanium.Web.Proxy/Http/HeaderParser.cs index 5d1c399be..18ad4e3a9 100644 --- a/src/Titanium.Web.Proxy/Http/HeaderParser.cs +++ b/src/Titanium.Web.Proxy/Http/HeaderParser.cs @@ -3,42 +3,66 @@ using System.Threading.Tasks; using Titanium.Web.Proxy.Extensions; using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.StreamExtended.Network; namespace Titanium.Web.Proxy.Http; +internal readonly struct HeaderBlockBudget +{ + internal HeaderBlockBudget(int maxCount, long maxAggregateBytes, PolicyMode mode, bool isRequest) + { + MaxCount = maxCount; + MaxAggregateBytes = maxAggregateBytes; + Mode = mode; + IsRequest = isRequest; + } + + internal int MaxCount { get; } + internal long MaxAggregateBytes { get; } + internal PolicyMode Mode { get; } + internal bool IsRequest { get; } + +} + internal static class HeaderParser { internal static ValueTask ReadHeaders(ILineStream reader, HeaderCollection headerCollection, - CancellationToken cancellationToken) + CancellationToken cancellationToken, HeaderBlockBudget budget = default) { // Sync-complete as many header lines as are already buffered (keep-alive leftovers often // hold the entire header block), then await only when a fill is required. + var count = 0; + long aggregate = 0; + var observed = false; while (reader.DataAvailable) { var lineVt = reader.ReadLineAsync(cancellationToken); if (!lineVt.IsCompletedSuccessfully) return ReadHeadersContinueAsync(reader, headerCollection, lineVt, hasPending: true, - cancellationToken); + cancellationToken, budget, count, aggregate, observed); var buffered = lineVt.Result; if (string.IsNullOrEmpty(buffered)) return default; + Account(ref count, ref aggregate, ref observed, buffered.Length, budget); AddHeaderLine(headerCollection, buffered); } return ReadHeadersContinueAsync(reader, headerCollection, default, hasPending: false, - cancellationToken); + cancellationToken, budget, count, aggregate, observed); } private static async ValueTask ReadHeadersContinueAsync(ILineStream reader, HeaderCollection headerCollection, ValueTask pendingLine, bool hasPending, - CancellationToken cancellationToken) + CancellationToken cancellationToken, HeaderBlockBudget budget, int count, long aggregate, bool observed) { if (hasPending) { var pending = await pendingLine; if (string.IsNullOrEmpty(pending)) return; + Account(ref count, ref aggregate, ref observed, pending.Length, budget); AddHeaderLine(headerCollection, pending); } @@ -46,6 +70,7 @@ private static async ValueTask ReadHeadersContinueAsync(ILineStream reader, { var tmpLine = await reader.ReadLineAsync(cancellationToken); if (string.IsNullOrEmpty(tmpLine)) break; + Account(ref count, ref aggregate, ref observed, tmpLine.Length, budget); AddHeaderLine(headerCollection, tmpLine); } } @@ -54,31 +79,37 @@ private static async ValueTask ReadHeadersContinueAsync(ILineStream reader, /// Reads headers without throwing on cancellation. Returns when cancelled. /// internal static ValueTask TryReadHeadersAsync(HttpStream reader, - HeaderCollection headerCollection, CancellationToken cancellationToken) + HeaderCollection headerCollection, CancellationToken cancellationToken, + HeaderBlockBudget budget = default) { + var count = 0; + long aggregate = 0; + var observed = false; // Prefer byte-span parse while complete lines are already buffered (keep-alive leftover). while (reader.TryConsumeHeaderLineFromBuffer(out var emptyLine, out var lineBytes)) { if (emptyLine) return new ValueTask(true); + Account(ref count, ref aggregate, ref observed, lineBytes.Length, budget); AddHeaderLine(headerCollection, lineBytes); } // Incomplete line or empty buffer — fall through to the string path (handles multi-fill lines). return TryReadHeadersContinueAsync(reader, headerCollection, default, hasPending: false, - cancellationToken); + cancellationToken, budget, count, aggregate, observed); } private static async ValueTask TryReadHeadersContinueAsync(HttpStream reader, HeaderCollection headerCollection, ValueTask<(string? Line, bool Cancelled)> pendingLine, bool hasPending, - CancellationToken cancellationToken) + CancellationToken cancellationToken, HeaderBlockBudget budget, int count, long aggregate, bool observed) { if (hasPending) { var (pending, cancelled) = await pendingLine; if (cancelled) return false; if (string.IsNullOrEmpty(pending)) return true; + Account(ref count, ref aggregate, ref observed, pending.Length, budget); AddHeaderLine(headerCollection, pending); } @@ -88,16 +119,44 @@ private static async ValueTask TryReadHeadersContinueAsync(HttpStream read while (reader.TryConsumeHeaderLineFromBuffer(out var emptyLine, out var lineBytes)) { if (emptyLine) return true; + Account(ref count, ref aggregate, ref observed, lineBytes.Length, budget); AddHeaderLine(headerCollection, lineBytes); } var (tmpLine, cancelled) = await reader.ReadLineWithResultAsync(cancellationToken); if (cancelled) return false; if (string.IsNullOrEmpty(tmpLine)) return true; + Account(ref count, ref aggregate, ref observed, tmpLine.Length, budget); AddHeaderLine(headerCollection, tmpLine); } } + private static void Account(ref int count, ref long aggregate, ref bool observed, int lineLength, + in HeaderBlockBudget budget) + { + if (budget.Mode == PolicyMode.Disabled) return; + count++; + aggregate += lineLength + 2L; + var countBreach = count > budget.MaxCount; + var sizeBreach = aggregate > budget.MaxAggregateBytes; + if (!countBreach && !sizeBreach) return; + + var id = countBreach ? LimitId.HeaderCount : LimitId.HeaderAggregate; + var observedValue = countBreach ? count : aggregate; + var limit = countBreach ? budget.MaxCount : budget.MaxAggregateBytes; + if (budget.Mode == PolicyMode.Observe) + { + if (observed) return; + observed = true; + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, id, PolicyMode.Observe, observedValue, limit, "passed"); + return; + } + + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, id, PolicyMode.Enforce, observedValue, limit, + budget.IsRequest ? "431" : "502"); + throw new HeaderLimitsExceededException(countBreach, observedValue, limit, budget.IsRequest); + } + private static void AddHeaderLine(HeaderCollection headerCollection, string tmpLine) { // RFC 9112 §5.2: obs-fold (field-value continuation with leading SP or HTAB) is forbidden. diff --git a/src/Titanium.Web.Proxy/Http2/Http2ConnectionState.cs b/src/Titanium.Web.Proxy/Http2/Http2ConnectionState.cs index c7e3ad7dc..2775843ad 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2ConnectionState.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2ConnectionState.cs @@ -23,11 +23,21 @@ namespace Titanium.Web.Proxy.Http2; internal sealed class Http2ConnectionState { public Http2ConnectionState(long connectionId, CancellationTokenSource cancellationTokenSource, - int maxPooledStreamStates = 256) + int maxPooledStreamStates = 256, int maxDeferredBytesPerStream = 0, int windowUpdateTimeoutSeconds = 0) { ConnectionId = connectionId; CancellationTokenSource = cancellationTokenSource; StreamStatePool = new Http2StreamStatePool(maxPooledStreamStates); + var window = windowUpdateTimeoutSeconds > 0 + ? TimeSpan.FromSeconds(windowUpdateTimeoutSeconds) + : (TimeSpan?)null; + ClientSendFlow = new Http2FlowController(window); + ServerSendFlow = new Http2FlowController(window); + var deferred = maxDeferredBytesPerStream > 0 + ? maxDeferredBytesPerStream + : Http2DeferredOutboundData.HardMaxDeferredBytesPerStream; + ClientOutboundDeferred = new Http2DeferredOutboundData(deferred); + ServerOutboundDeferred = new Http2DeferredOutboundData(deferred); } public long ConnectionId { get; } @@ -44,25 +54,25 @@ public Http2ConnectionState(long connectionId, CancellationTokenSource cancellat /// Governs writes toward the client (used by the server->client relay task; fed by WINDOW_UPDATE/ /// SETTINGS_INITIAL_WINDOW_SIZE frames read from the client on the client->server relay task). /// - public Http2FlowController ClientSendFlow { get; } = new(); + public Http2FlowController ClientSendFlow { get; } /// /// Governs writes toward the server (used by the client->server relay task; fed by WINDOW_UPDATE/ /// SETTINGS_INITIAL_WINDOW_SIZE frames read from the server on the server->client relay task). /// - public Http2FlowController ServerSendFlow { get; } = new(); + public Http2FlowController ServerSendFlow { get; } /// /// Outbound DATA toward the client that is waiting for credit. /// Drained when the client→server relay reads WINDOW_UPDATE. /// - public Http2DeferredOutboundData ClientOutboundDeferred { get; } = new(); + public Http2DeferredOutboundData ClientOutboundDeferred { get; } /// /// Outbound DATA toward the server that is waiting for credit. /// Drained when the server→client relay reads WINDOW_UPDATE. /// - public Http2DeferredOutboundData ServerOutboundDeferred { get; } = new(); + public Http2DeferredOutboundData ServerOutboundDeferred { get; } /// All currently open (or draining) streams, keyed by the stream id used identically on both legs. public ConcurrentDictionary Streams { get; } = new(); diff --git a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs index 4b9eaba59..d1811c6be 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2DeferredOutboundData.cs @@ -35,7 +35,15 @@ internal sealed class Http2DeferredOutboundData private const int CoalesceTarget = 16 * 1024; + private readonly int maxDeferredBytes; private readonly object gate = new(); + + internal Http2DeferredOutboundData(int maxDeferredBytesPerStream = HardMaxDeferredBytesPerStream) + { + maxDeferredBytes = maxDeferredBytesPerStream > 0 + ? maxDeferredBytesPerStream + : HardMaxDeferredBytesPerStream; + } // LinkedList so a partial drain can AddFirst the remainder without reordering later frames. private readonly Dictionary> byStream = new(); private readonly List roundRobinOrder = new(); @@ -131,7 +139,7 @@ public bool TryEnqueue(int streamId, byte[] rented, int offset, int payloadLengt var queuedBytes = 0; foreach (var pending in q) queuedBytes += pending.PayloadLength; - if (queuedBytes + payloadLength > HardMaxDeferredBytesPerStream || q.Count >= HardMaxFramesPerStream) + if (queuedBytes + payloadLength > maxDeferredBytes || q.Count >= HardMaxFramesPerStream) return false; q.AddLast(new PendingFrame(rented, offset, payloadLength, endStream)); diff --git a/src/Titanium.Web.Proxy/Http2/Http2FlowController.cs b/src/Titanium.Web.Proxy/Http2/Http2FlowController.cs index 9275621ce..190554044 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2FlowController.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2FlowController.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Titanium.Web.Proxy.Logging; +using Titanium.Web.Proxy.Options; namespace Titanium.Web.Proxy.Http2; @@ -36,6 +38,14 @@ internal sealed class Http2FlowController private int initialStreamWindow = InitialConnectionWindow; private TaskCompletionSource creditAvailable = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TimeSpan reservationTimeout; + + internal Http2FlowController(TimeSpan? reservationTimeout = null) + { + this.reservationTimeout = reservationTimeout is { } configured && configured > TimeSpan.Zero + ? configured + : ReservationTimeout; + } /// RFC 7540 §6.9.2 default initial flow-control window size for both the connection and every stream. internal const int InitialConnectionWindow = 65535; @@ -286,12 +296,14 @@ private async ValueTask ReserveSlowAsync(int streamId, int bytes, CancellationTo try { - await wait.WaitAsync(ReservationTimeout, cancellationToken); + await wait.WaitAsync(reservationTimeout, cancellationToken); } catch (TimeoutException) { + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, LimitId.Http2WindowUpdate, PolicyMode.Enforce, + (long)reservationTimeout.TotalSeconds, (long)reservationTimeout.TotalSeconds, "stream reset"); throw new TimeoutException( - $"HTTP/2 flow-control reservation for stream {streamId} timed out after {ReservationTimeout} " + + $"HTTP/2 flow-control reservation for stream {streamId} timed out after {reservationTimeout} " + "waiting for WINDOW_UPDATE credit from the peer."); } } diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs index deb6146fd..350ef2ac9 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.Headers.cs @@ -129,6 +129,8 @@ internal partial class Http2Helper catch (Http2HeaderListTooLargeException ex) { // Policy rejection (not a structural HPACK error) - hpack.Decoder state is intact. + ProxyLog.LimitExceeded(logger, LimitId.DecodedHeaderList, PolicyMode.Enforce, + maxDecodedHeaderListBytes, maxDecodedHeaderListBytes, "RST_STREAM"); ReportException(logger, new ProxyHttpException( "HTTP/2 header list too large: " + ex.Message, ex, sessionArgs)); await LockedWriteAsync(ownLegWriteLock, cancellationToken, () => SendRstStreamAsync(new Http2FrameHeader(), new byte[9], hbStreamId, diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs index b0627f52e..47a00a78d 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.Copy.cs @@ -31,8 +31,6 @@ namespace Titanium.Web.Proxy.Http2 { internal partial class Http2Helper { - private const int MaxHeaderBlockBytes = 256 * 1024; - private static readonly HashSet ForbiddenConnectionSpecificHeaders = new(StringComparer.OrdinalIgnoreCase) { "connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade" @@ -69,6 +67,7 @@ private static async Task CopyHttp2FrameAsync(Stream input, Stream output, // NO bool forceStaticHpackForMitmUnchangedRelay = false) { resourceLimits ??= ProxyResourceLimits.Default; + var maxHeaderBlockBytes = resourceLimits.MaxHttp2CompressedHeaderBlockBytes; var cancellationTokenSource = connectionState.CancellationTokenSource; // Same-protocol H2↔H2 (not NullOrigin / RFC 8441): compressed-relay topology. @@ -1225,8 +1224,10 @@ await pendingHeaderBlock.WriteAsync(buffer.AsMemory(offset, fragmentLength), return; } - if (pendingHeaderBlock.Length + length > MaxHeaderBlockBytes) + if (pendingHeaderBlock.Length + length > maxHeaderBlockBytes) { + ProxyLog.LimitExceeded(logger, LimitId.Http2CompressedHeaderBlock, PolicyMode.Enforce, + pendingHeaderBlock.Length + length, maxHeaderBlockBytes, "GOAWAY"); ReportException(logger, new ProxyHttpException( "HTTP/2 header block exceeded the maximum allowed compressed size.", null, pendingHeaderArgs)); @@ -1589,6 +1590,8 @@ await lockedOwnLegWrite(() => SendRstStreamAsync( if (bodyBudgetBreached && bodyBudgetMode == PolicyMode.Enforce) { + ProxyLog.LimitExceeded(logger, LimitId.BufferedBody, PolicyMode.Enforce, + data.Length + length, maxBufferedBodyBytes, "RST_STREAM"); // Intentional policy enforcement, not a proxy defect — Debug only. ProxyDiagnostics.ReportBenign(logger, $"HTTP/2 {(isClient ? "request" : "response")} body exceeded the configured " + diff --git a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs index 84308a69f..fd8bc9d9a 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2Helper.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2Helper.cs @@ -228,7 +228,9 @@ internal static async Task SendHttp2(Stream clientStream, Stream serverStream, / { resourceLimits ??= ProxyResourceLimits.Default; var connectionState = new Http2ConnectionState(connectionId, cancellationTokenSource, - resourceLimits.MaxConcurrentStreamsPerConnection); + resourceLimits.MaxConcurrentStreamsPerConnection, + resourceLimits.MaxDeferredOutboundBytesPerStream, + resourceLimits.Http2WindowUpdateTimeoutSeconds); try { diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs index 4e4f5adbc..8f0e05814 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2OriginConnection.cs @@ -55,7 +55,10 @@ internal sealed class Http2OriginConnection : IDisposable "HTTP/2 protocol error: DATA padding length is the payload length or longer."; /// Maximum total header block (HEADERS + CONTINUATION fragments) we accept from origin before treating it as a protocol violation. - private const int MaxHeaderBlockBytes = 256 * 1024; + private readonly int maxCompressedHeaderBlockBytes; + + /// Decoded header list cap for the origin HPACK decoder. Read once per connection. + private readonly int maxDecodedHeaderListBytes; /// Retire the connection before odd client stream ids wrap (RFC 7540 §5.1.1). private const int StreamIdExhaustionThreshold = int.MaxValue - 10_000; @@ -67,7 +70,7 @@ internal sealed class Http2OriginConnection : IDisposable private readonly ILogger logger; private readonly ProxyResourceLimits resourceLimits; private readonly SemaphoreSlim writeLock = new(1, 1); - private readonly Http2FlowController sendFlow = new(); + private readonly Http2FlowController sendFlow; private readonly Http2Settings originSettings = new(); // Client-initiated stream ids are odd (1,3,5,…). Index = streamId >> 1. // Volatile slot publishes replace ConcurrentDictionary on the origin ReadLoop / SendAsync path. @@ -99,13 +102,16 @@ internal sealed class Http2OriginConnection : IDisposable private readonly byte[] encodeFrameHeaderBuffer = new byte[9]; private Http2OriginConnection(TcpServerConnection connection, ILogger logger, - ProxyResourceLimits resourceLimits) + ProxyResourceLimits resourceLimits, int decodedHeaderListBytes) { this.connection = connection; socket = connection.Stream; stream = socket; this.logger = logger; this.resourceLimits = resourceLimits; + sendFlow = new Http2FlowController(TimeSpan.FromSeconds(resourceLimits.Http2WindowUpdateTimeoutSeconds)); + maxCompressedHeaderBlockBytes = resourceLimits.MaxHttp2CompressedHeaderBlockBytes; + maxDecodedHeaderListBytes = decodedHeaderListBytes > 0 ? decodedHeaderListBytes : 64 * 1024; } /// True while this connection may still be leased for a new request. @@ -311,12 +317,12 @@ private void TryDisposeIfRetiredAndIdle() /// internal static async Task CreateAsync(TcpServerConnection connection, ILogger logger, CancellationToken cancellationToken, - ProxyResourceLimits? resourceLimits = null) + ProxyResourceLimits? resourceLimits = null, int maxDecodedHeaderListBytes = 64 * 1024) { // 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); + resourceLimits ?? ProxyResourceLimits.Default, maxDecodedHeaderListBytes); var preface = Http2Helper.ConnectionPreface; connection.Http2SessionStarted = true; @@ -1125,10 +1131,12 @@ private void FlushOriginStreamCredit(int streamId, PendingStream pending, bool f headerBlockStreamId = streamId; headerBlockEndStream = (flags & Http2FrameFlag.EndStream) != 0; var data = StripHeadersFraming(payloadSpan, flags); - if (data.Length > MaxHeaderBlockBytes) + if (data.Length > maxCompressedHeaderBlockBytes) { + ProxyLog.LimitExceeded(logger, LimitId.Http2CompressedHeaderBlock, PolicyMode.Enforce, + data.Length, maxCompressedHeaderBlockBytes, "connection closed"); Fail(new IOException( - $"HTTP/2 protocol error: origin header block exceeded {MaxHeaderBlockBytes} bytes.")); + $"HTTP/2 protocol error: origin header block exceeded {maxCompressedHeaderBlockBytes} bytes.")); return; } @@ -1164,10 +1172,12 @@ private void FlushOriginStreamCredit(int streamId, PendingStream pending, bool f return; } - if (headerBlockBuffer.Length + length > MaxHeaderBlockBytes) + if (headerBlockBuffer.Length + length > maxCompressedHeaderBlockBytes) { + ProxyLog.LimitExceeded(logger, LimitId.Http2CompressedHeaderBlock, PolicyMode.Enforce, + headerBlockBuffer.Length + length, maxCompressedHeaderBlockBytes, "connection closed"); Fail(new IOException( - $"HTTP/2 protocol error: origin CONTINUATION block exceeded {MaxHeaderBlockBytes} bytes.")); + $"HTTP/2 protocol error: origin CONTINUATION block exceeded {maxCompressedHeaderBlockBytes} bytes.")); return; } @@ -1607,9 +1617,18 @@ private static void ThrowIfDataPaddingTooLong(int padLength, int payloadLength) try { - decoder ??= new Decoder(8192, 4096); + decoder ??= new Decoder(maxDecodedHeaderListBytes, 4096); decoder.Decode(compressed, headerCollector); - decoder.EndHeaderBlock(); + if (decoder.EndHeaderBlock()) + { + // Truncation keeps the dynamic table consistent (indexed entries are still inserted). + // Fail only this stream so siblings on the connection stay healthy. + ProxyLog.LimitExceeded(logger, LimitId.DecodedHeaderList, PolicyMode.Enforce, + maxDecodedHeaderListBytes + 1L, maxDecodedHeaderListBytes, "502"); + FailStream(streamId, new ProxyHttpException( + "Origin HTTP/2 header list exceeded maxDecodedHeaderListBytes.", null, null)); + return; + } } catch (Exception ex) { diff --git a/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs b/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs index aa02879e2..3d393bb3f 100644 --- a/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs +++ b/src/Titanium.Web.Proxy/Http2/Http2OriginRelayPool.cs @@ -267,9 +267,10 @@ await Http2Helper.SendHttp2ClientConnectionStartupAsync(connection.Stream, cance } } - private static OriginLeg CreateLeg(TcpServerConnection connection, SemaphoreSlim writeLock, bool ownsWriteLock) + private OriginLeg CreateLeg(TcpServerConnection connection, SemaphoreSlim writeLock, bool ownsWriteLock) { - return new OriginLeg(connection, new Http2FrameWriter(connection.Stream, writeLock), writeLock, ownsWriteLock); + return new OriginLeg(connection, new Http2FrameWriter(connection.Stream, writeLock), writeLock, ownsWriteLock, + resourceLimits.MaxDeferredOutboundBytesPerStream, resourceLimits.Http2WindowUpdateTimeoutSeconds); } private static async Task DisposeLegAsync(OriginLeg leg) @@ -304,7 +305,7 @@ public async ValueTask DisposeAsync() internal sealed class OriginLeg { public OriginLeg(TcpServerConnection connection, Http2FrameWriter writer, SemaphoreSlim writeLock, - bool ownsWriteLock) + bool ownsWriteLock, int maxDeferredBytesPerStream = 0, int windowUpdateTimeoutSeconds = 0) { Connection = connection; Writer = writer; @@ -312,8 +313,12 @@ public OriginLeg(TcpServerConnection connection, Http2FrameWriter writer, Semaph WriteLock = writeLock; OwnsWriteLock = ownsWriteLock; NextStreamId = 1; - SendFlow = new Http2FlowController(); - OutboundDeferred = new Http2DeferredOutboundData(); + SendFlow = new Http2FlowController(windowUpdateTimeoutSeconds > 0 + ? TimeSpan.FromSeconds(windowUpdateTimeoutSeconds) + : null); + OutboundDeferred = new Http2DeferredOutboundData(maxDeferredBytesPerStream > 0 + ? maxDeferredBytesPerStream + : Http2DeferredOutboundData.HardMaxDeferredBytesPerStream); } public TcpServerConnection Connection { get; } diff --git a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs index ef7f235b3..f60bdc154 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs @@ -5,6 +5,8 @@ using System.Net.Quic; using System.Threading; using System.Threading.Tasks; +using Titanium.Web.Proxy.Logging; +using Titanium.Web.Proxy.Options; namespace Titanium.Web.Proxy.Http3; @@ -26,6 +28,15 @@ internal sealed class Http3Frame /// internal const long DefaultMaxPayloadBytes = 4 * 1024 * 1024; + /// + /// Declared lengths above this are rented as data arrives, starting at this size, so a peer + /// that advertises a huge length and sends nothing cannot reserve the full cap up front. + /// + internal const int ProgressiveRentChunkBytes = 256 * 1024; + + /// Last payload rent size on the progressive path. Tests read this; production does not branch on it. + internal static int TestLastRentBytes; + public ulong Type { get; } public ReadOnlyMemory Payload { get; } @@ -47,6 +58,12 @@ public void ReturnPayload() ArrayPool.Shared.Return(buffer); } + internal readonly struct FrameHeader + { + public ulong Type { get; init; } + public long Length { get; init; } + } + /// /// Reads one HTTP/3 frame from . /// Returns when the stream is cleanly closed (end-of-data). @@ -57,37 +74,116 @@ public void ReturnPayload() long maxPayloadBytes, CancellationToken cancellationToken) { - var frameType = await Http3VarInt.ReadAsync(stream, cancellationToken); + var header = await ReadFrameHeaderAsync(stream, cancellationToken).ConfigureAwait(false); + if (header is null) return null; + return await ReadPayloadAfterHeaderAsync(stream, header.Value, maxPayloadBytes, cancellationToken) + .ConfigureAwait(false); + } + + /// Reads the type and length varints. Null on a clean end of stream before the type. + internal static async ValueTask ReadFrameHeaderAsync(Stream stream, CancellationToken cancellationToken) + { + var frameType = await Http3VarInt.ReadAsync(stream, cancellationToken).ConfigureAwait(false); if (frameType is null) return null; - var payloadLength = await Http3VarInt.ReadAsync(stream, cancellationToken) + var payloadLength = await Http3VarInt.ReadAsync(stream, cancellationToken).ConfigureAwait(false) ?? throw new Http3ConnectionException(Http3ErrorCode.FrameError, "Unexpected end of stream reading frame length."); + if (payloadLength > long.MaxValue) + throw new Http3ConnectionException(Http3ErrorCode.FrameError, "HTTP/3 frame length does not fit."); + return new FrameHeader { Type = frameType.Value, Length = (long)payloadLength }; + } - // 0 means "use the dedicated frame limit", not unlimited. A hostile peer picks - // payloadLength; checking before the int cast is what stops the rent. + /// Reads a payload whose header was already consumed. Enforces . + internal static ValueTask ReadPayloadAfterHeaderAsync( + Stream stream, FrameHeader header, long maxPayloadBytes, CancellationToken cancellationToken) + { var limit = maxPayloadBytes > 0 ? maxPayloadBytes : DefaultMaxPayloadBytes; - if (payloadLength > (ulong)limit || payloadLength > int.MaxValue) + if ((ulong)header.Length > (ulong)limit || header.Length > int.MaxValue) + { + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, LimitId.Http3FramePayload, PolicyMode.Enforce, + header.Length, limit, "HTTP/3 stream reset"); throw new Http3ConnectionException(Http3ErrorCode.ExcessiveLoad, - $"HTTP/3 frame payload {payloadLength} bytes exceeds limit {limit}."); + $"HTTP/3 frame payload {header.Length} bytes exceeds limit {limit}."); + } + if (header.Length == 0) + return new ValueTask(new Http3Frame(header.Type, ReadOnlyMemory.Empty, null)); + return ReadPayloadBytesAsync(stream, header.Type, (int)header.Length, cancellationToken); + } - if (payloadLength == 0) - return new Http3Frame(frameType.Value, ReadOnlyMemory.Empty, null); + /// + /// Copies a DATA payload in slices without renting the declared length. Used by the no-hook + /// relay so a frame larger than the whole-frame cap can still stream. + /// + internal static async ValueTask CopyPayloadAsync( + Stream stream, long length, int sliceBytes, + Func, CancellationToken, ValueTask> consume, + CancellationToken cancellationToken) + { + if (length < 0) + throw new Http3ConnectionException(Http3ErrorCode.FrameError, "HTTP/3 frame length is negative."); + if (length == 0) return; + var slice = sliceBytes > 0 ? sliceBytes : 16 * 1024; + var buffer = ArrayPool.Shared.Rent(slice); + try + { + var remaining = length; + while (remaining > 0) + { + var n = (int)Math.Min(slice, remaining); + var filled = 0; + while (filled < n) + { + var read = await stream.ReadAsync(buffer.AsMemory(filled, n - filled), cancellationToken) + .ConfigureAwait(false); + if (read == 0) + throw new Http3ConnectionException(Http3ErrorCode.FrameError, + $"Unexpected end of stream reading frame payload (expected {length})."); + filled += read; + } + + await consume(buffer.AsMemory(0, n), cancellationToken).ConfigureAwait(false); + remaining -= n; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } - var length = (int)payloadLength; - var rented = ArrayPool.Shared.Rent(length); + private static async ValueTask ReadPayloadBytesAsync( + Stream stream, ulong frameType, int length, CancellationToken cancellationToken) + { + // Small frames keep the single rent. Larger declared lengths grow as bytes arrive. + var initial = length <= ProgressiveRentChunkBytes ? length : ProgressiveRentChunkBytes; + var rented = ArrayPool.Shared.Rent(initial); + if (length > ProgressiveRentChunkBytes) + TestLastRentBytes = initial; + var capacity = initial; try { var offset = 0; while (offset < length) { - var read = await stream.ReadAsync(rented.AsMemory(offset, length - offset), cancellationToken); + if (offset == capacity) + { + var next = Math.Min(length, capacity * 2); + var bigger = ArrayPool.Shared.Rent(next); + Buffer.BlockCopy(rented, 0, bigger, 0, offset); + ArrayPool.Shared.Return(rented); + rented = bigger; + capacity = next; + } + + var read = await stream.ReadAsync(rented.AsMemory(offset, Math.Min(capacity, length) - offset), + cancellationToken).ConfigureAwait(false); if (read == 0) throw new Http3ConnectionException(Http3ErrorCode.FrameError, - $"Unexpected end of stream reading frame payload (expected {payloadLength}, got {offset})."); + $"Unexpected end of stream reading frame payload (expected {length}, got {offset})."); offset += read; } - return new Http3Frame(frameType.Value, rented.AsMemory(0, length), rented); + return new Http3Frame(frameType, rented.AsMemory(0, length), rented); } catch { diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs index c60f3284b..be5678572 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Http2.cs @@ -218,7 +218,7 @@ private static async Task SendHttp2OriginFastWithGoAwayRetr null, sessionArgs); } - return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits, server.MaxDecodedHeaderListBytes); } finally { @@ -485,7 +485,7 @@ private readonly record struct Http2OriginTarget( null, sessionArgs); } - return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits); + return await Http2OriginConnection.CreateAsync(tcp, logger, ct, server.ResourceLimits, server.MaxDecodedHeaderListBytes); }, cancellationToken); } diff --git a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs index a8b353e4d..f009c8963 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3OriginBridge.Quic.cs @@ -17,6 +17,7 @@ using Titanium.Web.Proxy.Http.Responses; using Titanium.Web.Proxy.Http2; using Titanium.Web.Proxy.Http3.Qpack; +using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network; using Titanium.Web.Proxy.Network.Quic; @@ -173,7 +174,7 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio originStream.CompleteWrites(); sessionArgs.Timing?.MarkRequestSent(); - const int maxInterimResponses = 20; + var maxInterimResponses = server.ResourceLimits.MaxInterimResponses; int interimCount = 0; Http3Frame? responseHeadersFrame; @@ -212,8 +213,12 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio if (finalStatus is >= 100 and < 200) { if (++interimCount > maxInterimResponses) + { + ProxyLog.LimitExceeded(logger, LimitId.InterimResponses, PolicyMode.Enforce, + interimCount, maxInterimResponses, "stream reset"); throw new Http3StreamException(Http3ErrorCode.InternalError, $"Origin sent more than {maxInterimResponses} interim responses."); + } if (onInterimResponse != null) { @@ -254,7 +259,7 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio } } - var maxPayload = Http3Frame.DefaultMaxPayloadBytes; + var maxPayload = server.ResourceLimits.MaxHttp3FramePayloadBytes; // 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 @@ -367,8 +372,18 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio { while (true) { - var frame = await Http3Frame.ReadAsync(streamToClient, maxPayloadBytes: maxPayload, ct); - if (frame == null) break; + var header = await Http3Frame.ReadFrameHeaderAsync(streamToClient, ct); + if (header is null) break; + if (header.Value.Type == Http3FrameType.Data) + { + if (header.Value.Length > 0) + await Http3Frame.CopyPayloadAsync(streamToClient, header.Value.Length, 16 * 1024, + (slice, token) => clientBodyStream.WriteAsync(slice, token), ct); + continue; + } + + var frame = await Http3Frame.ReadPayloadAfterHeaderAsync( + streamToClient, header.Value, maxPayload, ct); try { if (frame.Type == Http3FrameType.Headers) @@ -381,10 +396,6 @@ await Http3Frame.WriteAsync(originStream, Http3FrameType.Data, body, cancellatio break; } - if (frame.Type != Http3FrameType.Data || frame.Payload.Length == 0) - continue; - - await clientBodyStream.WriteAsync(frame.Payload, ct); } finally { @@ -666,7 +677,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(Http3Frame.DefaultMaxPayloadBytes, server.MaxDecodedHeaderListBytes); + var maxPayload = Math.Max(server.ResourceLimits.MaxHttp3FramePayloadBytes, server.MaxDecodedHeaderListBytes); var captureForMitm = clientStream is null; var sawFinalHeaders = false; while (true) diff --git a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs index ec7f9e341..c006677c3 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3RequestStream.cs @@ -12,6 +12,7 @@ using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Http3.Qpack; +using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network.Quic; using Titanium.Web.Proxy.Network.Streams; @@ -268,6 +269,8 @@ await HandleH3OriginFastPathAsync( catch (BodySizeLimitExceededException) { // GetRequestBody() during BeforeRequest hit MaxBufferedBodyBytes. + ProxyLog.LimitExceeded(server.Logger, LimitId.BufferedBody, PolicyMode.Enforce, + server.MaxBufferedBodyBytes, server.MaxBufferedBodyBytes, "413"); await SendSimpleStatusResponseAsync(stream, 413, qpackContext, cancellationToken); stream.Abort(QuicAbortDirection.Read, (long)Http3ErrorCode.ExcessiveLoad); streamState.RequestClosed = true; @@ -1006,12 +1009,13 @@ private static async Task BufferRequestBodyAsync( { var body = new MemoryStream(); var maxBufferedBodyBytes = sessionArgs.MaxBufferedBodyBytes ?? server.MaxBufferedBodyBytes; + var maxFrame = server.ResourceLimits.MaxHttp3FramePayloadBytes; var boundedBody = new BoundedWriteStream(body, maxBufferedBodyBytes, server.PolicyModes[PolicyFamily.BodyBudget]); try { while (true) { - var frame = await Http3Frame.ReadAsync(stream, maxPayloadBytes: 0, ct); + var frame = await Http3Frame.ReadAsync(stream, maxPayloadBytes: maxFrame, ct); if (frame is null) break; try { @@ -1087,25 +1091,28 @@ await StreamRequestBodyToWriteAsync( if (!hasHook) { + var maxFrame = server.ResourceLimits.MaxHttp3FramePayloadBytes; while (true) { - var frame = await Http3Frame.ReadAsync(clientStream, maxPayloadBytes: 0, ct); - if (frame is null) break; + var header = await Http3Frame.ReadFrameHeaderAsync(clientStream, ct); + if (header is null) break; + if (header.Value.Type == Http3FrameType.Data) + { + if (header.Value.Length > 0) + await Http3Frame.CopyPayloadAsync(clientStream, header.Value.Length, 16 * 1024, writeData, ct); + continue; + } + + var frame = await Http3Frame.ReadPayloadAfterHeaderAsync(clientStream, header.Value, maxFrame, ct); try { - switch (frame.Type) + if (frame.Type == Http3FrameType.Headers) { - case Http3FrameType.Data: - if (frame.Payload.Length > 0) - await writeData(frame.Payload, ct); - break; - case Http3FrameType.Headers: - if (onTrailerFrame != null) - await onTrailerFrame(frame.Payload, ct); - var trailers = QpackDecoder.Decode(frame.Payload.Span); - foreach (var (name, value) in trailers) - request.TrailingHeaders.AddHeader(new HttpHeader(name, value)); - break; + if (onTrailerFrame != null) + await onTrailerFrame(frame.Payload, ct); + var trailers = QpackDecoder.Decode(frame.Payload.Span); + foreach (var (name, value) in trailers) + request.TrailingHeaders.AddHeader(new HttpHeader(name, value)); } } finally @@ -1116,10 +1123,11 @@ await StreamRequestBodyToWriteAsync( } else { - var current = await Http3Frame.ReadAsync(clientStream, maxPayloadBytes: 0, ct); + var maxFrame = server.ResourceLimits.MaxHttp3FramePayloadBytes; + var current = await Http3Frame.ReadAsync(clientStream, maxPayloadBytes: maxFrame, ct); while (current != null) { - var next = await Http3Frame.ReadAsync(clientStream, maxPayloadBytes: 0, ct); + var next = await Http3Frame.ReadAsync(clientStream, maxPayloadBytes: maxFrame, ct); var isLast = next == null || next.Type == Http3FrameType.Headers; try diff --git a/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs b/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs new file mode 100644 index 000000000..e411e54ed --- /dev/null +++ b/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs @@ -0,0 +1,192 @@ +using System; +using System.Threading; +using Titanium.Web.Proxy.Options; + +namespace Titanium.Web.Proxy.Logging; + +/// +/// Stable id for one limit-driven failure. The rate limiter indexes this; do not renumber. +/// +internal enum LimitId +{ + BufferedBody = 0, + HeaderCount = 1, + HeaderAggregate = 2, + HeaderLine = 3, + Http3FramePayload = 4, + DeferredOutboundBytes = 5, + DecodedHeaderList = 6, + TrailerHeaderCount = 7, + TrailerHeaderBlock = 8, + Http2CompressedHeaderBlock = 9, + InterimResponses = 10, + AuthChallengeRounds = 11, + UpstreamProxyAuthAttempts = 12, + WinAuthToken = 13, + Http2WindowUpdate = 14, + UpgradeHandshakeHeaders = 15, + AdmissionGlobal = 16, + AdmissionEndpoint = 17, + TimeoutConnect = 18, + TimeoutResponseHeader = 19, + TimeoutIdleRead = 20, + TimeoutIdleWrite = 21, + TimeoutRequest = 22, + TimeoutClientHeader = 23, + WebSocketFrame = 24, + Count = 25 +} + +/// +/// One row of the operator-facing limit reference: property, CLI key, default, and the hint +/// printed when the limit rejects a request. Docs and the config applier are checked against this. +/// +internal readonly struct LimitEntry +{ + public LimitEntry(LimitId id, string property, string cliKey, string what, string familyKey, + string hint, PolicyFamily? family) + { + Id = id; + Property = property; + CliKey = cliKey; + What = what; + FamilyKey = familyKey; + Hint = hint; + Family = family; + } + + public LimitId Id { get; } + public string Property { get; } + public string CliKey { get; } + public string What { get; } + public string FamilyKey { get; } + public string Hint { get; } + public PolicyFamily? Family { get; } +} + +/// Single source of truth for limit names, CLI keys, and log hints. +internal static class LimitCatalog +{ + private static readonly LimitEntry[] Entries = Build(); + + internal static LimitEntry Get(LimitId id) => Entries[(int)id]; + + internal static ReadOnlySpan All => Entries; + + internal static string Reason(LimitId id) => id.ToString(); + + private static LimitEntry[] Build() + { + var all = new LimitEntry[(int)LimitId.Count]; + void Add(LimitEntry entry) => all[(int)entry.Id] = entry; + + Add(Row(LimitId.BufferedBody, "MaxBufferedBodyBytes", "server.limits.maxBufferedBodyBytes", + "buffered body", "bodyBudget", PolicyFamily.BodyBudget)); + Add(Row(LimitId.HeaderCount, "MaxHeaderCount", "server.limits.maxHeaderCount", + "header count", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.HeaderAggregate, "MaxHeaderAggregateBytes", "server.limits.maxHeaderAggregateBytes", + "header block size", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.HeaderLine, "MaxHeaderLineBytes", "server.limits.maxHeaderLineBytes", + "header line", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.Http3FramePayload, "MaxHttp3FramePayloadBytes", "server.limits.maxHttp3FramePayloadBytes", + "HTTP/3 frame payload", "bodyBudget", PolicyFamily.BodyBudget)); + Add(Row(LimitId.DeferredOutboundBytes, "MaxDeferredOutboundBytesPerStream", + "server.limits.maxDeferredOutboundBytesPerStream", + "HTTP/2 deferred DATA", "bodyBudget", PolicyFamily.BodyBudget)); + Add(Row(LimitId.DecodedHeaderList, "MaxDecodedHeaderListBytes", "server.limits.maxDecodedHeaderListBytes", + "decoded header list", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.TrailerHeaderCount, "MaxTrailerHeaderCount", "server.limits.maxTrailerHeaderCount", + "trailer header count", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.TrailerHeaderBlock, "MaxTrailerHeaderBlockBytes", "server.limits.maxTrailerHeaderBlockBytes", + "trailer header block", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.Http2CompressedHeaderBlock, "MaxHttp2CompressedHeaderBlockBytes", + "server.limits.maxHttp2CompressedHeaderBlockBytes", + "compressed header block", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.InterimResponses, "MaxInterimResponses", "server.limits.maxInterimResponses", + "interim responses", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.AuthChallengeRounds, "MaxAuthChallengeRounds", "server.limits.maxAuthChallengeRounds", + "authentication challenge rounds", "admissionControl", PolicyFamily.AdmissionControl)); + Add(Row(LimitId.UpstreamProxyAuthAttempts, "MaxUpstreamProxyAuthenticationAttempts", + "server.limits.maxUpstreamProxyAuthenticationAttempts", + "upstream proxy authentication attempts", "admissionControl", PolicyFamily.AdmissionControl)); + Add(Row(LimitId.WinAuthToken, "MaxWinAuthTokenBytes", "server.limits.maxWinAuthTokenBytes", + "Windows authentication token", "admissionControl", PolicyFamily.AdmissionControl)); + Add(Row(LimitId.Http2WindowUpdate, "Http2WindowUpdateTimeoutSeconds", + "server.limits.http2WindowUpdateTimeoutSeconds", + "HTTP/2 flow-control wait", "http2AbuseBudget", PolicyFamily.Http2AbuseBudget)); + Add(Row(LimitId.UpgradeHandshakeHeaders, "MaxHeaderCount", "server.limits.maxHeaderCount", + "upgrade handshake headers", "headerLimits", PolicyFamily.HeaderLimits)); + Add(Row(LimitId.AdmissionGlobal, "MaxConcurrentClientConnections", + "server.pooling.maxConcurrentClientConnections", + "client connections", "admissionControl", PolicyFamily.AdmissionControl)); + Add(Row(LimitId.AdmissionEndpoint, "MaxConcurrentClients", "server.limits.maxConcurrentClients", + "endpoint client connections", "admissionControl", PolicyFamily.AdmissionControl)); + Add(Timeout(LimitId.TimeoutConnect, "ConnectTimeOutSeconds", "server.timeouts.connectTimeOutSeconds", + "connect")); + Add(Timeout(LimitId.TimeoutResponseHeader, "ResponseHeaderTimeoutSeconds", + "server.timeouts.responseHeaderTimeoutSeconds", "response header")); + Add(Timeout(LimitId.TimeoutIdleRead, "IdleReadTimeoutSeconds", "server.timeouts.idleReadTimeoutSeconds", + "idle read")); + Add(Timeout(LimitId.TimeoutIdleWrite, "IdleWriteTimeoutSeconds", "server.timeouts.idleWriteTimeoutSeconds", + "idle write")); + Add(Timeout(LimitId.TimeoutRequest, "RequestTimeoutSeconds", "server.timeouts.requestTimeoutSeconds", + "request")); + Add(Timeout(LimitId.TimeoutClientHeader, "ClientHeaderTimeoutSeconds", + "server.timeouts.clientHeaderTimeoutSeconds", "client header")); + Add(Row(LimitId.WebSocketFrame, "MaxWebSocketFramePayloadBytes", + "server.limits.maxWebSocketFramePayloadBytes", + "WebSocket frame", "webSocketFrameBudget", PolicyFamily.WebSocketFrameBudget)); + return all; + } + + private static LimitEntry Row(LimitId id, string property, string cliKey, string what, string familyKey, + PolicyFamily family) => + new(id, property, cliKey, what, familyKey, + $"Raise {cliKey} or set server.policyModes.{familyKey} to observe or disabled.", family); + + private static LimitEntry Timeout(LimitId id, string property, string cliKey, string what) => + new(id, property, cliKey, what + " timeout", "timeouts", + $"Raise {cliKey}.", null); +} + +/// +/// Per-limit log throttle. First event is logged immediately; later events within 10 seconds +/// increment a suppressed count and are dropped. No locks. A suppressed call allocates nothing. +/// +internal static class LimitLogThrottle +{ + internal const long WindowTicks = 10L * TimeSpan.TicksPerSecond; + + private static readonly long[] LastTicks = new long[(int)LimitId.Count]; + private static readonly int[] Suppressed = new int[(int)LimitId.Count]; + + /// + /// Returns the number of events suppressed since the previous logged line (0 on the first), + /// or -1 when this call must not log. + /// + internal static int TryAcquire(LimitId id) + { + var index = (int)id; + var now = DateTime.UtcNow.Ticks; + var last = Volatile.Read(ref LastTicks[index]); + if (last != 0 && now - last < WindowTicks) + { + Interlocked.Increment(ref Suppressed[index]); + return -1; + } + + if (Interlocked.CompareExchange(ref LastTicks[index], now, last) != last) + { + Interlocked.Increment(ref Suppressed[index]); + return -1; + } + + return Interlocked.Exchange(ref Suppressed[index], 0); + } + + internal static void Reset() + { + Array.Clear(LastTicks); + Array.Clear(Suppressed); + } +} diff --git a/src/Titanium.Web.Proxy/Logging/ProxyDiagnostics.cs b/src/Titanium.Web.Proxy/Logging/ProxyDiagnostics.cs index 12525eb8f..d1199da55 100644 --- a/src/Titanium.Web.Proxy/Logging/ProxyDiagnostics.cs +++ b/src/Titanium.Web.Proxy/Logging/ProxyDiagnostics.cs @@ -158,6 +158,10 @@ internal static bool IsExpected(Exception exception) { OperationCanceledException => true, ObjectDisposedException => true, + // Policy rejections inherit IOException. They must stay visible at Error; a client + // disconnect is still the plain IOException arm below. + Titanium.Web.Proxy.Network.Streams.BodySizeLimitExceededException => false, + Titanium.Web.Proxy.Http2.Http2HeaderListTooLargeException => false, System.IO.IOException => true, System.Net.Sockets.SocketException => true, // Browser/OS clients routinely abort MITM handshakes (speculative CONNECT, idle tab diff --git a/src/Titanium.Web.Proxy/Logging/ProxyLog.cs b/src/Titanium.Web.Proxy/Logging/ProxyLog.cs index ee1469c22..6e9c1e783 100644 --- a/src/Titanium.Web.Proxy/Logging/ProxyLog.cs +++ b/src/Titanium.Web.Proxy/Logging/ProxyLog.cs @@ -111,9 +111,11 @@ internal static void OriginConnectionFailed(ILogger logger, string host, int por internal static void ClientConnectionAdmissionRejected(ILogger logger, Models.ProxyEndPoint endPoint, string reason) { - if (!logger.IsEnabled(LogLevel.Warning)) return; - logger.LogWarning("Rejected a client connection on {Endpoint} ({Reason}).", - $"{endPoint.IpAddress}:{endPoint.Port}", reason); + var id = string.Equals(reason, "endpoint limit", StringComparison.Ordinal) + ? LimitId.AdmissionEndpoint + : LimitId.AdmissionGlobal; + LimitExceeded(logger, id, Options.PolicyMode.Enforce, 0, reason, "connection rejected", + $"{endPoint.IpAddress}:{endPoint.Port}"); } /// @@ -146,11 +148,73 @@ internal static void EffectiveProfileAtStartup(ILogger logger, Options.ProxyProf internal static void PolicyBreach(ILogger logger, Options.PolicyFamily family, Options.PolicyMode mode, string detail) { - var level = mode == Options.PolicyMode.Enforce ? LogLevel.Warning : LogLevel.Debug; + // Enforce rejects a request, so it must be visible at the default Error minimum level. + var level = mode == Options.PolicyMode.Enforce ? LogLevel.Error : LogLevel.Debug; if (!logger.IsEnabled(level)) return; logger.Log(level, "Policy family {Family} breached under {Mode}: {Detail}", family, mode, detail); } + /// + /// A configured limit rejected or observed a request. Enforce logs at Error, Observe at Debug. + /// The line names the property, the limit, the client-visible action, and the CLI key. + /// Logged at most once per 10 seconds per ; metrics count every call. + /// Never pass URLs, header values, cookies, or bodies. is a host name only. + /// + internal static void LimitExceeded(ILogger logger, LimitId id, Options.PolicyMode mode, + long observed, object limit, string action, string? host = null) + { + var entry = LimitCatalog.Get(id); + if (entry.Family is { } family) + global::Titanium.Web.Proxy.Diagnostics.ProxyMetrics.PolicyBreach(family, mode); + global::Titanium.Web.Proxy.Diagnostics.ProxyMetrics.StreamRejected(entry.FamilyKey, LimitCatalog.Reason(id)); + + var suppressed = LimitLogThrottle.TryAcquire(id); + if (suppressed < 0) return; + + var level = mode == Options.PolicyMode.Enforce ? LogLevel.Error : LogLevel.Debug; + if (!logger.IsEnabled(level)) return; + + if (suppressed > 0) + { + logger.Log(level, + "{Family} ({Mode}): {What} exceeded {Property}={Limit}; observed {Observed}; client got {Action}. {Hint} ({Suppressed} similar events suppressed){HostSuffix}", + entry.FamilyKey, mode, entry.What, entry.Property, limit, observed, action, entry.Hint, + suppressed, FormatHostSuffix(host)); + return; + } + + logger.Log(level, + "{Family} ({Mode}): {What} exceeded {Property}={Limit}; observed {Observed}; client got {Action}. {Hint}{HostSuffix}", + entry.FamilyKey, mode, entry.What, entry.Property, limit, observed, action, entry.Hint, + FormatHostSuffix(host)); + } + + /// + /// Decrypt-bypass learning changed later CONNECTs for this host to opaque tunnels. + /// Warning, not Error: the current request was not failed. Host name only. + /// + internal static void DecryptFailureBypassLearned(ILogger logger, string host) + { + if (!logger.IsEnabled(LogLevel.Warning)) return; + logger.LogWarning( + "EnableDecryptFailureBypass learned {Host}; later CONNECTs tunnel opaque. Set server.enableDecryptFailureBypass to false to stop learning.", + host); + } + + /// + /// Warns when an operator set a limit the runtime still treats as reserved. + /// + internal static void ReservedLimit(ILogger logger, string cliKey, object value, string instead) + { + if (!logger.IsEnabled(LogLevel.Warning)) return; + logger.LogWarning( + "{CliKey}={Value} is reserved and not enforced. {Instead}", + cliKey, value, instead); + } + + private static string FormatHostSuffix(string? host) => + string.IsNullOrEmpty(host) ? string.Empty : " host " + host; + internal static void Http2ProbeResult(ILogger logger, string connectTarget, bool fromCache, bool supported, Exception? failure) { diff --git a/src/Titanium.Web.Proxy/Network/HttpWebClient.cs b/src/Titanium.Web.Proxy/Network/HttpWebClient.cs index 26429223c..60cec297d 100644 --- a/src/Titanium.Web.Proxy/Network/HttpWebClient.cs +++ b/src/Titanium.Web.Proxy/Network/HttpWebClient.cs @@ -403,7 +403,8 @@ private ValueTask ReceiveResponseAfterStatus(ResponseStatusInfo? httpStatus, Response.StatusCode = httpStatus.Value.StatusCode; Response.StatusDescription = httpStatus.Value.Description; - var headersVt = HeaderParser.TryReadHeadersAsync(Connection.Stream, Response.Headers, cancellationToken); + var headersVt = HeaderParser.TryReadHeadersAsync(Connection.Stream, Response.Headers, cancellationToken, + Connection.Stream.HeaderBudget(isRequest: false)); if (headersVt.IsCompletedSuccessfully) { if (!headersVt.Result) diff --git a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.Body.cs b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.Body.cs index 127150807..aeed112ac 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.Body.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.Body.cs @@ -67,7 +67,8 @@ public async ValueTask CopyBodyAsync(RequestResponseBase requestResponse, bool u : requestResponse.ContentEncoding; Stream s = limitedStream = new LimitedStream(this, bufferPool, isChunked, contentLength, - requestResponse.TrailingHeaders); + requestResponse.TrailingHeaders, server.ResourceLimits.MaxTrailerHeaderCount, + server.ResourceLimits.MaxTrailerHeaderBlockBytes); if (transformation == TransformationMode.Uncompress && contentEncoding != null) { @@ -227,7 +228,8 @@ async Task drainRemainingChunkedBody(long remainingInCurrentChunk) { // discard the trailer block too - it belongs to a message we chose not to forward in full await ChunkedTrailerHelper.ReadTrailingHeaders(this, new HeaderCollection(), null, - cancellationToken); + cancellationToken, server.ResourceLimits.MaxTrailerHeaderCount, + server.ResourceLimits.MaxTrailerHeaderBlockBytes); return; } @@ -263,7 +265,8 @@ async Task drainRemainingChunkedBody(long remainingInCurrentChunk) // line, populating requestResponse.TrailingHeaders (writeTerminator() below // re-emits them for `writer`). See ChunkedTrailerHelper for why this is bounded. await ChunkedTrailerHelper.ReadTrailingHeaders(this, requestResponse.TrailingHeaders, - null, cancellationToken); + null, cancellationToken, server.ResourceLimits.MaxTrailerHeaderCount, + server.ResourceLimits.MaxTrailerHeaderBlockBytes); await emit(Array.Empty(), true); break; } @@ -388,7 +391,8 @@ private async ValueTask CopyBodyChunkedAsync(IHttpStreamWriter writer, bool isRe // to `writer` byte-for-byte below, rather than re-serializing the parsed HeaderCollection. var rawTrailerLines = new List(); await ChunkedTrailerHelper.ReadTrailingHeaders(this, requestResponse.TrailingHeaders, - rawTrailerLines, cancellationToken); + rawTrailerLines, cancellationToken, server.ResourceLimits.MaxTrailerHeaderCount, + server.ResourceLimits.MaxTrailerHeaderBlockBytes); await ChunkedTrailerHelper.WriteRawTrailingLinesAsync(writer, rawTrailerLines, cancellationToken); diff --git a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs index fa4dae810..e21b34dc9 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/HttpStream.cs @@ -87,6 +87,10 @@ internal partial class HttpStream : Stream, IHttpStreamWriter, IHttpStreamReader /// When , skips the 8 KiB rent. Use for /// HTTP/3 session placeholders backed by that never read the client stream. /// + internal HeaderBlockBudget HeaderBudget(bool isRequest) => + new(server.ResourceLimits.MaxHeaderCount, server.ResourceLimits.MaxHeaderAggregateBytes, + server.PolicyModes[PolicyFamily.HeaderLimits], isRequest); + internal HttpStream(ProxyServer server, Stream baseStream, IBufferPool bufferPool, CancellationToken cancellationToken, bool leaveOpen = false, bool rentReadBuffer = true) { diff --git a/src/Titanium.Web.Proxy/Network/Streams/LimitedStream.cs b/src/Titanium.Web.Proxy/Network/Streams/LimitedStream.cs index 08a641b59..2e24fe327 100644 --- a/src/Titanium.Web.Proxy/Network/Streams/LimitedStream.cs +++ b/src/Titanium.Web.Proxy/Network/Streams/LimitedStream.cs @@ -15,6 +15,8 @@ internal class LimitedStream : Stream private readonly IBufferPool bufferPool; private readonly bool isChunked; private readonly HeaderCollection? trailingHeaders; + private readonly int maxTrailerHeaderCount; + private readonly int maxTrailerHeaderBlockBytes; private long bytesRemaining; private bool readChunkTrail; @@ -29,12 +31,15 @@ internal class LimitedStream : Stream /// populate a request/response's trailing headers the same way the pass-through relay path does. /// internal LimitedStream(IHttpStreamReader baseStream, IBufferPool bufferPool, bool isChunked, - long contentLength, HeaderCollection? trailingHeaders = null) + long contentLength, HeaderCollection? trailingHeaders = null, + int maxTrailerHeaderCount = 100, int maxTrailerHeaderBlockBytes = 16 * 1024) { baseReader = baseStream; this.bufferPool = bufferPool; this.isChunked = isChunked; this.trailingHeaders = trailingHeaders; + this.maxTrailerHeaderCount = maxTrailerHeaderCount; + this.maxTrailerHeaderBlockBytes = maxTrailerHeaderBlockBytes; if (isChunked) bytesRemaining = 0; else @@ -92,7 +97,7 @@ private async Task GetNextChunkAsync() // reading only a single line here (as before) left any additional trailer lines unread on the // source, corrupting a pooled keep-alive connection's next message. await ChunkedTrailerHelper.ReadTrailingHeaders(baseReader, trailingHeaders ?? new HeaderCollection(), - null); + null, default, maxTrailerHeaderCount, maxTrailerHeaderBlockBytes); } } diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs index 2f4387018..82312e559 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs @@ -22,6 +22,7 @@ using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Logging; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.ProxySocket; namespace Titanium.Web.Proxy.Network.Tcp; @@ -31,7 +32,6 @@ namespace Titanium.Web.Proxy.Network.Tcp; /// internal class TcpConnectionFactory : IDisposable { - private const int MaximumUpstreamProxyAuthenticationAttempts = 5; /// /// Maximum number of upstream CONNECT rejection body bytes retained for diagnostics. @@ -1315,6 +1315,7 @@ private static async Task EstablishHttpUpstreamConnectAsync(ProxyServer pr var authenticationData = new InternalDataStore(); var authenticationAttempts = 0; + var maxAuthAttempts = proxyServer.ResourceLimits.MaxUpstreamProxyAuthenticationAttempts; while (true) { @@ -1325,7 +1326,7 @@ private static async Task EstablishHttpUpstreamConnectAsync(ProxyServer pr ?? throw new IOException( "Upstream proxy closed the connection before sending a CONNECT response."); var headers = new HeaderCollection(); - await HeaderParser.ReadHeaders(stream, headers, cancellationToken); + await HeaderParser.ReadHeaders(stream, headers, cancellationToken, stream.HeaderBudget(isRequest: false)); if (httpStatus.StatusCode == (int)HttpStatusCode.OK || httpStatus.Description.EqualsIgnoreCase("Connection Established")) @@ -1333,9 +1334,13 @@ private static async Task EstablishHttpUpstreamConnectAsync(ProxyServer pr var bodyPreview = await DrainUpstreamProxyResponseBody(stream, headers, cancellationToken); + if (authenticationAttempts >= maxAuthAttempts) + ProxyLog.LimitExceeded(proxyServer.Logger, LimitId.UpstreamProxyAuthAttempts, PolicyMode.Enforce, + authenticationAttempts, maxAuthAttempts, "407"); + if (httpStatus.StatusCode != (int)HttpStatusCode.ProxyAuthenticationRequired || !proxy.UseDefaultCredentials || - authenticationAttempts >= MaximumUpstreamProxyAuthenticationAttempts || + authenticationAttempts >= maxAuthAttempts || !TryGetUpstreamProxyAuthenticationChallenge(headers, out var scheme, out var challenge)) { throw CreateUpstreamProxyConnectException(httpStatus, headers, bodyPreview); diff --git a/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs b/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs index 905063f58..db29f0cae 100644 --- a/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs +++ b/src/Titanium.Web.Proxy/Network/WinAuth/Security/WinAuthEndPoint.cs @@ -24,7 +24,7 @@ internal class WinAuthEndPoint /// /// internal static byte[]? AcquireInitialSecurityToken(string hostname, string authScheme, InternalDataStore data, - int attributes, WinAuthCredentials? credentials = null) + int attributes, WinAuthCredentials? credentials = null, int maxTokenBytes = 0) { if (!RunTime.IsWindows) return null; @@ -33,7 +33,7 @@ internal class WinAuthEndPoint // null for initial call var serverToken = new SecurityBufferDescription(); - var clientToken = new SecurityBufferDescription(MaximumTokenSize); + var clientToken = new SecurityBufferDescription(maxTokenBytes > 0 ? maxTokenBytes : MaximumTokenSize); MarshaledAuthIdentity? authIdentity = null; var state = new State(); var stateStored = false; @@ -203,7 +203,7 @@ private static void ZeroAndFreeUniString(ref IntPtr ptr, int knownCharLength) /// /// internal static byte[]? AcquireFinalSecurityToken(string hostname, byte[] serverChallenge, InternalDataStore data, - int attributes) + int attributes, int maxTokenBytes = 0) { if (!RunTime.IsWindows) return null; @@ -212,7 +212,7 @@ private static void ZeroAndFreeUniString(ref IntPtr ptr, int knownCharLength) // user server challenge var serverToken = new SecurityBufferDescription(serverChallenge); - var clientToken = new SecurityBufferDescription(MaximumTokenSize); + var clientToken = new SecurityBufferDescription(maxTokenBytes > 0 ? maxTokenBytes : MaximumTokenSize); var state = data.GetAs(AuthStateKey); var shouldDisposeState = false; diff --git a/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs b/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs index eaad65952..abc11320a 100644 --- a/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/Network/WinAuth/WinAuthHandler.cs @@ -25,10 +25,11 @@ internal static class WinAuthHandler /// Optional alternate credentials from WinAuthCredentialsProvider. /// internal static string GetInitialAuthToken(string serverHostname, string authScheme, InternalDataStore data, - WinAuthCredentials? credentials = null) + WinAuthCredentials? credentials = null, int maxTokenBytes = 0) { var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(serverHostname, authScheme, data, - IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection, credentials); + IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection, credentials, + maxTokenBytes); if (tokenBytes == null) throw new InvalidOperationException("Failed to acquire the initial authentication token."); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); @@ -41,11 +42,13 @@ internal static string GetInitialAuthToken(string serverHostname, string authSch /// /// /// - internal static string GetFinalAuthToken(string serverHostname, string serverToken, InternalDataStore data) + internal static string GetFinalAuthToken(string serverHostname, string serverToken, InternalDataStore data, + int maxTokenBytes = 0) { var tokenBytes = WinAuthEndPoint.AcquireFinalSecurityToken(serverHostname, Convert.FromBase64String(serverToken), - data, IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection); + data, IscReqConfidentiality | IscReqReplayDetect | IscReqSequenceDetect | IscReqConnection, + maxTokenBytes); if (tokenBytes == null) throw new InvalidOperationException("Failed to acquire the final authentication token."); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); @@ -54,9 +57,11 @@ internal static string GetFinalAuthToken(string serverHostname, string serverTok /// /// Get the initial authentication token for an upstream proxy using the current process identity. /// - internal static string GetInitialProxyAuthToken(string proxyHostname, string authScheme, InternalDataStore data) + internal static string GetInitialProxyAuthToken(string proxyHostname, string authScheme, InternalDataStore data, + int maxTokenBytes = 0) { - var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(proxyHostname, authScheme, data, 0); + var tokenBytes = WinAuthEndPoint.AcquireInitialSecurityToken(proxyHostname, authScheme, data, 0, + maxTokenBytes: maxTokenBytes); if (tokenBytes == null) throw new InvalidOperationException("Failed to acquire the initial proxy authentication token."); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); @@ -65,10 +70,11 @@ internal static string GetInitialProxyAuthToken(string proxyHostname, string aut /// /// Get the response token for an upstream proxy challenge. /// - internal static string GetFinalProxyAuthToken(string proxyHostname, string serverToken, InternalDataStore data) + internal static string GetFinalProxyAuthToken(string proxyHostname, string serverToken, InternalDataStore data, + int maxTokenBytes = 0) { var tokenBytes = WinAuthEndPoint.AcquireFinalSecurityToken(proxyHostname, - Convert.FromBase64String(serverToken), data, 0); + Convert.FromBase64String(serverToken), data, 0, maxTokenBytes); if (tokenBytes == null) throw new InvalidOperationException("Failed to acquire the final proxy authentication token."); return string.Concat(" ", Convert.ToBase64String(tokenBytes)); diff --git a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs index b3e6c61c7..792e99be0 100644 --- a/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs +++ b/src/Titanium.Web.Proxy/Options/ProxyPolicyModes.cs @@ -118,5 +118,12 @@ public ProxyPolicyModes WithAllowAmbiguousFramingEnabled() return new ProxyPolicyModes(new Dictionary(modes), true); } + /// + /// Returns a snapshot identical to this one with set explicitly. + /// A partial config overlay uses this so an omitted flag does not clear a value set by the profile. + /// + public ProxyPolicyModes WithAllowAmbiguousFraming(bool enabled) => + new(new Dictionary(modes), enabled); + internal IReadOnlyDictionary AsReadOnlyDictionary() => modes; } diff --git a/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs b/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs index 5716943e7..2f5b082c3 100644 --- a/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs +++ b/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs @@ -141,6 +141,45 @@ private ProxyResourceLimits() /// public int? MaxCertificateDiskCacheEntries { get; private init; } + /// Maximum HTTP/3 frame payload accepted before H3_EXCESSIVE_LOAD. Default 4 MiB. Ceiling 64 MiB. + public long MaxHttp3FramePayloadBytes { get; private init; } + + /// + /// Bytes of HTTP/2 DATA that may be parked per stream waiting for send-window credit. + /// Worst-case memory is concurrent streams times this value. Default 4 MiB + 16 KiB. Ceiling 64 MiB. + /// + public int MaxDeferredOutboundBytesPerStream { get; private init; } + + /// Maximum trailer header fields on one chunked message. Default 100. + public int MaxTrailerHeaderCount { get; private init; } + + /// Maximum trailer header block size in bytes. Default 16 KiB. + public int MaxTrailerHeaderBlockBytes { get; private init; } + + /// Maximum compressed HTTP/2 header block (HEADERS + CONTINUATION) in bytes. Default 256 KiB. + public int MaxHttp2CompressedHeaderBlockBytes { get; private init; } + + /// Maximum interim (1xx) responses accepted from one origin before the stream fails. Default 20. + public int MaxInterimResponses { get; private init; } + + /// Maximum Windows/proxy authentication challenge rounds per request. Default 3. + public int MaxAuthChallengeRounds { get; private init; } + + /// Maximum CONNECT authentication attempts against an upstream proxy. Default 5. + public int MaxUpstreamProxyAuthenticationAttempts { get; private init; } + + /// Maximum SSPI token buffer in bytes. Default 12288. + public int MaxWinAuthTokenBytes { get; private init; } + + /// Seconds to wait for an HTTP/2 WINDOW_UPDATE before failing the stream. Default 60. + public int Http2WindowUpdateTimeoutSeconds { get; private init; } + + /// Upper bound for . + public const long MaxHttp3FramePayloadCeiling = 64L * 1024 * 1024; + + /// Upper bound for . + public const int MaxDeferredOutboundBytesCeiling = 64 * 1024 * 1024; + /// /// Today's shipped values, carried forward as the Balanced profile's starting point. /// Header, decompression, CONTINUATION, and reset budgets stay high enough that ordinary @@ -234,7 +273,17 @@ private ProxyResourceLimits() // H1 ALPN Offer flood ~0.89×; MaxOrigin=1 SoftPick long ~0.95× H1 TLS. SoftGrow=8 // cleartext MaxOrigin=8 Offer-once regresses local H3→h2c. SoftGrow=16 Offer-once rejected. MaxOriginHttp2ConnectionsPerAuthority = 1, - MaxCertificateCacheEntries = maxCertificateCacheEntries + MaxCertificateCacheEntries = maxCertificateCacheEntries, + MaxHttp3FramePayloadBytes = 4 * 1024 * 1024, + MaxDeferredOutboundBytesPerStream = (4 * 1024 * 1024) + (16 * 1024), + MaxTrailerHeaderCount = 100, + MaxTrailerHeaderBlockBytes = 16 * 1024, + MaxHttp2CompressedHeaderBlockBytes = 256 * 1024, + MaxInterimResponses = 20, + MaxAuthChallengeRounds = 3, + MaxUpstreamProxyAuthenticationAttempts = 5, + MaxWinAuthTokenBytes = 12288, + Http2WindowUpdateTimeoutSeconds = 60 }; } @@ -261,7 +310,17 @@ public ProxyResourceLimits WithMaxConcurrentStreamsPerConnection(int maxConcurre MaxCachedConnectionsPerHost = MaxCachedConnectionsPerHost, MaxOriginHttp2ConnectionsPerAuthority = MaxOriginHttp2ConnectionsPerAuthority, MaxCertificateCacheEntries = MaxCertificateCacheEntries, - MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries + MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries, + MaxHttp3FramePayloadBytes = MaxHttp3FramePayloadBytes, + MaxDeferredOutboundBytesPerStream = MaxDeferredOutboundBytesPerStream, + MaxTrailerHeaderCount = MaxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = MaxTrailerHeaderBlockBytes, + MaxHttp2CompressedHeaderBlockBytes = MaxHttp2CompressedHeaderBlockBytes, + MaxInterimResponses = MaxInterimResponses, + MaxAuthChallengeRounds = MaxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = MaxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = MaxWinAuthTokenBytes, + Http2WindowUpdateTimeoutSeconds = Http2WindowUpdateTimeoutSeconds }; } @@ -288,7 +347,17 @@ public ProxyResourceLimits WithMaxOriginHttp2ConnectionsPerAuthority(int maxOrig MaxCachedConnectionsPerHost = MaxCachedConnectionsPerHost, MaxOriginHttp2ConnectionsPerAuthority = maxOriginHttp2ConnectionsPerAuthority, MaxCertificateCacheEntries = MaxCertificateCacheEntries, - MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries + MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries, + MaxHttp3FramePayloadBytes = MaxHttp3FramePayloadBytes, + MaxDeferredOutboundBytesPerStream = MaxDeferredOutboundBytesPerStream, + MaxTrailerHeaderCount = MaxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = MaxTrailerHeaderBlockBytes, + MaxHttp2CompressedHeaderBlockBytes = MaxHttp2CompressedHeaderBlockBytes, + MaxInterimResponses = MaxInterimResponses, + MaxAuthChallengeRounds = MaxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = MaxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = MaxWinAuthTokenBytes, + Http2WindowUpdateTimeoutSeconds = Http2WindowUpdateTimeoutSeconds }; } @@ -329,10 +398,124 @@ public ProxyResourceLimits WithCertificateCacheBounds( MaxCachedConnectionsPerHost = MaxCachedConnectionsPerHost, MaxOriginHttp2ConnectionsPerAuthority = MaxOriginHttp2ConnectionsPerAuthority, MaxCertificateCacheEntries = maxCertificateCacheEntries, - MaxCertificateDiskCacheEntries = maxCertificateDiskCacheEntries + MaxCertificateDiskCacheEntries = maxCertificateDiskCacheEntries, + MaxHttp3FramePayloadBytes = MaxHttp3FramePayloadBytes, + MaxDeferredOutboundBytesPerStream = MaxDeferredOutboundBytesPerStream, + MaxTrailerHeaderCount = MaxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = MaxTrailerHeaderBlockBytes, + MaxHttp2CompressedHeaderBlockBytes = MaxHttp2CompressedHeaderBlockBytes, + MaxInterimResponses = MaxInterimResponses, + MaxAuthChallengeRounds = MaxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = MaxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = MaxWinAuthTokenBytes, + Http2WindowUpdateTimeoutSeconds = Http2WindowUpdateTimeoutSeconds }; } + /// Returns a copy with replaced. + public ProxyResourceLimits WithMaxHttp3FramePayloadBytes(long maxHttp3FramePayloadBytes) + { + RequireInRange(maxHttp3FramePayloadBytes, MaxHttp3FramePayloadCeiling, nameof(maxHttp3FramePayloadBytes)); + return Copy(maxHttp3FramePayloadBytes: maxHttp3FramePayloadBytes); + } + + /// Returns a copy with replaced. + public ProxyResourceLimits WithMaxDeferredOutboundBytesPerStream(int maxDeferredOutboundBytesPerStream) + { + RequireInRange(maxDeferredOutboundBytesPerStream, MaxDeferredOutboundBytesCeiling, nameof(maxDeferredOutboundBytesPerStream)); + return Copy(maxDeferredOutboundBytesPerStream: maxDeferredOutboundBytesPerStream); + } + + /// Returns a copy with trailer header bounds replaced. + public ProxyResourceLimits WithTrailerHeaderBounds(int maxTrailerHeaderCount, int maxTrailerHeaderBlockBytes) + { + RequireInRange(maxTrailerHeaderCount, 10_000, nameof(maxTrailerHeaderCount)); + RequireInRange(maxTrailerHeaderBlockBytes, 1024 * 1024, nameof(maxTrailerHeaderBlockBytes)); + return Copy(maxTrailerHeaderCount: maxTrailerHeaderCount, maxTrailerHeaderBlockBytes: maxTrailerHeaderBlockBytes); + } + + /// Returns a copy with replaced. + public ProxyResourceLimits WithMaxHttp2CompressedHeaderBlockBytes(int maxHttp2CompressedHeaderBlockBytes) + { + RequireInRange(maxHttp2CompressedHeaderBlockBytes, 16 * 1024 * 1024, nameof(maxHttp2CompressedHeaderBlockBytes)); + return Copy(maxHttp2CompressedHeaderBlockBytes: maxHttp2CompressedHeaderBlockBytes); + } + + /// Returns a copy with replaced. + public ProxyResourceLimits WithMaxInterimResponses(int maxInterimResponses) + { + RequireInRange(maxInterimResponses, 100, nameof(maxInterimResponses)); + return Copy(maxInterimResponses: maxInterimResponses); + } + + /// Returns a copy with authentication bounds replaced. + public ProxyResourceLimits WithAuthenticationBounds(int maxAuthChallengeRounds, + int maxUpstreamProxyAuthenticationAttempts, int maxWinAuthTokenBytes) + { + RequireInRange(maxAuthChallengeRounds, 20, nameof(maxAuthChallengeRounds)); + RequireInRange(maxUpstreamProxyAuthenticationAttempts, 20, nameof(maxUpstreamProxyAuthenticationAttempts)); + RequireInRange(maxWinAuthTokenBytes, 1024 * 1024, nameof(maxWinAuthTokenBytes)); + return Copy( + maxAuthChallengeRounds: maxAuthChallengeRounds, + maxUpstreamProxyAuthenticationAttempts: maxUpstreamProxyAuthenticationAttempts, + maxWinAuthTokenBytes: maxWinAuthTokenBytes); + } + + /// Returns a copy with replaced. + public ProxyResourceLimits WithHttp2WindowUpdateTimeoutSeconds(int http2WindowUpdateTimeoutSeconds) + { + RequireInRange(http2WindowUpdateTimeoutSeconds, 3600, nameof(http2WindowUpdateTimeoutSeconds)); + return Copy(http2WindowUpdateTimeoutSeconds: http2WindowUpdateTimeoutSeconds); + } + + private ProxyResourceLimits Copy( + long? maxHttp3FramePayloadBytes = null, + int? maxDeferredOutboundBytesPerStream = null, + int? maxTrailerHeaderCount = null, + int? maxTrailerHeaderBlockBytes = null, + int? maxHttp2CompressedHeaderBlockBytes = null, + int? maxInterimResponses = null, + int? maxAuthChallengeRounds = null, + int? maxUpstreamProxyAuthenticationAttempts = null, + int? maxWinAuthTokenBytes = null, + int? http2WindowUpdateTimeoutSeconds = null) => + new() + { + MaxHeaderLineBytes = MaxHeaderLineBytes, + MaxHeaderCount = MaxHeaderCount, + MaxHeaderAggregateBytes = MaxHeaderAggregateBytes, + MaxEncodedBodyBytes = MaxEncodedBodyBytes, + MaxDecodedBodyBytes = MaxDecodedBodyBytes, + MaxDecompressionRatio = MaxDecompressionRatio, + MaxConcurrentClients = MaxConcurrentClients, + MaxConcurrentStreamsPerConnection = MaxConcurrentStreamsPerConnection, + MaxPeerInitiatedIncompleteStreamResets = MaxPeerInitiatedIncompleteStreamResets, + MaxOpenHeaderBlockFrames = MaxOpenHeaderBlockFrames, + MaxOpenHeaderBlockDuration = MaxOpenHeaderBlockDuration, + ConnectionPoolingEnabled = ConnectionPoolingEnabled, + MaxCachedConnectionsPerHost = MaxCachedConnectionsPerHost, + MaxOriginHttp2ConnectionsPerAuthority = MaxOriginHttp2ConnectionsPerAuthority, + MaxCertificateCacheEntries = MaxCertificateCacheEntries, + MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries, + MaxHttp3FramePayloadBytes = maxHttp3FramePayloadBytes ?? MaxHttp3FramePayloadBytes, + MaxDeferredOutboundBytesPerStream = maxDeferredOutboundBytesPerStream ?? MaxDeferredOutboundBytesPerStream, + MaxTrailerHeaderCount = maxTrailerHeaderCount ?? MaxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = maxTrailerHeaderBlockBytes ?? MaxTrailerHeaderBlockBytes, + MaxHttp2CompressedHeaderBlockBytes = maxHttp2CompressedHeaderBlockBytes ?? MaxHttp2CompressedHeaderBlockBytes, + MaxInterimResponses = maxInterimResponses ?? MaxInterimResponses, + MaxAuthChallengeRounds = maxAuthChallengeRounds ?? MaxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = maxUpstreamProxyAuthenticationAttempts ?? MaxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = maxWinAuthTokenBytes ?? MaxWinAuthTokenBytes, + Http2WindowUpdateTimeoutSeconds = http2WindowUpdateTimeoutSeconds ?? Http2WindowUpdateTimeoutSeconds + }; + + private static void RequireInRange(long value, long ceiling, string paramName) + { + RequirePositive(value, paramName); + if (value > ceiling) + throw new ArgumentOutOfRangeException(paramName, value, $"Value exceeds the ceiling of {ceiling}."); + } + private static void RequirePositive(long value, string paramName) { if (value <= 0) diff --git a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs index ccefc8ab3..3849a4e0f 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs @@ -103,6 +103,24 @@ public partial class ProxyServer : IDisposable // Name only, per the plan's rollout section - never hosts, URLs or secrets. ProxyLog.EffectiveProfileAtStartup(logger, profile, policyModes); + if (ResourceLimits.MaxDecompressionRatio is { } ratio && + ratio != ProxyResourceLimits.Default.MaxDecompressionRatio) + { + ProxyLog.ReservedLimit(logger, "server.limits.maxDecompressionRatio", ratio, + "Body expansion is bounded by maxBufferedBodyBytes."); + } + + if (ResourceLimits.MaxEncodedBodyBytes is { } encoded) + { + ProxyLog.ReservedLimit(logger, "server.limits.maxEncodedBodyBytes", encoded, + "The enforced whole-body budget is maxBufferedBodyBytes."); + } + + if (ResourceLimits.MaxDecodedBodyBytes is { } decoded) + { + ProxyLog.ReservedLimit(logger, "server.limits.maxDecodedBodyBytes", decoded, + "The enforced whole-body budget is maxBufferedBodyBytes."); + } _ = CertificateManager.ClearIdleCertificates(); diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 4e56afb8d..aa0434e90 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -440,7 +440,10 @@ internal bool TryRecordDecryptFailure(string? host, Exception? error, bool force } if (isActive && !wasActive) + { + ProxyLog.DecryptFailureBypassLearned(logger, host); RaiseDecryptFailureBypassChanged(host); + } return isActive; } @@ -474,7 +477,10 @@ internal bool TryRecordDecryptFailureFromHttpStatus(string? host, int statusCode } if (isActive && !wasActive) + { + ProxyLog.DecryptFailureBypassLearned(logger, host); RaiseDecryptFailureBypassChanged(host); + } return isActive; } diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt index c8c3f66e3..821302dd1 100644 --- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt @@ -281,3 +281,23 @@ Titanium.Web.Proxy.EventArguments.WebSocketFrameElidedEventArgs.IsFinal.get -> b 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 +const Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesCeiling = 67108864 -> int +const Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadCeiling = 67108864 -> long +Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes.get -> long +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes.get -> int +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds(int maxAuthChallengeRounds, int maxUpstreamProxyAuthenticationAttempts, int maxWinAuthTokenBytes) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(int http2WindowUpdateTimeoutSeconds) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(int maxDeferredOutboundBytesPerStream) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(int maxHttp2CompressedHeaderBlockBytes) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(long maxHttp3FramePayloadBytes) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(int maxInterimResponses) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(int maxTrailerHeaderCount, int maxTrailerHeaderBlockBytes) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! +Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(bool enabled) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! diff --git a/src/Titanium.Web.Proxy/Routing/ReverseProxySessionDispatch.cs b/src/Titanium.Web.Proxy/Routing/ReverseProxySessionDispatch.cs index 0285d51e0..4a0a8f5b9 100644 --- a/src/Titanium.Web.Proxy/Routing/ReverseProxySessionDispatch.cs +++ b/src/Titanium.Web.Proxy/Routing/ReverseProxySessionDispatch.cs @@ -58,6 +58,7 @@ destination is null || session.UpstreamConnectHost = destination.Address; session.UpstreamConnectPort = port; session.UpstreamDestinationId = destination.Id; + StreamDestinationDispatch.ApplyRouteLimits(route, session); if (options.LoadBalancer is LoadBalancer lb) { diff --git a/src/Titanium.Web.Proxy/Routing/StreamDestinationDispatch.cs b/src/Titanium.Web.Proxy/Routing/StreamDestinationDispatch.cs index 01af38e56..93fbefb43 100644 --- a/src/Titanium.Web.Proxy/Routing/StreamDestinationDispatch.cs +++ b/src/Titanium.Web.Proxy/Routing/StreamDestinationDispatch.cs @@ -2,6 +2,7 @@ using Titanium.Web.Proxy.Abstractions.Clusters; using Titanium.Web.Proxy.Abstractions.Routing; using Titanium.Web.Proxy.Clusters; +using Titanium.Web.Proxy.EventArguments; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Models; @@ -63,6 +64,25 @@ public static bool TrySelect( return true; } + /// + /// Applies when the route declares them. A null block is one reference + /// compare and does not touch the session. + /// + internal static void ApplyRouteLimits(RouteConfig route, SessionEventArgsBase session) + { + var limits = route.Limits; + if (limits is null || session is not SessionEventArgs args) + return; + if (limits.RequestTimeoutSeconds is int requestTimeout) + args.RequestTimeout = TimeSpan.FromSeconds(requestTimeout); + if (limits.IdleTimeoutSeconds is int idleTimeout) + args.IdleReadTimeout = TimeSpan.FromSeconds(idleTimeout); + if (limits.MaxBufferedBodyBytes is int body) + args.MaxBufferedBodyBytes = body; + if (limits.MaxWebSocketFramePayloadBytes is int frame) + args.MaxWebSocketFramePayloadBytes = frame; + } + /// /// Idempotent GET/HEAD connect retry to the next eligible destination after a failure. /// diff --git a/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs b/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs index aabb5c3d0..30e3ede8b 100644 --- a/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs +++ b/src/Titanium.Web.Proxy/WebSocket/WebSocketInterceptRelay.cs @@ -248,6 +248,9 @@ await destination.WriteAsync(wire.AsMemory(), cancellationToken) // (declared length violates the reserved-bit rule, exceeds int.MaxValue, or exceeds // the configured per-frame limit) - never forwarded, so nothing to unwind here beyond // reporting the close code the caller should send. + if (ex.CloseCode == 1009) + ProxyLog.LimitExceeded(ProxyDiagnostics.Logger, LimitId.WebSocketFrame, PolicyMode.Enforce, + 0, 0, "1009"); ProxyMetrics.ParserError("websocket"); messageTracker.Reset(); return ex.CloseCode; diff --git a/tests/Titanium.Cli.Tests/ServerConfigApplierTests.cs b/tests/Titanium.Cli.Tests/ServerConfigApplierTests.cs index f0bc9b910..019d543bf 100644 --- a/tests/Titanium.Cli.Tests/ServerConfigApplierTests.cs +++ b/tests/Titanium.Cli.Tests/ServerConfigApplierTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Reflection; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -389,4 +390,77 @@ public void ApplyIgnoreServerCertificateErrorsAfterListeners_ConfigFalseWins() proxy, new ServerConfig { IgnoreServerCertificateErrors = false }); Assert.IsFalse(proxy.IgnoreServerCertificateErrors); } + + [TestMethod] + public void PartialLimitsOverlay_PreservesEveryResourceLimitProperty() + { + using var proxy = new ProxyServer(userTrustRootCertificate: false); + proxy.Profile = ProxyProfile.PublicFacing; + var before = proxy.ResourceLimits; + + ServerConfigApplier.Apply(proxy, new ServerConfig + { + Limits = new LimitsConfig { MaxHeaderCount = 400 }, + }); + + Assert.AreEqual(400, proxy.ResourceLimits.MaxHeaderCount); + foreach (var property in typeof(ProxyResourceLimits).GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (property.GetMethod is null || property.GetIndexParameters().Length > 0) + continue; + if (property.Name == nameof(ProxyResourceLimits.MaxHeaderCount)) + continue; + Assert.AreEqual(property.GetValue(before), property.GetValue(proxy.ResourceLimits), property.Name); + } + } + + [TestMethod] + public void PartialPolicyModes_UnderPublicFacing_KeepsHttp1ReplaySafetyEnforce() + { + using var proxy = new ProxyServer(userTrustRootCertificate: false); + proxy.Profile = ProxyProfile.PublicFacing; + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[PolicyFamily.Http1ReplaySafety]); + + ServerConfigApplier.Apply(proxy, new ServerConfig + { + PolicyModes = new PolicyModesConfig { BodyBudget = "Observe" }, + }); + + Assert.AreEqual(PolicyMode.Observe, proxy.PolicyModes[PolicyFamily.BodyBudget]); + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[PolicyFamily.Http1ReplaySafety]); + Assert.IsFalse(proxy.PolicyModes.AllowAmbiguousFraming); + foreach (PolicyFamily family in Enum.GetValues()) + { + if (family == PolicyFamily.BodyBudget) + continue; + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[family], family.ToString()); + } + } + + [TestMethod] + public void ProfileThenExplicitOverride_OnLoadAndOnReload() + { + using var proxy = new ProxyServer(userTrustRootCertificate: false); + ServerConfigApplier.Apply(proxy, new ServerConfig + { + Profile = "PublicFacing", + Limits = new LimitsConfig { MaxConcurrentStreamsPerConnection = 1000 }, + Timeouts = new TimeoutsConfig { RequestTimeoutSeconds = 5 }, + }); + + Assert.IsTrue(proxy.BlockPrivateNetworkDestinations); + Assert.AreEqual(1000, proxy.ResourceLimits.MaxConcurrentStreamsPerConnection); + Assert.AreEqual(5, proxy.RequestTimeoutSeconds); + Assert.AreEqual(PolicyMode.Enforce, proxy.PolicyModes[PolicyFamily.Http1ReplaySafety]); + + ServerConfigApplier.Apply(proxy, new ServerConfig + { + Timeouts = new TimeoutsConfig { IdleReadTimeoutSeconds = 9 }, + }); + + Assert.AreEqual(1000, proxy.ResourceLimits.MaxConcurrentStreamsPerConnection); + Assert.AreEqual(9, proxy.IdleReadTimeoutSeconds); + Assert.AreEqual(5, proxy.RequestTimeoutSeconds); + Assert.IsTrue(proxy.BlockPrivateNetworkDestinations); + } } diff --git a/tests/Titanium.Cli.Tests/UnknownKeyStrictTests.cs b/tests/Titanium.Cli.Tests/UnknownKeyStrictTests.cs new file mode 100644 index 000000000..11884608a --- /dev/null +++ b/tests/Titanium.Cli.Tests/UnknownKeyStrictTests.cs @@ -0,0 +1,31 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Cli.Config; + +namespace Titanium.Cli.Tests; + +[TestClass] +public class UnknownKeyStrictTests +{ + [TestMethod] + public async Task TestStrict_Exits1_OnUnknownKey_AndPassesWithoutStrict() + { + var path = Path.Combine(Path.GetTempPath(), "twp-unknown-" + Guid.NewGuid().ToString("n") + ".yaml"); + await File.WriteAllTextAsync(path, """ + server: + limtis: + maxHeaderCount: 10 + """); + + try + { + var strict = await TestCommand.ExecuteAsync(["test", "-c", path, "--strict"]); + var loose = await TestCommand.ExecuteAsync(["test", "-c", path]); + Assert.AreEqual(1, strict); + Assert.AreEqual(0, loose); + } + finally + { + File.Delete(path); + } + } +} diff --git a/tests/Titanium.Web.Proxy.Configuration.Tests/EscapeHatchConfigTests.cs b/tests/Titanium.Web.Proxy.Configuration.Tests/EscapeHatchConfigTests.cs new file mode 100644 index 000000000..7fcf23471 --- /dev/null +++ b/tests/Titanium.Web.Proxy.Configuration.Tests/EscapeHatchConfigTests.cs @@ -0,0 +1,100 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.Configuration; +using Titanium.Web.Proxy.Configuration.Models; + +namespace Titanium.Web.Proxy.Configuration.Tests; + +[TestClass] +public class EscapeHatchConfigTests +{ + [TestMethod] + public void UnknownKey_SuggestsDidYouMean() + { + const string yaml = """ + server: + limtis: + maxHeaderCount: 10 + """; + + var warnings = UnknownConfigKeyScanner.Scan(yaml, yaml: true); + + Assert.IsTrue(warnings.Count > 0); + StringAssert.Contains(warnings[0], "limtis"); + StringAssert.Contains(warnings[0], "limits"); + } + + [TestMethod] + public void HatchCeiling_RejectsAboveAndAcceptsAtCeiling() + { + var tooHigh = TwpConfigValidator.Validate(new TwpConfig + { + Server = new ServerConfig + { + Limits = new LimitsConfig { MaxHttp3FramePayloadBytes = (64L * 1024 * 1024) + 1 }, + }, + }); + Assert.IsTrue(tooHigh.Any(e => e.Contains("maxHttp3FramePayloadBytes", StringComparison.Ordinal))); + + var atCeiling = TwpConfigValidator.Validate(new TwpConfig + { + Server = new ServerConfig + { + Limits = new LimitsConfig { MaxHttp3FramePayloadBytes = 64L * 1024 * 1024 }, + }, + }); + Assert.IsFalse(atCeiling.Any(e => e.Contains("maxHttp3FramePayloadBytes", StringComparison.Ordinal))); + } + + [TestMethod] + public void ReservedBodyKeys_WarnWithoutFailingValidation() + { + var config = new TwpConfig + { + Server = new ServerConfig + { + Limits = new LimitsConfig { MaxDecompressionRatio = 50, MaxEncodedBodyBytes = 10 }, + }, + }; + + Assert.AreEqual(0, TwpConfigValidator.Validate(config).Count); + var warnings = TwpConfigValidator.CollectWarnings(config); + Assert.IsTrue(warnings.Any(w => w.Contains("maxDecompressionRatio", StringComparison.Ordinal))); + Assert.IsTrue(warnings.Any(w => w.Contains("maxEncodedBodyBytes", StringComparison.Ordinal))); + } + + [TestMethod] + public void SchemaFile_MatchesTheModel() + { + var generated = TwpConfigSchema.Generate().Replace("\r\n", "\n"); + var path = FindRepoFile(Path.Combine("src", "Titanium.Web.Proxy.Configuration", "twp.schema.json")); + if (string.Equals(Environment.GetEnvironmentVariable("UPDATE_TWP_SCHEMA"), "1", StringComparison.Ordinal)) + File.WriteAllText(path, generated); + + var committed = File.ReadAllText(path).Replace("\r\n", "\n"); + Assert.AreEqual(generated, committed); + } + + private static string FindRepoFile(string relativeUnderRepo) + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, relativeUnderRepo); + if (File.Exists(candidate) || + string.Equals(Environment.GetEnvironmentVariable("UPDATE_TWP_SCHEMA"), "1", StringComparison.Ordinal) && + Directory.Exists(Path.Combine(dir.FullName, "src"))) + { + if (candidate.Contains("twp.schema.json", StringComparison.Ordinal) && + Directory.Exists(Path.GetDirectoryName(candidate))) + return candidate; + if (File.Exists(candidate)) + return candidate; + } + + dir = dir.Parent; + } + + Assert.Fail($"Could not locate {relativeUnderRepo}"); + return null!; + } +} diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/HeaderLimitEnforcementTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/HeaderLimitEnforcementTests.cs new file mode 100644 index 000000000..3b7a7f743 --- /dev/null +++ b/tests/Titanium.Web.Proxy.IntegrationTests/HeaderLimitEnforcementTests.cs @@ -0,0 +1,129 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.IntegrationTests.Setup; +using Titanium.Web.Proxy; +using Titanium.Web.Proxy.Options; + +namespace Titanium.Web.Proxy.IntegrationTests; + +[DoNotParallelize] +[TestClass] +public class HeaderLimitEnforcementTests +{ + 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 TooManyRequestHeaders_Returns431() + { + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + var reached = false; + server.HandleRequest(context => + { + reached = true; + return context.Response.WriteAsync("ok"); + }); + + var proxy = testSuite.GetProxy(); + SetHeaderBounds(proxy, maxCount: 4, maxAggregate: 256 * 1024); + var client = testSuite.GetClient(proxy); + using var request = new HttpRequestMessage(HttpMethod.Get, server.ListeningHttpUrl); + for (var i = 0; i < 12; i++) + request.Headers.TryAddWithoutValidation("X-Extra-" + i, "v"); + + using var response = await client.SendAsync(request); + Assert.AreEqual(HttpStatusCode.RequestHeaderFieldsTooLarge, response.StatusCode); + Assert.IsFalse(reached); + } + + [TestMethod] + [Timeout(60_000)] + public async Task TooManyOriginResponseHeaders_Returns502_ObserveAndRaisedLimitPass() + { + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(context => + { + for (var i = 0; i < 12; i++) + context.Response.Headers["X-Origin-" + i] = "v"; + return context.Response.WriteAsync("ok"); + }); + + var proxy = testSuite.GetProxy(); + SetHeaderBounds(proxy, maxCount: 6, maxAggregate: 256 * 1024); + var client = testSuite.GetClient(proxy); + using var rejected = await client.GetAsync(server.ListeningHttpUrl); + Assert.AreEqual(HttpStatusCode.BadGateway, rejected.StatusCode); + + proxy.PolicyModes = proxy.PolicyModes.With(PolicyFamily.HeaderLimits, PolicyMode.Observe); + using var observed = await client.GetAsync(server.ListeningHttpUrl); + Assert.AreEqual(HttpStatusCode.OK, observed.StatusCode); + + proxy.PolicyModes = proxy.PolicyModes.With(PolicyFamily.HeaderLimits, PolicyMode.Enforce); + SetHeaderBounds(proxy, maxCount: 64, maxAggregate: 256 * 1024); + using var raised = await client.GetAsync(server.ListeningHttpUrl); + Assert.AreEqual(HttpStatusCode.OK, raised.StatusCode); + } + + [TestMethod] + [Timeout(60_000)] + public async Task KeepAlive_TwoRequests_UnderTheLimit_BothSucceed() + { + using var testSuite = new TestSuite(sharedServer); + var server = testSuite.GetServer(); + server.HandleRequest(context => context.Response.WriteAsync("ok")); + var proxy = testSuite.GetProxy(); + SetHeaderBounds(proxy, maxCount: 64, maxAggregate: 256 * 1024); + var client = testSuite.GetClient(proxy); + + using var first = await client.GetAsync(server.ListeningHttpUrl); + using var second = await client.GetAsync(server.ListeningHttpUrl); + Assert.AreEqual(HttpStatusCode.OK, first.StatusCode); + Assert.AreEqual(HttpStatusCode.OK, second.StatusCode); + } + + private static void SetHeaderBounds(ProxyServer proxy, int maxCount, long maxAggregate) + { + var current = proxy.ResourceLimits; + proxy.ResourceLimits = ProxyResourceLimits.Create( + current.MaxHeaderLineBytes, + maxCount, + maxAggregate, + current.MaxEncodedBodyBytes, + current.MaxDecodedBodyBytes, + current.MaxDecompressionRatio, + current.MaxConcurrentClients, + current.MaxConcurrentStreamsPerConnection, + current.MaxPeerInitiatedIncompleteStreamResets, + current.MaxOpenHeaderBlockFrames, + current.MaxOpenHeaderBlockDuration, + current.ConnectionPoolingEnabled, + current.MaxCachedConnectionsPerHost, + current.MaxCertificateCacheEntries) + .WithCertificateCacheBounds(current.MaxCertificateCacheEntries, current.MaxCertificateDiskCacheEntries) + .WithMaxOriginHttp2ConnectionsPerAuthority(current.MaxOriginHttp2ConnectionsPerAuthority) + .WithMaxHttp3FramePayloadBytes(current.MaxHttp3FramePayloadBytes) + .WithMaxDeferredOutboundBytesPerStream(current.MaxDeferredOutboundBytesPerStream) + .WithTrailerHeaderBounds(current.MaxTrailerHeaderCount, current.MaxTrailerHeaderBlockBytes) + .WithMaxHttp2CompressedHeaderBlockBytes(current.MaxHttp2CompressedHeaderBlockBytes) + .WithMaxInterimResponses(current.MaxInterimResponses) + .WithAuthenticationBounds( + current.MaxAuthChallengeRounds, + current.MaxUpstreamProxyAuthenticationAttempts, + current.MaxWinAuthTokenBytes) + .WithHttp2WindowUpdateTimeoutSeconds(current.Http2WindowUpdateTimeoutSeconds); + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/HeaderParserTests.cs b/tests/Titanium.Web.Proxy.UnitTests/HeaderParserTests.cs index bbd71f5fd..26afe2224 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/HeaderParserTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/HeaderParserTests.cs @@ -7,6 +7,7 @@ using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Options; using Titanium.Web.Proxy.StreamExtended.BufferPool; using Titanium.Web.Proxy.StreamExtended.Network; @@ -140,6 +141,55 @@ public async Task TryReadHeadersAsync_continue_after_incomplete_line_across_fill Assert.AreEqual(new string('a', 40), header.Value); } + [TestMethod] + public async Task ReadHeaders_CountBreach_Throws_AtLimitPasses_ObservePasses() + { + var enforce = new HeaderBlockBudget(1, 100_000, PolicyMode.Enforce, isRequest: true); + var breached = await Assert.ThrowsExactlyAsync(() => + HeaderParser.ReadHeaders( + new ScriptedLineReader(dataAvailable: true, "A: 1", "B: 2", ""), + new HeaderCollection(), CancellationToken.None, enforce).AsTask()); + Assert.IsTrue(breached.IsRequest); + Assert.IsTrue(breached.IsCount); + + var headers = new HeaderCollection(); + await HeaderParser.ReadHeaders( + new ScriptedLineReader(dataAvailable: true, "A: 1", ""), + headers, CancellationToken.None, enforce); + Assert.AreEqual(1, headers.HeaderCount()); + + var observed = new HeaderCollection(); + await HeaderParser.ReadHeaders( + new ScriptedLineReader(dataAvailable: false, "A: 1", "B: 2", null), + observed, CancellationToken.None, + new HeaderBlockBudget(1, 100_000, PolicyMode.Observe, isRequest: true)); + Assert.AreEqual(2, observed.HeaderCount()); + + var disabled = new HeaderCollection(); + await HeaderParser.ReadHeaders( + new ScriptedLineReader(dataAvailable: true, "Connection: keep-alive", "A: 1", ""), + disabled, CancellationToken.None); + Assert.AreSame(KnownHeaders.ConnectionKeepAlive.String, + disabled.GetFirstHeader(KnownHeaders.Connection)!.Value); + } + + [TestMethod] + public async Task TryReadHeadersAsync_AggregateBreach_Is502ForResponses_RaisedLimitPasses() + { + var tight = new HeaderBlockBudget(100, 5, PolicyMode.Enforce, isRequest: false); + using var stream = CreateHttpStream(Encoding.ASCII.GetBytes("A: b\r\n\r\n")); + var breached = await Assert.ThrowsExactlyAsync(() => + HeaderParser.TryReadHeadersAsync(stream, new HeaderCollection(), CancellationToken.None, tight).AsTask()); + Assert.IsFalse(breached.IsRequest); + Assert.IsFalse(breached.IsCount); + + var raised = new HeaderBlockBudget(100, 64 * 1024, PolicyMode.Enforce, isRequest: true); + using var ok = CreateHttpStream(Encoding.ASCII.GetBytes("A: b\r\nHost: example\r\n\r\n")); + var headers = new HeaderCollection(); + Assert.IsTrue(await HeaderParser.TryReadHeadersAsync(ok, headers, CancellationToken.None, raised)); + Assert.AreEqual("example", headers.GetFirstHeader(KnownHeaders.Host)!.Value); + } + private static HttpStream CreateHttpStream(byte[] payload) => new(new ProxyServer(false, false, false), new MemoryStream(payload), new DefaultBufferPool(), CancellationToken.None, false); diff --git a/tests/Titanium.Web.Proxy.UnitTests/HpackTruncationConsistencyTests.cs b/tests/Titanium.Web.Proxy.UnitTests/HpackTruncationConsistencyTests.cs new file mode 100644 index 000000000..38d5cc2c9 --- /dev/null +++ b/tests/Titanium.Web.Proxy.UnitTests/HpackTruncationConsistencyTests.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.Extensions; +using Titanium.Web.Proxy.Http2.Hpack; +using Titanium.Web.Proxy.Models; + +namespace Titanium.Web.Proxy.UnitTests; + +/// +/// A truncated header block must leave the HPACK dynamic table usable. The next block on the +/// same connection can reference entries inserted before the truncation. +/// +[TestClass] +public class HpackTruncationConsistencyTests +{ + [TestMethod] + public void TruncatedBlock_KeepsEarlierDynamicEntries_ForTheNextBlock() + { + var encoder = new Encoder(4096); + var first = Encode(encoder, + ("x-session", "alpha"), + ("x-huge", new string('v', 200))); + + var headers = new List<(string Name, string Value)>(); + var decoder = new Decoder(maxHeaderSize: 64, maxHeaderTableSize: 4096); + decoder.Decode(first, new CollectingListener(headers)); + Assert.IsTrue(decoder.EndHeaderBlock(), "the oversized literal must report truncation"); + Assert.IsTrue(headers.Exists(h => h.Name == "x-session" && h.Value == "alpha")); + Assert.IsFalse(headers.Exists(h => h.Name == "x-huge")); + + var second = Encode(encoder, ("x-session", "alpha")); + headers.Clear(); + decoder.Decode(second, new CollectingListener(headers)); + Assert.IsFalse(decoder.EndHeaderBlock()); + Assert.AreEqual(1, headers.Count); + Assert.AreEqual("x-session", headers[0].Name); + Assert.AreEqual("alpha", headers[0].Value); + } + + private static byte[] Encode(Encoder encoder, params (string Name, string Value)[] fields) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + foreach (var (name, value) in fields) + encoder.EncodeHeader(writer, (ByteString)name, (ByteString)value); + writer.Flush(); + return stream.ToArray(); + } + + private sealed class CollectingListener(List<(string Name, string Value)> headers) : IHeaderListener + { + public void AddHeader(ByteString name, ByteString value, bool sensitive) => + headers.Add((name.GetString(), value.GetString())); + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs index 6671d5016..c8fe8194a 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http2BodyWriterAndOriginSettingsTests.cs @@ -478,8 +478,8 @@ private static async Task CreateOriginConnectionShellAsyn var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), - typeof(ProxyResourceLimits)], null)!; + typeof(ProxyResourceLimits), typeof(int)], null)!; return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, - ProxyResourceLimits.Default])!; + ProxyResourceLimits.Default, 64 * 1024])!; } } diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs index 63e53460e..fd061d077 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/Http2OriginActiveStreamCountTests.cs @@ -160,8 +160,8 @@ private static async Task CreateShellAsync(ProxyServer pr var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), - typeof(ProxyResourceLimits)], null)!; + typeof(ProxyResourceLimits), typeof(int)], null)!; return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, - ProxyResourceLimits.Default])!; + ProxyResourceLimits.Default, 64 * 1024])!; } } diff --git a/tests/Titanium.Web.Proxy.UnitTests/Http3FrameStreamingTests.cs b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameStreamingTests.cs new file mode 100644 index 000000000..bf18ed481 --- /dev/null +++ b/tests/Titanium.Web.Proxy.UnitTests/Http3FrameStreamingTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.Http3; + +namespace Titanium.Web.Proxy.UnitTests; + +[TestClass] +public class Http3FrameStreamingTests +{ + [TestMethod] + public async Task Declared64MiBFrame_WithNoPayload_RentsOnlyTheFirstChunk() + { + Http3Frame.TestLastRentBytes = 0; + var header = new byte[16]; + var written = Http3VarInt.Write(header, Http3FrameType.Data); + written += Http3VarInt.Write(header.AsSpan(written), 64L * 1024 * 1024); + await using var stream = new MemoryStream(header, 0, written); + + var ex = await Assert.ThrowsExactlyAsync(() => + Http3Frame.ReadAsync(stream, ProxyResourceLimitsCeiling, CancellationToken.None).AsTask()); + + Assert.AreEqual(Http3ErrorCode.FrameError, ex.ErrorCode); + Assert.IsTrue(Http3Frame.TestLastRentBytes > 0); + Assert.IsTrue(Http3Frame.TestLastRentBytes <= Http3Frame.ProgressiveRentChunkBytes); + } + + [TestMethod] + public async Task RaisedCap_AcceptsALengthTheDefaultCapRejects() + { + var header = FrameHeader(Http3FrameType.Data, 5 * 1024 * 1024); + await using var rejected = new MemoryStream(header); + var tooBig = await Assert.ThrowsExactlyAsync(() => + Http3Frame.ReadAsync(rejected, 4 * 1024 * 1024, CancellationToken.None).AsTask()); + Assert.AreEqual(Http3ErrorCode.ExcessiveLoad, tooBig.ErrorCode); + + await using var accepted = new MemoryStream(header); + var started = await Assert.ThrowsExactlyAsync(() => + Http3Frame.ReadAsync(accepted, 8 * 1024 * 1024, CancellationToken.None).AsTask()); + Assert.AreEqual(Http3ErrorCode.FrameError, started.ErrorCode); + } + + [TestMethod] + public async Task CopyPayload_SplitsReads_AndStreamsPastTheWholeFrameCap() + { + var payload = new byte[300]; + for (var i = 0; i < payload.Length; i++) + payload[i] = (byte)i; + await using var stream = new OneByteStream(payload); + var received = new List(); + + await Http3Frame.CopyPayloadAsync(stream, payload.Length, sliceBytes: 7, + (slice, _) => + { + received.AddRange(slice.ToArray()); + return ValueTask.CompletedTask; + }, + CancellationToken.None); + + CollectionAssert.AreEqual(payload, received); + } + + [TestMethod] + public async Task CopyPayload_ZeroLength_DoesNotRead() + { + await using var stream = new MemoryStream(new byte[] { 1, 2, 3 }); + var called = false; + await Http3Frame.CopyPayloadAsync(stream, 0, 16, (_, _) => + { + called = true; + return ValueTask.CompletedTask; + }, CancellationToken.None); + + Assert.IsFalse(called); + Assert.AreEqual(0, stream.Position); + } + + [TestMethod] + public async Task CopyPayload_Cancellation_ReturnsWithoutHanging() + { + await using var stream = new MemoryStream(new byte[64]); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsExactlyAsync(() => + Http3Frame.CopyPayloadAsync(stream, 64, 16, (_, _) => ValueTask.CompletedTask, cts.Token).AsTask()); + } + + private const long ProxyResourceLimitsCeiling = 64L * 1024 * 1024; + + private static byte[] FrameHeader(ulong type, long length) + { + var header = new byte[16]; + var written = Http3VarInt.Write(header, type); + written += Http3VarInt.Write(header.AsSpan(written), (ulong)length); + return header.AsSpan(0, written).ToArray(); + } + + private sealed class OneByteStream(byte[] data) : Stream + { + private int position; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => data.Length; + public override long Position { get => position; set => throw new NotSupportedException(); } + + public override int Read(byte[] buffer, int offset, int count) + { + if (position >= data.Length || count == 0) return 0; + buffer[offset] = data[position++]; + return 1; + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + if (position >= data.Length || buffer.Length == 0) return new ValueTask(0); + buffer.Span[0] = data[position++]; + return new ValueTask(1); + } + + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/LimitCatalogConsistencyTests.cs b/tests/Titanium.Web.Proxy.UnitTests/LimitCatalogConsistencyTests.cs new file mode 100644 index 000000000..2946c1235 --- /dev/null +++ b/tests/Titanium.Web.Proxy.UnitTests/LimitCatalogConsistencyTests.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.Logging; + +namespace Titanium.Web.Proxy.UnitTests; + +[TestClass] +public class LimitCatalogConsistencyTests +{ + [TestMethod] + public void EveryCatalogEntry_HasApplierValidatorAndDocs() + { + var applier = File.ReadAllText(FindRepoFile(Path.Combine("src", "Titanium.Cli", "Config", "ServerConfigApplier.cs"))); + var validator = File.ReadAllText(FindRepoFile(Path.Combine( + "src", "Titanium.Web.Proxy.Configuration", "TwpConfigValidator.cs"))); + var docs = File.ReadAllText(FindRepoFile(Path.Combine("website", "docs", "limits.md"))); + + foreach (var entry in LimitCatalog.All) + { + StringAssert.Contains(applier, entry.Property, $"applier missing {entry.Property}"); + StringAssert.Contains(validator, entry.Property, $"validator missing {entry.Property}"); + StringAssert.Contains(docs, entry.CliKey, $"docs missing {entry.CliKey}"); + } + } + + private static string FindRepoFile(string relativeUnderRepo) + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = Path.Combine(dir.FullName, relativeUnderRepo); + if (File.Exists(candidate)) + return candidate; + dir = dir.Parent; + } + + Assert.Fail($"Could not locate {relativeUnderRepo} from {AppContext.BaseDirectory}"); + return null!; + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/LimitExceededTests.cs b/tests/Titanium.Web.Proxy.UnitTests/LimitExceededTests.cs new file mode 100644 index 000000000..9768af0d0 --- /dev/null +++ b/tests/Titanium.Web.Proxy.UnitTests/LimitExceededTests.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +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.Options; + +namespace Titanium.Web.Proxy.UnitTests; + +[TestClass] +public class LimitExceededTests +{ + [TestMethod] + public void LimitExceeded_Enforce_LogsError_WithPropertyCliKeyAndHostOnly() + { + LimitLogThrottle.Reset(); + var capturing = new CapturingLogger(LogLevel.Trace); + + ProxyLog.LimitExceeded(capturing, LimitId.BufferedBody, PolicyMode.Enforce, 5000, 4096, "413", "cdn.example"); + + Assert.AreEqual(1, capturing.Entries.Count); + Assert.AreEqual(LogLevel.Error, capturing.Entries[0].Level); + var text = capturing.Entries[0].Message; + StringAssert.Contains(text, "MaxBufferedBodyBytes"); + StringAssert.Contains(text, "server.limits.maxBufferedBodyBytes"); + StringAssert.Contains(text, "413"); + StringAssert.Contains(text, "host cdn.example"); + StringAssert.Contains(text, "5000"); + Assert.IsFalse(text.Contains("http://", StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(text.Contains("Cookie", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void LimitExceeded_Observe_LogsDebug() + { + LimitLogThrottle.Reset(); + var capturing = new CapturingLogger(LogLevel.Trace); + + ProxyLog.LimitExceeded(capturing, LimitId.HeaderCount, PolicyMode.Observe, 300, 256, "passed", null); + + Assert.AreEqual(1, capturing.Entries.Count); + Assert.AreEqual(LogLevel.Debug, capturing.Entries[0].Level); + StringAssert.Contains(capturing.Entries[0].Message, "server.policyModes.headerLimits"); + } + + [TestMethod] + public void LimitExceeded_Flood_LogsOnce_AndMetricsCountEveryCall() + { + LimitLogThrottle.Reset(); + var capturing = new CapturingLogger(LogLevel.Error); + long rejected = 0; + using var listener = new MeterListener(); + listener.InstrumentPublished = (instrument, meterListener) => + { + if (instrument.Meter.Name == ProxyMetricsMeter && instrument.Name == "twp.streams.rejected") + meterListener.EnableMeasurementEvents(instrument); + }; + listener.SetMeasurementEventCallback((_, measurement, _, _) => + Interlocked.Add(ref rejected, measurement)); + listener.Start(); + + const int calls = 10_000; + for (var i = 0; i < calls; i++) + ProxyLog.LimitExceeded(capturing, LimitId.Http3FramePayload, PolicyMode.Enforce, i, 4, "HTTP/3 stream reset"); + + Assert.IsTrue(capturing.Entries.Count is > 0 and < 5, + $"expected a bounded log, got {capturing.Entries.Count}"); + Assert.IsTrue(rejected >= calls, $"metrics counted {rejected}, expected at least {calls}"); + var joined = string.Join('\n', capturing.Entries.Select(e => e.Message)); + Assert.IsFalse(joined.Contains("http://", StringComparison.OrdinalIgnoreCase)); + Assert.IsFalse(joined.Contains("Cookie", StringComparison.OrdinalIgnoreCase)); + } + + [TestMethod] + public void PolicyExceptions_AreNotExpectedClientDisconnects() + { + Assert.IsFalse(ProxyDiagnostics.IsExpected(new BodySizeLimitExceededException("body"))); + Assert.IsFalse(ProxyDiagnostics.IsExpected(new Http2HeaderListTooLargeException("headers"))); + Assert.IsTrue(ProxyDiagnostics.IsExpected(new IOException("client gone"))); + } + + [TestMethod] + public void NewHatches_DefaultToTodaysConstants_AndRejectHostileCeilings() + { + var limits = ProxyResourceLimits.Default; + Assert.AreEqual(4L * 1024 * 1024, limits.MaxHttp3FramePayloadBytes); + Assert.AreEqual((4 * 1024 * 1024) + (16 * 1024), limits.MaxDeferredOutboundBytesPerStream); + Assert.AreEqual(100, limits.MaxTrailerHeaderCount); + Assert.AreEqual(16 * 1024, limits.MaxTrailerHeaderBlockBytes); + Assert.AreEqual(256 * 1024, limits.MaxHttp2CompressedHeaderBlockBytes); + Assert.AreEqual(20, limits.MaxInterimResponses); + Assert.AreEqual(3, limits.MaxAuthChallengeRounds); + Assert.AreEqual(5, limits.MaxUpstreamProxyAuthenticationAttempts); + Assert.AreEqual(12288, limits.MaxWinAuthTokenBytes); + Assert.AreEqual(60, limits.Http2WindowUpdateTimeoutSeconds); + Assert.AreEqual(1000, limits.MaxConcurrentStreamsPerConnection); + + Assert.ThrowsExactly(() => + limits.WithMaxHttp3FramePayloadBytes(ProxyResourceLimits.MaxHttp3FramePayloadCeiling + 1)); + Assert.ThrowsExactly(() => + limits.WithMaxDeferredOutboundBytesPerStream(ProxyResourceLimits.MaxDeferredOutboundBytesCeiling + 1)); + var raised = limits.WithMaxHttp3FramePayloadBytes(8L * 1024 * 1024); + Assert.AreEqual(8L * 1024 * 1024, raised.MaxHttp3FramePayloadBytes); + Assert.AreEqual(limits.MaxDeferredOutboundBytesPerStream, raised.MaxDeferredOutboundBytesPerStream); + } + + private const string ProxyMetricsMeter = "Titanium.Web.Proxy"; + + private sealed class CapturingLogger : ILogger + { + public readonly List<(LogLevel Level, string Message)> Entries = new(); + private readonly LogLevel minimumLevel; + + public CapturingLogger(LogLevel minimumLevel) => this.minimumLevel = minimumLevel; + + public IDisposable? BeginScope(TState state) where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => logLevel >= minimumLevel; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) return; + Entries.Add((logLevel, formatter(state, exception))); + } + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/LoggingTests.cs b/tests/Titanium.Web.Proxy.UnitTests/LoggingTests.cs index 33f624077..b1886b85b 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/LoggingTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/LoggingTests.cs @@ -425,7 +425,7 @@ public void ProxyLog_ClientConnectionAdmissionRejected_LogsEndpointAndReason() ProxyLog.ClientConnectionAdmissionRejected(capturing, endPoint, "global limit"); Assert.AreEqual(1, capturing.Entries.Count); - Assert.AreEqual(LogLevel.Warning, capturing.Entries[0].Level); + Assert.AreEqual(LogLevel.Error, capturing.Entries[0].Level); StringAssert.Contains(capturing.Entries[0].Message, "127.0.0.1:8080"); StringAssert.Contains(capturing.Entries[0].Message, "global limit"); } @@ -451,7 +451,7 @@ public void ProxyLog_EffectiveProfileAtStartup_LogsProfileAndPolicyModes() } [TestMethod] - public void ProxyLog_PolicyBreach_Enforce_LogsWarning_Observe_LogsDebug() + public void ProxyLog_PolicyBreach_Enforce_LogsError_Observe_LogsDebug() { var capturing = CreateTraceCapturingLogger(); @@ -459,7 +459,7 @@ public void ProxyLog_PolicyBreach_Enforce_LogsWarning_Observe_LogsDebug() ProxyLog.PolicyBreach(capturing, PolicyFamily.HeaderLimits, PolicyMode.Observe, "header exceeded"); Assert.AreEqual(2, capturing.Entries.Count); - Assert.AreEqual(LogLevel.Warning, capturing.Entries[0].Level); + Assert.AreEqual(LogLevel.Error, capturing.Entries[0].Level); Assert.AreEqual(LogLevel.Debug, capturing.Entries[1].Level); StringAssert.Contains(capturing.Entries[0].Message, "BodyBudget"); StringAssert.Contains(capturing.Entries[1].Message, "HeaderLimits"); diff --git a/tests/Titanium.Web.Proxy.UnitTests/RouteLimitsDispatchTests.cs b/tests/Titanium.Web.Proxy.UnitTests/RouteLimitsDispatchTests.cs new file mode 100644 index 000000000..02b87d5e8 --- /dev/null +++ b/tests/Titanium.Web.Proxy.UnitTests/RouteLimitsDispatchTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Titanium.Web.Proxy.Abstractions; +using Titanium.Web.Proxy.Abstractions.Clusters; +using Titanium.Web.Proxy.Abstractions.Routing; +using Titanium.Web.Proxy.Clusters; +using Titanium.Web.Proxy.EventArguments; +using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Network.Tcp; +using Titanium.Web.Proxy.Routing; + +namespace Titanium.Web.Proxy.UnitTests; + +[TestClass] +public class RouteLimitsDispatchTests +{ + [TestMethod] + public async Task TryApply_SetsSessionOverrides_OnlyWhenTheRouteDeclaresLimits() + { + using var proxy = new ProxyServer(userTrustRootCertificate: false); + var clusters = new List + { + new() + { + Id = "c1", + Destinations = [new DestinationConfig { Id = "d1", Address = "10.0.0.5", Port = 8080 }], + }, + }; + var manager = new ClusterManager(); + await manager.ApplyAsync(clusters); + proxy.ReverseProxy = new ReverseProxyOptions + { + ClusterManager = manager, + Routes = + [ + new RouteConfig + { + Id = "limited", + ClusterId = "c1", + Match = new RouteMatch { Host = "limited.test", Path = "/", PathKind = PathMatchKind.Prefix }, + Limits = new RouteLimits + { + RequestTimeoutSeconds = 15, + IdleTimeoutSeconds = 7, + MaxBufferedBodyBytes = 2048, + MaxWebSocketFramePayloadBytes = 4096, + }, + }, + new RouteConfig + { + Id = "plain", + ClusterId = "c1", + Order = 1, + Match = new RouteMatch { Host = "plain.test", Path = "/", PathKind = PathMatchKind.Prefix }, + }, + ], + }; + + var limited = MakeSession(proxy, "http://limited.test/a"); + Assert.IsTrue(ReverseProxySessionDispatch.TryApply(proxy, limited)); + Assert.AreEqual(TimeSpan.FromSeconds(15), limited.RequestTimeout); + Assert.AreEqual(TimeSpan.FromSeconds(7), limited.IdleReadTimeout); + Assert.AreEqual(2048, limited.MaxBufferedBodyBytes); + Assert.AreEqual(4096, limited.MaxWebSocketFramePayloadBytes); + + var plain = MakeSession(proxy, "http://plain.test/a"); + Assert.IsTrue(ReverseProxySessionDispatch.TryApply(proxy, plain)); + Assert.IsNull(plain.RequestTimeout); + Assert.IsNull(plain.IdleReadTimeout); + Assert.IsNull(plain.MaxBufferedBodyBytes); + Assert.IsNull(plain.MaxWebSocketFramePayloadBytes); + } + + private static SessionEventArgs MakeSession(ProxyServer proxy, string url) + { + 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 = "GET"; + session.HttpClient.Request.RequestUriString = url; + return session; + } +} diff --git a/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs b/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs index 07d355fa4..cc538bdc6 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/SonarNewCodeCoverageTests.cs @@ -80,9 +80,9 @@ private static async Task CreateShellAsync(ProxyServer pr var ctor = typeof(Http2OriginConnection).GetConstructor(PrivateInstance, null, [typeof(TcpServerConnection), typeof(Microsoft.Extensions.Logging.ILogger), - typeof(ProxyResourceLimits)], null)!; + typeof(ProxyResourceLimits), typeof(int)], null)!; return (Http2OriginConnection)ctor.Invoke([serverConn, NullLogger.Instance, - ProxyResourceLimits.Default])!; + ProxyResourceLimits.Default, 64 * 1024])!; } // ───────────────────────────────────────────────────────────────────────── diff --git a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs index 2b7ca27d8..daf7db6c3 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs +++ b/tests/Titanium.Web.Proxy.UnitTests/WebSocketDecoderTests.cs @@ -367,6 +367,25 @@ public void Decode_EnforceOversize_StillThrows1009() Assert.AreEqual((ushort)1009, ex.CloseCode); } + [TestMethod] + public void Decode_Rsv1_IsDropped_SoPermessageDeflateIsNotInflated() + { + // Today's decoder does not keep RSV bits, so a permessage-deflate opener is a normal + // data frame and message tracking reports it uncompressed. See websocket-large-messages.md. + var decoder = CreateDecoder(); + var raw = BuildFrame(WebsocketOpCode.Text, Encoding.UTF8.GetBytes("hello")); + raw[0] |= 0x40; + + var frames = decoder.Decode(raw, 0, raw.Length).ToList(); + + Assert.AreEqual(1, frames.Count); + Assert.AreEqual("hello", frames[0].GetText()); + var tracker = new WebSocketMessageTracker(); + tracker.OnFrame(frames[0], out var compressed, out var error); + Assert.IsFalse(compressed); + Assert.IsFalse(error); + } + private static byte[] BuildFrame(WebsocketOpCode opCode, byte[] payload, bool mask = false, bool fin = true, uint maskKey = 0x11223344) { diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index afe1ee02f..f5a60086b 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -68,6 +68,8 @@ export default defineConfig({ { text: 'Streaming bodies', link: '/docs/streaming-bodies' }, { text: 'gRPC-JSON transcoding', link: '/docs/grpc-json-transcoding' }, { text: 'Security', link: '/docs/security' }, + { text: 'Limits and escape hatches', link: '/docs/limits' }, + { text: 'Why did my request fail', link: '/docs/troubleshooting' }, ], }, ], diff --git a/website/docs/cli.md b/website/docs/cli.md index 54d18c358..1f20568b8 100644 --- a/website/docs/cli.md +++ b/website/docs/cli.md @@ -204,5 +204,7 @@ Unlike a typical YARP process recycle, Titanium keeps the process and in-flight ## See also - [Configuration](/docs/configuration) +- [Limits and escape hatches](/docs/limits) +- [Why did my request fail](/docs/troubleshooting) - [Install](/docs/install) - [Download](/download) diff --git a/website/docs/configuration.md b/website/docs/configuration.md index cfadfc681..fa3463f15 100644 --- a/website/docs/configuration.md +++ b/website/docs/configuration.md @@ -86,6 +86,8 @@ Engine knobs live under `server:` ([reference](#server-reference)). Plus feature Match fields typically include host, path (`Exact` / `Prefix` / `Template`), method, headers, and query. Cluster algorithms include RoundRobin, Random, LeastRequests, and LeastTime; destinations support weight and sticky cookie/header. When any cluster uses `LeastTime`, the CLI automatically enables request timing capture so latency EWMA can drive selection. +Optional `limits` on a route overrides that session only: `requestTimeoutSeconds`, `idleTimeoutSeconds`, `maxBufferedBodyBytes`, `maxWebSocketFramePayloadBytes`. Omit it and the server limits apply. Full key list: [Limits and escape hatches](/docs/limits). + ### Route transforms Optional `transforms` on a route rewrite the upstream request (and can stage response header changes). Empty/absent transforms keep the reverse fast path. Supported kinds: @@ -137,7 +139,7 @@ Use real paths and emails in your environment. Do not commit private keys. MITM ```yaml logging: enabled: true - minimumLevel: "Error" + minimumLevel: "Error" # Error shows limit rejections. Use Debug, or titanium run -v, for Observe lines. enableConsole: true enableConsoleColors: true enableFile: false @@ -174,7 +176,7 @@ Null nested objects and null properties leave the library or profile default. Ap ```yaml server: - profile: PublicFacing # Balanced | LegacyCompatible | PublicFacing + profile: Balanced # Balanced | LegacyCompatible | PublicFacing. PublicFacing sets streams to 256, idle 60s, request 120s. enableHttp2: true enableHttp3: null enableRfc8441: false @@ -222,24 +224,36 @@ server: maxDecodedBodyBytes: null maxDecompressionRatio: 200 maxConcurrentClients: null - maxConcurrentStreamsPerConnection: 256 + maxConcurrentStreamsPerConnection: 1000 # PublicFacing profile uses 256 maxPeerInitiatedIncompleteStreamResets: 100 maxOpenHeaderBlockFrames: 128 maxOpenHeaderBlockDurationSeconds: 10 connectionPoolingEnabled: true maxCachedConnectionsPerHost: 128 - maxOriginHttp2ConnectionsPerAuthority: 8 + maxOriginHttp2ConnectionsPerAuthority: 1 maxCertificateCacheEntries: 1024 maxCertificateDiskCacheEntries: null maxBufferedBodyBytes: 4194304 maxDecodedHeaderListBytes: 65536 maxWebSocketFramePayloadBytes: 16777216 + maxHttp3FramePayloadBytes: 4194304 + maxDeferredOutboundBytesPerStream: 4210688 + maxTrailerHeaderCount: 100 + maxTrailerHeaderBlockBytes: 16384 + maxHttp2CompressedHeaderBlockBytes: 262144 + maxInterimResponses: 20 + maxAuthChallengeRounds: 3 + maxUpstreamProxyAuthenticationAttempts: 5 + maxWinAuthTokenBytes: 12288 + http2WindowUpdateTimeoutSeconds: 60 policyModes: bodyBudget: Enforce decompressionRatio: Enforce headerLimits: Enforce admissionControl: Enforce http2AbuseBudget: Enforce + webSocketFrameBudget: Enforce + http1ReplaySafety: Observe # PublicFacing keeps Enforce unless you set this allowAmbiguousFraming: false tls: supportedSslProtocols: [Tls12, Tls13] @@ -251,13 +265,18 @@ server: hostName: "proxy.example" port: 8080 proxyType: Http + useDefaultCredentials: false + bypassLocalhost: false + proxyDnsRequests: false + nextHop: null httpsProxy: null upStreamEndPoint: null upStreamEndPointIPv4: null upStreamEndPointIPv6: null decryptSkipHosts: [] # present ⇒ Replace tunnel-only list (omit key to keep Merge factory defaults) decryptOnlyHosts: [] # when non-empty, only these hosts are decrypted (Replace with skip/only) - # Decrypt failure bypass (learned tunnel) is ProxyServer.EnableDecryptFailureBypass / Inspector only — not a twp.yaml key + # enableDecryptFailureBypass, decryptFailureBypassTtlMinutes, decryptFailureBypassMaxEntries, + # decryptFailureBypassThreshold, and proxyAuthenticationRealm are server keys. See Limits. systemProxyBypassHosts: null # present ⇒ Replace OS bypass (omit ⇒ Merge identity defaults); SSO risk if removed proxyLoopback: true # localhost via proxy when building SystemProxySettings from config certificateManager: @@ -328,3 +347,9 @@ These cannot be set from YAML; wire them in Library / C# code: - `GetCustomUpStreamProxyFunc`, `CustomUpStreamProxyFailureFunc` - `ShouldInterceptHttp` - `BufferPool`, `Logging.LoggerFactory`, custom `CertificateStorage` + +## See also + +- [Limits and escape hatches](/docs/limits) +- [Why did my request fail](/docs/troubleshooting) +- [CLI](/docs/cli) diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md index 6af91423d..1af486d7f 100644 --- a/website/docs/getting-started.md +++ b/website/docs/getting-started.md @@ -44,7 +44,7 @@ titanium test -c twp.yaml titanium run -c twp.yaml ``` -`titanium run` stays in the foreground (Ctrl+C to stop). To start at boot: `titanium service install -c twp.yaml` — see [CLI — service](/docs/cli#service). More YAML: [Configuration](/docs/configuration). +`titanium run` stays in the foreground (Ctrl+C to stop). To start at boot: `titanium service install -c twp.yaml` — see [CLI — service](/docs/cli#service). More YAML: [Configuration](/docs/configuration). When a request fails, the Error log names the limit and the key: [Limits](/docs/limits) and [troubleshooting](/docs/troubleshooting). ## Inspect traffic (Inspector) diff --git a/website/docs/limits.md b/website/docs/limits.md new file mode 100644 index 000000000..4fac97dd1 --- /dev/null +++ b/website/docs/limits.md @@ -0,0 +1,100 @@ +# Limits and escape hatches + +> For CLI reverse-proxy operators and library users. Every limit that can reject a legitimate request has a key. A rejection is logged at Error with the limit, the observed value, and the key that raises it. + +The default log level is Error. Run `titanium run -c twp.yaml -v` or set `logging.minimumLevel` to `Debug` or `Information` when you need Observe-mode lines. Observe logs at Debug. A rejected request logs at Error so it shows up without `-v`. + +## Behavior changes at the previous defaults + +1. Origin HTTP/2 responses whose headers are between 8 KiB and 64 KiB now arrive intact. Above 64 KiB the stream fails with 502 and names `server.limits.maxDecodedHeaderListBytes`. Previously those headers were truncated in silence. +2. Under Balanced (the default profile), an HTTP/1 request or origin response with more than 256 headers or 256 KiB of header bytes is rejected: 431 for a request, 502 for an origin response. LegacyCompatible keeps HeaderLimits at Observe, so the same message is logged and passed. +3. Limit-driven failures appear in the default Error log. The line names the property, the configured limit, the observed value, and the CLI key. + +## How a rejection looks + +```text +headerLimits (Enforce): header count exceeded MaxHeaderCount=256; observed 300; client got 431. Raise server.limits.maxHeaderCount or set server.policyModes.headerLimits to observe or disabled. host cdn.example +``` + +The first line for a limit is immediate. Further hits of that same limit are folded into one line per 10 seconds, with a suppressed-count suffix. Metrics count every rejection. Logs never include URLs, header values, cookies, or bodies. The host name is the only request identity on the line. + +## Policy modes + +`server.policyModes.` is `Enforce`, `Observe`, or `Disabled`. + +| Mode | Effect | +|------|--------| +| Enforce | Reject the request and log at Error. | +| Observe | Let the request through and log at Debug. | +| Disabled | Skip the check. | + +Families: `bodyBudget`, `decompressionRatio`, `headerLimits`, `admissionControl`, `http2AbuseBudget`, `http2RelayValidation`, `webSocketFrameBudget`, `http1ReplaySafety`. `allowAmbiguousFraming` is a separate boolean. A partial `policyModes` block changes only the keys you set. Under PublicFacing, leaving `http1ReplaySafety` unset keeps Enforce. + +## Buffering and streaming + +`maxBufferedBodyBytes` applies when something materializes a whole body (`GetRequestBody`, `GetResponseBody`, a hook that needs the bytes). A streaming relay does not buffer the body and does not consult that cap. `CanBufferBody` reports whether a message is small enough to buffer. `webSocketFrameBudget` is the per-frame WebSocket cap (`maxWebSocketFramePayloadBytes`); a frame over it closes with 1009. Filtering hooks still see a whole frame. The raw relay (no subscriber) does not decode frames. + +`maxEncodedBodyBytes`, `maxDecodedBodyBytes`, and `maxDecompressionRatio` are accepted and **reserved, not enforced**. Body expansion is bounded by `maxBufferedBodyBytes`. Startup warns when you set them. + +## Profiles + +| Profile | Header limits | Streams / connection | Idle / request timeout | Who it is for | +|---------|---------------|----------------------|------------------------|---------------| +| Balanced (default) | Enforce, 256 headers / 256 KiB | 1000 | disabled (0) | Single-user and trusted clients | +| LegacyCompatible | Observe | 1000 | disabled | 4.x migrations | +| PublicFacing | Enforce | 256 | idle 60 s, request 120 s | Untrusted clients | + +PublicFacing idle and request deadlines cut long-lived SSE, gRPC streaming, and WebSocket sessions. Raise `server.timeouts.idleReadTimeoutSeconds`, `idleWriteTimeoutSeconds`, and `requestTimeoutSeconds`, or set them to 0, when those protocols must stay open. 256 concurrent streams is the PublicFacing cap, not the Balanced default. + +## Reference + +| What fails | Default | Ceiling | CLI key | +|------------|---------|---------|---------| +| Buffered body | 4 MiB | — | `server.limits.maxBufferedBodyBytes` | +| Header count | 256 | — | `server.limits.maxHeaderCount` | +| Header block size | 256 KiB | — | `server.limits.maxHeaderAggregateBytes` | +| Header line | 64 KiB | — | `server.limits.maxHeaderLineBytes` | +| HTTP/3 frame payload | 4 MiB | 64 MiB | `server.limits.maxHttp3FramePayloadBytes` | +| HTTP/2 deferred DATA per stream | 4 MiB + 16 KiB | 64 MiB | `server.limits.maxDeferredOutboundBytesPerStream` | +| Decoded header list | 64 KiB | — | `server.limits.maxDecodedHeaderListBytes` | +| Trailer count | 100 | 10000 | `server.limits.maxTrailerHeaderCount` | +| Trailer block | 16 KiB | 1 MiB | `server.limits.maxTrailerHeaderBlockBytes` | +| Compressed HTTP/2 header block | 256 KiB | 16 MiB | `server.limits.maxHttp2CompressedHeaderBlockBytes` | +| Interim 1xx responses | 20 | 100 | `server.limits.maxInterimResponses` | +| Auth challenge rounds | 3 | 20 | `server.limits.maxAuthChallengeRounds` | +| Upstream proxy auth attempts | 5 | 20 | `server.limits.maxUpstreamProxyAuthenticationAttempts` | +| Windows auth token | 12288 bytes | 1 MiB | `server.limits.maxWinAuthTokenBytes` | +| HTTP/2 flow-control wait | 60 s | 3600 s | `server.limits.http2WindowUpdateTimeoutSeconds` | +| Client connections (process) | unset | — | `server.pooling.maxConcurrentClientConnections` | +| Client connections (endpoint) | unset | — | `server.limits.maxConcurrentClients` | +| Connect timeout | 0 (off) | — | `server.timeouts.connectTimeOutSeconds` | +| Response header timeout | 0 (off) | — | `server.timeouts.responseHeaderTimeoutSeconds` | +| Idle read timeout | 0 (off) | — | `server.timeouts.idleReadTimeoutSeconds` | +| Idle write timeout | 0 (off) | — | `server.timeouts.idleWriteTimeoutSeconds` | +| Request timeout | 0 (off) | — | `server.timeouts.requestTimeoutSeconds` | +| Client header timeout | 0 (off) | — | `server.timeouts.clientHeaderTimeoutSeconds` | +| WebSocket frame | 16 MiB | — | `server.limits.maxWebSocketFramePayloadBytes` | + +Per-route overrides (request timeout, idle timeout, buffered body, WebSocket frame) live on `routes[].limits` and apply only to the matched session. Omit the block and the server limits are unchanged. + +## Memory + +HTTP/3 frame reads rent at most 256 KiB until bytes arrive, then grow. A peer that declares a huge length and sends nothing cannot reserve the ceiling times the stream count. Worst case for parked HTTP/2 DATA is concurrent streams times `maxDeferredOutboundBytesPerStream`. + +## Fixed on purpose + +These stay constant. They are protocol limits or anti-wrap guards, not traffic you should raise: + +- HTTP/2 `MAX_FRAME_SIZE` we accept is 16384. We do not advertise a larger value. +- HTTP/3 control-stream frames are 16 KiB. +- QPACK acknowledgement capacity. +- The 1 GiB chunk-size guard (stops a wrapped chunk length). +- SOCKS hostnames are 255 bytes (RFC 1928). + +414 and 421 are not produced by these limits. 431 is the request header-count or header-block rejection. 413 is the buffered-body rejection. + +## See also + +- [Why did my request fail](/docs/troubleshooting) +- [Configuration](/docs/configuration) +- [WebSocket large messages (design)](/docs/websocket-large-messages) diff --git a/website/docs/troubleshooting.md b/website/docs/troubleshooting.md new file mode 100644 index 000000000..32251cc55 --- /dev/null +++ b/website/docs/troubleshooting.md @@ -0,0 +1,36 @@ +# Why did my request fail + +> Start with the Error log. A limit rejection names the property, the configured value, the observed value, and the CLI key. See [Limits and escape hatches](/docs/limits). + +Show logs with `titanium run -c twp.yaml -v`, or set `logging.minimumLevel` below Error. The default Error level already includes Enforce rejections. Observe lines need Debug. + +## Status codes + +| Code | Usual cause | What to change | +|------|-------------|----------------| +| 400 | Malformed HTTP/1 framing (obs-fold, a header line with no colon) | Fix the client. This is not a limit. | +| 413 | Buffered body exceeded `maxBufferedBodyBytes` | Raise `server.limits.maxBufferedBodyBytes`, or stop materializing the body and stream it. | +| 431 | Request header count or header bytes exceeded the header limits | Raise `server.limits.maxHeaderCount` or `server.limits.maxHeaderAggregateBytes`. | +| 502 | Origin header list, origin HTTP/1 headers, or an origin failure | For header size, raise `server.limits.maxDecodedHeaderListBytes` or the HTTP/1 header keys. The log says which. | +| 503 | Admission: too many client connections | Raise `server.pooling.maxConcurrentClientConnections` or `server.limits.maxConcurrentClients`. | +| 504 | A timeout fired | The log names `server.timeouts.connectTimeOutSeconds`, `responseHeaderTimeoutSeconds`, `idleReadTimeoutSeconds`, `idleWriteTimeoutSeconds`, `requestTimeoutSeconds`, or `clientHeaderTimeoutSeconds`. | + +414 and 421 are not limit codes. + +## Resets and protocol errors + +| Symptom | Meaning | Key | +|---------|---------|-----| +| Connection reset while headers are large | HTTP/2 compressed header block or decoded list | `server.limits.maxHttp2CompressedHeaderBlockBytes`, `server.limits.maxDecodedHeaderListBytes` | +| `RST_STREAM` / `ENHANCE_YOUR_CALM` | Parked HTTP/2 DATA exceeded the per-stream queue | `server.limits.maxDeferredOutboundBytesPerStream` | +| HTTP/2 stream wait, then reset | Flow-control window was not updated in time | `server.limits.http2WindowUpdateTimeoutSeconds` | +| HTTP/3 `ExcessiveLoad` | A frame declared more than `maxHttp3FramePayloadBytes` | Raise that key (ceiling 64 MiB). DATA with no body hook streams past the cap; headers and hooks still need the whole frame. | +| HTTP/3 `FrameError` | The frame ended before the declared payload | The peer closed early. Not a limit. | +| WebSocket close 1009 | One frame exceeded `maxWebSocketFramePayloadBytes` | `server.limits.maxWebSocketFramePayloadBytes` | +| WebSocket close 1002 | Protocol error (bad opcode, fragmented control frame) | Fix the peer. | +| 401 / 407 after a few challenges | Auth rounds, upstream proxy attempts, or the Windows token size | `server.limits.maxAuthChallengeRounds`, `maxUpstreamProxyAuthenticationAttempts`, `maxWinAuthTokenBytes` | +| Many `103` / `100` then a failure | Interim responses | `server.limits.maxInterimResponses` | + +PublicFacing's 60 s idle and 120 s request deadlines close SSE, gRPC streams, and WebSockets. Raise those timeouts or use Balanced when the client is trusted. + +Decrypt-bypass learning is a Warning, not an Error: the current request was not failed. The line names `server.enableDecryptFailureBypass`. Later CONNECTs to that host tunnel without decryption until the TTL expires. diff --git a/website/docs/websocket-large-messages.md b/website/docs/websocket-large-messages.md new file mode 100644 index 000000000..1a5d406ee --- /dev/null +++ b/website/docs/websocket-large-messages.md @@ -0,0 +1,21 @@ +# WebSocket large messages + +> Design only. The relay behavior below is what the code does today. Do not implement the chunk callback until this spec is approved. + +## What the relay does today + +The frame decoder does not keep RSV1–RSV3. A `permessage-deflate` opener is delivered as an ordinary data frame, and message tracking reports it uncompressed. Tests lock that in: an RSV1 text frame still yields the original payload and `isCompressed == false`. + +When a subscriber filters frames, the decoder waits for the whole frame and enforces `maxWebSocketFramePayloadBytes` (close 1009). The raw relay, with no subscriber, copies bytes and does not decode frames. Hooks that filter content keep that whole-frame behavior. + +`DefaultMaxWebSocketMessageBytes` is not a second enforced cap. The live cap is the per-frame budget (`webSocketFrameBudget` / `server.limits.maxWebSocketFramePayloadBytes`). + +## Proposed shape (not implemented) + +1. Add an optional chunk callback beside the existing whole-frame event. Subscribers that do not set it keep today's whole-frame delivery. +2. Message-level limits, separate from the frame cap: maximum reassembled message bytes, and a fragmentation bound (frames per message). Both need `Enforce` / `Observe` / `Disabled` under `webSocketFrameBudget`. +3. Decide whether `DefaultMaxWebSocketMessageBytes` becomes that message cap or is removed so one name remains. +4. `permessage-deflate` inflates per chunk only when the extension was negotiated and RSV1 is preserved. Until then, RSV stays dropped and the extension offer is still stripped when interception is on, and preserved on the raw relay. +5. Cancellation and backpressure must return pooled buffers. A chunk subscriber must not force the raw relay to buffer. + +Gate before any implementation: `compare-ws-h1tls` and `compare-ws-h2`. The raw relay stays untouched. diff --git a/wiki/Home.md b/wiki/Home.md index 1ae5ed463..d62611ec3 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -25,6 +25,8 @@ Short catalog: [Features](Features) · full tables: [website Features](https://t - [Screenshots](#user-content-screenshots) - [Performance](Performance) — measured throughput vs peers - [Security considerations](Security-Considerations) +- [Limits and escape hatches](Limits-and-Escape-Hatches) +- [Why did my request fail](Troubleshooting) - [Protocol feature support](Protocol-Support) ([bridges](Protocol-Support#user-content-protocol-bridges)) - [Migrating from 4.x](Migration-from-4.x) @@ -451,7 +453,7 @@ QUIC endpoint is visible without extra config. | `EnableIpv6UnreachableSoftSkip` | `true` | disable for strict IPv6 preference | After one IPv6 `NetworkUnreachable`-class Happy Eyeballs failure, skip IPv6 addresses for 5 minutes (filter after address-family interleave). | | `MaxCachedConnections` | `128` | raise for high fan-out **per origin** | Live knob on `ProxyServer`. Cap is **per upstream host**, not process-wide. No upper clamp — set `512`/`1024` on large hosts. Keep in sync with `ResourceLimits.MaxCachedConnectionsPerHost` when you replace the snapshot. | | `ProxyEndPoint.MaxCachedConnections` | `null` (use server) | deeper pool for one reverse EP | Optional per-endpoint override applied when that EP owns the session. | -| `ResourceLimits.MaxConcurrentStreamsPerConnection` | `256` | raise for heavy H2 fan-in | Replace via `ProxyResourceLimits.Create(...)` — validated positive only, no max ceiling. | +| `ResourceLimits.MaxConcurrentStreamsPerConnection` | `1000` | PublicFacing uses `256`; raise further for heavy H2 fan-in | Replace via `ProxyResourceLimits.Create(...)` — validated positive only, no max ceiling. | | `ForwardCleartext` (transparent) | `false` | `true` for TLS-terminating reverse proxy | With `DecryptSsl`, terminate client TLS and open **cleartext** TCP to `ForwardHost`/`ForwardPort`. For H2 clients, also set `UpstreamHttpProtocol.Http11` + `AllowHttpProtocolTranslation`. | | `EnableTcpServerConnectionPrefetch` | `true` | — | Overlaps origin connect with client work. | | `NoDelay` | `true` | — | Disables Nagle. | diff --git a/wiki/Limits-and-Escape-Hatches.md b/wiki/Limits-and-Escape-Hatches.md new file mode 100644 index 000000000..267cea586 --- /dev/null +++ b/wiki/Limits-and-Escape-Hatches.md @@ -0,0 +1,45 @@ +> For CLI reverse-proxy operators and library users. The website copy is [Limits and escape hatches](https://titaniumproxy.com/docs/limits). + +The default log level is Error. Run `titanium run -c twp.yaml -v` or set `logging.minimumLevel` when you need Observe-mode lines (Debug). An Enforce rejection is an Error, so it shows up without `-v`. + +## Behavior changes at the previous defaults + +1. Origin HTTP/2 responses whose headers are between 8 KiB and 64 KiB now arrive intact. Above 64 KiB the stream fails with 502 and names `server.limits.maxDecodedHeaderListBytes`. +2. Under Balanced, an HTTP/1 request or origin response with more than 256 headers or 256 KiB of header bytes is rejected: 431 for a request, 502 for an origin response. LegacyCompatible keeps HeaderLimits at Observe. +3. Limit-driven failures appear in the default Error log, with the property, the configured limit, the observed value, and the CLI key. + +## Policy modes and profiles + +`server.policyModes.` is `Enforce` (reject, Error), `Observe` (pass, Debug), or `Disabled` (skip). A partial block changes only the keys you set. PublicFacing keeps `http1ReplaySafety` at Enforce unless you override it. + +Balanced (default) enforces header limits and allows 1000 concurrent streams, with timeouts disabled. PublicFacing caps streams at 256 and sets idle timeouts to 60 s and the request timeout to 120 s. Those deadlines cut SSE, gRPC streaming, and WebSocket. LegacyCompatible observes header limits. + +`maxBufferedBodyBytes` applies when a body is materialized. Streaming relays do not use it. `CanBufferBody` reports whether a message fits. `webSocketFrameBudget` is the per-frame WebSocket cap. `maxEncodedBodyBytes`, `maxDecodedBodyBytes`, and `maxDecompressionRatio` are reserved and not enforced; startup warns when they are set. Expansion is bounded by `maxBufferedBodyBytes`. + +## Keys + +| What fails | Default | CLI key | +|------------|---------|---------| +| Buffered body | 4 MiB | `server.limits.maxBufferedBodyBytes` | +| Header count | 256 | `server.limits.maxHeaderCount` | +| Header block size | 256 KiB | `server.limits.maxHeaderAggregateBytes` | +| Header line | 64 KiB | `server.limits.maxHeaderLineBytes` | +| HTTP/3 frame payload | 4 MiB (ceiling 64 MiB) | `server.limits.maxHttp3FramePayloadBytes` | +| HTTP/2 deferred DATA per stream | 4 MiB + 16 KiB (ceiling 64 MiB) | `server.limits.maxDeferredOutboundBytesPerStream` | +| Decoded header list | 64 KiB | `server.limits.maxDecodedHeaderListBytes` | +| Trailer count / block | 100 / 16 KiB | `server.limits.maxTrailerHeaderCount`, `server.limits.maxTrailerHeaderBlockBytes` | +| Compressed HTTP/2 header block | 256 KiB | `server.limits.maxHttp2CompressedHeaderBlockBytes` | +| Interim responses | 20 | `server.limits.maxInterimResponses` | +| Auth rounds / upstream attempts / token | 3 / 5 / 12288 | `server.limits.maxAuthChallengeRounds`, `server.limits.maxUpstreamProxyAuthenticationAttempts`, `server.limits.maxWinAuthTokenBytes` | +| HTTP/2 flow-control wait | 60 s | `server.limits.http2WindowUpdateTimeoutSeconds` | +| Client connections | unset | `server.pooling.maxConcurrentClientConnections`, `server.limits.maxConcurrentClients` | +| Timeouts | 0 (off) unless PublicFacing | `server.timeouts.connectTimeOutSeconds`, `server.timeouts.responseHeaderTimeoutSeconds`, `server.timeouts.idleReadTimeoutSeconds`, `server.timeouts.idleWriteTimeoutSeconds`, `server.timeouts.requestTimeoutSeconds`, `server.timeouts.clientHeaderTimeoutSeconds` | +| WebSocket frame | 16 MiB | `server.limits.maxWebSocketFramePayloadBytes` | + +`routes[].limits` can set `requestTimeoutSeconds`, `idleTimeoutSeconds`, `maxBufferedBodyBytes`, and `maxWebSocketFramePayloadBytes` for one route. A missing block does not touch the session. + +HTTP/3 rents at most 256 KiB until payload bytes arrive. Worst-case parked HTTP/2 DATA is concurrent streams times `maxDeferredOutboundBytesPerStream`. + +Fixed on purpose: HTTP/2 max frame 16384, HTTP/3 control frames 16 KiB, QPACK ack capacity, the 1 GiB chunk-size guard, SOCKS hostnames of 255 bytes. 431 is a real header-limit status. 414 and 421 are not limit codes. + +Misspelled keys are warned at startup and on reload. `titanium test --strict` exits 1 when any unknown key is present. diff --git a/wiki/Protocol-Support.md b/wiki/Protocol-Support.md index 2baeab975..c6ab44872 100644 --- a/wiki/Protocol-Support.md +++ b/wiki/Protocol-Support.md @@ -77,7 +77,7 @@ tunnel). See [RFC 8441](#user-content-http2-safety-and-frame-validation) below. | `SETTINGS` negotiation | N/A | N/A | Yes | Yes | HTTP/3 SETTINGS are sent on the outbound unidirectional control stream (RFC 9114 §7.2.4). | | `RST_STREAM` / per-stream cancellation | N/A | N/A | Yes | Yes | HTTP/3 uses QUIC `RESET_STREAM`/`STOP_SENDING`; stream-level cancellation cleans up session state and guarantees exactly one `AfterResponse`. | | `GOAWAY` / connection shutdown | N/A | N/A | Yes | Yes | HTTP/3 GOAWAY carries the last stream ID; `Http3Connection` sends and handles GOAWAY for graceful shutdown. | -| `MAX_CONCURRENT_STREAMS` admission | N/A | N/A | Yes | Yes | HTTP/2: advertised value is clamped to `ProxyResourceLimits.MaxConcurrentStreamsPerConnection` (default 256) and rewritten on the wire toward the client; the proxy enforces that cap only after the client's SETTINGS ACK (RFC 9113). Over-cap streams get `RST_STREAM(REFUSED_STREAM)` (Debug log). HTTP/3: `TransparentQuicProxyEndPoint.MaxInboundBidirectionalStreams` (default 100) limits concurrent request streams per QUIC connection. | +| `MAX_CONCURRENT_STREAMS` admission | N/A | N/A | Yes | Yes | HTTP/2: advertised value is clamped to `ProxyResourceLimits.MaxConcurrentStreamsPerConnection` (default 1000; PublicFacing uses 256) and rewritten on the wire toward the client; the proxy enforces that cap only after the client's SETTINGS ACK (RFC 9113). Over-cap streams get `RST_STREAM(REFUSED_STREAM)` (Debug log). HTTP/3: `TransparentQuicProxyEndPoint.MaxInboundBidirectionalStreams` (default 100) limits concurrent request streams per QUIC connection. | | Flow-control reservation timeout | N/A | N/A | Yes | N/A | HTTP/3 relies on QUIC's own idle-timeout and flow-control mechanisms. | ## Body handling and streaming diff --git a/wiki/Troubleshooting.md b/wiki/Troubleshooting.md new file mode 100644 index 000000000..0afaaa182 --- /dev/null +++ b/wiki/Troubleshooting.md @@ -0,0 +1,26 @@ +> Start with the Error log. A limit rejection names the property, the configured value, the observed value, and the CLI key. The key list is [Limits and escape hatches](Limits-and-Escape-Hatches). + +Show logs with `titanium run -c twp.yaml -v`, or set `logging.minimumLevel`. Enforce rejections are already Error. + +| Code | Usual cause | What to change | +|------|-------------|----------------| +| 400 | Malformed HTTP/1 framing | Fix the client. Not a limit. | +| 413 | Buffered body over `maxBufferedBodyBytes` | Raise `server.limits.maxBufferedBodyBytes`, or stream the body. | +| 431 | Request headers over the count or byte cap | `server.limits.maxHeaderCount` or `server.limits.maxHeaderAggregateBytes`. | +| 502 | Origin headers or an origin failure | `server.limits.maxDecodedHeaderListBytes` or the HTTP/1 header keys. The log says which. | +| 503 | Too many client connections | `server.pooling.maxConcurrentClientConnections` or `server.limits.maxConcurrentClients`. | +| 504 | A timeout | The log names the `server.timeouts.*` key. | + +414 and 421 are not limit codes. + +| Symptom | Key | +|---------|-----| +| HTTP/2 header GOAWAY or stream failure | `server.limits.maxHttp2CompressedHeaderBlockBytes`, `server.limits.maxDecodedHeaderListBytes` | +| `RST_STREAM` / `ENHANCE_YOUR_CALM` | `server.limits.maxDeferredOutboundBytesPerStream` | +| HTTP/2 flow-control wait | `server.limits.http2WindowUpdateTimeoutSeconds` | +| HTTP/3 `ExcessiveLoad` | `server.limits.maxHttp3FramePayloadBytes` | +| WebSocket 1009 | `server.limits.maxWebSocketFramePayloadBytes` | +| WebSocket 1002 | Protocol error. Fix the peer. | +| 401 / 407 after a few rounds | `server.limits.maxAuthChallengeRounds`, `maxUpstreamProxyAuthenticationAttempts`, `maxWinAuthTokenBytes` | + +PublicFacing's 60 s idle and 120 s request deadlines close SSE, gRPC streams, and WebSockets. Decrypt-bypass learning is a Warning (`server.enableDecryptFailureBypass`): the current request was not failed. From cccd51aaae40d6f49eb2bd1acc07e9e2a6cee013 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 05:41:08 +0000 Subject: [PATCH 05/19] Update documentation --- ...Proxy.EventArguments.SessionEventArgs.html | 50 +- ...um.Web.Proxy.Options.ProxyPolicyModes.html | 49 ++ ...Web.Proxy.Options.ProxyResourceLimits.html | 802 +++++++++++++++++- docs/api/Titanium.Web.Proxy.ProxyServer.html | 182 ++-- docs/index.json | 4 +- docs/xrefmap.yml | 270 ++++++ 6 files changed, 1200 insertions(+), 157 deletions(-) diff --git a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html index 0761bafa6..66f46544c 100644 --- a/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html +++ b/docs/api/Titanium.Web.Proxy.EventArguments.SessionEventArgs.html @@ -750,7 +750,7 @@

Methods Edit this page - View Source + View Source

CanBufferBody(RequestResponseBase)

@@ -802,7 +802,7 @@
Returns
Edit this page - View Source + View Source

Dispose(bool)

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

DrainClientBodyAsync(CancellationToken)

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

DrainServerBodyAsync(CancellationToken)

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

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

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

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

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

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

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

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

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

GetRequestBody(CancellationToken)

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

GetRequestBodyAsString(CancellationToken)

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

GetResponseBody(CancellationToken)

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

GetResponseBodyAsString(CancellationToken)

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

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

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

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

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

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

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

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

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

Redirect(string, bool)

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

Respond(Response, bool)

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

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

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

RespondStreaming(StreamingProxyResult, bool)

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

SetRequestBody(byte[])

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

SetRequestBodyString(string)

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

SetResponseBody(byte[])

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

SetResponseBodyString(string)

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

TerminateServerConnection()

diff --git a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html index 66f3c2701..335e05cdc 100644 --- a/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html +++ b/docs/api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html @@ -418,6 +418,55 @@
Returns
+ + | + Edit this page + + + View Source + + +

WithAllowAmbiguousFraming(bool)

+

Returns a snapshot identical to this one with AllowAmbiguousFraming set explicitly. +A partial config overlay uses this so an omitted flag does not clear a value set by the profile.

+
+
+
Declaration
+
+
public ProxyPolicyModes WithAllowAmbiguousFraming(bool enabled)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
boolenabled
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyPolicyModes
| Edit this page diff --git a/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html b/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html index fe7974958..7703a1db3 100644 --- a/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html +++ b/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html @@ -134,6 +134,68 @@
Syntax
public sealed class ProxyResourceLimits
+

Fields +

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

MaxDeferredOutboundBytesCeiling

+

Upper bound for MaxDeferredOutboundBytesPerStream.

+
+
+
Declaration
+
+
public const int MaxDeferredOutboundBytesCeiling = 67108864
+
+
Field Value
+ + + + + + + + + + + + + +
TypeDescription
int
+ + | + Edit this page + + + View Source + +

MaxHttp3FramePayloadCeiling

+

Upper bound for MaxHttp3FramePayloadBytes.

+
+
+
Declaration
+
+
public const long MaxHttp3FramePayloadCeiling = 67108864
+
+
Field Value
+ + + + + + + + + + + + + +
TypeDescription
long

Properties

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

Default

@@ -217,6 +279,68 @@
Property Value
+ + | + Edit this page + + + View Source + + +

Http2WindowUpdateTimeoutSeconds

+

Seconds to wait for an HTTP/2 WINDOW_UPDATE before failing the stream. Default 60.

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

MaxAuthChallengeRounds

+

Maximum Windows/proxy authentication challenge rounds per request. Default 3.

+
+
+
Declaration
+
+
public int MaxAuthChallengeRounds { get; }
+
+
Property Value
+ + + + + + + + + + + + + +
TypeDescription
int
| Edit this page @@ -457,6 +581,38 @@
Property Value
+ + | + Edit this page + + + View Source + + +

MaxDeferredOutboundBytesPerStream

+

Bytes of HTTP/2 DATA that may be parked per stream waiting for send-window credit. +Worst-case memory is concurrent streams times this value. Default 4 MiB + 16 KiB. Ceiling 64 MiB.

+
+
+
Declaration
+
+
public int MaxDeferredOutboundBytesPerStream { get; }
+
+
Property Value
+ + + + + + + + + + + + + +
TypeDescription
int
| Edit this page @@ -582,6 +738,99 @@
Property Value
+ + | + Edit this page + + + View Source + + +

MaxHttp2CompressedHeaderBlockBytes

+

Maximum compressed HTTP/2 header block (HEADERS + CONTINUATION) in bytes. Default 256 KiB.

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

MaxHttp3FramePayloadBytes

+

Maximum HTTP/3 frame payload accepted before H3_EXCESSIVE_LOAD. Default 4 MiB. Ceiling 64 MiB.

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

MaxInterimResponses

+

Maximum interim (1xx) responses accepted from one origin before the stream fails. Default 20.

+
+
+
Declaration
+
+
public int MaxInterimResponses { get; }
+
+
Property Value
+ + + + + + + + + + + + + +
TypeDescription
int
| Edit this page @@ -717,68 +966,192 @@
Property Value
-

Methods -

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

Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)

-

Validates and constructs a ProxyResourceLimits snapshot. Every non-nullable -bound must be strictly positive; every nullable bound, if supplied, must also be strictly -positive (use null to disable rather than a sentinel number).

+ +

MaxTrailerHeaderBlockBytes

+

Maximum trailer header block size in bytes. Default 16 KiB.

Declaration
-
public static ProxyResourceLimits Create(long maxHeaderLineBytes, int maxHeaderCount, long maxHeaderAggregateBytes, long? maxEncodedBodyBytes, long? maxDecodedBodyBytes, double? maxDecompressionRatio, int? maxConcurrentClients, int maxConcurrentStreamsPerConnection, int? maxPeerInitiatedIncompleteStreamResets, int maxOpenHeaderBlockFrames, TimeSpan maxOpenHeaderBlockDuration, bool connectionPoolingEnabled, int maxCachedConnectionsPerHost, int? maxCertificateCacheEntries)
+
public int MaxTrailerHeaderBlockBytes { get; }
-
Parameters
+
Property Value
- - - - - - - - - - - - - - - - - + +
TypeName Description
longmaxHeaderLineBytes
intmaxHeaderCount
longmaxHeaderAggregateBytes
long?maxEncodedBodyBytes
+ + | + Edit this page + + + View Source + + +

MaxTrailerHeaderCount

+

Maximum trailer header fields on one chunked message. Default 100.

+
+
+
Declaration
+
+
public int MaxTrailerHeaderCount { get; }
+
+
Property Value
+ + - - - + + + + - - + - - + +
long?maxDecodedBodyBytesTypeDescription
double?maxDecompressionRatioint
int?
+ + | + Edit this page + + + View Source + + +

MaxUpstreamProxyAuthenticationAttempts

+

Maximum CONNECT authentication attempts against an upstream proxy. Default 5.

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

MaxWinAuthTokenBytes

+

Maximum SSPI token buffer in bytes. Default 12288.

+
+
+
Declaration
+
+
public int MaxWinAuthTokenBytes { get; }
+
+
Property Value
+ + + + + + + + + + + + + +
TypeDescription
int
+

Methods +

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

Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?)

+

Validates and constructs a ProxyResourceLimits snapshot. Every non-nullable +bound must be strictly positive; every nullable bound, if supplied, must also be strictly +positive (use null to disable rather than a sentinel number).

+
+
+
Declaration
+
+
public static ProxyResourceLimits Create(long maxHeaderLineBytes, int maxHeaderCount, long maxHeaderAggregateBytes, long? maxEncodedBodyBytes, long? maxDecodedBodyBytes, double? maxDecompressionRatio, int? maxConcurrentClients, int maxConcurrentStreamsPerConnection, int? maxPeerInitiatedIncompleteStreamResets, int maxOpenHeaderBlockFrames, TimeSpan maxOpenHeaderBlockDuration, bool connectionPoolingEnabled, int maxCachedConnectionsPerHost, int? maxCertificateCacheEntries)
+
+
Parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -850,12 +1223,70 @@
Exceptions
TypeNameDescription
longmaxHeaderLineBytes
intmaxHeaderCount
longmaxHeaderAggregateBytes
long?maxEncodedBodyBytes
long?maxDecodedBodyBytes
double?maxDecompressionRatio
int? maxConcurrentClients
+ + | + Edit this page + + + View Source + + +

WithAuthenticationBounds(int, int, int)

+

Returns a copy with authentication bounds replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithAuthenticationBounds(int maxAuthChallengeRounds, int maxUpstreamProxyAuthenticationAttempts, int maxWinAuthTokenBytes)
+
+
Parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TypeNameDescription
intmaxAuthChallengeRounds
intmaxUpstreamProxyAuthenticationAttempts
intmaxWinAuthTokenBytes
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
| Edit this page - View Source + View Source

WithCertificateCacheBounds(int?, int?)

@@ -925,12 +1356,60 @@
Exceptions
+ + | + Edit this page + + + View Source + + +

WithHttp2WindowUpdateTimeoutSeconds(int)

+

Returns a copy with Http2WindowUpdateTimeoutSeconds replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithHttp2WindowUpdateTimeoutSeconds(int http2WindowUpdateTimeoutSeconds)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
inthttp2WindowUpdateTimeoutSeconds
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
| Edit this page - View Source + View Source

WithMaxConcurrentStreamsPerConnection(int)

@@ -973,12 +1452,204 @@
Returns
+ + | + Edit this page + + + View Source + + +

WithMaxDeferredOutboundBytesPerStream(int)

+

Returns a copy with MaxDeferredOutboundBytesPerStream replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithMaxDeferredOutboundBytesPerStream(int maxDeferredOutboundBytesPerStream)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
intmaxDeferredOutboundBytesPerStream
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
+ + | + Edit this page + + + View Source + + +

WithMaxHttp2CompressedHeaderBlockBytes(int)

+

Returns a copy with MaxHttp2CompressedHeaderBlockBytes replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithMaxHttp2CompressedHeaderBlockBytes(int maxHttp2CompressedHeaderBlockBytes)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
intmaxHttp2CompressedHeaderBlockBytes
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
+ + | + Edit this page + + + View Source + + +

WithMaxHttp3FramePayloadBytes(long)

+

Returns a copy with MaxHttp3FramePayloadBytes replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithMaxHttp3FramePayloadBytes(long maxHttp3FramePayloadBytes)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
longmaxHttp3FramePayloadBytes
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
+ + | + Edit this page + + + View Source + + +

WithMaxInterimResponses(int)

+

Returns a copy with MaxInterimResponses replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithMaxInterimResponses(int maxInterimResponses)
+
+
Parameters
+ + + + + + + + + + + + + + + +
TypeNameDescription
intmaxInterimResponses
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
| Edit this page - View Source + View Source

WithMaxOriginHttp2ConnectionsPerAuthority(int)

@@ -1021,6 +1692,59 @@
Returns
+ + | + Edit this page + + + View Source + + +

WithTrailerHeaderBounds(int, int)

+

Returns a copy with trailer header bounds replaced.

+
+
+
Declaration
+
+
public ProxyResourceLimits WithTrailerHeaderBounds(int maxTrailerHeaderCount, int maxTrailerHeaderBlockBytes)
+
+
Parameters
+ + + + + + + + + + + + + + + + + + + + +
TypeNameDescription
intmaxTrailerHeaderCount
intmaxTrailerHeaderBlockBytes
+
Returns
+ + + + + + + + + + + + + +
TypeDescription
ProxyResourceLimits
diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index 757fd157c..7923eeb58 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

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

CheckCertificateRevocation

@@ -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

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

CompatibilityMode100Continue

@@ -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

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

DnsServerEndPoint

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

Enable100ContinueBehaviour

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

EnableConnectionPool

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

EnableHttp3

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

EnableHttpInterception

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

EnableHttpsSvcbDnsDiscovery

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

EnableIpv6UnreachableSoftSkip

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

EnableQpackDynamicTable

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

EnableRequestTimingCapture

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

EnableRfc8441

@@ -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

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

IgnoreServerCertificateErrors

@@ -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,7 +1845,7 @@
Property Value
Edit this page - View Source + View Source

MaxBufferedBodyBytes

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

MaxCachedConnections

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

MaxConcurrentClientConnections

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

MaxConcurrentHttp11HttpsOriginCreates

@@ -2027,7 +2027,7 @@
Exceptions
Edit this page - View Source + View Source

MaxDecodedHeaderListBytes

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

MaxWebSocketFramePayloadBytes

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

NoDelay

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

OriginHttpVersionPolicy

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

PolicyModes

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

Profile

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

ProxyAuthenticationRealm

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

ProxyAuthenticationSchemes

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

ProxyBasicAuthenticateFunc

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

ProxyEndPoints

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

ProxySchemeAuthenticateFunc

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

RequestTimeoutSeconds

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

ResourceLimits

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

ResponseHeaderContributor

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

ResponseHeaderTimeoutSeconds

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

ReuseSocket

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

ReverseProxy

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

ServerConnectionCount

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

ShouldInterceptHttp

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

SupportedServerSslProtocols

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

SupportedSslProtocols

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

TcpTimeWaitSeconds

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

ThreadPoolWorkerThread

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

UpStreamEndPoint

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

UpStreamEndPointIPv4

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

UpStreamEndPointIPv6

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

UpStreamHttpProxy

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

UpStreamHttpsProxy

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

ViaHeaderPseudonym

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

ApplyLoggingConfiguration()

@@ -3329,7 +3329,7 @@
Parameters
Edit this page - View Source + View Source

Dispose()

@@ -3345,7 +3345,7 @@
Declaration
Edit this page - View Source + View Source

Dispose(bool)

@@ -3791,7 +3791,7 @@
Parameters
Edit this page - View Source + View Source

SetHttp3Enabled(bool)

@@ -3935,7 +3935,7 @@
Parameters
Edit this page - View Source + View Source

Stop()

@@ -3954,7 +3954,7 @@
Declaration
Edit this page - View Source + View Source

StopAsync(TimeSpan?)

@@ -4084,7 +4084,7 @@
Returns
Edit this page - View Source + View Source

TryEnableHttp3IfSupported()

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

Events Edit this page - View Source + View Source

AfterResponse

Intercept after response event from server.

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

BeforeRequest

Intercept request event to server.

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

BeforeResponse

Intercept response event from server.

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

BeforeUpStreamConnectRequest

Intercept connect request sent to upstream proxy.

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

ClientCertificateSelectionCallback

Event to override client certificate selection during mutual SSL authentication.

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

ClientConnectionCountChanged

Event occurs when client connection count changed.

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

Http3ClientConnectionCountChanged

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

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

Http3ServerConnectionCountChanged

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

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

OnClientConnectionCreate

Customize TcpClient used for client connection upon create.

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

OnRequestBodyWrite

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

Event Type
Edit this page - View Source + View Source

OnResponseBodyWrite

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

Event Type
Edit this page - View Source + View Source

OnServerConnectionCreate

Customize TcpClient used for server connection upon create.

@@ -4605,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.

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

ServerConnectionCountChanged

Event occurs when server connection count changed.

diff --git a/docs/index.json b/docs/index.json index d78964710..2a122455b 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) 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" + "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 WithAllowAmbiguousFraming(bool) Returns a snapshot identical to this one with AllowAmbiguousFraming set explicitly. A partial config overlay uses this so an omitted flag does not clear a value set by the profile. Declaration public ProxyPolicyModes WithAllowAmbiguousFraming(bool enabled) Parameters Type Name Description bool enabled 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", @@ -532,7 +532,7 @@ "api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html", "title": "Class ProxyResourceLimits | Titanium Web Proxy", - "summary": "Class 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. Inheritance object ProxyResourceLimits 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 ProxyResourceLimits Properties | Edit this page View Source ConnectionPoolingEnabled Whether upstream TCP connection pooling is enabled. Disabling pooling is an explicit choice represented by this flag, not by giving MaxCachedConnectionsPerHost a sentinel value that also has to be validated as \"not zero, not negative, unless it means disabled\". Declaration public bool ConnectionPoolingEnabled { get; } Property Value Type Description bool | Edit this page View Source Default Today's shipped values, carried forward as the Balanced profile's starting point. Header, decompression, CONTINUATION, and reset budgets stay high enough that ordinary browser traffic does not trip them. MaxConcurrentStreamsPerConnection is 1000, matching Chrome's own concurrent-stream setting: 256 reset later requests on a single x.com connection (REFUSED_STREAM) while the same page succeeded with no proxy. The check runs only when a stream is opened, so traffic under the old cap takes the same DATA path. PublicFacing pins the cap back to 256. MaxCertificateCacheEntries is the one deliberate exception to \"unchanged for existing traffic\": measurement showed process memory holding steady at ~100 MB above baseline after closing every browser tab and idling for minutes, tracking the number of distinct MITM'd hosts rather than any live connection or session count. Unbounded in-memory certificate retention is a defect, not a compatibility guarantee, so the shipped default now bounds it at 1024 entries (roughly 10 MB, comfortably above real single-session browsing) rather than leaving it unbounded like every other nullable limit here defaults to. Declaration public static ProxyResourceLimits Default { get; } Property Value Type Description ProxyResourceLimits | Edit this page View Source MaxCachedConnectionsPerHost Maximum pooled connections cached per remote host. Only meaningful when ConnectionPoolingEnabled is true; always validated as strictly positive regardless, so a future caller cannot re-introduce the \"0 spins forever holding the pool lock\" defect by flipping the flag without also fixing this value. Declaration public int MaxCachedConnectionsPerHost { get; } Property Value Type Description int | Edit this page View Source MaxCertificateCacheEntries Maximum number of generated leaf certificates held in the in-memory certificate cache. Each entry holds a full X509Certificate2 with its private key, so unlike most other limits in this type this one defends against unbounded memory growth from ordinary browsing (many distinct MITM'd hosts), not just against an adversarial peer. null disables the bound and is not the shipped default - see Default. Declaration public int? MaxCertificateCacheEntries { get; } Property Value Type Description int? | Edit this page View Source MaxCertificateDiskCacheEntries Maximum number of generated leaf certificate files retained in the on-disk cache (Certificates.CertificateManager.SaveFakeCertificates), pruned independently of MaxCertificateCacheEntries. Disk is far cheaper than the in-memory cache's live X509Certificate2 handles, and a warm disk cache avoids repeating expensive certificate generation across process restarts, so this bound is deliberately independent and typically much larger (or unbounded). null disables the bound. Declaration public int? MaxCertificateDiskCacheEntries { get; } Property Value Type Description int? | Edit this page View Source MaxConcurrentClients Maximum number of concurrently admitted client connections, checked by the admission gate at handler entry/exit rather than the delayed ClientConnectionCount. null disables global admission control. Declaration public int? MaxConcurrentClients { get; } Property Value Type Description int? | Edit this page View Source MaxConcurrentStreamsPerConnection Proxy-owned cap on concurrent HTTP/2 streams per connection. Always enforced: this is the single source of truth consolidating what were previously two independent mechanisms, and is also the value advertised to the origin in the relayed SETTINGS frame so the advertised and enforced values never disagree. Declaration public int MaxConcurrentStreamsPerConnection { get; } Property Value Type Description int | Edit this page View Source MaxDecodedBodyBytes Maximum cumulative decompressed body bytes produced for a single request or response. null disables the budget. Always checked alongside MaxDecompressionRatio, since a ratio alone cannot bound total memory for a small compressed input that expands enormously without also capping the output side. Declaration public long? MaxDecodedBodyBytes { get; } Property Value Type Description long? | Edit this page View Source MaxDecompressionRatio Maximum allowed ratio of decompressed to compressed bytes. null disables the ratio check (relying on MaxDecodedBodyBytes alone). Declaration public double? MaxDecompressionRatio { get; } Property Value Type Description double? | Edit this page View Source MaxEncodedBodyBytes Maximum cumulative compressed/on-wire body bytes read for a single request or response. null disables the budget. Declaration public long? MaxEncodedBodyBytes { get; } Property Value Type Description long? | Edit this page View Source MaxHeaderAggregateBytes Maximum aggregate size of all header fields in one request or response, in bytes. Declaration public long MaxHeaderAggregateBytes { get; } Property Value Type Description long | Edit this page View Source MaxHeaderCount Maximum number of header fields accepted in one request or response. Declaration public int MaxHeaderCount { get; } Property Value Type Description int | Edit this page View Source MaxHeaderLineBytes Maximum length of a single header line (request/status line or one header field), in bytes. Declaration public long MaxHeaderLineBytes { get; } Property Value Type Description long | Edit this page View Source MaxOpenHeaderBlockDuration Maximum wall-clock duration an HTTP/2 header block may stay open (from the initial HEADERS/PUSH_PROMISE frame sent without END_HEADERS through its terminating CONTINUATION), independent of MaxOpenHeaderBlockFrames. Bounds a slow CONTINUATION-trickle variant that stays under the frame-count cap by pacing itself, which a frame-count-only bound cannot catch on its own. Always enforced. Declaration public TimeSpan MaxOpenHeaderBlockDuration { get; } Property Value Type Description TimeSpan | Edit this page View Source MaxOpenHeaderBlockFrames Maximum number of CONTINUATION frames tolerated for a single open HTTP/2 header block, independent of the existing byte cap. Always enforced: zero-length CONTINUATION frames never trip a byte-based check, and only one header block may be open per connection direction, so an unbounded frame count also head-of-line blocks every other stream. Declaration public int MaxOpenHeaderBlockFrames { get; } Property Value Type Description int | Edit this page View Source MaxOriginHttp2ConnectionsPerAuthority Maximum number of concurrent origin HTTP/2 connections the proxy may open for one authority (host+port+scheme) when multiplexing streams from a single client connection (SocketsHttpHandler EnableMultipleHttp2Connections analog). Always at least 1. Declaration public int MaxOriginHttp2ConnectionsPerAuthority { get; } Property Value Type Description int | Edit this page View Source MaxPeerInitiatedIncompleteStreamResets Maximum number of peer-initiated resets of streams that never completed, per connection, before the proxy tears the connection down. Proxy-initiated resets (e.g. in response to a client cancellation) do not count. null disables the reset budget. Declaration public int? MaxPeerInitiatedIncompleteStreamResets { get; } Property Value Type Description int? Methods | Edit this page View Source Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?) Validates and constructs a ProxyResourceLimits snapshot. Every non-nullable bound must be strictly positive; every nullable bound, if supplied, must also be strictly positive (use null to disable rather than a sentinel number). Declaration public static ProxyResourceLimits Create(long maxHeaderLineBytes, int maxHeaderCount, long maxHeaderAggregateBytes, long? maxEncodedBodyBytes, long? maxDecodedBodyBytes, double? maxDecompressionRatio, int? maxConcurrentClients, int maxConcurrentStreamsPerConnection, int? maxPeerInitiatedIncompleteStreamResets, int maxOpenHeaderBlockFrames, TimeSpan maxOpenHeaderBlockDuration, bool connectionPoolingEnabled, int maxCachedConnectionsPerHost, int? maxCertificateCacheEntries) Parameters Type Name Description long maxHeaderLineBytes int maxHeaderCount long maxHeaderAggregateBytes long? maxEncodedBodyBytes long? maxDecodedBodyBytes double? maxDecompressionRatio int? maxConcurrentClients int maxConcurrentStreamsPerConnection int? maxPeerInitiatedIncompleteStreamResets int maxOpenHeaderBlockFrames TimeSpan maxOpenHeaderBlockDuration bool connectionPoolingEnabled int maxCachedConnectionsPerHost int? maxCertificateCacheEntries Returns Type Description ProxyResourceLimits Exceptions Type Condition ArgumentOutOfRangeException A supplied value is zero or negative. | Edit this page View Source WithCertificateCacheBounds(int?, int?) Returns a copy of this instance with MaxCertificateCacheEntries and MaxCertificateDiskCacheEntries replaced, leaving every other limit unchanged. Added instead of extending Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?) - which is public API already shipped with a fixed parameter list - so that adding the independent disk-cache bound could not be a breaking change for existing callers. Declaration public ProxyResourceLimits WithCertificateCacheBounds(int? maxCertificateCacheEntries, int? maxCertificateDiskCacheEntries) Parameters Type Name Description int? maxCertificateCacheEntries See MaxCertificateCacheEntries. null disables the bound. int? maxCertificateDiskCacheEntries See MaxCertificateDiskCacheEntries. null disables the bound. Returns Type Description ProxyResourceLimits Exceptions Type Condition ArgumentOutOfRangeException A supplied value is zero or negative. | Edit this page View Source WithMaxConcurrentStreamsPerConnection(int) Returns a copy with MaxConcurrentStreamsPerConnection replaced. Declaration public ProxyResourceLimits WithMaxConcurrentStreamsPerConnection(int maxConcurrentStreamsPerConnection) Parameters Type Name Description int maxConcurrentStreamsPerConnection Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxOriginHttp2ConnectionsPerAuthority(int) Returns a copy with MaxOriginHttp2ConnectionsPerAuthority replaced. Declaration public ProxyResourceLimits WithMaxOriginHttp2ConnectionsPerAuthority(int maxOriginHttp2ConnectionsPerAuthority) Parameters Type Name Description int maxOriginHttp2ConnectionsPerAuthority Returns Type Description ProxyResourceLimits" + "summary": "Class 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. Inheritance object ProxyResourceLimits 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 ProxyResourceLimits Fields | Edit this page View Source MaxDeferredOutboundBytesCeiling Upper bound for MaxDeferredOutboundBytesPerStream. Declaration public const int MaxDeferredOutboundBytesCeiling = 67108864 Field Value Type Description int | Edit this page View Source MaxHttp3FramePayloadCeiling Upper bound for MaxHttp3FramePayloadBytes. Declaration public const long MaxHttp3FramePayloadCeiling = 67108864 Field Value Type Description long Properties | Edit this page View Source ConnectionPoolingEnabled Whether upstream TCP connection pooling is enabled. Disabling pooling is an explicit choice represented by this flag, not by giving MaxCachedConnectionsPerHost a sentinel value that also has to be validated as \"not zero, not negative, unless it means disabled\". Declaration public bool ConnectionPoolingEnabled { get; } Property Value Type Description bool | Edit this page View Source Default Today's shipped values, carried forward as the Balanced profile's starting point. Header, decompression, CONTINUATION, and reset budgets stay high enough that ordinary browser traffic does not trip them. MaxConcurrentStreamsPerConnection is 1000, matching Chrome's own concurrent-stream setting: 256 reset later requests on a single x.com connection (REFUSED_STREAM) while the same page succeeded with no proxy. The check runs only when a stream is opened, so traffic under the old cap takes the same DATA path. PublicFacing pins the cap back to 256. MaxCertificateCacheEntries is the one deliberate exception to \"unchanged for existing traffic\": measurement showed process memory holding steady at ~100 MB above baseline after closing every browser tab and idling for minutes, tracking the number of distinct MITM'd hosts rather than any live connection or session count. Unbounded in-memory certificate retention is a defect, not a compatibility guarantee, so the shipped default now bounds it at 1024 entries (roughly 10 MB, comfortably above real single-session browsing) rather than leaving it unbounded like every other nullable limit here defaults to. Declaration public static ProxyResourceLimits Default { get; } Property Value Type Description ProxyResourceLimits | Edit this page View Source Http2WindowUpdateTimeoutSeconds Seconds to wait for an HTTP/2 WINDOW_UPDATE before failing the stream. Default 60. Declaration public int Http2WindowUpdateTimeoutSeconds { get; } Property Value Type Description int | Edit this page View Source MaxAuthChallengeRounds Maximum Windows/proxy authentication challenge rounds per request. Default 3. Declaration public int MaxAuthChallengeRounds { get; } Property Value Type Description int | Edit this page View Source MaxCachedConnectionsPerHost Maximum pooled connections cached per remote host. Only meaningful when ConnectionPoolingEnabled is true; always validated as strictly positive regardless, so a future caller cannot re-introduce the \"0 spins forever holding the pool lock\" defect by flipping the flag without also fixing this value. Declaration public int MaxCachedConnectionsPerHost { get; } Property Value Type Description int | Edit this page View Source MaxCertificateCacheEntries Maximum number of generated leaf certificates held in the in-memory certificate cache. Each entry holds a full X509Certificate2 with its private key, so unlike most other limits in this type this one defends against unbounded memory growth from ordinary browsing (many distinct MITM'd hosts), not just against an adversarial peer. null disables the bound and is not the shipped default - see Default. Declaration public int? MaxCertificateCacheEntries { get; } Property Value Type Description int? | Edit this page View Source MaxCertificateDiskCacheEntries Maximum number of generated leaf certificate files retained in the on-disk cache (Certificates.CertificateManager.SaveFakeCertificates), pruned independently of MaxCertificateCacheEntries. Disk is far cheaper than the in-memory cache's live X509Certificate2 handles, and a warm disk cache avoids repeating expensive certificate generation across process restarts, so this bound is deliberately independent and typically much larger (or unbounded). null disables the bound. Declaration public int? MaxCertificateDiskCacheEntries { get; } Property Value Type Description int? | Edit this page View Source MaxConcurrentClients Maximum number of concurrently admitted client connections, checked by the admission gate at handler entry/exit rather than the delayed ClientConnectionCount. null disables global admission control. Declaration public int? MaxConcurrentClients { get; } Property Value Type Description int? | Edit this page View Source MaxConcurrentStreamsPerConnection Proxy-owned cap on concurrent HTTP/2 streams per connection. Always enforced: this is the single source of truth consolidating what were previously two independent mechanisms, and is also the value advertised to the origin in the relayed SETTINGS frame so the advertised and enforced values never disagree. Declaration public int MaxConcurrentStreamsPerConnection { get; } Property Value Type Description int | Edit this page View Source MaxDecodedBodyBytes Maximum cumulative decompressed body bytes produced for a single request or response. null disables the budget. Always checked alongside MaxDecompressionRatio, since a ratio alone cannot bound total memory for a small compressed input that expands enormously without also capping the output side. Declaration public long? MaxDecodedBodyBytes { get; } Property Value Type Description long? | Edit this page View Source MaxDecompressionRatio Maximum allowed ratio of decompressed to compressed bytes. null disables the ratio check (relying on MaxDecodedBodyBytes alone). Declaration public double? MaxDecompressionRatio { get; } Property Value Type Description double? | Edit this page View Source MaxDeferredOutboundBytesPerStream Bytes of HTTP/2 DATA that may be parked per stream waiting for send-window credit. Worst-case memory is concurrent streams times this value. Default 4 MiB + 16 KiB. Ceiling 64 MiB. Declaration public int MaxDeferredOutboundBytesPerStream { get; } Property Value Type Description int | Edit this page View Source MaxEncodedBodyBytes Maximum cumulative compressed/on-wire body bytes read for a single request or response. null disables the budget. Declaration public long? MaxEncodedBodyBytes { get; } Property Value Type Description long? | Edit this page View Source MaxHeaderAggregateBytes Maximum aggregate size of all header fields in one request or response, in bytes. Declaration public long MaxHeaderAggregateBytes { get; } Property Value Type Description long | Edit this page View Source MaxHeaderCount Maximum number of header fields accepted in one request or response. Declaration public int MaxHeaderCount { get; } Property Value Type Description int | Edit this page View Source MaxHeaderLineBytes Maximum length of a single header line (request/status line or one header field), in bytes. Declaration public long MaxHeaderLineBytes { get; } Property Value Type Description long | Edit this page View Source MaxHttp2CompressedHeaderBlockBytes Maximum compressed HTTP/2 header block (HEADERS + CONTINUATION) in bytes. Default 256 KiB. Declaration public int MaxHttp2CompressedHeaderBlockBytes { get; } Property Value Type Description int | Edit this page View Source MaxHttp3FramePayloadBytes Maximum HTTP/3 frame payload accepted before H3_EXCESSIVE_LOAD. Default 4 MiB. Ceiling 64 MiB. Declaration public long MaxHttp3FramePayloadBytes { get; } Property Value Type Description long | Edit this page View Source MaxInterimResponses Maximum interim (1xx) responses accepted from one origin before the stream fails. Default 20. Declaration public int MaxInterimResponses { get; } Property Value Type Description int | Edit this page View Source MaxOpenHeaderBlockDuration Maximum wall-clock duration an HTTP/2 header block may stay open (from the initial HEADERS/PUSH_PROMISE frame sent without END_HEADERS through its terminating CONTINUATION), independent of MaxOpenHeaderBlockFrames. Bounds a slow CONTINUATION-trickle variant that stays under the frame-count cap by pacing itself, which a frame-count-only bound cannot catch on its own. Always enforced. Declaration public TimeSpan MaxOpenHeaderBlockDuration { get; } Property Value Type Description TimeSpan | Edit this page View Source MaxOpenHeaderBlockFrames Maximum number of CONTINUATION frames tolerated for a single open HTTP/2 header block, independent of the existing byte cap. Always enforced: zero-length CONTINUATION frames never trip a byte-based check, and only one header block may be open per connection direction, so an unbounded frame count also head-of-line blocks every other stream. Declaration public int MaxOpenHeaderBlockFrames { get; } Property Value Type Description int | Edit this page View Source MaxOriginHttp2ConnectionsPerAuthority Maximum number of concurrent origin HTTP/2 connections the proxy may open for one authority (host+port+scheme) when multiplexing streams from a single client connection (SocketsHttpHandler EnableMultipleHttp2Connections analog). Always at least 1. Declaration public int MaxOriginHttp2ConnectionsPerAuthority { get; } Property Value Type Description int | Edit this page View Source MaxPeerInitiatedIncompleteStreamResets Maximum number of peer-initiated resets of streams that never completed, per connection, before the proxy tears the connection down. Proxy-initiated resets (e.g. in response to a client cancellation) do not count. null disables the reset budget. Declaration public int? MaxPeerInitiatedIncompleteStreamResets { get; } Property Value Type Description int? | Edit this page View Source MaxTrailerHeaderBlockBytes Maximum trailer header block size in bytes. Default 16 KiB. Declaration public int MaxTrailerHeaderBlockBytes { get; } Property Value Type Description int | Edit this page View Source MaxTrailerHeaderCount Maximum trailer header fields on one chunked message. Default 100. Declaration public int MaxTrailerHeaderCount { get; } Property Value Type Description int | Edit this page View Source MaxUpstreamProxyAuthenticationAttempts Maximum CONNECT authentication attempts against an upstream proxy. Default 5. Declaration public int MaxUpstreamProxyAuthenticationAttempts { get; } Property Value Type Description int | Edit this page View Source MaxWinAuthTokenBytes Maximum SSPI token buffer in bytes. Default 12288. Declaration public int MaxWinAuthTokenBytes { get; } Property Value Type Description int Methods | Edit this page View Source Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?) Validates and constructs a ProxyResourceLimits snapshot. Every non-nullable bound must be strictly positive; every nullable bound, if supplied, must also be strictly positive (use null to disable rather than a sentinel number). Declaration public static ProxyResourceLimits Create(long maxHeaderLineBytes, int maxHeaderCount, long maxHeaderAggregateBytes, long? maxEncodedBodyBytes, long? maxDecodedBodyBytes, double? maxDecompressionRatio, int? maxConcurrentClients, int maxConcurrentStreamsPerConnection, int? maxPeerInitiatedIncompleteStreamResets, int maxOpenHeaderBlockFrames, TimeSpan maxOpenHeaderBlockDuration, bool connectionPoolingEnabled, int maxCachedConnectionsPerHost, int? maxCertificateCacheEntries) Parameters Type Name Description long maxHeaderLineBytes int maxHeaderCount long maxHeaderAggregateBytes long? maxEncodedBodyBytes long? maxDecodedBodyBytes double? maxDecompressionRatio int? maxConcurrentClients int maxConcurrentStreamsPerConnection int? maxPeerInitiatedIncompleteStreamResets int maxOpenHeaderBlockFrames TimeSpan maxOpenHeaderBlockDuration bool connectionPoolingEnabled int maxCachedConnectionsPerHost int? maxCertificateCacheEntries Returns Type Description ProxyResourceLimits Exceptions Type Condition ArgumentOutOfRangeException A supplied value is zero or negative. | Edit this page View Source WithAuthenticationBounds(int, int, int) Returns a copy with authentication bounds replaced. Declaration public ProxyResourceLimits WithAuthenticationBounds(int maxAuthChallengeRounds, int maxUpstreamProxyAuthenticationAttempts, int maxWinAuthTokenBytes) Parameters Type Name Description int maxAuthChallengeRounds int maxUpstreamProxyAuthenticationAttempts int maxWinAuthTokenBytes Returns Type Description ProxyResourceLimits | Edit this page View Source WithCertificateCacheBounds(int?, int?) Returns a copy of this instance with MaxCertificateCacheEntries and MaxCertificateDiskCacheEntries replaced, leaving every other limit unchanged. Added instead of extending Create(long, int, long, long?, long?, double?, int?, int, int?, int, TimeSpan, bool, int, int?) - which is public API already shipped with a fixed parameter list - so that adding the independent disk-cache bound could not be a breaking change for existing callers. Declaration public ProxyResourceLimits WithCertificateCacheBounds(int? maxCertificateCacheEntries, int? maxCertificateDiskCacheEntries) Parameters Type Name Description int? maxCertificateCacheEntries See MaxCertificateCacheEntries. null disables the bound. int? maxCertificateDiskCacheEntries See MaxCertificateDiskCacheEntries. null disables the bound. Returns Type Description ProxyResourceLimits Exceptions Type Condition ArgumentOutOfRangeException A supplied value is zero or negative. | Edit this page View Source WithHttp2WindowUpdateTimeoutSeconds(int) Returns a copy with Http2WindowUpdateTimeoutSeconds replaced. Declaration public ProxyResourceLimits WithHttp2WindowUpdateTimeoutSeconds(int http2WindowUpdateTimeoutSeconds) Parameters Type Name Description int http2WindowUpdateTimeoutSeconds Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxConcurrentStreamsPerConnection(int) Returns a copy with MaxConcurrentStreamsPerConnection replaced. Declaration public ProxyResourceLimits WithMaxConcurrentStreamsPerConnection(int maxConcurrentStreamsPerConnection) Parameters Type Name Description int maxConcurrentStreamsPerConnection Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxDeferredOutboundBytesPerStream(int) Returns a copy with MaxDeferredOutboundBytesPerStream replaced. Declaration public ProxyResourceLimits WithMaxDeferredOutboundBytesPerStream(int maxDeferredOutboundBytesPerStream) Parameters Type Name Description int maxDeferredOutboundBytesPerStream Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxHttp2CompressedHeaderBlockBytes(int) Returns a copy with MaxHttp2CompressedHeaderBlockBytes replaced. Declaration public ProxyResourceLimits WithMaxHttp2CompressedHeaderBlockBytes(int maxHttp2CompressedHeaderBlockBytes) Parameters Type Name Description int maxHttp2CompressedHeaderBlockBytes Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxHttp3FramePayloadBytes(long) Returns a copy with MaxHttp3FramePayloadBytes replaced. Declaration public ProxyResourceLimits WithMaxHttp3FramePayloadBytes(long maxHttp3FramePayloadBytes) Parameters Type Name Description long maxHttp3FramePayloadBytes Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxInterimResponses(int) Returns a copy with MaxInterimResponses replaced. Declaration public ProxyResourceLimits WithMaxInterimResponses(int maxInterimResponses) Parameters Type Name Description int maxInterimResponses Returns Type Description ProxyResourceLimits | Edit this page View Source WithMaxOriginHttp2ConnectionsPerAuthority(int) Returns a copy with MaxOriginHttp2ConnectionsPerAuthority replaced. Declaration public ProxyResourceLimits WithMaxOriginHttp2ConnectionsPerAuthority(int maxOriginHttp2ConnectionsPerAuthority) Parameters Type Name Description int maxOriginHttp2ConnectionsPerAuthority Returns Type Description ProxyResourceLimits | Edit this page View Source WithTrailerHeaderBounds(int, int) Returns a copy with trailer header bounds replaced. Declaration public ProxyResourceLimits WithTrailerHeaderBounds(int maxTrailerHeaderCount, int maxTrailerHeaderBlockBytes) Parameters Type Name Description int maxTrailerHeaderCount int maxTrailerHeaderBlockBytes Returns Type Description ProxyResourceLimits" }, "api/Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html": { "href": "api/Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html", diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml index f6be8c6d7..c203e984b 100644 --- a/docs/xrefmap.yml +++ b/docs/xrefmap.yml @@ -8222,6 +8222,22 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllObservedExceptDisabled nameWithType: ProxyPolicyModes.WithAllObservedExceptDisabled +- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(System.Boolean) + name: WithAllowAmbiguousFraming(bool) + href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_WithAllowAmbiguousFraming_System_Boolean_ + commentId: M:Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(System.Boolean) + name.vb: WithAllowAmbiguousFraming(Boolean) + fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(bool) + fullName.vb: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(Boolean) + nameWithType: ProxyPolicyModes.WithAllowAmbiguousFraming(bool) + nameWithType.vb: ProxyPolicyModes.WithAllowAmbiguousFraming(Boolean) +- uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming* + name: WithAllowAmbiguousFraming + href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_WithAllowAmbiguousFraming_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming + nameWithType: ProxyPolicyModes.WithAllowAmbiguousFraming - uid: Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFramingEnabled name: WithAllowAmbiguousFramingEnabled() href: api/Titanium.Web.Proxy.Options.ProxyPolicyModes.html#Titanium_Web_Proxy_Options_ProxyPolicyModes_WithAllowAmbiguousFramingEnabled @@ -8495,6 +8511,32 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.Default nameWithType: ProxyResourceLimits.Default +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds + name: Http2WindowUpdateTimeoutSeconds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_Http2WindowUpdateTimeoutSeconds + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds + nameWithType: ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds* + name: Http2WindowUpdateTimeoutSeconds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_Http2WindowUpdateTimeoutSeconds_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds + nameWithType: ProxyResourceLimits.Http2WindowUpdateTimeoutSeconds +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds + name: MaxAuthChallengeRounds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxAuthChallengeRounds + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds + nameWithType: ProxyResourceLimits.MaxAuthChallengeRounds +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds* + name: MaxAuthChallengeRounds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxAuthChallengeRounds_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxAuthChallengeRounds + nameWithType: ProxyResourceLimits.MaxAuthChallengeRounds - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxCachedConnectionsPerHost name: MaxCachedConnectionsPerHost href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxCachedConnectionsPerHost @@ -8586,6 +8628,25 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDecompressionRatio nameWithType: ProxyResourceLimits.MaxDecompressionRatio +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesCeiling + name: MaxDeferredOutboundBytesCeiling + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxDeferredOutboundBytesCeiling + commentId: F:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesCeiling + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesCeiling + nameWithType: ProxyResourceLimits.MaxDeferredOutboundBytesCeiling +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream + name: MaxDeferredOutboundBytesPerStream + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxDeferredOutboundBytesPerStream + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream + nameWithType: ProxyResourceLimits.MaxDeferredOutboundBytesPerStream +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream* + name: MaxDeferredOutboundBytesPerStream + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxDeferredOutboundBytesPerStream_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxDeferredOutboundBytesPerStream + nameWithType: ProxyResourceLimits.MaxDeferredOutboundBytesPerStream - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxEncodedBodyBytes name: MaxEncodedBodyBytes href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxEncodedBodyBytes @@ -8638,6 +8699,51 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHeaderLineBytes nameWithType: ProxyResourceLimits.MaxHeaderLineBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes + name: MaxHttp2CompressedHeaderBlockBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxHttp2CompressedHeaderBlockBytes + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes + nameWithType: ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes* + name: MaxHttp2CompressedHeaderBlockBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxHttp2CompressedHeaderBlockBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes + nameWithType: ProxyResourceLimits.MaxHttp2CompressedHeaderBlockBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes + name: MaxHttp3FramePayloadBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxHttp3FramePayloadBytes + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes + nameWithType: ProxyResourceLimits.MaxHttp3FramePayloadBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes* + name: MaxHttp3FramePayloadBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxHttp3FramePayloadBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadBytes + nameWithType: ProxyResourceLimits.MaxHttp3FramePayloadBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadCeiling + name: MaxHttp3FramePayloadCeiling + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxHttp3FramePayloadCeiling + commentId: F:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadCeiling + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxHttp3FramePayloadCeiling + nameWithType: ProxyResourceLimits.MaxHttp3FramePayloadCeiling +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses + name: MaxInterimResponses + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxInterimResponses + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses + nameWithType: ProxyResourceLimits.MaxInterimResponses +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses* + name: MaxInterimResponses + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxInterimResponses_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxInterimResponses + nameWithType: ProxyResourceLimits.MaxInterimResponses - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxOpenHeaderBlockDuration name: MaxOpenHeaderBlockDuration href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxOpenHeaderBlockDuration @@ -8690,6 +8796,74 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxPeerInitiatedIncompleteStreamResets nameWithType: ProxyResourceLimits.MaxPeerInitiatedIncompleteStreamResets +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes + name: MaxTrailerHeaderBlockBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxTrailerHeaderBlockBytes + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes + nameWithType: ProxyResourceLimits.MaxTrailerHeaderBlockBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes* + name: MaxTrailerHeaderBlockBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxTrailerHeaderBlockBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderBlockBytes + nameWithType: ProxyResourceLimits.MaxTrailerHeaderBlockBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount + name: MaxTrailerHeaderCount + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxTrailerHeaderCount + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount + nameWithType: ProxyResourceLimits.MaxTrailerHeaderCount +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount* + name: MaxTrailerHeaderCount + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxTrailerHeaderCount_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxTrailerHeaderCount + nameWithType: ProxyResourceLimits.MaxTrailerHeaderCount +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts + name: MaxUpstreamProxyAuthenticationAttempts + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxUpstreamProxyAuthenticationAttempts + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts + nameWithType: ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts* + name: MaxUpstreamProxyAuthenticationAttempts + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxUpstreamProxyAuthenticationAttempts_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts + nameWithType: ProxyResourceLimits.MaxUpstreamProxyAuthenticationAttempts +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes + name: MaxWinAuthTokenBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxWinAuthTokenBytes + commentId: P:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes + nameWithType: ProxyResourceLimits.MaxWinAuthTokenBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes* + name: MaxWinAuthTokenBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_MaxWinAuthTokenBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.MaxWinAuthTokenBytes + nameWithType: ProxyResourceLimits.MaxWinAuthTokenBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds(System.Int32,System.Int32,System.Int32) + name: WithAuthenticationBounds(int, int, int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithAuthenticationBounds_System_Int32_System_Int32_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds(System.Int32,System.Int32,System.Int32) + name.vb: WithAuthenticationBounds(Integer, Integer, Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds(int, int, int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds(Integer, Integer, Integer) + nameWithType: ProxyResourceLimits.WithAuthenticationBounds(int, int, int) + nameWithType.vb: ProxyResourceLimits.WithAuthenticationBounds(Integer, Integer, Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds* + name: WithAuthenticationBounds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithAuthenticationBounds_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithAuthenticationBounds + nameWithType: ProxyResourceLimits.WithAuthenticationBounds - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithCertificateCacheBounds(System.Nullable{System.Int32},System.Nullable{System.Int32}) name: WithCertificateCacheBounds(int?, int?) href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithCertificateCacheBounds_System_Nullable_System_Int32__System_Nullable_System_Int32__ @@ -8706,6 +8880,22 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithCertificateCacheBounds nameWithType: ProxyResourceLimits.WithCertificateCacheBounds +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(System.Int32) + name: WithHttp2WindowUpdateTimeoutSeconds(int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithHttp2WindowUpdateTimeoutSeconds_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(System.Int32) + name.vb: WithHttp2WindowUpdateTimeoutSeconds(Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(Integer) + nameWithType: ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(int) + nameWithType.vb: ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds(Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds* + name: WithHttp2WindowUpdateTimeoutSeconds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithHttp2WindowUpdateTimeoutSeconds_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds + nameWithType: ProxyResourceLimits.WithHttp2WindowUpdateTimeoutSeconds - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxConcurrentStreamsPerConnection(System.Int32) name: WithMaxConcurrentStreamsPerConnection(int) href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxConcurrentStreamsPerConnection_System_Int32_ @@ -8722,6 +8912,70 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxConcurrentStreamsPerConnection nameWithType: ProxyResourceLimits.WithMaxConcurrentStreamsPerConnection +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(System.Int32) + name: WithMaxDeferredOutboundBytesPerStream(int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxDeferredOutboundBytesPerStream_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(System.Int32) + name.vb: WithMaxDeferredOutboundBytesPerStream(Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(Integer) + nameWithType: ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(int) + nameWithType.vb: ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream(Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream* + name: WithMaxDeferredOutboundBytesPerStream + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxDeferredOutboundBytesPerStream_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream + nameWithType: ProxyResourceLimits.WithMaxDeferredOutboundBytesPerStream +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(System.Int32) + name: WithMaxHttp2CompressedHeaderBlockBytes(int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxHttp2CompressedHeaderBlockBytes_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(System.Int32) + name.vb: WithMaxHttp2CompressedHeaderBlockBytes(Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(Integer) + nameWithType: ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(int) + nameWithType.vb: ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes(Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes* + name: WithMaxHttp2CompressedHeaderBlockBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxHttp2CompressedHeaderBlockBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes + nameWithType: ProxyResourceLimits.WithMaxHttp2CompressedHeaderBlockBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(System.Int64) + name: WithMaxHttp3FramePayloadBytes(long) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxHttp3FramePayloadBytes_System_Int64_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(System.Int64) + name.vb: WithMaxHttp3FramePayloadBytes(Long) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(long) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(Long) + nameWithType: ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(long) + nameWithType.vb: ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(Long) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes* + name: WithMaxHttp3FramePayloadBytes + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxHttp3FramePayloadBytes_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes + nameWithType: ProxyResourceLimits.WithMaxHttp3FramePayloadBytes +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(System.Int32) + name: WithMaxInterimResponses(int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxInterimResponses_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(System.Int32) + name.vb: WithMaxInterimResponses(Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(Integer) + nameWithType: ProxyResourceLimits.WithMaxInterimResponses(int) + nameWithType.vb: ProxyResourceLimits.WithMaxInterimResponses(Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses* + name: WithMaxInterimResponses + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxInterimResponses_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses + nameWithType: ProxyResourceLimits.WithMaxInterimResponses - uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxOriginHttp2ConnectionsPerAuthority(System.Int32) name: WithMaxOriginHttp2ConnectionsPerAuthority(int) href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithMaxOriginHttp2ConnectionsPerAuthority_System_Int32_ @@ -8738,6 +8992,22 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxOriginHttp2ConnectionsPerAuthority nameWithType: ProxyResourceLimits.WithMaxOriginHttp2ConnectionsPerAuthority +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(System.Int32,System.Int32) + name: WithTrailerHeaderBounds(int, int) + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithTrailerHeaderBounds_System_Int32_System_Int32_ + commentId: M:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(System.Int32,System.Int32) + name.vb: WithTrailerHeaderBounds(Integer, Integer) + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(int, int) + fullName.vb: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(Integer, Integer) + nameWithType: ProxyResourceLimits.WithTrailerHeaderBounds(int, int) + nameWithType.vb: ProxyResourceLimits.WithTrailerHeaderBounds(Integer, Integer) +- uid: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds* + name: WithTrailerHeaderBounds + href: api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html#Titanium_Web_Proxy_Options_ProxyResourceLimits_WithTrailerHeaderBounds_ + commentId: Overload:Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds + isSpec: "True" + fullName: Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds + nameWithType: ProxyResourceLimits.WithTrailerHeaderBounds - uid: Titanium.Web.Proxy.Options.ProxyTimeoutOptions name: ProxyTimeoutOptions href: api/Titanium.Web.Proxy.Options.ProxyTimeoutOptions.html From 765729a7bc4b34a3eef43887acd12d1715632797 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 11:16:38 +0530 Subject: [PATCH 06/19] fix(sonar): clear the new-code issues from the limit hatches. The quality gate treats a floating-point inequality as a bug, and the new helpers tripped complexity, visibility, and parameter-count rules. --- .../TwpConfigSchema.cs | 21 +++--- .../UnknownConfigKeyScanner.cs | 62 ++++++++++------ .../Http/HeaderLimitsExceededException.cs | 20 ++++-- src/Titanium.Web.Proxy/Http/HeaderParser.cs | 30 ++++++-- src/Titanium.Web.Proxy/Http3/Http3Frame.cs | 8 ++- .../Logging/LimitCatalog.cs | 31 ++++---- .../TcpConnection/TcpConnectionFactory.cs | 11 ++- .../Options/ProxyResourceLimits.cs | 72 +++++++++++-------- .../ProxyServer.Lifecycle.cs | 12 +++- .../PublicAPI.Unshipped.txt | 6 ++ 10 files changed, 181 insertions(+), 92 deletions(-) diff --git a/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs b/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs index a22410cdf..d1b4afd14 100644 --- a/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs +++ b/src/Titanium.Web.Proxy.Configuration/TwpConfigSchema.cs @@ -8,6 +8,9 @@ namespace Titanium.Web.Proxy.Configuration; ///
public static class TwpConfigSchema { + private const string ObjectType = "object"; + private const string StringType = "string"; + public static string Generate() { var root = new JsonObject @@ -15,7 +18,7 @@ public static string Generate() ["$schema"] = "https://json-schema.org/draft/2020-12/schema", ["$id"] = "https://titaniumproxy.com/twp.schema.json", ["title"] = "Titanium Web Proxy configuration", - ["type"] = "object", + ["type"] = ObjectType, ["additionalProperties"] = false, ["properties"] = PropertiesOf(typeof(Models.TwpConfig), []), }; @@ -35,11 +38,11 @@ private static JsonObject PropertiesOf(Type type, HashSet stack) return props; } - private static JsonNode SchemaFor(Type type, HashSet stack) + private static JsonObject SchemaFor(Type type, HashSet stack) { type = Nullable.GetUnderlyingType(type) ?? type; if (type == typeof(string)) - return new JsonObject { ["type"] = "string" }; + return new JsonObject { ["type"] = StringType }; if (type == typeof(bool)) return new JsonObject { ["type"] = "boolean" }; if (type == typeof(int) || type == typeof(long) || type == typeof(short)) @@ -47,13 +50,13 @@ private static JsonNode SchemaFor(Type type, HashSet stack) if (type == typeof(double) || type == typeof(float) || type == typeof(decimal)) return new JsonObject { ["type"] = "number" }; if (type.IsEnum) - return new JsonObject { ["type"] = "string" }; + return new JsonObject { ["type"] = StringType }; if (IsDictionary(type)) return new JsonObject { - ["type"] = "object", - ["additionalProperties"] = new JsonObject { ["type"] = "string" }, + ["type"] = ObjectType, + ["additionalProperties"] = new JsonObject { ["type"] = StringType }, }; var item = ItemType(type); @@ -61,16 +64,16 @@ private static JsonNode SchemaFor(Type type, HashSet stack) return new JsonObject { ["type"] = "array", ["items"] = SchemaFor(item, stack) }; if (!type.IsClass) - return new JsonObject { ["type"] = "string" }; + return new JsonObject { ["type"] = StringType }; if (!stack.Add(type)) - return new JsonObject { ["type"] = "object" }; + return new JsonObject { ["type"] = ObjectType }; try { return new JsonObject { - ["type"] = "object", + ["type"] = ObjectType, ["additionalProperties"] = false, ["properties"] = PropertiesOf(type, stack), }; diff --git a/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs b/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs index 5aefcf657..32a7bff3b 100644 --- a/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs +++ b/src/Titanium.Web.Proxy.Configuration/UnknownConfigKeyScanner.cs @@ -39,31 +39,49 @@ private static void Walk(JsonElement element, Type type, string path, List props, List found) + { + var childPath = path.Length == 0 ? property.Name : path + "." + property.Name; + if (!props.TryGetValue(property.Name, out var clr)) { - var childPath = path.Length == 0 ? property.Name : path + "." + property.Name; - if (!props.TryGetValue(property.Name, out var clr)) - { - var suggestion = Suggest(property.Name, props.Keys); - var hinted = path.Length == 0 ? suggestion : path + "." + suggestion; - found.Add(suggestion is null - ? $"Unknown key '{childPath}'." - : $"Unknown key '{childPath}'. Did you mean '{hinted}'?"); - continue; - } + found.Add(UnknownKeyMessage(property.Name, childPath, path, props.Keys)); + return; + } - var next = Nullable.GetUnderlyingType(clr) ?? clr; - if (property.Value.ValueKind == JsonValueKind.Object && !IsDictionary(next)) - Walk(property.Value, next, childPath, found); - else if (property.Value.ValueKind == JsonValueKind.Array) - { - var item = ItemType(next); - if (item != null && !IsDictionary(item)) - { - foreach (var entry in property.Value.EnumerateArray()) - Walk(entry, item, childPath, found); - } - } + VisitKnown(property.Value, clr, childPath, found); + } + + private static string UnknownKeyMessage(string name, string childPath, string path, IEnumerable keys) + { + var suggestion = Suggest(name, keys); + if (suggestion is null) + return $"Unknown key '{childPath}'."; + + var hinted = path.Length == 0 ? suggestion : path + "." + suggestion; + return $"Unknown key '{childPath}'. Did you mean '{hinted}'?"; + } + + private static void VisitKnown(JsonElement value, Type clr, string childPath, List found) + { + var next = Nullable.GetUnderlyingType(clr) ?? clr; + if (value.ValueKind == JsonValueKind.Object && !IsDictionary(next)) + { + Walk(value, next, childPath, found); + return; } + + if (value.ValueKind != JsonValueKind.Array) + return; + + var item = ItemType(next); + if (item == null || IsDictionary(item)) + return; + + foreach (var entry in value.EnumerateArray()) + Walk(entry, item, childPath, found); } private static Dictionary Properties(Type type) diff --git a/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs b/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs index 9e0ba969d..e776de457 100644 --- a/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs +++ b/src/Titanium.Web.Proxy/Http/HeaderLimitsExceededException.cs @@ -7,9 +7,10 @@ namespace Titanium.Web.Proxy.Http; /// while HeaderLimits was Enforce. Not an , /// so it is not treated as a client disconnect. ///
-internal sealed class HeaderLimitsExceededException : Exception +public sealed class HeaderLimitsExceededException : Exception { - internal HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest) + /// Creates an exception for one header-block breach. + public HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest) : base(isCount ? $"Header count {observed} exceeded limit {limit}." : $"Header block size {observed} exceeded limit {limit}.") @@ -20,8 +21,15 @@ internal HeaderLimitsExceededException(bool isCount, long observed, long limit, IsRequest = isRequest; } - internal bool IsCount { get; } - internal long Observed { get; } - internal long Limit { get; } - internal bool IsRequest { get; } + /// True when the breach is the header count. False when it is the byte total. + public bool IsCount { get; } + + /// The count or byte total that crossed the limit. + public long Observed { get; } + + /// The configured limit that was crossed. + public long Limit { get; } + + /// True when the breach was on request headers. + public bool IsRequest { get; } } diff --git a/src/Titanium.Web.Proxy/Http/HeaderParser.cs b/src/Titanium.Web.Proxy/Http/HeaderParser.cs index 18ad4e3a9..91b3ce518 100644 --- a/src/Titanium.Web.Proxy/Http/HeaderParser.cs +++ b/src/Titanium.Web.Proxy/Http/HeaderParser.cs @@ -27,6 +27,20 @@ internal HeaderBlockBudget(int maxCount, long maxAggregateBytes, PolicyMode mode } +internal readonly struct HeaderScan +{ + internal HeaderScan(int count, long aggregate, bool observed) + { + Count = count; + Aggregate = aggregate; + Observed = observed; + } + + internal int Count { get; } + internal long Aggregate { get; } + internal bool Observed { get; } +} + internal static class HeaderParser { internal static ValueTask ReadHeaders(ILineStream reader, HeaderCollection headerCollection, @@ -42,7 +56,7 @@ internal static ValueTask ReadHeaders(ILineStream reader, HeaderCollection heade var lineVt = reader.ReadLineAsync(cancellationToken); if (!lineVt.IsCompletedSuccessfully) return ReadHeadersContinueAsync(reader, headerCollection, lineVt, hasPending: true, - cancellationToken, budget, count, aggregate, observed); + cancellationToken, budget, new HeaderScan(count, aggregate, observed)); var buffered = lineVt.Result; if (string.IsNullOrEmpty(buffered)) return default; @@ -51,13 +65,16 @@ internal static ValueTask ReadHeaders(ILineStream reader, HeaderCollection heade } return ReadHeadersContinueAsync(reader, headerCollection, default, hasPending: false, - cancellationToken, budget, count, aggregate, observed); + cancellationToken, budget, new HeaderScan(count, aggregate, observed)); } private static async ValueTask ReadHeadersContinueAsync(ILineStream reader, HeaderCollection headerCollection, ValueTask pendingLine, bool hasPending, - CancellationToken cancellationToken, HeaderBlockBudget budget, int count, long aggregate, bool observed) + CancellationToken cancellationToken, HeaderBlockBudget budget, HeaderScan scan) { + var count = scan.Count; + var aggregate = scan.Aggregate; + var observed = scan.Observed; if (hasPending) { var pending = await pendingLine; @@ -95,15 +112,18 @@ internal static ValueTask TryReadHeadersAsync(HttpStream reader, // Incomplete line or empty buffer — fall through to the string path (handles multi-fill lines). return TryReadHeadersContinueAsync(reader, headerCollection, default, hasPending: false, - cancellationToken, budget, count, aggregate, observed); + cancellationToken, budget, new HeaderScan(count, aggregate, observed)); } private static async ValueTask TryReadHeadersContinueAsync(HttpStream reader, HeaderCollection headerCollection, ValueTask<(string? Line, bool Cancelled)> pendingLine, bool hasPending, - CancellationToken cancellationToken, HeaderBlockBudget budget, int count, long aggregate, bool observed) + CancellationToken cancellationToken, HeaderBlockBudget budget, HeaderScan scan) { + var count = scan.Count; + var aggregate = scan.Aggregate; + var observed = scan.Observed; if (hasPending) { var (pending, cancelled) = await pendingLine; diff --git a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs index f60bdc154..7928ef3e1 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs @@ -35,7 +35,13 @@ internal sealed class Http3Frame internal const int ProgressiveRentChunkBytes = 256 * 1024; /// Last payload rent size on the progressive path. Tests read this; production does not branch on it. - internal static int TestLastRentBytes; + private static int testLastRentBytes; + + internal static int TestLastRentBytes + { + get => testLastRentBytes; + set => testLastRentBytes = value; + } public ulong Type { get; } public ReadOnlyMemory Payload { get; } diff --git a/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs b/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs index e411e54ed..d6e6bd3df 100644 --- a/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs +++ b/src/Titanium.Web.Proxy/Logging/LimitCatalog.cs @@ -67,6 +67,9 @@ public LimitEntry(LimitId id, string property, string cliKey, string what, strin /// Single source of truth for limit names, CLI keys, and log hints. internal static class LimitCatalog { + private const string HeaderLimitsKey = "headerLimits"; + private const string AdmissionControlKey = "admissionControl"; + private static readonly LimitEntry[] Entries = Build(); internal static LimitEntry Get(LimitId id) => Entries[(int)id]; @@ -83,44 +86,44 @@ private static LimitEntry[] Build() Add(Row(LimitId.BufferedBody, "MaxBufferedBodyBytes", "server.limits.maxBufferedBodyBytes", "buffered body", "bodyBudget", PolicyFamily.BodyBudget)); Add(Row(LimitId.HeaderCount, "MaxHeaderCount", "server.limits.maxHeaderCount", - "header count", "headerLimits", PolicyFamily.HeaderLimits)); + "header count", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.HeaderAggregate, "MaxHeaderAggregateBytes", "server.limits.maxHeaderAggregateBytes", - "header block size", "headerLimits", PolicyFamily.HeaderLimits)); + "header block size", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.HeaderLine, "MaxHeaderLineBytes", "server.limits.maxHeaderLineBytes", - "header line", "headerLimits", PolicyFamily.HeaderLimits)); + "header line", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.Http3FramePayload, "MaxHttp3FramePayloadBytes", "server.limits.maxHttp3FramePayloadBytes", "HTTP/3 frame payload", "bodyBudget", PolicyFamily.BodyBudget)); Add(Row(LimitId.DeferredOutboundBytes, "MaxDeferredOutboundBytesPerStream", "server.limits.maxDeferredOutboundBytesPerStream", "HTTP/2 deferred DATA", "bodyBudget", PolicyFamily.BodyBudget)); Add(Row(LimitId.DecodedHeaderList, "MaxDecodedHeaderListBytes", "server.limits.maxDecodedHeaderListBytes", - "decoded header list", "headerLimits", PolicyFamily.HeaderLimits)); + "decoded header list", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.TrailerHeaderCount, "MaxTrailerHeaderCount", "server.limits.maxTrailerHeaderCount", - "trailer header count", "headerLimits", PolicyFamily.HeaderLimits)); + "trailer header count", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.TrailerHeaderBlock, "MaxTrailerHeaderBlockBytes", "server.limits.maxTrailerHeaderBlockBytes", - "trailer header block", "headerLimits", PolicyFamily.HeaderLimits)); + "trailer header block", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.Http2CompressedHeaderBlock, "MaxHttp2CompressedHeaderBlockBytes", "server.limits.maxHttp2CompressedHeaderBlockBytes", - "compressed header block", "headerLimits", PolicyFamily.HeaderLimits)); + "compressed header block", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.InterimResponses, "MaxInterimResponses", "server.limits.maxInterimResponses", - "interim responses", "headerLimits", PolicyFamily.HeaderLimits)); + "interim responses", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.AuthChallengeRounds, "MaxAuthChallengeRounds", "server.limits.maxAuthChallengeRounds", - "authentication challenge rounds", "admissionControl", PolicyFamily.AdmissionControl)); + "authentication challenge rounds", AdmissionControlKey, PolicyFamily.AdmissionControl)); Add(Row(LimitId.UpstreamProxyAuthAttempts, "MaxUpstreamProxyAuthenticationAttempts", "server.limits.maxUpstreamProxyAuthenticationAttempts", - "upstream proxy authentication attempts", "admissionControl", PolicyFamily.AdmissionControl)); + "upstream proxy authentication attempts", AdmissionControlKey, PolicyFamily.AdmissionControl)); Add(Row(LimitId.WinAuthToken, "MaxWinAuthTokenBytes", "server.limits.maxWinAuthTokenBytes", - "Windows authentication token", "admissionControl", PolicyFamily.AdmissionControl)); + "Windows authentication token", AdmissionControlKey, PolicyFamily.AdmissionControl)); Add(Row(LimitId.Http2WindowUpdate, "Http2WindowUpdateTimeoutSeconds", "server.limits.http2WindowUpdateTimeoutSeconds", "HTTP/2 flow-control wait", "http2AbuseBudget", PolicyFamily.Http2AbuseBudget)); Add(Row(LimitId.UpgradeHandshakeHeaders, "MaxHeaderCount", "server.limits.maxHeaderCount", - "upgrade handshake headers", "headerLimits", PolicyFamily.HeaderLimits)); + "upgrade handshake headers", HeaderLimitsKey, PolicyFamily.HeaderLimits)); Add(Row(LimitId.AdmissionGlobal, "MaxConcurrentClientConnections", "server.pooling.maxConcurrentClientConnections", - "client connections", "admissionControl", PolicyFamily.AdmissionControl)); + "client connections", AdmissionControlKey, PolicyFamily.AdmissionControl)); Add(Row(LimitId.AdmissionEndpoint, "MaxConcurrentClients", "server.limits.maxConcurrentClients", - "endpoint client connections", "admissionControl", PolicyFamily.AdmissionControl)); + "endpoint client connections", AdmissionControlKey, PolicyFamily.AdmissionControl)); Add(Timeout(LimitId.TimeoutConnect, "ConnectTimeOutSeconds", "server.timeouts.connectTimeOutSeconds", "connect")); Add(Timeout(LimitId.TimeoutResponseHeader, "ResponseHeaderTimeoutSeconds", diff --git a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs index 82312e559..936292d10 100644 --- a/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/TcpConnection/TcpConnectionFactory.cs @@ -1334,9 +1334,7 @@ private static async Task EstablishHttpUpstreamConnectAsync(ProxyServer pr var bodyPreview = await DrainUpstreamProxyResponseBody(stream, headers, cancellationToken); - if (authenticationAttempts >= maxAuthAttempts) - ProxyLog.LimitExceeded(proxyServer.Logger, LimitId.UpstreamProxyAuthAttempts, PolicyMode.Enforce, - authenticationAttempts, maxAuthAttempts, "407"); + NoteUpstreamAuthCap(proxyServer, authenticationAttempts, maxAuthAttempts); if (httpStatus.StatusCode != (int)HttpStatusCode.ProxyAuthenticationRequired || !proxy.UseDefaultCredentials || @@ -1368,6 +1366,13 @@ private static async Task EstablishHttpUpstreamConnectAsync(ProxyServer pr } } + private static void NoteUpstreamAuthCap(ProxyServer proxyServer, int authenticationAttempts, int maxAuthAttempts) + { + if (authenticationAttempts >= maxAuthAttempts) + ProxyLog.LimitExceeded(proxyServer.Logger, LimitId.UpstreamProxyAuthAttempts, PolicyMode.Enforce, + authenticationAttempts, maxAuthAttempts, "407"); + } + private static bool TryGetUpstreamProxyAuthenticationChallenge(HeaderCollection headers, out string? scheme, out string? challenge) { diff --git a/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs b/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs index 2f5b082c3..cb7fbb29b 100644 --- a/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs +++ b/src/Titanium.Web.Proxy/Options/ProxyResourceLimits.cs @@ -416,14 +416,14 @@ public ProxyResourceLimits WithCertificateCacheBounds( public ProxyResourceLimits WithMaxHttp3FramePayloadBytes(long maxHttp3FramePayloadBytes) { RequireInRange(maxHttp3FramePayloadBytes, MaxHttp3FramePayloadCeiling, nameof(maxHttp3FramePayloadBytes)); - return Copy(maxHttp3FramePayloadBytes: maxHttp3FramePayloadBytes); + return Copy(new HatchOverrides { MaxHttp3FramePayloadBytes = maxHttp3FramePayloadBytes }); } /// Returns a copy with replaced. public ProxyResourceLimits WithMaxDeferredOutboundBytesPerStream(int maxDeferredOutboundBytesPerStream) { RequireInRange(maxDeferredOutboundBytesPerStream, MaxDeferredOutboundBytesCeiling, nameof(maxDeferredOutboundBytesPerStream)); - return Copy(maxDeferredOutboundBytesPerStream: maxDeferredOutboundBytesPerStream); + return Copy(new HatchOverrides { MaxDeferredOutboundBytesPerStream = maxDeferredOutboundBytesPerStream }); } /// Returns a copy with trailer header bounds replaced. @@ -431,21 +431,25 @@ public ProxyResourceLimits WithTrailerHeaderBounds(int maxTrailerHeaderCount, in { RequireInRange(maxTrailerHeaderCount, 10_000, nameof(maxTrailerHeaderCount)); RequireInRange(maxTrailerHeaderBlockBytes, 1024 * 1024, nameof(maxTrailerHeaderBlockBytes)); - return Copy(maxTrailerHeaderCount: maxTrailerHeaderCount, maxTrailerHeaderBlockBytes: maxTrailerHeaderBlockBytes); + return Copy(new HatchOverrides + { + MaxTrailerHeaderCount = maxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = maxTrailerHeaderBlockBytes + }); } /// Returns a copy with replaced. public ProxyResourceLimits WithMaxHttp2CompressedHeaderBlockBytes(int maxHttp2CompressedHeaderBlockBytes) { RequireInRange(maxHttp2CompressedHeaderBlockBytes, 16 * 1024 * 1024, nameof(maxHttp2CompressedHeaderBlockBytes)); - return Copy(maxHttp2CompressedHeaderBlockBytes: maxHttp2CompressedHeaderBlockBytes); + return Copy(new HatchOverrides { MaxHttp2CompressedHeaderBlockBytes = maxHttp2CompressedHeaderBlockBytes }); } /// Returns a copy with replaced. public ProxyResourceLimits WithMaxInterimResponses(int maxInterimResponses) { RequireInRange(maxInterimResponses, 100, nameof(maxInterimResponses)); - return Copy(maxInterimResponses: maxInterimResponses); + return Copy(new HatchOverrides { MaxInterimResponses = maxInterimResponses }); } /// Returns a copy with authentication bounds replaced. @@ -455,30 +459,36 @@ public ProxyResourceLimits WithAuthenticationBounds(int maxAuthChallengeRounds, RequireInRange(maxAuthChallengeRounds, 20, nameof(maxAuthChallengeRounds)); RequireInRange(maxUpstreamProxyAuthenticationAttempts, 20, nameof(maxUpstreamProxyAuthenticationAttempts)); RequireInRange(maxWinAuthTokenBytes, 1024 * 1024, nameof(maxWinAuthTokenBytes)); - return Copy( - maxAuthChallengeRounds: maxAuthChallengeRounds, - maxUpstreamProxyAuthenticationAttempts: maxUpstreamProxyAuthenticationAttempts, - maxWinAuthTokenBytes: maxWinAuthTokenBytes); + return Copy(new HatchOverrides + { + MaxAuthChallengeRounds = maxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = maxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = maxWinAuthTokenBytes + }); } /// Returns a copy with replaced. public ProxyResourceLimits WithHttp2WindowUpdateTimeoutSeconds(int http2WindowUpdateTimeoutSeconds) { RequireInRange(http2WindowUpdateTimeoutSeconds, 3600, nameof(http2WindowUpdateTimeoutSeconds)); - return Copy(http2WindowUpdateTimeoutSeconds: http2WindowUpdateTimeoutSeconds); + return Copy(new HatchOverrides { Http2WindowUpdateTimeoutSeconds = http2WindowUpdateTimeoutSeconds }); + } + + private readonly struct HatchOverrides + { + internal long? MaxHttp3FramePayloadBytes { get; init; } + internal int? MaxDeferredOutboundBytesPerStream { get; init; } + internal int? MaxTrailerHeaderCount { get; init; } + internal int? MaxTrailerHeaderBlockBytes { get; init; } + internal int? MaxHttp2CompressedHeaderBlockBytes { get; init; } + internal int? MaxInterimResponses { get; init; } + internal int? MaxAuthChallengeRounds { get; init; } + internal int? MaxUpstreamProxyAuthenticationAttempts { get; init; } + internal int? MaxWinAuthTokenBytes { get; init; } + internal int? Http2WindowUpdateTimeoutSeconds { get; init; } } - private ProxyResourceLimits Copy( - long? maxHttp3FramePayloadBytes = null, - int? maxDeferredOutboundBytesPerStream = null, - int? maxTrailerHeaderCount = null, - int? maxTrailerHeaderBlockBytes = null, - int? maxHttp2CompressedHeaderBlockBytes = null, - int? maxInterimResponses = null, - int? maxAuthChallengeRounds = null, - int? maxUpstreamProxyAuthenticationAttempts = null, - int? maxWinAuthTokenBytes = null, - int? http2WindowUpdateTimeoutSeconds = null) => + private ProxyResourceLimits Copy(HatchOverrides over = default) => new() { MaxHeaderLineBytes = MaxHeaderLineBytes, @@ -497,16 +507,16 @@ private ProxyResourceLimits Copy( MaxOriginHttp2ConnectionsPerAuthority = MaxOriginHttp2ConnectionsPerAuthority, MaxCertificateCacheEntries = MaxCertificateCacheEntries, MaxCertificateDiskCacheEntries = MaxCertificateDiskCacheEntries, - MaxHttp3FramePayloadBytes = maxHttp3FramePayloadBytes ?? MaxHttp3FramePayloadBytes, - MaxDeferredOutboundBytesPerStream = maxDeferredOutboundBytesPerStream ?? MaxDeferredOutboundBytesPerStream, - MaxTrailerHeaderCount = maxTrailerHeaderCount ?? MaxTrailerHeaderCount, - MaxTrailerHeaderBlockBytes = maxTrailerHeaderBlockBytes ?? MaxTrailerHeaderBlockBytes, - MaxHttp2CompressedHeaderBlockBytes = maxHttp2CompressedHeaderBlockBytes ?? MaxHttp2CompressedHeaderBlockBytes, - MaxInterimResponses = maxInterimResponses ?? MaxInterimResponses, - MaxAuthChallengeRounds = maxAuthChallengeRounds ?? MaxAuthChallengeRounds, - MaxUpstreamProxyAuthenticationAttempts = maxUpstreamProxyAuthenticationAttempts ?? MaxUpstreamProxyAuthenticationAttempts, - MaxWinAuthTokenBytes = maxWinAuthTokenBytes ?? MaxWinAuthTokenBytes, - Http2WindowUpdateTimeoutSeconds = http2WindowUpdateTimeoutSeconds ?? Http2WindowUpdateTimeoutSeconds + MaxHttp3FramePayloadBytes = over.MaxHttp3FramePayloadBytes ?? MaxHttp3FramePayloadBytes, + MaxDeferredOutboundBytesPerStream = over.MaxDeferredOutboundBytesPerStream ?? MaxDeferredOutboundBytesPerStream, + MaxTrailerHeaderCount = over.MaxTrailerHeaderCount ?? MaxTrailerHeaderCount, + MaxTrailerHeaderBlockBytes = over.MaxTrailerHeaderBlockBytes ?? MaxTrailerHeaderBlockBytes, + MaxHttp2CompressedHeaderBlockBytes = over.MaxHttp2CompressedHeaderBlockBytes ?? MaxHttp2CompressedHeaderBlockBytes, + MaxInterimResponses = over.MaxInterimResponses ?? MaxInterimResponses, + MaxAuthChallengeRounds = over.MaxAuthChallengeRounds ?? MaxAuthChallengeRounds, + MaxUpstreamProxyAuthenticationAttempts = over.MaxUpstreamProxyAuthenticationAttempts ?? MaxUpstreamProxyAuthenticationAttempts, + MaxWinAuthTokenBytes = over.MaxWinAuthTokenBytes ?? MaxWinAuthTokenBytes, + Http2WindowUpdateTimeoutSeconds = over.Http2WindowUpdateTimeoutSeconds ?? Http2WindowUpdateTimeoutSeconds }; private static void RequireInRange(long value, long ceiling, string paramName) diff --git a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs index 3849a4e0f..c0965ae72 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs @@ -52,6 +52,16 @@ public partial class ProxyServer : IDisposable /// 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. /// + private static bool DecompressionRatioDiffersFromDefault(double configured) + { + if (ProxyResourceLimits.Default.MaxDecompressionRatio is not double expected) + return true; + + // 200 is exactly representable. A range check avoids an exact floating-point equality. + var delta = configured - expected; + return delta > 0d || delta < 0d; + } + public void Start(bool changeSystemProxySettings = true) // NOSONAR S3776 -- This protocol/state-machine path shares mutable parsing or transport state; splitting it further would create disproportionate regression risk. { if (ProxyRunning) throw new InvalidOperationException("Proxy is already running."); @@ -104,7 +114,7 @@ public partial class ProxyServer : IDisposable // Name only, per the plan's rollout section - never hosts, URLs or secrets. ProxyLog.EffectiveProfileAtStartup(logger, profile, policyModes); if (ResourceLimits.MaxDecompressionRatio is { } ratio && - ratio != ProxyResourceLimits.Default.MaxDecompressionRatio) + DecompressionRatioDiffersFromDefault(ratio)) { ProxyLog.ReservedLimit(logger, "server.limits.maxDecompressionRatio", ratio, "Body expansion is bounded by maxBufferedBodyBytes."); diff --git a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt index 821302dd1..efce5c80e 100644 --- a/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt +++ b/src/Titanium.Web.Proxy/PublicAPI.Unshipped.txt @@ -301,3 +301,9 @@ Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxHttp3FramePayloadBytes(lon Titanium.Web.Proxy.Options.ProxyResourceLimits.WithMaxInterimResponses(int maxInterimResponses) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! Titanium.Web.Proxy.Options.ProxyResourceLimits.WithTrailerHeaderBounds(int maxTrailerHeaderCount, int maxTrailerHeaderBlockBytes) -> Titanium.Web.Proxy.Options.ProxyResourceLimits! Titanium.Web.Proxy.Options.ProxyPolicyModes.WithAllowAmbiguousFraming(bool enabled) -> Titanium.Web.Proxy.Options.ProxyPolicyModes! +Titanium.Web.Proxy.Http.HeaderLimitsExceededException +Titanium.Web.Proxy.Http.HeaderLimitsExceededException.HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest) -> void +Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount.get -> bool +Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest.get -> bool +Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit.get -> long +Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed.get -> long From c5a4c2d1c40e4b33c5a8eddf7f4a6693aed04444 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:05:52 +0000 Subject: [PATCH 07/19] Update documentation --- ...xy.Http.HeaderLimitsExceededException.html | 376 ++++++++++++++++++ docs/api/Titanium.Web.Proxy.Http.html | 5 + ...Web.Proxy.Options.ProxyResourceLimits.html | 8 +- docs/api/Titanium.Web.Proxy.ProxyServer.html | 25 +- docs/api/toc.html | 3 + docs/api/toc.json | 2 +- docs/index.json | 9 +- docs/xrefmap.yml | 77 ++++ 8 files changed, 480 insertions(+), 25 deletions(-) create mode 100644 docs/api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html diff --git a/docs/api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html b/docs/api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html new file mode 100644 index 000000000..920e45c16 --- /dev/null +++ b/docs/api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html @@ -0,0 +1,376 @@ + + + + + + + + Class HeaderLimitsExceededException | Titanium Web Proxy + + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + + + +
+
+
+
+ +
+
+
+
+
+ +
+
+
    +
  • +
+
+
+
+
+ +
+
Search Results for
+
+

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

    Class HeaderLimitsExceededException

    +

    An HTTP/1 header block exceeded MaxHeaderCount or MaxHeaderAggregateBytes +while HeaderLimits was Enforce. Not an IOException, +so it is not treated as a client disconnect.

    +
    +
    +
    +
    Inheritance
    +
    object
    +
    Exception
    +
    HeaderLimitsExceededException
    +
    +
    +
    Implements
    +
    ISerializable
    +
    +
    +
    Inherited Members
    +
    + Exception.GetBaseException() +
    +
    + Exception.GetType() +
    +
    + Exception.ToString() +
    +
    + Exception.Data +
    +
    + Exception.HelpLink +
    +
    + Exception.HResult +
    +
    + Exception.InnerException +
    +
    + Exception.Message +
    +
    + Exception.Source +
    +
    + Exception.StackTrace +
    +
    + Exception.TargetSite +
    +
    + object.Equals(object) +
    +
    + object.Equals(object, object) +
    +
    + object.GetHashCode() +
    +
    + object.ReferenceEquals(object, object) +
    +
    +
    Namespace: Titanium.Web.Proxy.Http
    +
    Assembly: Titanium.Web.Proxy.dll
    +
    Syntax
    +
    +
    public sealed class HeaderLimitsExceededException : Exception, ISerializable
    +
    +

    Constructors +

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

    HeaderLimitsExceededException(bool, long, long, bool)

    +

    Creates an exception for one header-block breach.

    +
    +
    +
    Declaration
    +
    +
    public HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest)
    +
    +
    Parameters
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TypeNameDescription
    boolisCount
    longobserved
    longlimit
    boolisRequest
    +

    Properties +

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

    IsCount

    +

    True when the breach is the header count. False when it is the byte total.

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

    IsRequest

    +

    True when the breach was on request headers.

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

    Limit

    +

    The configured limit that was crossed.

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

    Observed

    +

    The count or byte total that crossed the limit.

    +
    +
    +
    Declaration
    +
    +
    public long Observed { get; }
    +
    +
    Property Value
    + + + + + + + + + + + + + +
    TypeDescription
    long
    +

    Implements

    +
    + ISerializable +
    + +
    +
    + +
    +
    +
    +
      +
    • + Edit this page +
    • +
    • + View Source +
    • +
    +
    +
    +
    In this article
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + Back to top + + + Generated by DocFX +
    +
    +
    +
    + + + + + + diff --git a/docs/api/Titanium.Web.Proxy.Http.html b/docs/api/Titanium.Web.Proxy.Http.html index b39df4e74..053f55b5f 100644 --- a/docs/api/Titanium.Web.Proxy.Http.html +++ b/docs/api/Titanium.Web.Proxy.Http.html @@ -93,6 +93,11 @@

    ConnectR

    HeaderCollection

    The http header collection.

    +
    +

    HeaderLimitsExceededException

    +

    An HTTP/1 header block exceeded MaxHeaderCount or MaxHeaderAggregateBytes +while HeaderLimits was Enforce. Not an IOException, +so it is not treated as a client disconnect.

    HttpWebClient

    Used to communicate with the server over HTTP(S)

    diff --git a/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html b/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html index 7703a1db3..d2bd86435 100644 --- a/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html +++ b/docs/api/Titanium.Web.Proxy.Options.ProxyResourceLimits.html @@ -1228,7 +1228,7 @@
    Exceptions
    Edit this page - View Source + View Source

    WithAuthenticationBounds(int, int, int)

    @@ -1361,7 +1361,7 @@
    Exceptions
    Edit this page - View Source + View Source

    WithHttp2WindowUpdateTimeoutSeconds(int)

    @@ -1505,7 +1505,7 @@
    Returns
    Edit this page - View Source + View Source

    WithMaxHttp2CompressedHeaderBlockBytes(int)

    @@ -1601,7 +1601,7 @@
    Returns
    Edit this page - View Source + View Source

    WithMaxInterimResponses(int)

    diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index 7923eeb58..addb5f23c 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyServer.html +++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html @@ -3329,7 +3329,7 @@
    Parameters
    Edit this page - View Source + View Source

    Dispose()

    @@ -3345,7 +3345,7 @@
    Declaration
    Edit this page - View Source + View Source

    Dispose(bool)

    @@ -3892,20 +3892,11 @@
    Returns
    Edit this page - View Source + 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
    @@ -3924,9 +3915,7 @@
    Parameters
    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.

    - + @@ -3935,7 +3924,7 @@
    Parameters
    Edit this page - View Source + View Source

    Stop()

    @@ -3954,7 +3943,7 @@
    Declaration
    Edit this page - View Source + View Source

    StopAsync(TimeSpan?)

    diff --git a/docs/api/toc.html b/docs/api/toc.html index 0abf3c616..79a07af42 100644 --- a/docs/api/toc.html +++ b/docs/api/toc.html @@ -234,6 +234,9 @@
  • HeaderCollection.Enumerator
  • +
  • + HeaderLimitsExceededException +
  • HttpWebClient
  • diff --git a/docs/api/toc.json b/docs/api/toc.json index d50245d4b..0cfbe2781 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":"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"} +{"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":"HeaderLimitsExceededException","href":"Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html","topicHref":"Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html","topicUid":"Titanium.Web.Proxy.Http.HeaderLimitsExceededException","type":"Class"},{"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 2a122455b..81ab6eed9 100644 --- a/docs/index.json +++ b/docs/index.json @@ -244,6 +244,11 @@ "title": "Class HeaderCollection | Titanium Web Proxy", "summary": "Class HeaderCollection The http header collection. Inheritance object HeaderCollection Implements IEnumerable IEnumerable 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.Http Assembly: Titanium.Web.Proxy.dll Syntax [TypeConverter(typeof(ExpandableObjectConverter))] public class HeaderCollection : IEnumerable, IEnumerable Constructors | Edit this page View Source HeaderCollection() Initializes a new instance of the HeaderCollection class. Declaration public HeaderCollection() Properties | Edit this page View Source Headers Unique Request header collection. Declaration public ReadOnlyDictionary Headers { get; } Property Value Type Description ReadOnlyDictionary | Edit this page View Source NonUniqueHeaders Non-unique headers. Values are read-only views over the internal lists so callers cannot Add/Clear storage that still belongs to this collection. Declaration public ReadOnlyDictionary> NonUniqueHeaders { get; } Property Value Type Description ReadOnlyDictionary> Methods | Edit this page View Source AddHeader(string, string) Add a new header with given name and value Declaration public void AddHeader(string name, string value) Parameters Type Name Description string name string value | Edit this page View Source AddHeader(HttpHeader) Adds the given header object to Request Declaration public void AddHeader(HttpHeader newHeader) Parameters Type Name Description HttpHeader newHeader | Edit this page View Source AddHeaders(IEnumerable>?) Adds the given header objects to Request Declaration public void AddHeaders(IEnumerable>? newHeaders) Parameters Type Name Description IEnumerable> newHeaders | Edit this page View Source AddHeaders(IEnumerable>?) Adds the given header objects to Request Declaration public void AddHeaders(IEnumerable>? newHeaders) Parameters Type Name Description IEnumerable> newHeaders | Edit this page View Source AddHeaders(IEnumerable?) Adds the given header objects to Request Declaration public void AddHeaders(IEnumerable? newHeaders) Parameters Type Name Description IEnumerable newHeaders | Edit this page View Source Clear() Removes all the headers. Declaration public void Clear() | Edit this page View Source GetAllHeaders() Returns all headers Declaration public List GetAllHeaders() Returns Type Description List | Edit this page View Source GetEnumerator() Returns an enumerator that iterates through the collection. Declaration public HeaderCollection.Enumerator GetEnumerator() Returns Type Description HeaderCollection.Enumerator An enumerator that can be used to iterate through the collection. Remarks Returns the concrete HeaderCollection.Enumerator struct rather than an interface type, matching the pattern used by List/Dictionary in the BCL: a foreach over a variable statically typed as HeaderCollection binds to this overload directly and allocates nothing, whereas the previous implementation (chaining Concat(IEnumerable, IEnumerable) and SelectMany(IEnumerable, Func>) over the two backing dictionaries) allocated two LINQ iterator objects on every call. This matters here specifically because every outgoing request and response header block is enumerated at least once to serialize it onto the wire, making this one of the hottest paths in the proxy. Code that holds this collection through the IEnumerable interface (e.g. LINQ operators) still gets a correct enumerator via the explicit interface implementation below, at the cost of one boxing allocation - unavoidable through that surface, and unchanged from before this optimization. | Edit this page View Source GetFirstHeader(string) Declaration public HttpHeader? GetFirstHeader(string name) Parameters Type Name Description string name Returns Type Description HttpHeader | Edit this page View Source GetHeaders(string) Returns all headers with given name if exists Returns null if doesn't exist Declaration public List? GetHeaders(string name) Parameters Type Name Description string name Returns Type Description List | Edit this page View Source HeaderExists(string) True if header exists Declaration public bool HeaderExists(string name) Parameters Type Name Description string name Returns Type Description bool | Edit this page View Source RemoveHeader(string) removes all headers with given name Declaration public bool RemoveHeader(string headerName) Parameters Type Name Description string headerName Returns Type Description bool True if header was removed False if no header exists with given name | Edit this page View Source RemoveHeader(KnownHeader) removes all headers with given name Declaration public bool RemoveHeader(KnownHeader headerName) Parameters Type Name Description KnownHeader headerName Returns Type Description bool True if header was removed False if no header exists with given name | Edit this page View Source RemoveHeader(HttpHeader) Removes given header object if it exist Declaration public bool RemoveHeader(HttpHeader header) Parameters Type Name Description HttpHeader header Returns true if header exists and was removed Returns Type Description bool Implements IEnumerable IEnumerable" }, + "api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html": { + "href": "api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html", + "title": "Class HeaderLimitsExceededException | Titanium Web Proxy", + "summary": "Class HeaderLimitsExceededException An HTTP/1 header block exceeded MaxHeaderCount or MaxHeaderAggregateBytes while HeaderLimits was Enforce. Not an IOException, so it is not treated as a client disconnect. Inheritance object Exception HeaderLimitsExceededException Implements ISerializable Inherited Members Exception.GetBaseException() Exception.GetType() Exception.ToString() Exception.Data Exception.HelpLink Exception.HResult Exception.InnerException Exception.Message Exception.Source Exception.StackTrace Exception.TargetSite object.Equals(object) object.Equals(object, object) object.GetHashCode() object.ReferenceEquals(object, object) Namespace: Titanium.Web.Proxy.Http Assembly: Titanium.Web.Proxy.dll Syntax public sealed class HeaderLimitsExceededException : Exception, ISerializable Constructors | Edit this page View Source HeaderLimitsExceededException(bool, long, long, bool) Creates an exception for one header-block breach. Declaration public HeaderLimitsExceededException(bool isCount, long observed, long limit, bool isRequest) Parameters Type Name Description bool isCount long observed long limit bool isRequest Properties | Edit this page View Source IsCount True when the breach is the header count. False when it is the byte total. Declaration public bool IsCount { get; } Property Value Type Description bool | Edit this page View Source IsRequest True when the breach was on request headers. Declaration public bool IsRequest { get; } Property Value Type Description bool | Edit this page View Source Limit The configured limit that was crossed. Declaration public long Limit { get; } Property Value Type Description long | Edit this page View Source Observed The count or byte total that crossed the limit. Declaration public long Observed { get; } Property Value Type Description long Implements ISerializable" + }, "api/Titanium.Web.Proxy.Http.HttpWebClient.html": { "href": "api/Titanium.Web.Proxy.Http.HttpWebClient.html", "title": "Class HttpWebClient | Titanium Web Proxy", @@ -312,7 +317,7 @@ "api/Titanium.Web.Proxy.Http.html": { "href": "api/Titanium.Web.Proxy.Http.html", "title": "Namespace Titanium.Web.Proxy.Http | Titanium Web Proxy", - "summary": "Namespace Titanium.Web.Proxy.Http Classes ConnectRequest The tcp tunnel Connect request. ConnectResponse The tcp tunnel connect response object. HeaderCollection The http header collection. HttpWebClient Used to communicate with the server over HTTP(S) KnownHeader KnownHeaders Well known http headers. ProxyResults Factory methods that build synthetic Response or StreamingProxyResult objects for use with Respond(Response, bool) and RespondStreaming(StreamingProxyResult, bool). Request Http(s) request object RequestResponseBase Abstract base class for similar objects shared by both request and response objects. Response Http(s) response object Structs HeaderCollection.Enumerator Walks first the unique-header dictionary's values, then each list in the non-unique-header dictionary's values in turn - the same effective order as the previous Concat(...SelectMany(...)) implementation - without allocating any LINQ iterator objects. See the remarks on GetEnumerator() for why this is worth a hand-written enumerator instead of the equivalent LINQ expression. StreamingProxyResult Pairs a synthetic response (status and headers) with a delegate that writes the body on the fly. Pass to RespondStreaming(StreamingProxyResult, bool) to stream without buffering the whole body in memory. Enums TunnelType" + "summary": "Namespace Titanium.Web.Proxy.Http Classes ConnectRequest The tcp tunnel Connect request. ConnectResponse The tcp tunnel connect response object. HeaderCollection The http header collection. HeaderLimitsExceededException An HTTP/1 header block exceeded MaxHeaderCount or MaxHeaderAggregateBytes while HeaderLimits was Enforce. Not an IOException, so it is not treated as a client disconnect. HttpWebClient Used to communicate with the server over HTTP(S) KnownHeader KnownHeaders Well known http headers. ProxyResults Factory methods that build synthetic Response or StreamingProxyResult objects for use with Respond(Response, bool) and RespondStreaming(StreamingProxyResult, bool). Request Http(s) request object RequestResponseBase Abstract base class for similar objects shared by both request and response objects. Response Http(s) response object Structs HeaderCollection.Enumerator Walks first the unique-header dictionary's values, then each list in the non-unique-header dictionary's values in turn - the same effective order as the previous Concat(...SelectMany(...)) implementation - without allocating any LINQ iterator objects. See the remarks on GetEnumerator() for why this is worth a hand-written enumerator instead of the equivalent LINQ expression. StreamingProxyResult Pairs a synthetic response (status and headers) with a delegate that writes the body on the fly. Pass to RespondStreaming(StreamingProxyResult, bool) to stream without buffering the whole body in memory. Enums TunnelType" }, "api/Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html": { "href": "api/Titanium.Web.Proxy.Http3.Http3NativeBootstrap.html", @@ -557,7 +562,7 @@ "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 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" + "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) Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings | 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", diff --git a/docs/xrefmap.yml b/docs/xrefmap.yml index c203e984b..6c6c17fd4 100644 --- a/docs/xrefmap.yml +++ b/docs/xrefmap.yml @@ -3804,6 +3804,83 @@ references: isSpec: "True" fullName: Titanium.Web.Proxy.Http.HeaderCollection.RemoveHeader nameWithType: HeaderCollection.RemoveHeader +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException + name: HeaderLimitsExceededException + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html + commentId: T:Titanium.Web.Proxy.Http.HeaderLimitsExceededException + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException + nameWithType: HeaderLimitsExceededException +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.#ctor(System.Boolean,System.Int64,System.Int64,System.Boolean) + name: HeaderLimitsExceededException(bool, long, long, bool) + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException__ctor_System_Boolean_System_Int64_System_Int64_System_Boolean_ + commentId: M:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.#ctor(System.Boolean,System.Int64,System.Int64,System.Boolean) + name.vb: New(Boolean, Long, Long, Boolean) + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.HeaderLimitsExceededException(bool, long, long, bool) + fullName.vb: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.New(Boolean, Long, Long, Boolean) + nameWithType: HeaderLimitsExceededException.HeaderLimitsExceededException(bool, long, long, bool) + nameWithType.vb: HeaderLimitsExceededException.New(Boolean, Long, Long, Boolean) +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.#ctor* + name: HeaderLimitsExceededException + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException__ctor_ + commentId: Overload:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.#ctor + isSpec: "True" + name.vb: New + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.HeaderLimitsExceededException + fullName.vb: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.New + nameWithType: HeaderLimitsExceededException.HeaderLimitsExceededException + nameWithType.vb: HeaderLimitsExceededException.New +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount + name: IsCount + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_IsCount + commentId: P:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount + nameWithType: HeaderLimitsExceededException.IsCount +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount* + name: IsCount + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_IsCount_ + commentId: Overload:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount + isSpec: "True" + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsCount + nameWithType: HeaderLimitsExceededException.IsCount +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest + name: IsRequest + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_IsRequest + commentId: P:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest + nameWithType: HeaderLimitsExceededException.IsRequest +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest* + name: IsRequest + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_IsRequest_ + commentId: Overload:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest + isSpec: "True" + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.IsRequest + nameWithType: HeaderLimitsExceededException.IsRequest +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit + name: Limit + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_Limit + commentId: P:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit + nameWithType: HeaderLimitsExceededException.Limit +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit* + name: Limit + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_Limit_ + commentId: Overload:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit + isSpec: "True" + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Limit + nameWithType: HeaderLimitsExceededException.Limit +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed + name: Observed + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_Observed + commentId: P:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed + nameWithType: HeaderLimitsExceededException.Observed +- uid: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed* + name: Observed + href: api/Titanium.Web.Proxy.Http.HeaderLimitsExceededException.html#Titanium_Web_Proxy_Http_HeaderLimitsExceededException_Observed_ + commentId: Overload:Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed + isSpec: "True" + fullName: Titanium.Web.Proxy.Http.HeaderLimitsExceededException.Observed + nameWithType: HeaderLimitsExceededException.Observed - uid: Titanium.Web.Proxy.Http.HttpWebClient name: HttpWebClient href: api/Titanium.Web.Proxy.Http.HttpWebClient.html From 41aab8b64b9d35962696381c3f6760bcc1e5b77e Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 11:58:59 +0530 Subject: [PATCH 08/19] fix(sonar): compare the decompression ratio with a tolerance. An exact floating-point test and a hand-written property still failed the new-code gate. --- src/Titanium.Web.Proxy/Http3/Http3Frame.cs | 8 +------- .../ProxyServer.Lifecycle.cs | 18 ++++++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs index 7928ef3e1..ccee60190 100644 --- a/src/Titanium.Web.Proxy/Http3/Http3Frame.cs +++ b/src/Titanium.Web.Proxy/Http3/Http3Frame.cs @@ -35,13 +35,7 @@ internal sealed class Http3Frame internal const int ProgressiveRentChunkBytes = 256 * 1024; /// Last payload rent size on the progressive path. Tests read this; production does not branch on it. - private static int testLastRentBytes; - - internal static int TestLastRentBytes - { - get => testLastRentBytes; - set => testLastRentBytes = value; - } + internal static int TestLastRentBytes { get; set; } public ulong Type { get; } public ReadOnlyMemory Payload { get; } diff --git a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs index c0965ae72..b7841cf2a 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.Lifecycle.cs @@ -52,16 +52,6 @@ public partial class ProxyServer : IDisposable /// 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. /// - private static bool DecompressionRatioDiffersFromDefault(double configured) - { - if (ProxyResourceLimits.Default.MaxDecompressionRatio is not double expected) - return true; - - // 200 is exactly representable. A range check avoids an exact floating-point equality. - var delta = configured - expected; - return delta > 0d || delta < 0d; - } - public void Start(bool changeSystemProxySettings = true) // NOSONAR S3776 -- This protocol/state-machine path shares mutable parsing or transport state; splitting it further would create disproportionate regression risk. { if (ProxyRunning) throw new InvalidOperationException("Proxy is already running."); @@ -480,4 +470,12 @@ private void DisposeOwnedLoggerFactory() // A misbehaving sink must never prevent proxy disposal from completing. } } + + private static bool DecompressionRatioDiffersFromDefault(double configured) + { + if (ProxyResourceLimits.Default.MaxDecompressionRatio is not double expected) + return true; + + return Math.Abs(configured - expected) > 1e-6; + } } From bbb8f02c00ed37ee4d8d7e322c6d35cb856876e7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:51:53 +0000 Subject: [PATCH 09/19] Update documentation --- docs/api/Titanium.Web.Proxy.ProxyServer.html | 25 ++++++++++++++------ docs/index.json | 2 +- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/api/Titanium.Web.Proxy.ProxyServer.html b/docs/api/Titanium.Web.Proxy.ProxyServer.html index addb5f23c..7923eeb58 100644 --- a/docs/api/Titanium.Web.Proxy.ProxyServer.html +++ b/docs/api/Titanium.Web.Proxy.ProxyServer.html @@ -3329,7 +3329,7 @@
    Parameters
    Edit this page - View Source + View Source

    Dispose()

    @@ -3345,7 +3345,7 @@
    Declaration
    Edit this page - View Source + View Source

    Dispose(bool)

    @@ -3892,11 +3892,20 @@
    Returns
    Edit this page - View Source + 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
    @@ -3915,7 +3924,9 @@
    Parameters
    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.

    + @@ -3924,7 +3935,7 @@
    Parameters
    Edit this page - View Source + View Source

    Stop()

    @@ -3943,7 +3954,7 @@
    Declaration
    Edit this page - View Source + View Source

    StopAsync(TimeSpan?)

    diff --git a/docs/index.json b/docs/index.json index 81ab6eed9..123092119 100644 --- a/docs/index.json +++ b/docs/index.json @@ -562,7 +562,7 @@ "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 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) Declaration public void Start(bool changeSystemProxySettings = true) Parameters Type Name Description bool changeSystemProxySettings | 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", From cbfbb85e1eb5000e502fdd57c140e74439b7f436 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 12:58:50 +0530 Subject: [PATCH 10/19] feat(inspector): label filtered session count in status bar --- src/Titanium.Inspector/Services/SessionSearch.cs | 2 +- tests/Titanium.Inspector.Tests/SessionPipelineTests.cs | 6 +++--- .../SessionSearchAndArchiveTests.cs | 4 ++-- website/docs/inspector.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Titanium.Inspector/Services/SessionSearch.cs b/src/Titanium.Inspector/Services/SessionSearch.cs index 85224fce3..80fd13da0 100644 --- a/src/Titanium.Inspector/Services/SessionSearch.cs +++ b/src/Titanium.Inspector/Services/SessionSearch.cs @@ -195,7 +195,7 @@ public static string BuildSessionCountText( { var searching = !string.IsNullOrWhiteSpace(searchQuery); var text = searching - ? $"Sessions: showing {visibleCount:N0} of {totalCount:N0}" + ? $"Sessions: showing {visibleCount:N0} of {totalCount:N0} (filtered)" : $"Sessions: {totalCount:N0}"; var emptySearch = searching && visibleCount == 0 && totalCount > 0; diff --git a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs index 2533d9987..db675bc5d 100644 --- a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs @@ -96,7 +96,7 @@ public async Task SessionAdded_UpdatesSessionCount_WithoutOverwritingStatusText( Assert.IsTrue(vm.HideTunnelsFilter); vm.ErrorsOnlyFilter = true; Assert.IsTrue(vm.SearchQuery.Contains("is:error", StringComparison.Ordinal)); - Assert.AreEqual("Sessions: showing 0 of 1", vm.SessionCountText); + Assert.AreEqual("Sessions: showing 0 of 1 (filtered)", vm.SessionCountText); vm.ClearFiltersCommand.Execute(null); await Task.Delay(50); @@ -180,7 +180,7 @@ public async Task SessionCountText_BodySearchFindsSpilledBodies() vm.SearchQuery = "body:alpha-body"; Assert.AreEqual(1, vm.Sessions.Count); Assert.AreEqual(1, vm.Sessions[0].Id); - StringAssert.Contains(vm.SessionCountText, "Sessions: showing 1 of 2"); + StringAssert.Contains(vm.SessionCountText, "Sessions: showing 1 of 2 (filtered)"); Assert.IsFalse(vm.SessionCountText.Contains("in-memory only", StringComparison.Ordinal)); } finally @@ -235,7 +235,7 @@ public void SessionCountText_RetentionEviction_SurfacesCaptureWindowOnZeroMatch( Assert.IsNull(registry.TryGet(1)); vm.SearchQuery = "host:does-not-exist"; - StringAssert.Contains(vm.SessionCountText, "Sessions: showing 0 of 2"); + StringAssert.Contains(vm.SessionCountText, "Sessions: showing 0 of 2 (filtered)"); StringAssert.Contains(vm.SessionCountText, "since "); StringAssert.Contains(vm.SessionCountText, "no matches in current list"); StringAssert.Contains(vm.SessionCountText, "removed by retention"); diff --git a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs index 608049872..c8efec496 100644 --- a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs @@ -399,7 +399,7 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "body:needle", retentionEvictedTotal: 0, oldestStartedUtc: null); - Assert.AreEqual("Sessions: showing 0 of 10", withBody); + Assert.AreEqual("Sessions: showing 0 of 10 (filtered)", withBody); var oldest = new DateTimeOffset(2026, 9, 2, 19, 2, 0, TimeSpan.Zero); var withRetention = SessionSearch.BuildSessionCountText( @@ -408,7 +408,7 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "host:missing", retentionEvictedTotal: 120, oldestStartedUtc: oldest); - StringAssert.Contains(withRetention, "Sessions: showing 0 of 50"); + StringAssert.Contains(withRetention, "Sessions: showing 0 of 50 (filtered)"); StringAssert.Contains(withRetention, "since "); StringAssert.Contains(withRetention, "no matches in current list · 120 removed by retention"); } diff --git a/website/docs/inspector.md b/website/docs/inspector.md index 9cf4e58d6..a7ea7785c 100644 --- a/website/docs/inspector.md +++ b/website/docs/inspector.md @@ -92,7 +92,7 @@ Use **Tools → Composer / Breakpoints / AutoResponder / Scripts…** to open th **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 **Sessions: showing X of Y** when a search or quick filter is active (X matching, Y currently kept). If the session retention limit has removed older sessions, it adds **since HH:mm** — the start time of the oldest session still kept. +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 **Sessions: showing X of Y (filtered)** when a search or quick filter is active (X matching, Y currently kept). If the session retention limit has removed older sessions, it adds **since HH:mm** — the start time of the oldest session still kept. **Network throttle:** use the toolbar **Throttle** combo (`None`, `Slow 3G`, `Fast 3G`, `LTE`) to add latency and bandwidth shaping on body writes / WebSocket frames during capture. Off by default (`None`); the hot path skips delay work when no profile is enabled. From 3493d02b837d04d3818585976373b84e623d5e26 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 13:53:48 +0530 Subject: [PATCH 11/19] feat(inspector): say 10k most recent and match filter in the status bar Make the kept-session count read as a rolling window after retention, without jargon for new users. --- .../Services/SessionSearch.cs | 25 +++++++++++-------- .../SessionPipelineTests.cs | 9 +++---- .../SessionSearchAndArchiveTests.cs | 20 ++++++++++++--- website/docs/inspector.md | 2 +- 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/Titanium.Inspector/Services/SessionSearch.cs b/src/Titanium.Inspector/Services/SessionSearch.cs index 80fd13da0..7ddb65494 100644 --- a/src/Titanium.Inspector/Services/SessionSearch.cs +++ b/src/Titanium.Inspector/Services/SessionSearch.cs @@ -194,18 +194,20 @@ public static string BuildSessionCountText( DateTimeOffset? oldestStartedUtc) { var searching = !string.IsNullOrWhiteSpace(searchQuery); + var kept = FormatSessionCount(totalCount); + if (retentionEvictedTotal > 0) + kept += " most recent"; + var text = searching - ? $"Sessions: showing {visibleCount:N0} of {totalCount:N0} (filtered)" - : $"Sessions: {totalCount:N0}"; + ? $"Sessions: {FormatSessionCount(visibleCount)} of {kept} match filter" + : $"Sessions: {kept}"; var emptySearch = searching && visibleCount == 0 && totalCount > 0; if (retentionEvictedTotal > 0 && oldestStartedUtc is { } oldest) { - // Retention limit dropped older sessions: the list only covers "since ". + // Retention dropped older sessions: the list is the most recent kept window. text += $" · since {oldest.ToLocalTime():HH:mm}"; - if (!emptySearch) - text += " (older removed by retention limit)"; } if (emptySearch) @@ -214,16 +216,19 @@ public static string BuildSessionCountText( return text; } + /// Compact round thousands (10k) so a retention cap is readable in the status bar. + private static string FormatSessionCount(int count) => + count >= 1_000 && count % 1_000 == 0 + ? $"{count / 1_000}k" + : count.ToString("N0"); + private static string FormatEmptySearchRetentionHint(int retentionEvictedTotal) { if (retentionEvictedTotal <= 0) return ""; - var retention = retentionEvictedTotal == 1 - ? "1 removed by retention" - : $"{retentionEvictedTotal} removed by retention"; - - return $" · no matches in current list · {retention}"; + // Removed sessions are no longer searchable, so a zero-match result may not be the whole story. + return $" · {retentionEvictedTotal:N0} older removed by retention (not searched)"; } private static List<(string Key, string Value)> Tokenize(string query) diff --git a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs index db675bc5d..55dc0172f 100644 --- a/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionPipelineTests.cs @@ -96,7 +96,7 @@ public async Task SessionAdded_UpdatesSessionCount_WithoutOverwritingStatusText( Assert.IsTrue(vm.HideTunnelsFilter); vm.ErrorsOnlyFilter = true; Assert.IsTrue(vm.SearchQuery.Contains("is:error", StringComparison.Ordinal)); - Assert.AreEqual("Sessions: showing 0 of 1 (filtered)", vm.SessionCountText); + Assert.AreEqual("Sessions: 0 of 1 match filter", vm.SessionCountText); vm.ClearFiltersCommand.Execute(null); await Task.Delay(50); @@ -180,7 +180,7 @@ public async Task SessionCountText_BodySearchFindsSpilledBodies() vm.SearchQuery = "body:alpha-body"; Assert.AreEqual(1, vm.Sessions.Count); Assert.AreEqual(1, vm.Sessions[0].Id); - StringAssert.Contains(vm.SessionCountText, "Sessions: showing 1 of 2 (filtered)"); + StringAssert.Contains(vm.SessionCountText, "Sessions: 1 of 2 match filter"); Assert.IsFalse(vm.SessionCountText.Contains("in-memory only", StringComparison.Ordinal)); } finally @@ -235,10 +235,9 @@ public void SessionCountText_RetentionEviction_SurfacesCaptureWindowOnZeroMatch( Assert.IsNull(registry.TryGet(1)); vm.SearchQuery = "host:does-not-exist"; - StringAssert.Contains(vm.SessionCountText, "Sessions: showing 0 of 2 (filtered)"); + StringAssert.Contains(vm.SessionCountText, "Sessions: 0 of 2 most recent match filter"); StringAssert.Contains(vm.SessionCountText, "since "); - StringAssert.Contains(vm.SessionCountText, "no matches in current list"); - StringAssert.Contains(vm.SessionCountText, "removed by retention"); + StringAssert.Contains(vm.SessionCountText, "older removed by retention (not searched)"); } finally { diff --git a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs index c8efec496..b2e4c410e 100644 --- a/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs +++ b/tests/Titanium.Inspector.Tests/SessionSearchAndArchiveTests.cs @@ -399,7 +399,7 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "body:needle", retentionEvictedTotal: 0, oldestStartedUtc: null); - Assert.AreEqual("Sessions: showing 0 of 10 (filtered)", withBody); + Assert.AreEqual("Sessions: 0 of 10 match filter", withBody); var oldest = new DateTimeOffset(2026, 9, 2, 19, 2, 0, TimeSpan.Zero); var withRetention = SessionSearch.BuildSessionCountText( @@ -408,9 +408,23 @@ public void BuildSessionCountText_IncludesRetentionHints() searchQuery: "host:missing", retentionEvictedTotal: 120, oldestStartedUtc: oldest); - StringAssert.Contains(withRetention, "Sessions: showing 0 of 50 (filtered)"); + StringAssert.Contains(withRetention, "Sessions: 0 of 50 most recent match filter"); StringAssert.Contains(withRetention, "since "); - StringAssert.Contains(withRetention, "no matches in current list · 120 removed by retention"); + StringAssert.Contains(withRetention, "120 older removed by retention (not searched)"); + + var atDefaultCap = SessionSearch.BuildSessionCountText( + visibleCount: 25, + totalCount: 10_000, + searchQuery: "host:api", + retentionEvictedTotal: 40, + oldestStartedUtc: oldest); + Assert.AreEqual( + $"Sessions: 25 of 10k most recent match filter · since {oldest.ToLocalTime():HH:mm}", + atDefaultCap); + + Assert.AreEqual( + $"Sessions: 10k most recent · since {oldest.ToLocalTime():HH:mm}", + SessionSearch.BuildSessionCountText(10_000, 10_000, null, 40, oldest)); } [TestMethod] diff --git a/website/docs/inspector.md b/website/docs/inspector.md index a7ea7785c..bf89eb9dd 100644 --- a/website/docs/inspector.md +++ b/website/docs/inspector.md @@ -92,7 +92,7 @@ Use **Tools → Composer / Breakpoints / AutoResponder / Scripts…** to open th **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 **Sessions: showing X of Y (filtered)** when a search or quick filter is active (X matching, Y currently kept). If the session retention limit has removed older sessions, it adds **since HH:mm** — the start time of the oldest session still kept. +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 **Sessions: X of Y match filter** when a search or quick filter is active (X matching, Y currently kept). After the session retention limit has dropped older rows, Y is labeled **most recent** (round thousands as **10k**, e.g. `Sessions: 10k most recent · since 14:05`) and the strip adds **since HH:mm** — the start time of the oldest session still kept. If a filter matches nothing, it also shows how many older sessions retention removed, since those are no longer searched. **Network throttle:** use the toolbar **Throttle** combo (`None`, `Slow 3G`, `Fast 3G`, `LTE`) to add latency and bandwidth shaping on body writes / WebSocket frames during capture. Off by default (`None`); the hot path skips delay work when no profile is enabled. From 0946f4a397aad44d2bb440696233fe73dba05921 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 14:21:02 +0530 Subject: [PATCH 12/19] feat(inspector): open session retention from the status-bar count Make the session count a button with a tooltip, and explain the exclusions summary on hover. --- src/Titanium.Inspector/Views/MainWindow.axaml | 11 +++++++++-- .../UiHeadless/AutomationIdCoverageHeadlessTests.cs | 1 + .../UiHeadless/MenuActionsHeadlessTests.cs | 6 ++++++ website/docs/inspector.md | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Titanium.Inspector/Views/MainWindow.axaml b/src/Titanium.Inspector/Views/MainWindow.axaml index a198193fe..f64257362 100644 --- a/src/Titanium.Inspector/Views/MainWindow.axaml +++ b/src/Titanium.Inspector/Views/MainWindow.axaml @@ -737,13 +737,20 @@ Foreground="{Binding StatusSeverity, Converter={StaticResource StatusSeverityBrushConverter}}" AutomationProperties.AutomationId="StatusText" /> - + @@ -750,7 +749,6 @@ Command="{Binding OpenExclusionSummaryCommand}" IsVisible="{Binding HasExclusionSummary}" Opacity="0.85" FontSize="11" - ToolTip.Tip="Hosts Inspector does not decrypt. OS bypass: skipped by the system proxy (traffic never reaches Inspector). Tunnel-only: reaches Inspector but is passed through encrypted. Learned: tunneled automatically after a decrypt failure. Click to edit." AutomationProperties.AutomationId="ExclusionSummaryLink" /> From c096455cb02cc25e1ad2ca09d84571d26c96f796 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 20:03:47 +0530 Subject: [PATCH 14/19] chore(release): bump product version to 7.0.19 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 2b7d38d57..c271e6f1f 100644 --- a/src/Titanium.Cli/Titanium.Cli.csproj +++ b/src/Titanium.Cli/Titanium.Cli.csproj @@ -7,7 +7,7 @@ latest enable false - 7.0.18 + 7.0.19 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 1fda90cf1..95c3d84f2 100644 --- a/src/Titanium.Inspector/Titanium.Inspector.csproj +++ b/src/Titanium.Inspector/Titanium.Inspector.csproj @@ -8,7 +8,7 @@ enable true false - 7.0.18 + 7.0.19 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 7f6796f05..15571b0bf 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.18 + 7.0.19 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 7612d5453..a1591ec10 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.18 + 7.0.19 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 133a243c3..5c531fbc0 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.18 + 7.0.19 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 b62d50043..010078847 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.18.0")] -[assembly: AssemblyFileVersion("7.0.18.0")] +[assembly: AssemblyVersion("7.0.19.0")] +[assembly: AssemblyFileVersion("7.0.19.0")] diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index 9a2cce5e1..1a55fe166 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.18 + 7.0.19 Date: Tue, 6 Oct 2026 20:47:25 +0530 Subject: [PATCH 17/19] feat(inspector): match an exact process ID from the search box Typing a PID from the Process column should find that row. Match only by equality so fragments like 443 do not hit PID 4430. --- src/Titanium.Inspector/Services/SessionSearch.cs | 15 +++++++++++---- src/Titanium.Inspector/Views/MainWindow.axaml | 2 +- .../SessionSearchAndArchiveTests.cs | 11 ++++++++--- website/docs/inspector.md | 4 ++-- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Titanium.Inspector/Services/SessionSearch.cs b/src/Titanium.Inspector/Services/SessionSearch.cs index 11850894f..bd82cbe21 100644 --- a/src/Titanium.Inspector/Services/SessionSearch.cs +++ b/src/Titanium.Inspector/Services/SessionSearch.cs @@ -3,8 +3,8 @@ namespace Titanium.Inspector.Services; /// -/// Session search/filter syntax. A bare word matches URL, host, process name, method (exact), -/// or status code (exact). Prefixes limit the match to one field: +/// Session search/filter syntax. A bare word matches URL, host, process name, process ID (exact), +/// method (exact), or status code (exact). Prefixes limit the match to one field: /// method:, status: (exact or 2xx–5xx), host:, url:, body:, process:, pid:, protocol:, content-type:, /// is:ws|grpc|tunnel|multipart|error, hide:tunnel|image|static /// @@ -345,8 +345,9 @@ private static bool MatchProcess(SessionSnapshot s, string value) => (s.ProcessId > 0 && s.ProcessId.ToString().Equals(value, StringComparison.Ordinal)); /// - /// Free-text match over identifying columns. Process name only — not PID or - /// , which would treat port-like numbers as hits. + /// Free-text match over identifying columns. Process matches by name, or by exact PID — + /// never by substring of (chrome:4430), + /// which would treat port-like numbers as hits. /// private static bool MatchBareText(SessionSnapshot s, string value) { @@ -366,6 +367,12 @@ private static bool MatchBareText(SessionSnapshot s, string value) return true; } + if (s.ProcessId > 0 && + s.ProcessId.ToString().Equals(value, StringComparison.Ordinal)) + { + return true; + } + if (s.Method.Equals(value, StringComparison.OrdinalIgnoreCase)) { return true; diff --git a/src/Titanium.Inspector/Views/MainWindow.axaml b/src/Titanium.Inspector/Views/MainWindow.axaml index 653f6f44b..2a163abc4 100644 --- a/src/Titanium.Inspector/Views/MainWindow.axaml +++ b/src/Titanium.Inspector/Views/MainWindow.axaml @@ -143,7 +143,7 @@ Date: Tue, 6 Oct 2026 21:09:01 +0530 Subject: [PATCH 18/19] build(deps-dev): bump source-map-js from 1.2.1 to 1.2.2 in /website Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/7rulnik/source-map-js/releases) - [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: source-map-js dependency-version: 1.2.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- website/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 34fdf1c21..e78c54426 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -2379,9 +2379,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { From dfe74e4fdb4f87996ecdd96f72346e98d5e49ef9 Mon Sep 17 00:00:00 2001 From: justcoding121 Date: Tue, 6 Oct 2026 21:12:36 +0530 Subject: [PATCH 19/19] fix(sonar): drop the unused proxy waiter and the constant assertion. --- src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs | 1 - tests/Titanium.Inspector.Tests/LoopbackExemptUxTests.cs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs index f5cc9eb65..1ccd10ea1 100644 --- a/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs +++ b/src/Titanium.Inspector/ViewModels/MainWindowViewModel.cs @@ -932,7 +932,6 @@ private async Task TryToggleSystemProxyAsync() SystemProxy = true; } - private Task AwaitCancellableAsync(Task task) => task.WaitAsync(StatusCancelToken); private Task AwaitCancellableAsync(Task task) => task.WaitAsync(StatusCancelToken); /// diff --git a/tests/Titanium.Inspector.Tests/LoopbackExemptUxTests.cs b/tests/Titanium.Inspector.Tests/LoopbackExemptUxTests.cs index c2bbbe5b0..10d4507e1 100644 --- a/tests/Titanium.Inspector.Tests/LoopbackExemptUxTests.cs +++ b/tests/Titanium.Inspector.Tests/LoopbackExemptUxTests.cs @@ -43,7 +43,10 @@ public void Copy_WarningAndUnchanged_MatchReadiness() StringAssert.Contains( LoopbackExemptCopy.ClearedStatus(LoopbackCaptureReadiness.Ready, refreshed: true), "refreshed"); - Assert.AreEqual("Store apps unchanged.", LoopbackExemptCopy.ClosedUnchanged); + var closed = new LoopbackExemptResult(); + Assert.IsFalse(closed.Changed); + Assert.AreEqual(LoopbackExemptCopy.ClosedUnchanged, closed.StatusText); + StringAssert.Contains(closed.StatusText, "unchanged"); } [TestMethod]