Skip to content

M1: SwiftNIO MITM proxy core#72

Open
kalil0321 wants to merge 5 commits into
mainfrom
claude/system-proxy-monitor-UEsLp
Open

M1: SwiftNIO MITM proxy core#72
kalil0321 wants to merge 5 commits into
mainfrom
claude/system-proxy-monitor-UEsLp

Conversation

@kalil0321
Copy link
Copy Markdown
Owner

@kalil0321 kalil0321 commented May 19, 2026

First slice of the macOS app: a pure-Swift MITM HTTP/HTTPS proxy built on SwiftNIO + NIOSSL + swift-certificates.

What's in

  • macos/ Swift Package targeting macOS 14+
  • ReverseAPIProxy library
    • CertificateAuthority — generates an in-memory P-256 root CA via swift-certificates
    • LeafCertificateFactory — mints per-host leaf certs on demand (DNS + IPv4/IPv6 SAN), caches them, returns NIOSSLCertificate/NIOSSLPrivateKey
    • TLSContextFactory — builds and caches NIOSSLContext per host
    • ProxyEngine — bootstraps a NIO server on 127.0.0.1:8888
    • ProxyHandler — handles plain HTTP forwarding and CONNECT-based TLS bumping with synchronous pipeline reconfiguration
    • UpstreamPump — opens upstream connections (TLS or plain), forwards the buffered request, returns the response
    • FlowBus / CapturedFlow — actor-backed async event stream for captured requests
  • rae-proxy CLI — generates a root CA, starts the proxy, prints captured flows live
  • XCTest target with initial CA tests

Out of scope (later milestones)

  • CA persistence + Keychain install (M2)
  • System proxy toggle (M2)
  • SwiftUI UI (M3)
  • HTTP/2, WebSockets, streaming bodies (M4)
  • Agent panel + Python sidecar (M5)
  • Signing, notarization, DMG (M6)

How to try (on macOS)

cd macos
swift run rae-proxy
# in another terminal:
curl -x http://127.0.0.1:8888 https://example.com --cacert /tmp/reverseapi-root.pem

Stacked PRs will follow on top of this branch.


Generated by Claude Code


Summary by cubic

Add a pure‑Swift MITM HTTP/HTTPS proxy core for macOS with TLS bumping and live flow capture. Ships the ReverseAPIProxy library and rae-proxy CLI, with fixes for IPv6 parsing, upstream disconnects, device system‑proxy capture, and removal of the macOS certificate trust prompt.

  • Bug Fixes

    • Fixed IPv6 default‑port parsing in HostPort (HTTP now defaults to 80); prevented hangs on mid‑response upstream disconnects; enforced a 32 MiB max request body with 413.
    • Bounded per‑subscriber buffering in FlowBus; added LRU cap to LeafCertificateFactory; deduplicated TLS context creation in TLSContextFactory; improved ProxyEngine.stop() cleanup.
    • rae-proxy: removed the certificate trust prompt and fixed system proxy application to capture device traffic.
  • Dependencies

    • Added NIOFoundationCompat to ReverseAPIProxy and restored the SwiftPM macOS build.

Written for commit a16b471. Summary will update on new commits. Review in cubic

Greptile Summary

This PR introduces the core SwiftNIO MITM proxy for the macOS app: a pure-Swift HTTP/HTTPS interceptor built on NIO + NIOSSL + swift-certificates, along with the rae-proxy CLI and a solid XCTest suite covering CA, LRU caching, HostPort parsing, FlowBus, and TLS context deduplication.

  • ReverseAPIProxy library ships CertificateAuthority + LeafCertificateFactory (LRU-capped per-host cert minting), TLSContextFactory (actor with inflight-task deduplication), ProxyHandler (CONNECT bump + plain HTTP forwarding, 32 MiB body cap), UpstreamPump (async request/response with a lock-guarded CheckedContinuation), and FlowBus (bounded async event stream with per-subscriber backpressure).
  • Bug fixes included in this slice: IPv6 default-port-80 in HostPort, upstream mid-response disconnect now always resumes the continuation via channelInactive, and per-subscriber buffer bounding in FlowBus.

