Skip to content

Commit b9ececb

Browse files
authored
Merge pull request #105 from c64bob/codex/implement-dynamic-static-files-handling
Build setup static-asset allowlist from manifest
2 parents dbbce60 + 1a1d6a6 commit b9ececb

3 files changed

Lines changed: 41 additions & 12 deletions

File tree

internal/handler/middleware_test.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
"caldo/internal/assets"
1112
"caldo/internal/logging"
1213
"caldo/internal/view"
1314
"github.com/google/uuid"
@@ -254,7 +255,7 @@ func TestReverseProxyAuthMiddlewareAllowsHealthWithoutAuth(t *testing.T) {
254255
func TestSetupGateMiddlewareRedirectsDisallowedRoutesWhenSetupIncomplete(t *testing.T) {
255256
t.Parallel()
256257

257-
h := SetupGateMiddleware(NewSetupState(false))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
258+
h := SetupGateMiddleware(NewSetupState(false), assets.Manifest{"app.css": "app.8f3a1c2.css", "htmx.min.js": "htmx.5e741aa.min.js"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
258259
w.WriteHeader(http.StatusNoContent)
259260
}))
260261

@@ -270,11 +271,10 @@ func TestSetupGateMiddlewareRedirectsDisallowedRoutesWhenSetupIncomplete(t *test
270271
}
271272
}
272273

273-
274274
func TestSetupGateMiddlewareAllowsStaticAssetsWhenSetupIncomplete(t *testing.T) {
275275
t.Parallel()
276276

277-
h := SetupGateMiddleware(NewSetupState(false))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
277+
h := SetupGateMiddleware(NewSetupState(false), assets.Manifest{"app.css": "app.8f3a1c2.css", "htmx.min.js": "htmx.5e741aa.min.js"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
278278
w.WriteHeader(http.StatusNoContent)
279279
}))
280280

@@ -286,10 +286,26 @@ func TestSetupGateMiddlewareAllowsStaticAssetsWhenSetupIncomplete(t *testing.T)
286286
t.Fatalf("unexpected status code: got %d want %d", rr.Code, http.StatusNoContent)
287287
}
288288
}
289+
func TestSetupGateMiddlewareRedirectsUnknownStaticAssetWhenSetupIncomplete(t *testing.T) {
290+
t.Parallel()
291+
292+
h := SetupGateMiddleware(NewSetupState(false), assets.Manifest{"app.css": "app.8f3a1c2.css"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
293+
w.WriteHeader(http.StatusNoContent)
294+
}))
295+
296+
rr := httptest.NewRecorder()
297+
req := httptest.NewRequest(http.MethodGet, "/static/unknown.js", nil)
298+
h.ServeHTTP(rr, req)
299+
300+
if rr.Code != http.StatusFound {
301+
t.Fatalf("unexpected status code: got %d want %d", rr.Code, http.StatusFound)
302+
}
303+
}
304+
289305
func TestSetupGateMiddlewareAllowsSetupRoutesWhenSetupIncomplete(t *testing.T) {
290306
t.Parallel()
291307

292-
h := SetupGateMiddleware(NewSetupState(false))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
308+
h := SetupGateMiddleware(NewSetupState(false), assets.Manifest{"app.css": "app.8f3a1c2.css", "htmx.min.js": "htmx.5e741aa.min.js"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
293309
w.WriteHeader(http.StatusNoContent)
294310
}))
295311

@@ -305,7 +321,7 @@ func TestSetupGateMiddlewareAllowsSetupRoutesWhenSetupIncomplete(t *testing.T) {
305321
func TestSetupGateMiddlewareAllowsAllRoutesWhenSetupComplete(t *testing.T) {
306322
t.Parallel()
307323

308-
h := SetupGateMiddleware(NewSetupState(true))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
324+
h := SetupGateMiddleware(NewSetupState(true), assets.Manifest{"app.css": "app.8f3a1c2.css"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
309325
w.WriteHeader(http.StatusNoContent)
310326
}))
311327

@@ -322,7 +338,7 @@ func TestSetupGateMiddlewareReflectsRuntimeCompletionState(t *testing.T) {
322338
t.Parallel()
323339

324340
state := NewSetupState(false)
325-
h := SetupGateMiddleware(state)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
341+
h := SetupGateMiddleware(state, assets.Manifest{"app.css": "app.8f3a1c2.css"})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
326342
w.WriteHeader(http.StatusNoContent)
327343
}))
328344

internal/handler/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func NewRouter(logger *slog.Logger, proxyUserHeader string, manifest assets.Mani
2929
router.Use(SafeLoggingMiddleware(logger))
3030
router.Use(SecurityHeadersMiddleware())
3131
router.Use(ReverseProxyAuthMiddleware(proxyUserHeader))
32-
router.Use(SetupGateMiddleware(setupState))
32+
router.Use(SetupGateMiddleware(setupState, manifest))
3333
router.Use(AssetManifestMiddleware(manifest))
3434

3535
router.Get("/health", Health)

internal/handler/setup_gate.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package handler
22

33
import (
44
"net/http"
5-
"strings"
5+
6+
"caldo/internal/assets"
67
)
78

89
// SetupGateMiddleware blocks normal routes until setup is completed.
9-
func SetupGateMiddleware(state *SetupState) func(http.Handler) http.Handler {
10+
func SetupGateMiddleware(state *SetupState, manifest assets.Manifest) func(http.Handler) http.Handler {
1011
allowedWhenIncomplete := map[string]struct{}{
1112
routeKey(http.MethodGet, "/setup"): {},
1213
routeKey(http.MethodGet, "/setup/"): {},
@@ -19,6 +20,8 @@ func SetupGateMiddleware(state *SetupState) func(http.Handler) http.Handler {
1920
routeKey(http.MethodGet, "/health"): {},
2021
}
2122

23+
allowedStaticAssets := resolveSetupStaticAssetPaths(manifest)
24+
2225
return func(next http.Handler) http.Handler {
2326
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2427
if state != nil && state.IsComplete() {
@@ -31,9 +34,11 @@ func SetupGateMiddleware(state *SetupState) func(http.Handler) http.Handler {
3134
return
3235
}
3336

34-
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/static/") {
35-
next.ServeHTTP(w, r)
36-
return
37+
if r.Method == http.MethodGet {
38+
if _, ok := allowedStaticAssets[r.URL.Path]; ok {
39+
next.ServeHTTP(w, r)
40+
return
41+
}
3742
}
3843

3944
http.Redirect(w, r, "/setup", http.StatusFound)
@@ -44,3 +49,11 @@ func SetupGateMiddleware(state *SetupState) func(http.Handler) http.Handler {
4449
func routeKey(method string, path string) string {
4550
return method + " " + path
4651
}
52+
53+
func resolveSetupStaticAssetPaths(manifest assets.Manifest) map[string]struct{} {
54+
allowed := make(map[string]struct{}, len(manifest))
55+
for _, resolved := range manifest {
56+
allowed["/static/"+resolved] = struct{}{}
57+
}
58+
return allowed
59+
}

0 commit comments

Comments
 (0)