Migrates the runtime to Kestrel - #1854
Open
waldekmastykarz wants to merge 7 commits into
Open
Conversation
* Add throwaway Kestrel proxy spike (Phase 0B de-risking) Validates selective decrypt, non-destructive ClientHello ALPN peek, h2-only/gRPC blind-tunnel, keep-alive isolation, and unbuffered streaming. Not part of DevProxy.sln. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add canonical HTTP proxy model to DevProxy.Abstractions (Phase 1) Introduce engine-agnostic Dev Proxy model that will replace the leaked Unobtanium/Titanium types in the plugin SDK: - IHttpHeader/IHeaderCollection/IHttpMessage/IHttpRequest/IHttpResponse and IProxySession (logical SessionId, Respond() mocking primitive), with concrete HttpHeader + HeaderCollection. - Bodies exposed as ReadOnlyMemory<byte> (decompressed-body contract). - Body strategy: BodyMode, BodyCapabilities, BodyModeResolver (pure, reconciles streaming vs full-body plugin access). - ForwardingInvariants documenting the forwarding contract + shared HopByHopHeaders set. - New DevProxy.Abstractions.Tests (xUnit), 33 tests. Purely additive: existing Titanium-based code is untouched and the full solution builds clean. Adapter wiring follows in Phase 2/3. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Titanium adapter mapping engine types onto canonical model (Phase 2) Introduces DevProxy.Proxy.Titanium, the sole project referencing Unobtanium.Web.Proxy, which projects Titanium request/response/header/session types onto the engine-agnostic canonical model in DevProxy.Abstractions. - TitaniumHeaderCollection, TitaniumHttpMessageAdapter (shared DRY base over RequestResponseBase), TitaniumRequestAdapter, TitaniumResponseAdapter, TitaniumProxySession. - Body access guarded by HasBody (Titanium throws BodyNotFoundException otherwise); mutation routed through the session's SetRequestBody/ SetResponseBody since Titanium exposes no public body setter. - DevProxy.Proxy.Titanium.Tests: 38 adapter-fidelity unit tests. Purely additive and ship-safe: the existing engine and plugins are untouched. Full solution builds 0/0; 71 tests green. Cross-engine parity tests are deferred to Phase 4 (require the Kestrel engine to exist). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Wire canonical IProxySession through ProxyEvents; migrate first plugin (Phase 3 slice 1) Introduces the dual-field strangler that lets plugins migrate off Titanium types one at a time while staying green: - ProxyHttpEventArgsBase exposes IProxySession ProxySession alongside the existing Titanium SessionEventArgs Session. HasRequestUrlMatch now reads the canonical request. - ProxyEngine builds a TitaniumProxySession (keyed by the existing per-session GetHashCode key) and threads it through the request/response arg construction sites; DevProxy host references DevProxy.Proxy.Titanium. - CachingGuidancePlugin fully migrated to e.ProxySession.Request and is now Titanium-free. Verified live: the cache-window warning fires through the real proxy pipeline reading IHttpRequest.RequestUri via the adapter. Full solution builds 0/0; 71 tests green. The other 21 Titanium-touching plugins are untouched and keep compiling against Session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 3 Wave 1: add IHttpRequest overloads to ProxyUtils Add canonical IHttpRequest overloads alongside the existing Titanium Request overloads for IsGraphRequest/IsSdkRequest/IsGraphBetaRequest/BuildGraphResponseHeaders. Extract a shared BuildGraphResponseHeadersCore to keep both overloads DRY. Additive only; Titanium overloads remain until the final wave. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 3 guidance wave: migrate 4 guidance plugins off Titanium Migrate ODSPSearchGuidancePlugin, GraphSelectGuidancePlugin, GraphSdkGuidancePlugin, and GraphClientRequestIdGuidancePlugin to the canonical model: read request/response via e.ProxySession, helpers take IHttpRequest, header checks via Contains. Each plugin is now Titanium-free in source (LoggingContext still takes e.Session until the final wave). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 3 mocking wave: migrate 5 mocking plugins off Titanium Migrate AuthPlugin, CrudApiPlugin, GraphMockResponsePlugin, MockResponsePlugin, and OpenAIMockResponsePlugin to the canonical model: request/response via e.ProxySession, GenericResponse -> IProxySession.Respond, canonical HttpHeader, header methods via the canonical IHeaderCollection. Helper signatures and the CrudApi action-handler delegate now use IProxySession/IHttpRequest/ProxyRequestArgs. LoggingContext still takes e.Session until the final wave. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 3 inspection-lite: migrate HttpUtils + OpenAI usage/telemetry streaming off Titanium Change HttpUtils.IsStreamingResponse/GetBodyFromStreamingResponse to take the canonical IHttpResponse. Migrate OpenAIUsageDebuggingPlugin fully to e.ProxySession (status comparisons cast HttpStatusCode to int). Point OpenAITelemetryPlugin's streaming call site at e.ProxySession.Response. DevToolsPlugin and the OpenAITelemetry RequestLog.Context.Session readers remain for the final LoggingContext wave. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 3 throttling group: migrate ThrottlerInfo + 5 throttling plugins off Titanium Flip ThrottlerInfo.ShouldThrottle delegate from Func<Request,...> to Func<IHttpRequest,...> (PluginEvents) and migrate the coordinated consumers: GraphUtils.BuildThrottleKey, RateLimitingPlugin, GraphRandomErrorPlugin, GenericRandomErrorPlugin, LanguageModelRateLimitingPlugin, RetryAfterPlugin. RetryAfter's GenericResponse becomes IProxySession.Respond with canonical HttpHeader. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Migrate plugins off Titanium onto canonical HTTP model (Phase 3 final wave) Complete the strangler migration: flip LoggingContext and the proxy event args to the engine-agnostic IProxySession model, migrate all remaining plugins off Titanium.Web.Proxy types, and drop the Unobtanium.Web.Proxy package reference from DevProxy.Abstractions and DevProxy.Plugins. - LoggingContext/RequestLog now carry IProxySession (was SessionEventArgs); ProxyHttpEventArgsBase no longer exposes the Titanium Session. - All Behavior/Generation/Guidance/Inspection/Manipulation/Mocking/Reporting plugins read e.ProxySession.Request/Response and the canonical header API. - DevToolsPlugin re-keys _responseBody/CDP requestId on the phase-stable ProxySession.SessionId instead of the Titanium Request hash code. - Add Url setter and response HttpVersion to the canonical model (+ Titanium adapters) to preserve Rewrite and HAR behavior. - Remove dead Titanium FuncExtensions helper. DevProxy.Abstractions and DevProxy.Plugins are now Titanium-free; only the host engine and the Titanium adapter retain the dependency. Build 0/0, 33+38 tests green, live smoke test verified. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Kestrel proxy engine vertical slice (Phase 4) Introduce DevProxy.Proxy.Kestrel: a forward-proxy engine built on ASP.NET Core Kestrel that runs the Dev Proxy plugin pipeline against the canonical HTTP model, as the eventual replacement for the Titanium engine. Slice-1 scope (dev-toggle via DEV_PROXY_ENGINE=kestrel, for golden-output comparison against Titanium; not a shipped fallback): - KestrelProxyEngine hosts a raw TCP endpoint via UseConnectionHandler. - ProxyConnectionHandler: HTTP/1.1 parse, CONNECT->SslStream MITM, forward, write-back; catches client disconnect/cancel as normal teardown. - PluginPipeline mirrors ProxyEngine semantics (watched/mocked/notwatched, Before/After request+response, per-exchange SessionData) and emits request logs inside the requestId scope so the console formatter groups plugin and engine lines identically to Titanium. - UpstreamForwarder/ResponseWriter apply ForwardingInvariants (hop-by-hop stripping, decompressed bodies, Content-Length/Encoding fix-up). - In-memory CertificateAuthority + DuplexPipeStream ported from the POC. Engine selection wired via DEV_PROXY_ENGINE in DI; ProxyConsoleFormatter suppresses the engine-name prefix for KestrelProxyEngine (as for ProxyEngine). Deferred hardening (tracked): keep-alive, blind-tunnel for non-watched and h2-only/gRPC (ClientHello peek), WebSocket relay, chunked/trailers/100-continue, body-mode streaming, process filter, persistent CA, host-watch DRY consolidation. Full solution builds 0/0; 71 existing tests green; live-verified plain HTTP passthrough, HTTPS MITM, GenericRandomError/RetryAfter mocking with correct console output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Kestrel proxy engine test project (Phase 4) Add DevProxy.Proxy.Kestrel.Tests with 28 unit tests covering the HTTP/1.1 parser, host watch list (including order-dependent exclusion semantics), response writer, and the mutable HTTP model / CanonicalProxySession. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add selective decrypt + ALPN blind-tunnel to Kestrel engine (Phase 4) At CONNECT time the engine now non-destructively peeks the TLS ClientHello (SNI + ALPN) and decides whether to intercept: - non-watched hosts are blind-tunnelled (encrypted bytes relayed, never MITM'd) - watched hosts whose client is h2-only (gRPC, no http/1.1 fallback) are blind-tunnelled so they stop failing - otherwise the engine terminates TLS advertising http/1.1 only, so h2-capable clients downgrade and are intercepted as HTTP/1.1 Adds a minimal tolerant TlsClientHello parser, wires HostWatchList into the connection handler, and relays blind tunnels bidirectionally with a linked cancellation token so both copy tasks finish before the streams are disposed. Covered by 9 new TlsClientHello unit tests (37 Kestrel tests total). Verified live: watched host downgrades h2->http/1.1 and is intercepted; example.com (non-watched) validates its real cert without -k, proving it is never MITM'd. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add keep-alive + per-request state isolation to Kestrel engine (Phase 4 slice 3) Replace the static one-shot HTTP/1.1 reader with a stateful per-connection Http1ConnectionReader that owns the inter-request buffer, so bytes read past one request's body (a pipelined next request) are retained instead of dropped. This makes client-side keep-alive correct. - Http1ConnectionReader: stateful ReadHeadAsync/ReadBodyAsync owning _pending. - Http1RequestReader: reduced to stateless parse helpers (ParseHead, GetContentLength, IndexOfDoubleCrlf). - ProxyConnectionHandler.ServeConnectionAsync: keep-alive loop over both plain HTTP and decrypted CONNECT streams; each exchange builds a fresh CanonicalProxySession (new GUID + Interlocked requestId) so plugin state never leaks across requests on a reused connection. - ShouldKeepAlive (RFC 9112 9.3): persistent by default on HTTP/1.1, opt-in on HTTP/1.0; forces Connection: close for Connection: close, Transfer-Encoding (chunked, not yet reframable) and Expect: 100-continue (unsupported), so an unframable body can never corrupt the next request. - ResponseWriter.WriteAsync: keepAlive flag drives the Connection header. Tests: +15 Kestrel (stateful reader incl. pipelining/surplus-retention, ShouldKeepAlive matrix, keep-alive/close response-writer). 123 green total. Verified live: two URLs in one curl invocation reuse one TCP connection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WebSocket transparent relay to Kestrel engine (Phase 4 slice 4) Watched-host WebSocket upgrades (ws:// plain + wss:// after MITM) now relay through the Kestrel engine for parity with Titanium, which relays WS opaquely (no plugin sees frames). HttpClient can't carry a 101 upgrade, so WebSocketRelay opens its own raw socket to the origin (TLS w/ http/1.1 ALPN for wss), replays the handshake in origin-form (preserving Upgrade/Connection/Sec-WebSocket-*, stripping Proxy-*), writes the origin's 101 back to the client verbatim, then splices frames raw via a shared StreamRelay (also now used by BlindTunnelAsync, DRY). Critical ordering: the response pipeline (which flushes the buffered req-log) runs in the onHandshakeResponse callback, right after the 101 and before the splice blocks, else a watched WS never logs until the socket closes. A handshakeObserved fallback flush covers origin-connect failure. Tests: ParseResponseHead unit tests, StreamRelay bidirectional copy, and an end-to-end RelayAsync test against a loopback fake origin (asserts origin-form handshake replay, header preservation, 101 verbatim write-back, frame splicing, and onHandshakeResponse invocation). 128 tests green (+5 Kestrel). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add chunked request bodies + 100-continue to Kestrel engine (Phase 4 slice 5) Closes the biggest remaining HTTP/1.1 correctness gap on the request read path. Previously a chunked request body was silently dropped (Content-Length defaulted to 0, so an empty body was forwarded) and Expect: 100-continue hung (the proxy never sent the interim 100). Response-side chunked already worked via HttpClient. - Http1ConnectionReader.ReadChunkedBodyAsync decodes chunked framing (size lines, chunk extensions, CRLF delimiters) into one buffer, consumes and drops trailers, and retains surplus for the next pipelined request. Shared _pending fill primitives (PullAsync/ReadExactlyAsync/ReadLineAsync/TakeFromPending), reused by ReadBodyAsync. - Http1RequestReader.DetectBodyFraming (None/ContentLength/Chunked/Conflicting) and HasExpectContinue. A request declaring both Content-Length and chunked Transfer-Encoding is Conflicting and refused with 400 (RFC 9112 6.3.3 smuggling guard). - ResponseWriter.WriteContinueAsync sends 100 Continue; ExchangeAsync answers Expect: 100-continue itself before reading the body (the buffering model can't relay the origin's interim 100). Expect is stripped on forward. - ShouldKeepAlive no longer forces close for chunked/Expect; malformed chunked framing replies 400 and closes. 146 tests green (+18). Verified live through the MITM proxy: a chunked upload reached httpbin with the body intact and reframed to Content-Length (no Transfer-Encoding), the client got 100 Continue, two chunked POSTs reused one TCP connection, and a raw Content-Length+Transfer-Encoding request got 400. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Kestrel engine: incremental SSE streaming with capped tee (Slice 6) Forward text/event-stream responses to the client piece-by-piece (re-framed as HTTP/1.1 chunked) instead of buffering the whole body. Buffering withheld every event until the stream ended and hung the engine on unbounded streams. - UpstreamForwarder.ForwardAsync returns an OriginResponse: buffered for normal responses, or a live body stream for text/event-stream (message stays open). - StreamingResponseWriter pumps origin->client one chunk per read (flushed so events arrive live), accumulating a capped copy (4 MiB, reusing BodyModeResolver.DefaultInMemoryLimitBytes); beyond the cap accumulation is dropped but relaying continues, so the engine never hangs or OOMs. - PluginPipeline.RunStreamingResponseAsync runs the body pump between BeforeResponse and AfterResponse, exposing the accumulated body to read-only AfterResponse inspectors (OpenAITelemetry/OpenAIUsageDebugging) for parity. - Keep-alive preserved (chunked is self-delimiting). Tests: +12 (StreamingResponseWriter chunk framing/accumulation/cap, forwarder streaming detection). 158 green. Live-verified: local finite SSE streamed ~0.5s per event (not buffered), Transfer-Encoding: chunked, Connection: keep-alive, connection reused across two streamed responses, non-watched passthrough intact. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 4 Slice 7: process filtering at CONNECT (--watch-pids/--watch-process-names) Implements the last functional parity gap with the Titanium engine: restricting MITM to specific processes via --watch-pids / --watch-process-names. Matches Titanium semantics exactly — filtering applies ONLY at the CONNECT decision (IsProxiedProcess in ProxyEngine.cs), so plain HTTP stays unfiltered (documented parity quirk). - ProcessFilter resolves the client's PID from its source port and matches against WatchPids then WatchProcessNames (ordinal/case-sensitive HashSet to mirror Titanium's default Contains comparer). No filter configured -> watch all (IsEmpty short-circuit, zero overhead). - ConnectionProcessResolver shells out per-platform: lsof -i on Unix, netstat -ano on Windows. LsofParser/NetstatParser are pure, fully unit-tested partial classes (GeneratedRegex). Chosen over Windows P/Invoke (GetExtendedTcpTable) for testability and boring-tech symmetry. NOTE: the Windows netstat path is parser-tested but not live-verifiable on macOS. - Wired into ProxyConnectionHandler.HandleConnectAsync (after the h2-only check): unresolved/non-matching process -> blind-tunnel (never MITM'd). KestrelProxyEngine constructs the filter from configuration.WatchPids/WatchProcessNames. - Updated the CONNECT-flow doc diagram. Tests: +21 Kestrel (12 ProcessFilter + 4 Lsof + 5 Netstat) -> 179 green (33 Abstractions + 38 Titanium + 108 Kestrel). Live-verified on macOS: matching process name -> MITM intercept + req-log; non-matching -> blind tunnel (real cert, no -k, zero req-log lines). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 5 Slice 5a: persistent certificate authority (disk root + leaf cache) The Kestrel engine previously minted a brand-new in-memory root CA on every start, so the OS would distrust it after each restart. This makes the root (and per-host leaves) persistent on disk, mirroring the Titanium engine's CertificateDiskCache layout exactly: root at <configDir>/rootCert.pfx (overridable via DEV_PROXY_CERT_PATH), leaves at <configDir>/crts/<host>.pfx, both PKCS#12 with an empty password. configDir matches Titanium's resolution (ApplicationData/dev-proxy on mac/Linux, the executable dir on Windows). EUREKA: because the format + path match, the persistent CA loads a root that the Titanium engine ALREADY created and the OS ALREADY trusts — existing users get interception with zero new trust code (verified live: curl WITHOUT -k against a watched HTTPS host returned 200 + full MITM req-log). - LoadOrCreateRoot: load + validate (isCA, has private key, not expired) the on-disk root; regenerate + re-save (and purge the now-stale leaf cache) if absent/invalid/ expired. Per decision #5 there is no cross-version compatibility contract. - Leaves are minted on demand, cached in memory + on disk, and reused across restarts. Leaf validity is clamped to the issuer's window so reusing a near-expiry root can't produce a leaf that outlives it (CertificateRequest.Create rejects that). Filename- unsafe hosts (IPv6 ':', wildcard '*', '/') are sanitized for the disk cache key. - Disk writes are best-effort: an unwritable location degrades to in-memory-only for the run instead of failing interception. - KestrelProxyEngine now builds the CA via CertificateAuthority.CreateDefault(logger). OS-trust install (mac keychain / Windows root store) + the first-run prompt remain Slice 5b (host-side, reusing the existing trust helpers). This slice only makes the root persistent. Tests: +12 Kestrel (CertificateAuthorityTests: first-run create+persist, reuse across instances, invalid/expired/non-CA regeneration, stale-leaf purge, leaf mint+sign+ persist+reuse, leaf-not-outliving-root clamp, IP-literal SAN, filename sanitization) -> 191 green (33 + 38 + 120). Live-verified on macOS: existing root reused (fingerprint unchanged, "Loaded persisted root certificate"), leaf cached + reused across restarts, MITM works without -k. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add OS-trust install + first-run wiring for the Kestrel engine (Slice 5b) Wire root-certificate OS trust for the Kestrel engine via a new IRootCertificateTrust abstraction (DevProxy.Abstractions/Proxy), implemented host-side by RootCertificateTrust (DevProxy/Proxy). The engine mints/persists its root (Slice 5a) then calls EnsureTrusted; the host performs the platform install: - macOS: trust in the login keychain via MacCertificateHelper, gated by the first-run flow (InstallCert + NoFirstRun + HasRunFlag + Y/n prompt, auto-"y" under CI/redirected stdin) — mirrors Titanium's FirstRunSetup. - Windows: install the public-only root into CurrentUser/Root, idempotent by identity (CA5380 suppressed — installing the Dev Proxy root is the proxy's explicit, user-consented purpose). NOT live-verifiable on macOS. - Linux: warn to trust manually. The trust decision is factored into a pure RootTrustPolicy.Decide table in Abstractions so it's exhaustively unit-testable without platform/console I/O; the host impl is just the thin I/O around it (option X — new interface, leave Titanium's FirstRunSetup untouched for low blast radius). CertCommand (ensure/remove) already works for Kestrel mode unchanged thanks to Slice 5a's on-disk path parity. The Kestrel engine ctor takes an optional IRootCertificateTrust (null = no trust, e.g. tests); the DEV_PROXY_ENGINE=kestrel DI toggle registers and injects RootCertificateTrust. 207 tests green (49 Abstractions incl. +16 RootTrustPolicy, 38 Titanium, 120 Kestrel). Live-verified on macOS: with an already-trusted persisted root and --no-first-run, a watched HTTPS request returned 200 WITHOUT -k plus a full MITM req-log — trust wiring doesn't regress the existing-root happy path. Fresh-machine keychain trust is covered by the unit-tested decision table (not live-exercised to avoid keychain GUI pollution); Windows store install is parser/logic-only on macOS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Graceful-teardown audit: DRY connection-close classifier (Slice 5c) Extract a single ConnectionTeardown.IsExpected(exception) classifier and use it at every client/origin read/copy/write boundary, replacing the per-site hardcoded `ex is IOException or OperationCanceledException or ...` lists. An exception is "expected" (treat as EOF/close, don't log as error) when it's a ConnectionResetException, ConnectionAbortedException, OperationCanceledException, IOException, or SocketException — the normal result of a peer disconnecting, resetting, or the connection being cancelled mid-exchange. Boundaries updated to the shared classifier: - ProxyConnectionHandler.OnConnectedAsync outer catch (keep-alive read, body read, response write, streaming pump, blind tunnel) — now also covers SocketException/ConnectionAbortedException. - StreamRelay bidirectional-copy teardown. - WriteErrorAsync best-effort write to a possibly-gone client. Fixes the one genuine noise gap: the WebSocket relay catch logged a client disconnect mid-handshake / verbatim-101-write as an ERROR. It now logs at Debug when IsExpected and reserves Error for genuine faults. 216 tests green (49 Abstractions, 38 Titanium, 129 Kestrel; +9 ConnectionTeardown covering OCE/TaskCanceled/IOException/SocketException/ConnectionReset/ ConnectionAborted true and InvalidOperation/NullReference/unrelated false). Live-verified on macOS: aborting curl mid-stream (httpbin /drip, --max-time 1) produced ZERO error/exception log lines and the engine kept serving (a following request returned 200). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden CONNECT authority parsing: IPv6, ports, malformed targets (Slice 5d) Replace the naive "split on the last colon" SplitHostPort with a pure, explicit ConnectAuthorityParser that correctly handles: - IPv6 literals: [::1] and [2001:db8::1]:8443 — the bare host (no brackets) is used for socket connect / cert minting, and a UrlHost property re-adds brackets for composing the absolute URL (https://[::1]:8443/path). - Explicit ports, including validation (reject port 0 / >65535 / non-numeric / empty). - Malformed targets: empty host, unterminated bracket, junk after ], bare (unbracketed) IPv6, invalid host names — all rejected. The authority is now parsed + validated BEFORE the proxy writes "200 Connection Established", so a malformed CONNECT is refused with a 400 instead of establishing an unusable tunnel. The decrypted-tunnel URL is composed from UrlHost so IPv6 targets produce valid absolute URLs. 237 tests green (49 Abstractions, 38 Titanium, 150 Kestrel; +21 parser tests covering reg-name/IPv4/punycode/IPv6 with and without ports plus 15 malformed cases). Live-verified on macOS: normal + explicit-:443 watched MITM return 200 (no -k); malformed CONNECT targets (port 0, bare IPv6 ::1:443, unterminated [::1) each return 400 Bad Request while a valid target returns 200 Connection Established; zero error-log noise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Slice 5e: --port 0 actual-port logging + DRY host extraction Log the OS-assigned port (not the configured 0) for the Kestrel engine by reading the bound ListenOptions endpoint after StartAsync, so users running with --port 0 can connect. Extract the duplicated urlsToWatch host-derivation logic into a shared WatchedHostExtractor.ToHostRegex in DevProxy.Abstractions, used by both the Titanium ProxyEngine.LoadHostNamesFromUrls and the Kestrel HostWatchList. One implementation, one test suite (11 new tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 6 slice: engine-agnostic system-proxy on/off (no Titanium) Re-implement turning the OS system-proxy on and off without Titanium, behind a new ISystemProxyManager abstraction (Abstractions) so the Kestrel engine can use it without referencing the host (mirrors IRootCertificateTrust): - SystemProxyManager (host): Windows via WinINET (registry ProxyServer/ProxyEnable + InternetSetOption settings-changed/refresh broadcast), macOS via the existing toggle-proxy.sh, Linux warns. DllImport (not LibraryImport) to avoid enabling AllowUnsafeBlocks project-wide. - SystemProxyAddress (Abstractions, pure): normalizes wildcard/empty bind addresses to loopback for the client-facing proxy address; unit-tested. - KestrelProxyEngine: enable on start (using the actually-bound port, matters for --port 0), disable on stop. Previously the Kestrel engine set no system proxy at all — a parity gap. - StopCommand --force crash-cleanup now uses the shared manager, so a force-killed daemon restores the system proxy on Windows too (was macOS-only before). 259 tests green (+11 SystemProxyAddress). Windows WinINET path is parser/logic-tested but needs verification on a Windows host (cannot run on macOS). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Phase 6 slice: replace Titanium RunTime with System.OperatingSystem Swap the Titanium RunTime OS-check helper for the .NET built-in equivalents (RunTime.IsWindows → OperatingSystem.IsWindows(), RunTime.IsMac → OperatingSystem.IsMacOS()) in ProxyEngine and CertCommand, and drop the now-unused `using Titanium.Web.Proxy.Helpers;` import from both. Strictly equivalent behavior; reduces Titanium API surface ahead of the cut-over. Left untouched (intentionally): - CertificateDiskCache.RunTime.IsUwpOnWindows — no built-in equivalent, and the file (a Titanium ICertificateCache impl) is deleted at cut-over anyway. - The Titanium.Web.Proxy.* log filters — still suppress real Titanium engine logs while it remains the default engine; their removal belongs in the cut-over commit. 259 tests green; Titanium engine boot-smoked clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add hermetic Kestrel parity-test gate (controllable rows) Build the DevProxy.Parity.Tests project that boots the real KestrelProxyEngine on a random localhost port against a deterministic FakeOrigin and asserts the observable proxy contract for every controllable parity-matrix row: - plain HTTP: GET/POST (small/large), large download, status relay, 204 empty-body framing, request-header passthrough - chunked request body reframing + SSE chunked response streaming - keep-alive reuse + Connection: close - plugin Respond() mocking short-circuit (origin never contacted) - Content-Length + Transfer-Encoding smuggling -> 400 guard 15 hermetic rows green. External/OS rows (HTTPS real-cert MITM, h2/gRPC, WebSocket, system-proxy, process filter) remain the scripted live pass and are already slice-verified; see files/parity-results.md for the full matrix. The harness boots only the Kestrel engine (the Titanium engine's process-global static ProxyServer + IServer make in-process co-hosting flaky, and it is deleted at cut-over); it asserts the reference behavior Titanium already exhibits. Total: 274 tests green (71 Abstractions + 38 Titanium + 150 Kestrel + 15 Parity). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Hard cut-over to the Kestrel proxy engine; remove Titanium Make the ASP.NET Core Kestrel engine the sole proxy engine and remove the Unobtanium/Titanium.Web.Proxy adapter, dependency, and dev toggle in one change. - Remove the DEV_PROXY_ENGINE toggle; AddProxyEngine always wires Kestrel, the shared CertificateAuthority, IRootCertificateTrust, and ISystemProxyManager. - Introduce a single shared CertificateAuthority DI singleton (Kestrel AddKestrelCertificateAuthority) with the root X509Certificate2 derived from it; the engine, CertCommand, ProxyController, and EntraMockResponsePlugin all resolve the same root cert. The engine no longer self-creates/disposes its CA. - Add Trust/Untrust to IRootCertificateTrust and implement them host-side (mac keychain / Windows root store / Linux warn); CertCommand uses them. - Delete ProxyEngine, CertificateDiskCache, the DevProxy.Proxy.Titanium and DevProxy.Proxy.Titanium.Tests projects, the Unobtanium PackageReference, the Titanium.Web.Proxy.* log filters, and the THIRD PARTY NOTICES Unobtanium entry. - Update docs (CONTRIBUTING, copilot-instructions) and stale comments to Kestrel. Build 0/0; 236 tests green (71 Abstractions + 150 Kestrel + 15 Parity). Live-verified on macOS: watched HTTPS MITM without -k + req-log, cert-download API, and `cert ensure` trust path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix detached-daemon state registration and bound graceful-shutdown drain Two cut-over regressions surfaced by the macOS system-proxy live test: 1. Detached mode (`--detach`) was broken. The deleted Titanium ProxyEngine wrote the initial ProxyInstanceState (gated on IsInternalDaemon); the Kestrel engine never did, so the daemon orphaned, the parent's readiness poll timed out, and the system proxy was left on. The host's UpdateStateWithApiUrlAsync only updated an existing state file, so it silently no-opped. - KestrelProxyEngine now publishes its actually-bound port back to the shared IProxyConfiguration after binding (resolves --port 0 to the OS-assigned port). - DevProxyCommand.WriteDaemonStateAsync writes the full ProxyInstanceState (Pid, ApiUrl, LogFile, ConfigFile, resolved Port, AsSystemProxy) once the engine has published its port, satisfying the parent's `Port > 0` readiness contract used by `devproxy stop` and `devproxy status`. 2. Graceful shutdown stalled after real traffic. The engine awaited app.StopAsync(CancellationToken.None), which waited unbounded for lingering client keep-alive connections to drain — fast when idle, but >10s after traffic, making `devproxy stop` falsely report "did not stop in time". The drain is now bounded (5s) so remaining connections are force-closed. Adds EnginePortPublishingTests regression coverage (verified failing without the port write-back). 237 tests green. Live-verified on macOS: `--detach` registers the daemon, `devproxy status`/`stop` find it, and the system proxy is restored on both graceful API stop and SIGTERM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename Parity.Tests to Integration.Tests post-cut-over With Titanium removed there is no second engine to compare against, so the "parity" framing no longer applies. The suite is now a black-box integration test that boots the Kestrel engine over a real socket against a FakeOrigin. Renamed the project, csproj, namespaces, *ParityTests classes/files, and the solution entry (project GUID unchanged); reworded "parity" comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add per-plugin integration tests: harness infra + behavior/manipulation/mocking plugins Adds a CapturingLoggerFactory (collects engine-emitted RequestLogs), an inline PluginConfig section builder, and extends KestrelProxyHarness to accept a logger factory and expose its urls-to-watch set. Covers 9 hermetic, HTTP-observable plugins end-to-end through the Kestrel engine: GenericRandomError, Latency, RateLimiting, RetryAfter, Rewrite, MockResponse, Auth (reject + allow), and GraphRandomError. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CrudApi + MockRequest integration tests with InitializeAsync infra Adds PluginTestHost (a minimal DI ServiceProvider supplying HttpClient/ILogger<>/ IProxyConfiguration so plugins whose InitializeAsync builds file loaders can run in-process), TestDefaults (shared HttpClient), a FakeOrigin request recorder + /json endpoint, and a settable ConfigFile on TestProxyConfiguration. Mirrors the host's runtime-only package assets in the test csproj so plugin runtime dependencies (OpenIdConnect, JWT, OpenApi, OpenTelemetry, EF Sqlite, Newtonsoft, Azure.Identity) resolve. Covers MockRequestPlugin (outbound fire) and CrudApiPlugin (in-memory REST served through the engine, origin never contacted). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add guidance plugin integration tests (7 hermetic plugins) Adds TestExchange, which constructs the engine's real canonical session/request/ response types so guidance plugins gated on a fixed upstream host (graph.microsoft.com) can be driven at their hooks with production-fidelity inputs the loopback origin can't impersonate. Covers GraphBetaSupport, GraphClientRequestId, GraphSdk, ODataPaging, ODSPSearch, GraphConnector, and CachingGuidance, asserting the advisory log each emits. GraphSelectGuidancePlugin is documented as non-hermetic (needs the Graph metadata DB) and deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add reporting plugin integration tests (UrlDiscovery, ExecutionSummary, MinimalPermissions) Drives reporter plugins through AfterRecordingStopAsync exactly as the host's recording controller does: feeds engine-shaped RequestLogs (TestExchange.AsRequestLog) into RecordingArgs with a seeded Reports GlobalData slot, then asserts the structured report stored under the plugin name. MinimalPermissions runs against a temp OpenAPI spec fixture to verify offline permission analysis. GraphMinimalPermissions(+Guidance), MinimalCsom, and ApiCenter reporters are documented as live-backend (Bucket 3) and deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add generation plugin integration tests (HAR, mock, .http, OpenAPI, TypeSpec) Drives each generator through AfterRecordingStopAsync with an InterceptedResponse log, asserting the stored report and the generated artifact. Generators write to the process CWD, so tests redirect it to a temp dir; assembly test parallelization is disabled to keep that global swap safe. A DisabledLanguageModelClient keeps the OpenAPI/TypeSpec generators hermetic (LM is cosmetic-only; they fall back to deterministic generation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add process-level smoke test for config-driven plugin loading Spawns the real DevProxy host with a devproxyrc.json + mocks file, waits for the Kestrel engine to report its bound port (--port 0), routes an HTTP request through it as an explicit proxy, and asserts MockResponsePlugin short-circuits with the configured body. Proves end-to-end that config-driven plugin discovery + the engine wire survive the Titanium -> Kestrel cut-over. Uses a plain-HTTP watched URL so no CA trust is needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove unwired body-mode subsystem; fix misleading body docs The BodyModeResolver + BodyMode/BodyCapabilities/BodyContext/BodyDirection types (319 LOC, 12 tests) were a spike-era design that the Kestrel engine never wired: Resolve() is never called in production, no plugin declares BodyCapabilities, and IHttpMessage exposes no capability property. The engine uses a fixed 4 MiB in-memory inspection cap instead. Worse, the IHttpMessage doc claimed body buffering was 'driven by plugin BodyCapabilities', misleading plugin authors toward an API that does not exist. - Delete BodyModeResolver.cs, BodyHandling.cs, and BodyModeResolverTests.cs. - Inline the one used constant as ProxyConnectionHandler.InMemoryInspectionCapBytes. - Correct the IHttpMessage body-visibility doc to describe actual behavior. - Drop stale 'every engine adapter' language (abandoned multi-engine design) from ForwardingInvariants and HeaderCollection docs. The design is preserved in git and can be restored from this parent commit if/when capability-based body handling is actually needed. A code comment + plan.md follow-up track the genuine, separate unbounded-buffered-body memory risk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename IProxyLogger.cs to LoggingContext.cs to match contents The file's 'I' prefix implied an interface, but it contains no interface — only the MessageType enum and the LoggingContext / StdioLoggingContext classes. Rename to match its primary type and theme. (StdioEvents.cs was left as-is: it correctly groups the stdio event-arg family, mirroring the existing ProxyEvents.cs convention.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add WebSocketMockResponsePlugin (reactive WS frame mocking) Net-new capability unlocked by the Kestrel engine: mock WebSocket frames end-to-end. The engine owns transport (101 handshake + framing via the framework's WebSocket.CreateFromStream); the plugin owns behavior (on-connect scripted sends + match incoming client messages by equals/contains/regex/json and reply with mapped messages, plus optional close). A watched WS upgrade is dispatched to the plugin responder (instead of the origin relay) when the plugin calls IProxySession.HandleWebSocket(handler), leaving the session in the Watched phase. The plugin must NOT set ResponseState.HasBeenSet, which would short-circuit to the mocked-HTTP ResponseWriter path before the WS dispatch branch runs. DRY scheme-normalization footgun fix: the engine reports an intercepted WS upgrade with an http(s) scheme, but authors naturally write wss://host/* in urlsToWatch and mock URLs. A shared ProxyUtils.NormalizeWebSocketScheme normalizes wss->https / ws->http and is applied at both the watch-regex build site (PluginServiceExtensions.ConvertToRegex) and the plugin's mock-URL match, so either form works and a wss:// watch entry is no longer silently relayed to origin. Tests: responder loopback unit tests, matcher unit tests, 2 e2e ClientWebSocket-over-wss-MITM integration tests, and NormalizeWebSocketScheme unit tests. Full suite green (284). Live-verified through the host binary with urlsToWatch ["wss://ws.example.test/*"]: OPEN -> welcome (onConnect) -> pong (reactive ping->pong); proxy logged "Mocking WebSocket" + "WebSocket mock established". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align WebSocket mock matching with HTTP/stdio mock vocabulary The WebSocketMockResponsePlugin invented a `matchType: equals|contains|regex| json` enum, diverging from every other Dev Proxy mock plugin, which matches with the `bodyFragment` (case-insensitive contains) + `bodyRegex` field pair. Replace the enum with the conventional vocabulary so authors familiar with the HTTP/stdio mocks reach for the same fields: - `bodyFragment` — OrdinalIgnoreCase substring - `bodyRegex` — regular expression (1s timeout) - `bodyJson` — structural JSON equality (JsonNode.DeepEquals); accepts a JSON value directly in the mocks file, matching how the HTTP response body accepts object/array/string Precedence when more than one is set: bodyJson > bodyRegex > bodyFragment (mirrors stdio's "bodyRegex wins over bodyFragment"). Null match or no criteria remains a catch-all. The genuinely WS-specific structure (url-keyed mock, onConnect pushes, reactive rules, multi-message responses) is unchanged — only the match expression is aligned. Updates the mocks-file schema and matcher unit tests accordingly. Full suite green (283). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore interactive console (hotkeys + --record) dropped in Kestrel cut-over The hard cut-over to the Kestrel engine deleted ProxyEngine.cs, which owned the interactive key loop (r/s/c/w) and the --record auto-start. KestrelProxyEngine never re-implemented them, so the documented keyboard shortcuts (technical-reference/keyboard-shortcuts.md) stopped working and `devproxy --record` no longer started recording at launch. Restore both as a host-side InteractiveConsoleService (BackgroundService), keeping the Kestrel engine headless: the engine depends only on the proxy abstractions and must not reach into host concerns like IProxyStateController. The service waits for ApplicationStarted so the banner prints after the engine's startup logs, honors --record, prints hotkeys (text mode) or API instructions (JSON mode), and dispatches keys via a testable ConsoleHotkeyHandler over an ISystemConsole seam. Adds DevProxy.Tests (the host had no unit test project) with 10 tests covering key dispatch and both banners. Full suite green (293 tests). Live-verified: --record starts recording at launch, JSON banner prints, proxy serves 200 through the new service. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Windows verification checklist for the Kestrel engine The Windows-specific runtime paths (WinINET system proxy, current-user root-store cert trust, netstat-based process filter) are unit-tested but were never live-verified on a real Windows host after the Kestrel cut-over. Add a self-contained, clone-and-run checklist covering build/tests, cert trust, system proxy on/off, detached daemon lifecycle, process filter, the restored interactive console, and a core proxy + plugin smoke pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve Content-Length on HEAD responses A HEAD response carries the Content-Length a GET would return but no body (RFC 9110 §9.3.2). The engine was recomputing Content-Length from the (empty) HEAD body, so clients saw Content-Length: 0 instead of the real resource size. ResponseWriter and UpstreamForwarder are now method-aware: HEAD preserves the origin's Content-Length, never takes the streaming path, and never writes a body. * Fix API URL banner showing port 0 with --api-port 0 Resolve the OS-assigned API port from the bound server address before printing the JSON-mode banner so the curl commands reference the actual port instead of the literal 0. * Return 504 on upstream timeout instead of silently dropping the connection The forward catch excluded all OperationCanceledException so client cancellation would bubble up as a silent teardown. But an HttpClient request timeout also throws TaskCanceledException (an OperationCanceledException), so a stalled origin was misclassified as a client disconnect: the client got a dropped/reset connection after the default 100s timeout instead of a gateway error. Add a pure UpstreamFailure.Classify helper (mirroring ConnectionTeardown) that tells the two apart by the connection token: OperationCanceledException with the token NOT cancelled is an upstream timeout -> 504 Gateway Timeout; with the token cancelled it is a genuine client teardown -> silent; anything else (TLS handshake failure on an invalid upstream cert, DNS, connection refused) -> 502 Bad Gateway. Wire it into the single forward catch in ProxyConnectionHandler and add the 504 reason phrase. Closes the "upstream timeout" and "invalid upstream cert" parity rows in code with 7 unit tests on the classifier. Full suite green (306 tests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Clear CA2201 warning in ConnectionTeardownTests The test deliberately constructs a NullReferenceException to prove the teardown classifier does not mask a latent bug as an expected connection close. CA2201 flags constructing reserved exception types, so scope a justified suppression to that single line instead of weakening the test. Solution now builds with 0 warnings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Bump version to 4.0.0 (breaking: Kestrel engine + canonical plugin API) This is a major version bump because the migration from Titanium/Unobtanium to the Kestrel-based proxy engine introduces breaking changes: - Plugin API surface moved from Titanium types (SessionEventArgs/Request/ Response/HttpHeader) to the canonical model (IProxySession/IHttpRequest/ IHttpResponse/HeaderCollection). Third-party plugins must recompile. - Behavioral changes around HTTP/2 handling (h2-only/gRPC blind-tunnel). Copies schemas/v3.1.0 -> schemas/v4.0.0 verbatim (41 files) and updates <Version> in all four shipping projects (incl. DevProxy.Proxy.Kestrel), schema URLs in config JSON + MockResponsePlugin, installer/script/Dockerfile version strings, and refreshes packages.lock.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
#1740) * Add WebSocket message capture to HAR generator Replace raw byte WebSocket relay with frame-aware relay using WebSocket.CreateFromStream on both client and origin sides. Each relayed message is captured as a WebSocketMessageRecord and stored on the session. The HAR generator now detects WebSocket upgrade entries and embeds captured messages using the _resourceType and _webSocketMessages custom HAR fields, following the Chrome DevTools / mitmproxy convention. Changes: - Add WebSocketMessageRecord abstraction (direction, type, data, timestamp) - Add IProxySession.WebSocketMessages property - Convert WebSocketRelay from raw byte splice to message-level relay with PrefixedStream for leftover handshake bytes - Add HarWebSocketMessage model and _resourceType/_webSocketMessages fields to HarEntry - Update HarGeneratorPlugin to populate WebSocket fields - Update WebSocketRelayTests for frame-level relay and message capture verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Add per-message WebSocket interception with origin passthrough Convert WebSocket mocking from full-connection takeover to per-message interception: the proxy connects to the origin and relays traffic, but each client message is offered to the interceptor first. Matched messages get mock responses; unmatched ones pass through to the origin. When the origin is unreachable but an interceptor is registered, the proxy falls back to mock-only mode — same as the old HandleWebSocket behavior. - Add InterceptWebSocketMessages to IProxySession with interceptor and onConnected callbacks - Split WebSocket relay into client-to-origin and origin-to-client tasks with thread-safe client sends via SemaphoreSlim - Add InterceptorClientConnection wrapper with send serialization and message capture for HAR - Convert WebSocketMockResponsePlugin from HandleWebSocket to InterceptWebSocketMessages - Add origin-unreachable fallback in ProxyConnectionHandler Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address PR review: HAR opcodes, capture scoping, docs, and tests - Map WebSocketMessageType to RFC 6455 opcodes (1=text, 2=binary, 8=close) in HAR output instead of the framework enum values (0/1/2) - Only capture WebSocket messages for watched requests to avoid unbounded memory growth on long-lived/high-volume sockets - Capture interceptor/onConnected responses in the interceptor-only fallback so mock-only HAR output includes receive entries - Update IProxySession.WebSocketMessages doc to reflect interception and mock-only fallback capture - Add HarGenerator_WritesWebSocketMessages test asserting correct type/time/opcode/data formatting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address PR review: forward non-101 leftover bytes, record close frames - Forward bytes read past the response head when the origin declines the WebSocket upgrade (non-101), so clients see the full response body - Record interceptor-initiated closes (InterceptorClientConnection and CapturingWebSocketConnection) as Receive/Close, matching how the relay loops record origin->client closes - Record the client->proxy close in the interceptor-only fallback loop as Send/Close so WebSocketMessages/HAR output is complete Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical protocol, forwarding, schema, and packaging issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request migrates Dev Proxy to a Kestrel-based HTTP and WebSocket runtime while introducing v4 configuration schemas and updated packaging.
Changes:
- Adds Kestrel proxying, TLS interception, forwarding, streaming, and WebSocket support.
- Introduces canonical HTTP abstractions with unit and integration coverage.
- Updates plugins, schemas, installers, containers, and release metadata.
File summaries
| File | Reviewed area |
|---|---|
THIRD PARTY NOTICES |
Removes obsolete proxy dependency notices. |
scripts/version.ps1 |
Updates release version and schema references. |
scripts/local-setup.ps1 |
Updates local setup versioning. |
scripts/Dockerfile_local |
Updates local container versioning. |
schemas/v4.0.0/websocketmockresponseplugin.schema.json |
Defines the v4 WebSocket mock response contract. |
schemas/v4.0.0/typespecgeneratorplugin.schema.json |
Defines the v4 TypeSpec generator configuration. |
schemas/v4.0.0/rewriteplugin.schema.json |
Defines the v4 rewrite plugin configuration. |
schemas/v4.0.0/rewriteplugin.rewritesfile.schema.json |
Defines the v4 rewrite file format. |
schemas/v4.0.0/ratelimitingplugin.schema.json |
Defines the v4 rate-limiting configuration. |
schemas/v4.0.0/ratelimitingplugin.customresponsefile.schema.json |
Defines rate-limit custom responses. |
schemas/v4.0.0/openapispecgeneratorplugin.schema.json |
Defines OpenAPI generator settings. |
schemas/v4.0.0/openaitelemetryplugin.schema.json |
Defines OpenTelemetry settings. |
schemas/v4.0.0/openaitelemetryplugin.pricesfile.schema.json |
Defines OpenTelemetry pricing data. |
schemas/v4.0.0/mockstdioresponseplugin.schema.json |
Defines stdio mock response settings. |
schemas/v4.0.0/mockstdioresponseplugin.mocksfile.schema.json |
Defines stdio mock files. |
schemas/v4.0.0/mockresponseplugin.schema.json |
Defines mock response settings. |
schemas/v4.0.0/mockresponseplugin.mocksfile.schema.json |
Defines mock response files. |
schemas/v4.0.0/mockrequestplugin.schema.json |
Defines mock request settings. |
schemas/v4.0.0/mockrequestplugin.mockfile.schema.json |
Defines mock request files. |
schemas/v4.0.0/minimalpermissionsplugin.schema.json |
Defines minimal permissions settings. |
schemas/v4.0.0/minimalpermissionsguidanceplugin.schema.json |
Defines permissions guidance settings. |
schemas/v4.0.0/minimalcsompermissionsplugin.schema.json |
Defines CSOM permissions settings. |
schemas/v4.0.0/minimalcsompermissions.types.schema.json |
Defines CSOM permission types. |
schemas/v4.0.0/latencyplugin.schema.json |
Defines latency plugin settings. |
schemas/v4.0.0/languagemodelratelimitingplugin.schema.json |
Defines language-model rate limiting. |
schemas/v4.0.0/languagemodelratelimitingplugin.customresponsefile.schema.json |
Defines language-model custom responses. |
schemas/v4.0.0/languagemodelfailureplugin.schema.json |
Defines language-model failure settings. |
schemas/v4.0.0/httpfilegeneratorplugin.schema.json |
Defines HTTP file generator settings. |
schemas/v4.0.0/hargeneratorplugin.schema.json |
Defines HAR generator settings. |
schemas/v4.0.0/graphrandomerrorplugin.schema.json |
Defines Graph random-error settings. |
schemas/v4.0.0/graphminimalpermissionsplugin.schema.json |
Defines Graph permissions settings. |
schemas/v4.0.0/graphminimalpermissionsguidanceplugin.schema.json |
Defines Graph permissions guidance. |
schemas/v4.0.0/genericrandomerrorplugin.schema.json |
Defines generic random-error settings. |
schemas/v4.0.0/genericrandomerrorplugin.errorsfile.schema.json |
Defines generic error files. |
schemas/v4.0.0/executionsummaryplugin.schema.json |
Defines execution summary settings. |
schemas/v4.0.0/devtoolsplugin.schema.json |
Defines DevTools settings. |
schemas/v4.0.0/crudapiplugin.schema.json |
Defines CRUD API settings. |
schemas/v4.0.0/cachingguidanceplugin.schema.json |
Defines caching guidance settings. |
schemas/v4.0.0/apicenterproductionversionplugin.schema.json |
Defines API Center production-version settings. |
schemas/v4.0.0/apicenteronboardingplugin.schema.json |
Defines API Center onboarding settings. |
schemas/v4.0.0/apicenterminimalpermissionsplugin.schema.json |
Defines API Center permissions settings. |
schemas/v3.1.0/websocketmockresponseplugin.schema.json |
Retains the legacy WebSocket mock schema. |
install.iss |
Updates the Windows installer. |
install-beta.iss |
Updates the beta Windows installer. |
Dockerfile_beta |
Updates beta container packaging. |
Dockerfile |
Updates production container packaging. |
DevProxy/Proxy/SystemConsole.cs |
Implements system console behavior. |
DevProxy/Proxy/ProxyStateController.cs |
Manages proxy state transitions. |
DevProxy/Proxy/ISystemConsole.cs |
Defines the system console abstraction. |
DevProxy/Proxy/ConsoleHotkeyHandler.cs |
Handles console hotkeys. |
DevProxy/Proxy/ConfigFileWatcher.cs |
Watches configuration files. |
DevProxy/Program.cs |
Updates application startup. |
DevProxy/Plugins/PluginServiceExtensions.cs |
Registers plugin services. |
DevProxy/packages.lock.json |
Locks application dependencies. |
DevProxy/Logging/ProxyConsoleFormatter.cs |
Formats proxy console logs. |
DevProxy/Extensions/IServiceCollectionExtensions.cs |
Registers proxy runtime services. |
DevProxy/Extensions/ILoggingBuilderExtensions.cs |
Configures application logging. |
DevProxy/devproxyrc.json |
Updates default proxy configuration. |
DevProxy/DevProxy.csproj |
Configures project assets and packaging. |
DevProxy/devproxy-errors.json |
Updates default error responses. |
DevProxy/config/spo-csom-types.json |
Updates bundled CSOM configuration. |
DevProxy/config/microsoft-graph.json |
Updates Microsoft Graph configuration. |
DevProxy/config/microsoft-graph-rate-limiting.json |
Updates Graph rate-limiting configuration. |
DevProxy/config/m365.json |
Updates Microsoft 365 configuration. |
DevProxy/config/m365-mocks.json |
Updates Microsoft 365 mock configuration. |
DevProxy/Commands/StopCommand.cs |
Updates the stop command. |
DevProxy/Commands/DevProxyCommand.cs |
Updates the main proxy command. |
DevProxy/Commands/CertCommand.cs |
Updates certificate commands. |
DevProxy/ApiControllers/ProxyController.cs |
Updates proxy API handling. |
DevProxy.Tests/Fakes.cs |
Provides application test fakes. |
DevProxy.Tests/DevProxy.Tests.csproj |
Configures application tests. |
DevProxy.Proxy.Kestrel/IServiceCollectionExtensions.cs |
Registers Kestrel proxy services. |
DevProxy.Proxy.Kestrel/Internal/UpstreamFailure.cs |
Represents upstream failures. |
DevProxy.Proxy.Kestrel/Internal/StreamRelay.cs |
Relays connection streams. |
DevProxy.Proxy.Kestrel/Internal/HostWatchList.cs |
Matches watched hosts and exclusions. |
DevProxy.Proxy.Kestrel/Internal/DuplexPipeStream.cs |
Adapts duplex pipes to streams. |
DevProxy.Proxy.Kestrel/Internal/ConnectionTeardown.cs |
Handles connection cleanup. |
DevProxy.Proxy.Kestrel/Http/MutableHttpResponse.cs |
Implements mutable HTTP responses. |
DevProxy.Proxy.Kestrel/Http/MutableHttpRequest.cs |
Implements mutable HTTP requests. |
DevProxy.Proxy.Kestrel/Http/MutableHttpMessage.cs |
Provides shared mutable HTTP behavior. |
DevProxy.Proxy.Kestrel/DevProxy.Proxy.Kestrel.csproj |
Configures the Kestrel project. |
DevProxy.Proxy.Kestrel.Tests/UpstreamFailureTests.cs |
Tests upstream failure handling. |
DevProxy.Proxy.Kestrel.Tests/TestSockets.cs |
Provides socket test helpers. |
DevProxy.Proxy.Kestrel.Tests/StreamRelayTests.cs |
Tests stream relaying. |
DevProxy.Proxy.Kestrel.Tests/ShouldKeepAliveTests.cs |
Tests connection persistence. |
DevProxy.Proxy.Kestrel.Tests/HostWatchListTests.cs |
Tests watched-host matching. |
DevProxy.Proxy.Kestrel.Tests/DevProxy.Proxy.Kestrel.Tests.csproj |
Configures Kestrel tests. |
DevProxy.Proxy.Kestrel.Tests/ConnectionTeardownTests.cs |
Tests connection teardown. |
DevProxy.Plugins/Utils/HttpUtils.cs |
Provides HTTP plugin utilities. |
DevProxy.Plugins/Utils/GraphUtils.cs |
Provides Graph plugin utilities. |
DevProxy.Plugins/Reporting/UrlDiscoveryPlugin.cs |
Updates URL discovery reporting. |
DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs |
Updates permissions reporting. |
DevProxy.Plugins/Reporting/MinimalPermissionsGuidancePlugin.cs |
Updates permissions guidance reporting. |
DevProxy.Plugins/Reporting/MinimalCsomPermissionsPlugin.cs |
Updates CSOM permissions reporting. |
DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs |
Updates Graph permissions reporting. |
DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePlugin.cs |
Updates Graph guidance reporting. |
DevProxy.Plugins/Reporting/ExecutionSummaryPlugin.cs |
Updates execution summary reporting. |
DevProxy.Plugins/Reporting/ApiCenterProductionVersionPlugin.cs |
Updates API Center version reporting. |
DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPlugin.cs |
Updates API Center permissions reporting. |
DevProxy.Plugins/packages.lock.json |
Locks plugin dependencies. |
DevProxy.Plugins/Models/Har.cs |
Defines HAR models. |
DevProxy.Plugins/Mocking/WebSocketMockResponsesLoader.cs |
Loads WebSocket mock responses. |
DevProxy.Plugins/Mocking/EntraMockResponsePlugin.cs |
Provides Entra mock responses. |
DevProxy.Plugins/Manipulation/RewritePlugin.cs |
Rewrites proxied requests. |
DevProxy.Plugins/Guidance/ODSPSearchGuidancePlugin.cs |
Provides OData guidance. |
DevProxy.Plugins/Guidance/GraphSelectGuidancePlugin.cs |
Provides Graph select guidance. |
DevProxy.Plugins/Guidance/GraphSdkGuidancePlugin.cs |
Provides Graph SDK guidance. |
DevProxy.Plugins/Guidance/GraphConnectorGuidancePlugin.cs |
Provides Graph connector guidance. |
DevProxy.Plugins/Guidance/GraphClientRequestIdGuidancePlugin.cs |
Provides client request ID guidance. |
DevProxy.Plugins/Guidance/GraphBetaSupportGuidancePlugin.cs |
Provides Graph beta guidance. |
DevProxy.Plugins/Guidance/CachingGuidancePlugin.cs |
Provides caching guidance. |
DevProxy.Plugins/Generation/MockGeneratorPlugin.cs |
Generates mock configurations. |
DevProxy.Plugins/Generation/HttpFileGeneratorPlugin.cs |
Generates HTTP files. |
DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs |
Extends OpenAPI document handling. |
DevProxy.Plugins/Extensions/ApiCenterExtensions.cs |
Extends API Center integration. |
DevProxy.Plugins/DevProxy.Plugins.csproj |
Configures the plugins project. |
DevProxy.Plugins/Behavior/LatencyPlugin.cs |
Implements latency behavior. |
DevProxy.Plugins/Behavior/LanguageModelFailurePlugin.cs |
Implements language-model failures. |
DevProxy.Integration.Tests/WebSocketMessageMatcherTests.cs |
Tests WebSocket message matching. |
DevProxy.Integration.Tests/TestProxyConfiguration.cs |
Defines integration proxy configuration. |
DevProxy.Integration.Tests/TestParallelization.cs |
Configures integration test parallelization. |
DevProxy.Integration.Tests/TestDefaults.cs |
Defines integration test defaults. |
DevProxy.Integration.Tests/StreamingAndConnectionIntegrationTests.cs |
Tests streaming and connections. |
DevProxy.Integration.Tests/PluginTestHost.cs |
Hosts plugins in integration tests. |
DevProxy.Integration.Tests/PluginConfig.cs |
Defines plugin test configuration. |
DevProxy.Integration.Tests/NetUtil.cs |
Provides network test utilities. |
DevProxy.Integration.Tests/MockShortCircuitPlugin.cs |
Provides a mock short-circuit plugin. |
DevProxy.Integration.Tests/MockingAndSmugglingIntegrationTests.cs |
Tests mocking and request handling. |
DevProxy.Integration.Tests/EnginePortPublishingTests.cs |
Tests engine port publishing. |
DevProxy.Integration.Tests/DisabledLanguageModelClient.cs |
Provides a disabled model client. |
DevProxy.Integration.Tests/DevProxy.Integration.Tests.csproj |
Configures integration tests. |
DevProxy.Integration.Tests/CapturingLoggerFactory.cs |
Captures integration test logs. |
DevProxy.Abstractions/Proxy/WatchedHostExtractor.cs |
Extracts hosts from watched URLs. |
DevProxy.Abstractions/Proxy/SystemProxyAddress.cs |
Formats system proxy addresses. |
DevProxy.Abstractions/Proxy/RootTrustPolicy.cs |
Defines root trust policy. |
DevProxy.Abstractions/Proxy/ProxyEvents.cs |
Defines proxy events. |
DevProxy.Abstractions/Proxy/LoggingContext.cs |
Defines proxy logging context. |
DevProxy.Abstractions/Proxy/ISystemProxyManager.cs |
Defines system proxy management. |
DevProxy.Abstractions/Proxy/IRootCertificateTrust.cs |
Defines root certificate trust. |
DevProxy.Abstractions/Proxy/Http/WebSocketMessageRecord.cs |
Represents WebSocket message records. |
DevProxy.Abstractions/Proxy/Http/IWebSocketConnection.cs |
Defines WebSocket connections. |
DevProxy.Abstractions/Proxy/Http/IHttpResponse.cs |
Defines HTTP responses. |
DevProxy.Abstractions/Proxy/Http/IHttpRequest.cs |
Defines HTTP requests. |
DevProxy.Abstractions/Proxy/Http/IHttpMessage.cs |
Defines shared HTTP messages. |
DevProxy.Abstractions/Proxy/Http/IHttpHeader.cs |
Defines HTTP headers. |
DevProxy.Abstractions/Proxy/Http/IHeaderCollection.cs |
Defines header collections. |
DevProxy.Abstractions/Proxy/Http/HttpHeader.cs |
Implements HTTP headers. |
DevProxy.Abstractions/Proxy/Http/HeaderCollection.cs |
Implements header collections. |
DevProxy.Abstractions/Proxy/Http/ForwardingInvariants.cs |
Defines forwarding invariants. |
DevProxy.Abstractions/Plugins/PluginEvents.cs |
Defines plugin events. |
DevProxy.Abstractions/packages.lock.json |
Locks abstraction dependencies. |
DevProxy.Abstractions/Extensions/FuncExtensions.cs |
Provides function extensions. |
DevProxy.Abstractions/DevProxy.Abstractions.csproj |
Configures the abstractions project. |
DevProxy.Abstractions.Tests/Utils/ProxyUtilsTests.cs |
Tests proxy utilities. |
DevProxy.Abstractions.Tests/Proxy/WatchedHostExtractorTests.cs |
Tests watched-host extraction. |
DevProxy.Abstractions.Tests/Proxy/SystemProxyAddressTests.cs |
Tests proxy address formatting. |
DevProxy.Abstractions.Tests/Proxy/Http/ForwardingInvariantsTests.cs |
Tests forwarding invariants. |
DevProxy.Abstractions.Tests/DevProxy.Abstractions.Tests.csproj |
Configures abstraction tests. |
CONTRIBUTING.md |
Updates contributor guidance. |
.github/copilot-instructions.md |
Updates repository instructions. |
Review details
Suppressed comments (11)
DevProxy.Abstractions/Proxy/SystemProxyAddress.cs:43
- This formatter emits
::1:portfor an IPv6 bind address. That is ambiguous as ahost:portvalue and is not a valid WindowsProxyServerregistry endpoint; IPv6 literals need brackets ([::1]:port).
DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs:31 - The default
$schemavalue is still pinned to v3.1.0, so WebSocket mock files generated or serialized with this configuration advertise an obsolete contract after the v4.0.0 schema was added. Update the default to the v4.0.0 schema URL.
DevProxy.Proxy.Kestrel/Internal/ProxyConnectionHandler.cs:442 - If the WebSocket origin cannot be connected to or closes before a handshake,
handshakeObservedstays false and this branch creates and processes a 502 response, but never writes it toclientStream. The client receives EOF instead of the generated HTTP error; write the processed response before closing the connection.
DevProxy.Proxy.Kestrel/Internal/WebSocketRelay.cs:409 - When an interceptor handles a client message, its mock response is sent first and records a
Receiveevent, then this code records the client'sSendevent. HAR/reporting consumers consequently see the response before the request that triggered it. Record the inbound message before invoking the interceptor so the queue follows wire order.
DevProxy.Proxy.Kestrel/Internal/WebSocketRelay.cs:222 - When a rewrite changes the authority, this raw handshake copies the original
Hostheader while usingtargetfor the request line. The origin can therefore receive a path for one authority with aHostfor another; omit the incoming Host and emit the target authority after rewrites.
schemas/v4.0.0/genericrandomerrorplugin.errorsfile.schema.json:61 - This v4 schema now rejects numeric and boolean response bodies, even though
GenericErrorResponseResponse.Bodyis dynamic and the v3.3 schema allowed those JSON values. Existing error files usingbody: 0orbody: falsewill fail validation after the version bump; restorenumberandbooleanin the type union.
schemas/v4.0.0/languagemodelratelimitingplugin.customresponsefile.schema.json:17 - This v4 custom-response schema rejects numeric and boolean bodies even though the language-model rate-limit loader uses the same dynamic
MockResponseResponse.Bodymodel and v3.3 accepted them. Restorenumberandbooleanso valid custom responses are not broken by the schema version change.
schemas/v4.0.0/mockrequestplugin.mockfile.schema.json:37 MockRequest.Bodyis dynamic, but the v4 schema narrows it to only object/string and removes array, number, and boolean values that v3.3 accepted. Valid mock requests with scalar or array JSON bodies will be rejected by schema validation; restore the full JSON-value union.
schemas/v4.0.0/mockrequestplugin.mockfile.schema.json:31- The v4 method enum drops
QUERY, which is present in the previous schema and is a supported method value in the mock model. Mock request files usingmethod: QUERYwill now fail validation; retain it in the enum.
schemas/v4.0.0/mockresponseplugin.mocksfile.schema.json:36 - The v4 method enum also drops
QUERY, so valid mock response files using that method can no longer pass schema validation even though the previous schema allowed it. KeepQUERYin the enum.
schemas/v4.0.0/mockstdioresponseplugin.mocksfile.schema.json:52 MockStdioResponseBody.Stderrhas the same scalar-value support asStdout, but this second type declaration also omitsnumberandboolean. Add those types here as well, otherwise valid stderr mocks remain rejected by v4 schema validation.
- Files reviewed: 222/237 changed files
- Comments generated: 21
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.