Confidence Score: 5/5

Safe to merge; all findings are non-blocking quality improvements with no correctness regressions on the happy path.

The core proxy logic is well-structured and all previously identified bugs (IPv6 default port, continuation hang on upstream disconnect) are demonstrably fixed with corresponding tests. The two remaining observations are improvements for a later milestone rather than blockers.

ProxyHandler.swift and UpstreamPump.swift carry the two P2 notes worth tracking for a follow-up; no files require blocking attention.

Important Files Changed

Filename Overview
macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift CONNECT MITM bump and plain HTTP forwarding logic; request body capped at 32 MiB with 413 on overflow; missing hop-by-hop header stripping (TE, Trailers, Upgrade) in sanitizeRequestHeaders
macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift ResponseCollector now always resumes the continuation on channelInactive (fixing previous hang); a new NIOSSLContext is allocated per upstream HTTPS request rather than reusing a shared client context
macos/Sources/ReverseAPIProxy/Proxy/TLSContextFactory.swift Actor-based per-host NIOSSLContext cache with inflight-task deduplication; concurrent calls for the same host share one Task and get the same result
macos/Sources/ReverseAPIProxy/CA/LeafCertificateFactory.swift LRU-capped per-host leaf cert factory (actor); correct DNS + IPv4/IPv6 SAN handling
macos/Sources/ReverseAPIProxy/Capture/FlowBus.swift Actor-backed pub/sub with bounded AsyncStream buffers; subscriber cleanup via weak onTermination Task is correct
macos/Sources/ReverseAPIProxy/ProxyEngine.swift Bootstraps the NIO server; concurrent start/stop race on serverChannel noted in previous review; stop() now surfaces errors and shuts down the event loop group

Comments Outside Diff (3)

  1. macos/Sources/ReverseAPIProxy/CA/LeafCertificateFactory.swift, line 137-141 (link)

    P2 Materials.rootCertificate is populated in LeafCertificateFactory but is never read by TLSContextFactory.serverContext or any other consumer — the field is dead code. If the intent was to include the root in the certificate chain sent to clients during the TLS handshake, TLSContextFactory would need to add it to certificateChain. If it was intentionally omitted (roots are typically not sent in the chain), the unused field should be removed to avoid confusion.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPIProxy/CA/LeafCertificateFactory.swift
    Line: 137-141
    
    Comment:
    `Materials.rootCertificate` is populated in `LeafCertificateFactory` but is never read by `TLSContextFactory.serverContext` or any other consumer — the field is dead code. If the intent was to include the root in the certificate chain sent to clients during the TLS handshake, `TLSContextFactory` would need to add it to `certificateChain`. If it was intentionally omitted (roots are typically not sent in the chain), the unused field should be removed to avoid confusion.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift, line 736-750 (link)

    P2 There is no connect or read timeout on the upstream channel. A slow or unresponsive upstream host will stall the Task inside ProxyHandler.dispatch indefinitely, holding the downstream client connection open and accumulating unresolved continuations. Even a conservative timeout (e.g. 30 s for connect, 60 s for the full response) would bound the resource leak.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift
    Line: 736-750
    
    Comment:
    There is no connect or read timeout on the upstream channel. A slow or unresponsive upstream host will stall the `Task` inside `ProxyHandler.dispatch` indefinitely, holding the downstream client connection open and accumulating unresolved continuations. Even a conservative timeout (e.g. 30 s for connect, 60 s for the full response) would bound the resource leak.
    
    How can I resolve this? If you propose a fix, please make it concise.
  3. macos/Sources/ReverseAPIProxy/ProxyEngine.swift, line 856-868 (link)

    P2 ProxyEngine is a class marked @unchecked Sendable with a mutable serverChannel property. start() and stop() are both async and can be called concurrently — neither is protected by an actor, a lock, or any other exclusion mechanism. Concurrent calls could race on the serverChannel nil-check and leave both serverChannel set and the channel closed, or start two server sockets. Converting ProxyEngine to an actor would eliminate this without requiring manual locking.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: macos/Sources/ReverseAPIProxy/ProxyEngine.swift
    Line: 856-868
    
    Comment:
    `ProxyEngine` is a `class` marked `@unchecked Sendable` with a mutable `serverChannel` property. `start()` and `stop()` are both `async` and can be called concurrently — neither is protected by an actor, a lock, or any other exclusion mechanism. Concurrent calls could race on the `serverChannel` nil-check and leave both `serverChannel` set and the channel closed, or start two server sockets. Converting `ProxyEngine` to an `actor` would eliminate this without requiring manual locking.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift:841-845
