-
Notifications
You must be signed in to change notification settings - Fork 655
Home
A lightweight, asynchronous HTTP(S) proxy server for .NET. This wiki documents the major features and the most common APIs. For the full type reference, see the API documentation.
- Getting started
- Endpoints
- Decrypting HTTPS
- Intercepting requests and responses
- Modifying bodies
- Custom and redirected responses
- Streaming bodies
- HTTP/2
- Tunnel (CONNECT) interception
- Upstream proxies
- Authentication
- Performance and pooling
- Logging and diagnostics
- Request timing
- Supported frameworks
- Breaking changes: unified logging and timing
- Protocol feature support
Install from NuGet:
dotnet add package Titanium.Web.ProxyStart an explicit proxy that logs every requested URL:
using System;
using System.Net;
using System.Threading.Tasks;
using Titanium.Web.Proxy;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Models;
using var proxyServer = new ProxyServer();
proxyServer.BeforeRequest += OnRequest;
var endPoint = new ExplicitProxyEndPoint(IPAddress.Loopback, 8000, decryptSsl: true);
proxyServer.AddEndPoint(endPoint);
// Create and trust the root certificate used to decrypt HTTPS traffic.
proxyServer.CertificateManager.EnsureRootCertificate(
userTrustRootCertificate: true,
machineTrustRootCertificate: false);
proxyServer.Start();
Console.WriteLine("Proxy listening on 127.0.0.1:8000. Press Enter to stop.");
Console.ReadLine();
proxyServer.Stop();
static Task OnRequest(object sender, SessionEventArgs e)
{
Console.WriteLine(e.HttpClient.Request.Url);
return Task.CompletedTask;
}Configure your client to use 127.0.0.1:8000 as its HTTP and HTTPS proxy.
Add one or more endpoints before calling Start():
-
ExplicitProxyEndPoint— the client is configured to use the proxy (standardHTTP_PROXY/ system proxy setup). SupportsCONNECTtunneling. -
TransparentProxyEndPoint— traffic is redirected to the proxy without the client knowing (e.g. via routing/NAT). SetGenericCertificateNamefor the server name to present. -
SocksProxyEndPoint— SOCKS4/SOCKS5 endpoint.
proxyServer.AddEndPoint(new ExplicitProxyEndPoint(IPAddress.Loopback, 8000));
proxyServer.AddEndPoint(new TransparentProxyEndPoint(IPAddress.Loopback, 8001, decryptSsl: true)
{
GenericCertificateName = "example.com"
});
proxyServer.AddEndPoint(new SocksProxyEndPoint(IPAddress.Loopback, 1080));To inspect HTTPS traffic the proxy generates per-host certificates signed by its own root certificate, which the client must trust.
// Generate (if needed) and trust the root certificate for the current user.
proxyServer.CertificateManager.EnsureRootCertificate(
userTrustRootCertificate: true,
machineTrustRootCertificate: false);Useful CertificateManager members:
-
RootCertificate/RootCertificateName/PfxFilePath— the CA used for signing. -
CreateRootCertificate(...),TrustRootCertificate(...),RemoveTrustedRootCertificate(...). -
SaveFakeCertificates— cache generated leaf certificates on disk. -
CertificateEngine—BouncyCastleor the built-in engine.
Only decrypt endpoints where you need to see content; leave decryptSsl: false to pass HTTPS through as an opaque tunnel.
Subscribe to the proxy lifecycle events. All handlers are async.
proxyServer.BeforeRequest += OnRequest; // before the request is sent upstream
proxyServer.BeforeResponse += OnResponse; // after response headers are received
proxyServer.AfterResponse += OnAfterResponse;SessionEventArgs exposes HttpClient.Request and HttpClient.Response, headers, the URL, client/process info, and per-session UserData.
Task OnRequest(object sender, SessionEventArgs e)
{
var request = e.HttpClient.Request;
request.Headers.AddHeader("X-Proxy", "titanium");
return Task.CompletedTask;
}Read and replace the whole body (buffers it in memory):
async Task OnResponse(object sender, SessionEventArgs e)
{
if (e.HttpClient.Response.ContentType?.Contains("text/html") == true)
{
var body = await e.GetResponseBodyAsString();
e.SetResponseBodyString(body.Replace("http://", "https://"));
}
}For large or unbounded bodies, prefer the streaming APIs below instead of GetResponseBody().
Answer the client directly, without contacting the server:
proxyServer.BeforeRequest += (sender, e) =>
{
if (e.HttpClient.Request.Url.Contains("blocked.example"))
e.Ok("<html><body>Blocked</body></html>");
return Task.CompletedTask;
};-
e.Ok(html)/e.Ok(bytes)— send a200response. -
e.Respond(response)— send an arbitraryResponse. -
e.Redirect(url)— send a redirect. -
e.TerminateServerConnection()— close the upstream connection instead of reusing it.
When you supply a response after the server was already contacted, the original server body is drained so the connection can be reused; see Draining bodies.
Inspect or modify bodies chunk-by-chunk, or generate a response body from scratch, without buffering it in memory — ideal for large downloads or endless streams (e.g. server-sent events).
proxyServer.OnResponseBodyWrite += (sender, e) =>
{
e.BodyBytes = Transform(e.BodyBytes); // modify each chunk as it streams
return Task.CompletedTask;
};See the dedicated Streaming Bodies page for OnRequestBodyWrite/OnResponseBodyWrite, RespondStreaming, draining, and the HTTP/1.x vs HTTP/2 details.
HTTP/2 support is on by default (negotiated via TLS ALPN only — no cleartext h2c upgrade). To opt out and force HTTP/1.1 only:
proxyServer.EnableHttp2 = false;Header/body modification in BeforeRequest/BeforeResponse, chunked trailers, interim (1xx) responses, and
the synthetic-response APIs (Ok/Respond/Redirect/GenericResponse/RespondStreaming) all work over
HTTP/2 the same as over HTTP/1.x — see Streaming Bodies. Not supported: HTTP/2 server
push and cleartext h2c upgrade. See Protocol Feature Support for the full breakdown.
On an ExplicitProxyEndPoint, decide per-CONNECT whether to decrypt:
explicitEndPoint.BeforeTunnelConnectRequest += (sender, e) =>
{
var host = e.HttpClient.Request.RequestUri.Host;
if (host.EndsWith("bank.example"))
e.DecryptSsl = false; // pass through without decrypting
return Task.CompletedTask;
};
explicitEndPoint.BeforeTunnelConnectResponse += (sender, e) => Task.CompletedTask;Chain through another proxy, globally or per request:
proxyServer.UpStreamHttpProxy = new ExternalProxy("upstream.example", 8888);
proxyServer.UpStreamHttpsProxy = new ExternalProxy("upstream.example", 8888);
// Or resolve the upstream proxy dynamically:
proxyServer.GetCustomUpStreamProxyFunc = async args =>
{
return new ExternalProxy("upstream.example", 8888);
};
// Detect and reuse the system's configured proxy:
proxyServer.ForwardToUpstreamGateway = true;ExternalProxy supports HTTP, HTTPS, and SOCKS4/5, with optional credentials.
-
Proxy authentication (Basic):
proxyServer.ProxyBasicAuthenticateFunc = async (args, userName, password) => userName == "user" && password == "secret";
-
Windows authentication (Kerberos/NTLM) to upstream servers:
proxyServer.EnableWinAuth = true;
-
Mutual TLS: provide the client certificate via
ClientCertificateSelectionCallback, and validate server certificates withServerCertificateValidationCallback.
-
EnableConnectionPool— reuse idle upstream TCP connections (enabled by default). Only connections that are safe to reuse under HTTP (persistent, body fully received, not authenticated to a specific identity) are pooled; set tofalseto force a fresh connection per client. -
ConnectionTimeOutSeconds,TcpTimeWaitSeconds,ReuseSocket— tune connection lifetime. -
BufferPool/BufferSize— reuse I/O buffers. -
CertificateManager.SaveFakeCertificates— cache generated certificates.
Every exception the proxy catches — even one handled internally and never surfaced to your code — is
reported through ProxyServer.Logging, a Microsoft.Extensions.Logging-based abstraction. This replaced
the old ExceptionFunc callback; see
Breaking changes: unified logging and timing below if you
are migrating.
// Master switch: false gives zero logging overhead (no timestamps read, no strings formatted).
proxyServer.Logging.Enabled = true;
// Only entries at or above this level are actually written to a sink. Every caught exception is still
// classified and reported to the gateway regardless - this only controls how much reaches a sink.
// Defaults to LogLevel.Error so out-of-the-box behavior stays quiet.
proxyServer.Logging.MinimumLevel = LogLevel.Information;
// Built-in sinks, both asynchronous and best-effort so they never block proxy traffic:
proxyServer.Logging.EnableConsole = true; // default on
proxyServer.Logging.EnableConsoleColors = true; // default on; colors each line by level
proxyServer.Logging.EnableFile = true; // default off
proxyServer.Logging.FilePath = "logs/proxy.log"; // size-based rolling file
proxyServer.Logging.MaxFileSizeBytes = 10 * 1024 * 1024;
proxyServer.Logging.MaxRolledFiles = 5;
// Changes to the Logging options above only take effect once you (re)apply them - Start() does this
// automatically, but call it yourself to change configuration while already running:
proxyServer.ApplyLoggingConfiguration();To bridge into an existing logging pipeline (Serilog, NLog, an ASP.NET Core host's ILoggerFactory, etc.)
instead of the built-in Console/File sinks, set LoggerFactory — this disables the built-in sinks entirely
and hands every log record to your factory verbatim:
proxyServer.Logging.LoggerFactory = hostLoggerFactory;
proxyServer.ApplyLoggingConfiguration();Exceptions the proxy considers expected/benign under normal operation (client disconnects, cancelled
operations, expected socket resets, retries, and similar) are logged at Debug/Trace so they never
contribute to Error-level noise in the default configuration, while genuinely unexpected failures are
always logged at Error or Critical.
The built-in console sink colors each line by level (dim Trace/Debug, default Information, yellow
Warning, red Error, bold red Critical) so failures stand out while scrolling through busy output.
Colors are automatically suppressed for a stream that is redirected (e.g. proxy.exe > out.log) or when
the NO_COLOR environment variable is set, regardless of
EnableConsoleColors — so redirected output and log files never end up with raw escape codes. The
rolling-file sink is always plain text.
Set EnableRequestTimingCapture to populate structured timing objects for every session; when left
false (the default) no timing object is ever allocated, so there is no cost at all when the feature is
unused.
proxyServer.EnableRequestTimingCapture = true;
proxyServer.AfterResponse += (sender, e) =>
{
var timing = e.Timing; // HttpRequestTiming, or null if capture is disabled
if (timing != null)
{
Console.WriteLine($"Time to first byte: {timing.TimeToFirstByte}");
Console.WriteLine($"Total duration: {timing.TotalDuration}");
Console.WriteLine($"Upstream connection reused: {timing.UpstreamConnectionReused}");
}
return Task.CompletedTask;
};-
SessionEventArgsBase.Timing(HttpRequestTiming) — per-request milestones: when the client's request headers were read, when an upstream connection became ready, when the request was sent, when response headers arrived, and when the session completed — plus derived durations (ConnectionWaitDuration,TimeToFirstByte,ResponseDeliveryDuration,TotalDuration) and retry bookkeeping (AttemptCount,UpstreamConnectionReused). -
SessionEventArgsBase.UpstreamConnectionTiming(UpstreamConnectionTiming) — timing of the underlying upstream TCP/TLS connection itself (DNS resolution, TCP handshake, optional upstream-proxy CONNECT tunnel, TLS handshake). Shared by every session that reuses the same pooled connection. -
TunnelConnectSessionEventArgs.ClientTlsTiming(ClientTlsTiming) — duration of the client-facing (browser-to-proxy) TLS handshake performed while decrypting an HTTPSCONNECTtunnel on an explicit endpoint.
- .NET 10
Versions prior to 4.0 also supported .NET Framework 4.6.2 and .NET 8; starting with 4.0, the package targets .NET 10 only.
-
ProxyServer.ExceptionFuncand theExceptionHandlerdelegate were removed. UseProxyServer.Logginginstead — every exception the old callback would have received is now reported through the logging gateway, classified by severity rather than delivered uniformly to a single callback. -
SessionEventArgsBase.TimeLine(the free-formDictionary<string, DateTime>of named milestones) was removed. UseTiming/UpstreamConnectionTiming/ClientTlsTiminginstead, which are strongly typed and only allocated whenEnableRequestTimingCaptureis set.
Wondering whether a specific HTTP/1.x or HTTP/2 feature (trailers, interim 1xx responses, HPACK, server push, ...) is supported? See the Protocol Feature Support page for a full Yes/No/Partial breakdown.