Skip to content

Commit a0485ea

Browse files
Reject requests targeting a cachew playpen when not running as one
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d0345-d564-743f-ad25-f1063ade61da
1 parent e81e5fe commit a0485ea

4 files changed

Lines changed: 76 additions & 0 deletions

File tree

.bk.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
selected_org: cash

cmd/cachewd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ func newServer(ctx context.Context, muxHandler http.Handler, bind string, metric
214214
)(handler)
215215

216216
handler = httputil.LoggingMiddleware(handler)
217+
if os.Getenv("IS_PLAYPEN") != "true" {
218+
handler = httputil.PlaypenGuardMiddleware(handler)
219+
}
217220

218221
logger := logging.FromContext(ctx)
219222
return &http.Server{

internal/httputil/playpen_guard.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package httputil
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
)
7+
8+
// PlaypenGuardMiddleware rejects requests that target a playpen instance
9+
// (via the Baggage header) when this instance is not a playpen.
10+
// This prevents requests from silently falling through to staging.
11+
func PlaypenGuardMiddleware(next http.Handler) http.Handler {
12+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
if baggage := r.Header.Get("Baggage"); hasCachewPlaypenKey(baggage) {
14+
http.Error(w, "no matching cachew playpen found — start one with: sq playpen sync", http.StatusServiceUnavailable)
15+
return
16+
}
17+
next.ServeHTTP(w, r)
18+
})
19+
}
20+
21+
func hasCachewPlaypenKey(baggage string) bool {
22+
if baggage == "" {
23+
return false
24+
}
25+
for entry := range strings.SplitSeq(baggage, ",") {
26+
if strings.HasPrefix(strings.TrimSpace(entry), "cachew-playpen=") {
27+
return true
28+
}
29+
}
30+
return false
31+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package httputil_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/alecthomas/assert/v2"
9+
10+
"github.com/block/cachew/internal/httputil"
11+
)
12+
13+
func TestPlaypenGuardMiddleware(t *testing.T) {
14+
ok := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
15+
w.WriteHeader(http.StatusOK)
16+
})
17+
guard := httputil.PlaypenGuardMiddleware(ok)
18+
19+
t.Run("allows normal requests", func(t *testing.T) {
20+
req := httptest.NewRequest(http.MethodGet, "/", nil)
21+
rr := httptest.NewRecorder()
22+
guard.ServeHTTP(rr, req)
23+
assert.Equal(t, http.StatusOK, rr.Code)
24+
})
25+
26+
t.Run("blocks requests with cachew playpen baggage", func(t *testing.T) {
27+
req := httptest.NewRequest(http.MethodGet, "/", nil)
28+
req.Header.Set("Baggage", "cachew-playpen=jrobotham")
29+
rr := httptest.NewRecorder()
30+
guard.ServeHTTP(rr, req)
31+
assert.Equal(t, http.StatusServiceUnavailable, rr.Code)
32+
})
33+
34+
t.Run("allows requests with unrelated baggage", func(t *testing.T) {
35+
req := httptest.NewRequest(http.MethodGet, "/", nil)
36+
req.Header.Set("Baggage", "blox-playpen=jrobotham")
37+
rr := httptest.NewRecorder()
38+
guard.ServeHTTP(rr, req)
39+
assert.Equal(t, http.StatusOK, rr.Code)
40+
})
41+
}

0 commit comments

Comments
 (0)