**New `NIOSSLContext` created per upstream request**

A fresh `NIOSSLContext` (wrapping an OpenSSL/BoringSSL `SSL_CTX`) is allocated for every HTTPS upstream request. `SSL_CTX` creation is non-trivial (key material, session cache setup, etc.) and can become a measurable hot path under moderate load. The `serverHostname` is passed to `NIOSSLClientHandler`, not to `NIOSSLContext`, so a single shared client context is sufficient for all upstream connections — it can be created once at init time and reused.

### Issue 2 of 2
macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift:691-695
**Hop-by-hop headers not stripped before forwarding**

RFC 7230 §6.1 requires proxies to strip hop-by-hop headers (`TE`, `Trailers`, `Upgrade`, `Transfer-Encoding`) before forwarding. Currently only `Proxy-Connection` and `Proxy-Authorization` are removed. The most impactful omission is `Upgrade`: if a client sends `Upgrade: websocket`, the header passes through verbatim to the upstream. An upstream that responds `101 Switching Protocols` would leave the connection in a broken state since the proxy has no WebSocket frame handling. Stripping `TE`, `Trailers`, and `Transfer-Encoding` is also needed for strict HTTP/1.1 conformance.

Reviews (3): Last reviewed commit: "M1 review fixes + tests" | Re-trigger Greptile

- SwiftPM package targeting macOS 14+
- ReverseAPIProxy library: proxy engine, CA + leaf cert factory,
  TLS bumping pipeline, upstream pump, flow capture bus
- rae-proxy executable for headless debug
- Initial XCTest suite for CA
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 54aa85d7ee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
try await channel.writeAndFlush(HTTPClientRequestPart.end(nil)).get()

let response = try await collector.awaitResponse()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register the response waiter before sending requests

For fast upstreams and small responses, the HTTP response can be fully delivered between the request flush on line 70 and this awaitResponse() call. In that case ResponseCollector.finish sees no stored continuation and drops the result, so this later call installs a continuation that will never be resumed and the proxied request hangs until the client disconnects. Start waiting before flushing the request, or have the collector retain a completed result for late waiters.

Useful? React with 👍 / 👎.

