Skip to content
Closed
Show file tree
Hide file tree
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
11 changes: 9 additions & 2 deletions apps/hosted/self-host/native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,10 @@ func serve(mode string) error {
}, time.Second, 30*time.Second, time.After)
defer stopMotel()

proxy := productProxy(filepath.Join(temporary, "product.sock"), workerdIdleTimeout)
// A proxy that ends TLS reaches this host over plain HTTP. The product derives its origin from
// Host and X-Forwarded-Proto, so the configured origin, never the client, names the scheme.
publicHTTPS := strings.HasPrefix(values["BETTER_AUTH_URL"], "https://")
proxy := productProxy(filepath.Join(temporary, "product.sock"), workerdIdleTimeout, publicHTTPS)
publicListener, err := net.Listen("tcp", net.JoinHostPort(setting("HOST", "0.0.0.0"), port))
if err != nil {
command.Process.Kill()
Expand Down Expand Up @@ -714,7 +717,7 @@ const workerdIdleTimeout = 5 * time.Second
// sends a request on a connection workerd is closing. Go replays only requests
// that are safe to repeat, so losing that race failed POSTs such as workflow runs.

func productProxy(socket string, upstreamIdleTimeout time.Duration) *httputil.ReverseProxy {
func productProxy(socket string, upstreamIdleTimeout time.Duration, publicHTTPS bool) *httputil.ReverseProxy {
return &httputil.ReverseProxy{
Rewrite: func(request *httputil.ProxyRequest) {
request.SetURL(&url.URL{Scheme: "http", Host: "product.internal"})
Expand All @@ -724,6 +727,10 @@ func productProxy(socket string, upstreamIdleTimeout time.Duration) *httputil.Re
address = request.In.RemoteAddr
}
request.Out.Header.Set("x-executor-client-ip", address)
// Rewrite has already removed any X-Forwarded-Proto the client sent.
if publicHTTPS {
request.Out.Header.Set("X-Forwarded-Proto", "https")
}
},
Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
Expand Down
45 changes: 42 additions & 3 deletions apps/hosted/self-host/native/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestProxyDoesNotReuseConnectionsWorkerdHasTimedOut(t *testing.T) {
socket := socketPath(t)
upstream := &fakeWorkerd{idleTimeout: 300 * time.Millisecond}
upstream.serve(t, socket)
front := httptest.NewServer(productProxy(socket, upstream.idleTimeout))
front := httptest.NewServer(productProxy(socket, upstream.idleTimeout, false))
defer front.Close()

if status, body := post(t, front.URL); status != 200 {
Expand All @@ -121,7 +121,7 @@ func TestProxyDoesNotRetryPostWorkerdMayHaveReceived(t *testing.T) {
socket := socketPath(t)
upstream := &fakeWorkerd{idleTimeout: time.Minute, dropFirst: true}
upstream.serve(t, socket)
front := httptest.NewServer(productProxy(socket, upstream.idleTimeout))
front := httptest.NewServer(productProxy(socket, upstream.idleTimeout, false))
defer front.Close()

status, body := post(t, front.URL)
Expand All @@ -134,14 +134,53 @@ func TestProxyDoesNotRetryPostWorkerdMayHaveReceived(t *testing.T) {
}

func TestProxyReportsStartingUntilWorkerdListens(t *testing.T) {
front := httptest.NewServer(productProxy(socketPath(t), workerdIdleTimeout))
front := httptest.NewServer(productProxy(socketPath(t), workerdIdleTimeout, false))
defer front.Close()

if status, body := post(t, front.URL); status != http.StatusServiceUnavailable || body != "Executor is starting" {
t.Fatalf("POST before workerd listens: %d %q", status, body)
}
}

// Behind a proxy that ends TLS, the product derives its origin from Host and X-Forwarded-Proto.
// The scheme workerd sees comes from the configured origin, whatever the client sent.
func TestProxyForwardsTheSchemeOfTheConfiguredOrigin(t *testing.T) {
for _, publicHTTPS := range []bool{true, false} {
socket := socketPath(t)
listener, err := net.Listen("unix", socket)
if err != nil {
t.Fatal(err)
}
seen := make(chan string, 1)
upstream := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen <- r.Host + " " + r.Header.Get("X-Forwarded-Proto")
})}
go upstream.Serve(listener)
t.Cleanup(func() { upstream.Close() })
front := httptest.NewServer(productProxy(socket, workerdIdleTimeout, publicHTTPS))
t.Cleanup(front.Close)

request, err := http.NewRequest("POST", front.URL+"/api/dashboard/batch", strings.NewReader("{}"))
if err != nil {
t.Fatal(err)
}
request.Host = "executor.example:4788"
claimed, want := "https", "executor.example:4788 "
if publicHTTPS {
claimed, want = "http", want+"https"
}
request.Header.Set("X-Forwarded-Proto", claimed)
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if got := <-seen; got != want {
t.Fatalf("publicHTTPS %v, client sent %q: workerd saw %q, want %q", publicHTTPS, claimed, got, want)
}
}
}

// The collector runs beside the product: an exit is followed by a restart, not by the product
// stopping. The waits double from the minimum to the maximum, so a collector that cannot start
// does not spin, and return to the minimum after a run longer than the maximum.
Expand Down