Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions server/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
Expand Down Expand Up @@ -172,6 +173,40 @@ func main() {
"webSocketDebuggerUrl": proxyWSURL,
})
})
// Proxy CDP JSON endpoints to upstream Chromium DevTools
cdpProxyHandler := func(path string) http.HandlerFunc {
client := &http.Client{Timeout: 10 * time.Second}
return func(w http.ResponseWriter, r *http.Request) {
current := upstreamMgr.Current()
if current == "" {
http.Error(w, "upstream not ready", http.StatusServiceUnavailable)
return
}
upstreamURL, err := url.Parse(current)
if err != nil {
http.Error(w, "invalid upstream URL", http.StatusInternalServerError)
return
}
Comment on lines +186 to +189
Copy link
Contributor

Choose a reason for hiding this comment

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

Might be useful to log the parse error here (right now the client gets a 500 but you lose the underlying url.Parse context in logs).

Suggested change
if err != nil {
http.Error(w, "invalid upstream URL", http.StatusInternalServerError)
return
}
if err != nil {
slogger.Error("failed to parse upstream URL", "err", err)
http.Error(w, "invalid upstream URL", http.StatusInternalServerError)
return
}

httpURL := &url.URL{
Scheme: "http",
Host: upstreamURL.Host,
Path: path,
RawQuery: r.URL.RawQuery,
}
resp, err := client.Get(httpURL.String())
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: client.Get won’t tie the upstream request to r.Context() (so it won’t cancel if the client disconnects / request times out earlier). Using a context-aware request also makes it easier to add headers later if needed.

Suggested change
resp, err := client.Get(httpURL.String())
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, httpURL.String(), nil)
if err != nil {
slogger.Error("failed to build upstream request", "path", path, "err", err)
http.Error(w, "failed to build upstream request", http.StatusInternalServerError)
return
}
resp, err := client.Do(req)

if err != nil {
slogger.Error("failed to proxy CDP endpoint", "path", path, "err", err)
http.Error(w, "failed to reach upstream", http.StatusBadGateway)
return
}
defer resp.Body.Close()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
Comment on lines +203 to +205
Copy link
Contributor

Choose a reason for hiding this comment

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

Two small things here: (1) consider preserving the upstream Content-Type instead of forcing JSON, and (2) io.Copy can fail (client hangup / upstream read error) so it’s worth at least logging the error.

Suggested change
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
if ct := resp.Header.Get("Content-Type"); ct != "" {
w.Header().Set("Content-Type", ct)
} else {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
slogger.Error("failed to proxy CDP response body", "path", path, "err", err)
}

Copy link

Choose a reason for hiding this comment

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

Proxied CDP responses expose unusable internal WebSocket URLs

Medium Severity

The /json/list and /json/new endpoints proxy raw upstream responses containing webSocketDebuggerUrl fields that point to internal Chromium addresses (e.g., ws://localhost:9223/...). External clients cannot reach these internal URLs. This is inconsistent with /json/version, which correctly rewrites the URL to use r.Host so clients connect back through the proxy. Additionally, the WebSocket proxy ignores the request path and always connects to the browser target, so even rewritten page-specific URLs wouldn't work as expected.

Fix in Cursor Fix in Web

}
}
rDevtools.Get("/json/list", cdpProxyHandler("/json/list"))
rDevtools.Get("/json/new", cdpProxyHandler("/json/new"))
rDevtools.Get("/*", func(w http.ResponseWriter, r *http.Request) {
devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz).ServeHTTP(w, r)
})
Expand Down
Loading