Comment thread macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift Outdated
Comment thread macos/Sources/ReverseAPIProxy/Proxy/HostPort.swift Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 15 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift:55">
P2: Non-CONNECT absolute URI parsing can route bracketed IPv6 HTTP requests to the wrong default port (443 instead of 80).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift
Comment thread macos/Sources/ReverseAPIProxy/ProxyEngine.swift
Comment thread macos/Sources/ReverseAPIProxy/Proxy/TLSContextFactory.swift Outdated
Comment thread macos/Sources/ReverseAPIProxy/CA/LeafCertificateFactory.swift Outdated
beginBump(channelContext: channelContext, head: head)
return
}
guard let parsed = HostPort.parseAbsoluteURI(head.uri) else {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Non-CONNECT absolute URI parsing can route bracketed IPv6 HTTP requests to the wrong default port (443 instead of 80).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift, line 55:

<comment>Non-CONNECT absolute URI parsing can route bracketed IPv6 HTTP requests to the wrong default port (443 instead of 80).</comment>

<file context>
@@ -0,0 +1,229 @@
+                beginBump(channelContext: channelContext, head: head)
+                return
+            }
+            guard let parsed = HostPort.parseAbsoluteURI(head.uri) else {
+                respondError(channelContext: channelContext, status: .badRequest)
+                return
</file context>

Comment thread macos/.gitignore Outdated
Comment thread macos/Sources/ReverseAPIProxy/Capture/FlowBus.swift Outdated
Comment thread macos/Sources/ReverseAPIProxy/Proxy/HostPort.swift Outdated
Comment thread macos/Sources/ReverseAPIProxy/Proxy/HostPort.swift Outdated
Comment thread macos/Sources/ReverseAPIProxy/Capture/CapturedFlow.swift Outdated
@kind-agent
Copy link
Copy Markdown

kind-agent Bot commented May 19, 2026

⚠️ Error — The test run failed unexpectedly.

Grok 4 Fast is deprecated. xAI recommends switching to Grok 4.3 (https://openrouter.ai/x-ai/grok-4.3)

This is likely a transient issue. You can re-trigger a run from the dashboard.

Fixes for PR #72 review comments (greptile, cubic, codex):

- HostPort: validate ports against 1..65535, fix IPv6 default-port bug
  (http://[::1]/path was routed to 443), handle absolute URIs whose
  query/fragment appears before any "/"
- CapturedFlow.url: wrap IPv6 host literals in brackets per RFC 3986
- FlowBus: bounded (.bufferingNewest) buffer per subscriber, expose
  subscriberCount for testing
- LeafCertificateFactory: bounded LRU cache with configurable limit
- TLSContextFactory: deduplicate concurrent context creation per host
- UpstreamPump: register response continuation BEFORE flushing so
  fast responses don't drop the result; channelInactive now always
  fails the continuation (not just when head is missing); cancel()
  hook for upstream connect/setup errors
- ProxyEngine.stop: surface real errors instead of swallowing them
  with try?; preserve serverChannel cleanup via defer
- ProxyHandler: enforce a maxBodyBytes ceiling (32 MiB by default)
  and reply 413 Payload Too Large when exceeded
- Package.swift: declare the NIOFoundationCompat dependency that
  ProxyHandler.swift already imports
- .gitignore: commit Package.resolved to pin dependencies

Tests:
- HostPortTests: 24 cases (IPv4/IPv6/bracketed, port validation,
  absolute URI parsing, query/fragment handling)
- CapturedFlowTests: IPv6 bracket wrapping, port-segment elision
- FlowBusTests: multi-subscriber delivery, unsubscribe on stream
  termination, bounded-buffer drops oldest under back-pressure
- LeafCertificateFactoryTests: caching, distinct hosts, LRU eviction,
  IPv4 host
- TLSContextFactoryTests: cache, concurrent dedupe, distinct hosts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 15 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift:75">
P2: Oversized request bodies are only rejected on `.end`, so clients can keep streaming indefinitely after crossing the limit. Return 413 and close as soon as the limit is exceeded.</violation>
</file>

<file name="macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift:89">
P2: Add an upstream response timeout; this await can block indefinitely when the upstream connection stalls without closing.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

guard case .buffering(var inflight) = phase else { return }
inflight.appendBody(buffer)
if inflight.body.readableBytes > maxBodyBytes {
phase = .rejected
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Oversized request bodies are only rejected on .end, so clients can keep streaming indefinitely after crossing the limit. Return 413 and close as soon as the limit is exceeded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift, line 75:

<comment>Oversized request bodies are only rejected on `.end`, so clients can keep streaming indefinitely after crossing the limit. Return 413 and close as soon as the limit is exceeded.</comment>

<file context>
@@ -66,10 +71,19 @@ final class ProxyHandler: ChannelInboundHandler, RemovableChannelHandler, @unche
         guard case .buffering(var inflight) = phase else { return }
         inflight.appendBody(buffer)
+        if inflight.body.readableBytes > maxBodyBytes {
+            phase = .rejected
+            return
+        }
</file context>

}

do {
let response = try await resultTask.value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add an upstream response timeout; this await can block indefinitely when the upstream connection stalls without closing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift, line 89:

<comment>Add an upstream response timeout; this await can block indefinitely when the upstream connection stalls without closing.</comment>

<file context>
@@ -35,74 +35,127 @@ actor UpstreamPump {
-        try? await channel.close().get()
-        return response
+        do {
+            let response = try await resultTask.value
+            try? await channel.close().get()
+            return response
</file context>
Suggested change
let response = try await resultTask.value
let response = try await withThrowingTaskGroup(of: UpstreamResponse.self) { group in
group.addTask { try await resultTask.value }
group.addTask {
try await Task.sleep(nanoseconds: 60_000_000_000)
throw UpstreamError.unexpected("upstream response timeout")
}
let first = try await group.next()!
group.cancelAll()
return first
}

Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

1 similar comment
Copy link
Copy Markdown
Owner Author

@greptile review


Generated by Claude Code

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift:161">
P2: Do not send `Content-Length` on successful CONNECT responses; RFC 7231 forbids `Content-Length`/`Transfer-Encoding` on 2xx CONNECT.</violation>
</file>

<file name="macos/Sources/rae-proxy/main.swift">

<violation number="1" location="macos/Sources/rae-proxy/main.swift:70">
P2: Using `-d` when trusting into `login.keychain-db` can cause trust installation to fail under normal user permissions.</violation>
</file>

<file name="macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift:59">
P2: Avoid creating `NIOSSLContext` inside `eventLoop.submit`; this moves expensive TLS context setup onto the event-loop thread and can degrade throughput/latency under concurrent HTTPS traffic.</violation>
</file>

<file name="macos/Sources/ReverseAPIProxy/CA/RootCertificateStore.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/CA/RootCertificateStore.swift:30">
P1: `loadOrCreate` uses an unsynchronized check-then-create flow; concurrent calls can persist a mismatched certificate/private-key pair.</violation>
</file>

<file name="macos/Sources/ReverseAPIProxy/CA/CertificateAuthority.swift">

<violation number="1" location="macos/Sources/ReverseAPIProxy/CA/CertificateAuthority.swift:64">
P1: Validate that the loaded certificate and private key belong to the same keypair before returning `RootCertificate`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
try fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)

if fileManager.fileExists(atPath: certificateURL.path), fileManager.fileExists(atPath: privateKeyURL.path) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: loadOrCreate uses an unsynchronized check-then-create flow; concurrent calls can persist a mismatched certificate/private-key pair.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/CA/RootCertificateStore.swift, line 30:

<comment>`loadOrCreate` uses an unsynchronized check-then-create flow; concurrent calls can persist a mismatched certificate/private-key pair.</comment>

<file context>
@@ -0,0 +1,51 @@
+        try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
+        try fileManager.setAttributes([.posixPermissions: 0o700], ofItemAtPath: directory.path)
+
+        if fileManager.fileExists(atPath: certificateURL.path), fileManager.fileExists(atPath: privateKeyURL.path) {
+            let certificatePEM = try String(contentsOf: certificateURL, encoding: .utf8)
+            let privateKeyPEM = try String(contentsOf: privateKeyURL, encoding: .utf8)
</file context>

public static func loadRoot(certificatePEM: String, privateKeyPEM: String) throws -> RootCertificate {
let certificate = try Certificate(pemEncoded: certificatePEM)
let privateKey = try Certificate.PrivateKey(pemEncoded: privateKeyPEM)
return RootCertificate(certificate: certificate, privateKey: privateKey)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate that the loaded certificate and private key belong to the same keypair before returning RootCertificate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/CA/CertificateAuthority.swift, line 64:

<comment>Validate that the loaded certificate and private key belong to the same keypair before returning `RootCertificate`.</comment>

<file context>
@@ -53,4 +57,10 @@ public enum CertificateAuthority {
+    public static func loadRoot(certificatePEM: String, privateKeyPEM: String) throws -> RootCertificate {
+        let certificate = try Certificate(pemEncoded: certificatePEM)
+        let privateKey = try Certificate.PrivateKey(pemEncoded: privateKeyPEM)
+        return RootCertificate(certificate: certificate, privateKey: privateKey)
+    }
 }
</file context>
Suggested change
return RootCertificate(certificate: certificate, privateKey: privateKey)
guard certificate.publicKey == privateKey.publicKey else {
throw NSError(domain: "CertificateAuthority", code: 1, userInfo: [NSLocalizedDescriptionKey: "Certificate and private key do not match"])
}
return RootCertificate(certificate: certificate, privateKey: privateKey)

let tlsContext = try await proxyContext.tlsContexts.serverContext(for: target.host)
try await eventLoop.submit {
var okHead = HTTPResponseHead(version: .http1_1, status: .ok)
okHead.headers.add(name: "Content-Length", value: "0")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not send Content-Length on successful CONNECT responses; RFC 7231 forbids Content-Length/Transfer-Encoding on 2xx CONNECT.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/Proxy/ProxyHandler.swift, line 161:

<comment>Do not send `Content-Length` on successful CONNECT responses; RFC 7231 forbids `Content-Length`/`Transfer-Encoding` on 2xx CONNECT.</comment>

<file context>
@@ -157,14 +157,15 @@ final class ProxyHandler: ChannelInboundHandler, RemovableChannelHandler, @unche
                 try await eventLoop.submit {
-                    let okHead = HTTPResponseHead(version: .http1_1, status: .ok)
+                    var okHead = HTTPResponseHead(version: .http1_1, status: .ok)
+                    okHead.headers.add(name: "Content-Length", value: "0")
                     channel.write(HTTPServerResponsePart.head(okHead), promise: nil)
                     channel.writeAndFlush(HTTPServerResponsePart.end(nil), promise: nil)
</file context>
Suggested change
okHead.headers.add(name: "Content-Length", value: "0")
// RFC 7231 §4.3.6: omit Content-Length for successful CONNECT

Comment on lines +70 to +71
"-d",
"-r",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using -d when trusting into login.keychain-db can cause trust installation to fail under normal user permissions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/rae-proxy/main.swift, line 70:

<comment>Using `-d` when trusting into `login.keychain-db` can cause trust installation to fail under normal user permissions.</comment>

<file context>
@@ -45,7 +54,166 @@ struct RAEProxyCLI {
+        process.executableURL = URL(fileURLWithPath: "/usr/bin/security")
+        process.arguments = [
+            "add-trusted-cert",
+            "-d",
+            "-r",
+            "trustRoot",
</file context>
Suggested change
"-d",
"-r",
"-r",

try await channel.eventLoop.submit {
var clientConfig = TLSConfiguration.makeClientConfiguration()
clientConfig.applicationProtocols = ["http/1.1"]
let sslContext = try NIOSSLContext(configuration: clientConfig)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid creating NIOSSLContext inside eventLoop.submit; this moves expensive TLS context setup onto the event-loop thread and can degrade throughput/latency under concurrent HTTPS traffic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At macos/Sources/ReverseAPIProxy/Proxy/UpstreamPump.swift, line 59:

<comment>Avoid creating `NIOSSLContext` inside `eventLoop.submit`; this moves expensive TLS context setup onto the event-loop thread and can degrade throughput/latency under concurrent HTTPS traffic.</comment>

<file context>
@@ -52,11 +53,13 @@ actor UpstreamPump {
+                try await channel.eventLoop.submit {
+                    var clientConfig = TLSConfiguration.makeClientConfiguration()
+                    clientConfig.applicationProtocols = ["http/1.1"]
+                    let sslContext = try NIOSSLContext(configuration: clientConfig)
+                    let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: host)
+                    try channel.pipeline.syncOperations.addHandler(sslHandler, position: .first)
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants