// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "encoding/json" "errors" "io" "log/slog" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/moby/moby/api/types/container" ) // fakeAudit is an in-memory AuditRecorder type fakeAudit struct { mu sync.Mutex rows []opRow err error } func (f *fakeAudit) RecordOperation(ctx context.Context, token, origin, operation string) error { f.mu.Lock() defer f.mu.Unlock() if f.err != nil { return f.err } f.rows = append(f.rows, opRow{token: token, origin: origin, operation: operation}) return nil } func (f *fakeAudit) Rows() []opRow { f.mu.Lock() defer f.mu.Unlock() return append([]opRow(nil), f.rows...) } // doRequest runs one request through the complete handler chain func doRequest(s *WebServer, method, target string, header http.Header) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, nil) req.RemoteAddr = "198.51.100.7:40000" for k, vs := range header { for _, v := range vs { req.Header.Add(k, v) } } rec := httptest.NewRecorder() s.HTTPServer.Handler.ServeHTTP(rec, req) return rec } // realStack wires the server with real SQLite stores and a fake Docker client type realStack struct { server *WebServer tokens *TokenStore audit *AuditStore docker *fakeDocker log *syncBuffer good string old string } func newRealStack(t *testing.T, level slog.Level) *realStack { t.Helper() log, buf := newTestLogger(level) db := openTestDB(t) clock := newFakeClock(t0, time.Millisecond) tokens := NewTokenStore(db, log, clock.Now, GenerateToken) audit := NewAuditStore(db, log, clock.Now) good, err := tokens.CreateToken(context.Background(), "good") if err != nil { t.Fatal(err) } old, err := tokens.CreateToken(context.Background(), "old") if err != nil { t.Fatal(err) } if err := tokens.DeactivateToken(context.Background(), old.Token); err != nil { t.Fatal(err) } docker := &fakeDocker{containers: []container.Summary{}} return &realStack{ server: NewWebServer(log, tokens, audit, docker), tokens: tokens, audit: audit, docker: docker, log: buf, good: good.Token, old: old.Token, } } func bearer(token string) http.Header { return http.Header{"Authorization": {"Bearer " + token}} } func assertJSONResponse(t *testing.T, rec *httptest.ResponseRecorder, status int, body string) { t.Helper() if rec.Code != status { t.Errorf("status = %d, want %d", rec.Code, status) } if ct := rec.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q", ct) } if got := rec.Body.String(); got != body { t.Errorf("body = %q, want %q", got, body) } if cl := rec.Header().Get("Content-Length"); cl != itoa(len(body)) { t.Errorf("Content-Length = %q, want %d", cl, len(body)) } } // nearMiss returns a different token differing only in its first character func nearMiss(token string) string { if token[0] == 'A' { return "B" + token[1:] } return "A" + token[1:] } func itoa(n int) string { b, _ := json.Marshal(n) return string(b) } func TestContainersNoContainers(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) rec := doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) assertJSONResponse(t, rec, 200, "[]") rows := readOperations(t, st.audit) if len(rows) != 1 || rows[0].token != st.good || rows[0].origin != "198.51.100.7" || rows[0].operation != "GET containers" { t.Fatalf("audit rows = %+v", rows) } if _, err := time.Parse(time.RFC3339Nano, rows[0].time); err != nil || !strings.HasSuffix(rows[0].time, "Z") { t.Fatalf("audit time %q", rows[0].time) } } func TestContainersNilSliceFromLister(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) st.docker.containers = nil rec := doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) // A nil slice from a lister still yields valid JSON (null); the real client never returns nil if rec.Code != 200 || rec.Body.String() != "null" { t.Fatalf("got %d %q", rec.Code, rec.Body.String()) } } func TestContainersOneAndMany(t *testing.T) { t.Parallel() for _, n := range []int{1, 250} { st := newRealStack(t, slog.LevelInfo) st.docker.containers = makeContainers(n) rec := doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) want, _ := json.Marshal(st.docker.containers) assertJSONResponse(t, rec, 200, string(want)) var got []map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatal(err) } if len(got) != n || got[0]["Id"] != st.docker.containers[0].ID || got[n-1]["State"] != "running" { t.Fatalf("n=%d: decoded %d items", n, len(got)) } } } func TestContainersQueryStringIgnored(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) rec := doRequest(st.server, "GET", "/api/v1/containers?all=1&x=%00", bearer(st.good)) assertJSONResponse(t, rec, 200, "[]") } func TestContainersDockerError(t *testing.T) { t.Parallel() st := newRealStack(t, LevelTrace) st.docker.err = errors.New("Cannot connect to the Docker daemon at unix:///var/run/docker.sock") rec := doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) assertJSONResponse(t, rec, 500, `{"error":"internal server error"}`) if strings.Contains(rec.Body.String(), "docker") { t.Fatal("internal detail leaked") } // The request was audited even though Docker failed if rows := readOperations(t, st.audit); len(rows) != 1 { t.Fatalf("audit rows = %d", len(rows)) } if !strings.Contains(st.log.String(), `level=ERROR msg="docker request failed" error="Cannot connect`) { t.Fatalf("missing error log:\n%s", st.log.String()) } } func TestContainersAuditError(t *testing.T) { t.Parallel() log, buf := newTestLogger(slog.LevelInfo) docker := &fakeDocker{} s := NewWebServer(log, newFakeLookup(), &fakeAudit{err: errors.New("SQL logic error: no such table")}, docker) rec := doRequest(s, "GET", "/api/v1/containers", bearer("good")) assertJSONResponse(t, rec, 500, `{"error":"internal server error"}`) if docker.Calls() != 0 { t.Fatal("Docker queried although the audit failed") } if !strings.Contains(buf.String(), `level=ERROR msg="audit insert failed" token_id=`+tokenID("good")) { t.Fatalf("missing error log:\n%s", buf.String()) } } func TestContainersAuditForeignKeyFailure(t *testing.T) { t.Parallel() // The lookup says the token exists but the real store has no such row: the FK rejects the audit row st := newRealStack(t, slog.LevelInfo) s := NewWebServer(traceDiscard(), &fakeLookup{tokens: map[string]Token{"ghost": {Token: "ghost", Active: true}}}, st.audit, st.docker) rec := doRequest(s, "GET", "/api/v1/containers", bearer("ghost")) assertJSONResponse(t, rec, 500, `{"error":"internal server error"}`) if st.docker.Calls() != 0 { t.Fatal("Docker queried although the audit failed") } } func TestContainersLookupError(t *testing.T) { t.Parallel() audit := &fakeAudit{} s := NewWebServer(traceDiscard(), &fakeLookup{err: errors.New("database is locked")}, audit, &fakeDocker{}) rec := doRequest(s, "GET", "/api/v1/containers", bearer("good")) assertJSONResponse(t, rec, 500, `{"error":"internal server error"}`) if len(audit.Rows()) != 0 { t.Fatal("audited without authentication") } } func TestContainersSerializationFailure(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) st.docker.containers = makeContainers(2) st.server.encode = func(any) ([]byte, error) { return nil, errors.New("encoder exploded") } rec := doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) assertJSONResponse(t, rec, 500, `{"error":"internal server error"}`) if !strings.Contains(st.log.String(), `msg="response serialization failed" error="encoder exploded"`) { t.Fatalf("missing log:\n%s", st.log.String()) } } func TestContainersUnauthorized(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelDebug) cases := []struct { name string header http.Header class string }{ {"missing", nil, authMissingHeader}, {"empty", http.Header{"Authorization": {""}}, authEmptyHeader}, {"wrong scheme", http.Header{"Authorization": {"Basic " + st.good}}, authBadScheme}, {"empty bearer", http.Header{"Authorization": {"Bearer "}}, authEmptyToken}, {"malformed bearer", http.Header{"Authorization": {"Bearer " + st.good}}, authUnknownToken}, {"duplicated", http.Header{"Authorization": {"Bearer " + st.good, "Bearer " + st.good}}, authMultipleHeaders}, {"unknown", bearer(nearMiss(st.good)), authUnknownToken}, {"case changed", bearer(strings.ToUpper(st.good)), authUnknownToken}, {"inactive", bearer(st.old), authInactiveToken}, {"sql injection", bearer("' OR '1'='1"), authUnknownToken}, } for _, c := range cases { rec := doRequest(st.server, "GET", "/api/v1/containers", c.header) assertJSONResponse(t, rec, 401, `{"error":"unauthorized"}`) if rec.Header().Get("WWW-Authenticate") != "Bearer" { t.Errorf("%s: WWW-Authenticate = %q", c.name, rec.Header().Get("WWW-Authenticate")) } if !strings.Contains(st.log.String(), `msg="authentication result" result=`+c.class) { t.Errorf("%s: missing class %s in log", c.name, c.class) } } // No audit row and no Docker call for any failed authentication if rows := readOperations(t, st.audit); len(rows) != 0 { t.Fatalf("audit rows after failed auth: %+v", rows) } if st.docker.Calls() != 0 { t.Fatal("Docker queried without authentication") } // Then a valid one creates exactly one row assertJSONResponse(t, doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)), 200, "[]") if rows := readOperations(t, st.audit); len(rows) != 1 { t.Fatalf("audit rows = %d", len(rows)) } } func TestContainersAuditBeforeDocker(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) var during int st.docker.onList = func(ctx context.Context) { during = len(readOperations(t, st.audit)) } st.docker.err = errors.New("boom") doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) if during != 1 { t.Fatalf("audit rows visible during Docker call = %d, want 1", during) } } func TestMethodNotAllowed(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) for _, m := range []string{"POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT", "FOO", "get"} { rec := doRequest(st.server, m, "/api/v1/containers", bearer(st.good)) assertJSONResponse(t, rec, 405, `{"error":"method not allowed"}`) if rec.Header().Get("Allow") != "GET" { t.Errorf("%s: Allow = %q", m, rec.Header().Get("Allow")) } } if rows := readOperations(t, st.audit); len(rows) != 0 { t.Fatalf("audit rows = %d", len(rows)) } if st.docker.Calls() != 0 { t.Fatal("Docker queried") } } func TestNotFound(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) for _, path := range []string{ "/", "/api", "/api/v1", "/api/v1/", "/api/v1/containers/", "/api/v1/containers/x", "/API/V1/CONTAINERS", "/api/v1/Containers", "//api/v1/containers", "/api//v1/containers", "/api/v1/./containers", "/api/v1/../v1/containers", "/api/v2/containers", "/api/v1/containers.json", "/api/v1/containers%2F", "/api/v1%2Fcontainers", "/api%2fv1%2fcontainers", "/version", "/favicon.ico", } { for _, m := range []string{"GET", "POST"} { rec := doRequest(st.server, m, path, bearer(st.good)) assertJSONResponse(t, rec, 404, `{"error":"not found"}`) if rec.Header().Get("Location") != "" { t.Errorf("%s %s redirected to %q", m, path, rec.Header().Get("Location")) } } } if rows := readOperations(t, st.audit); len(rows) != 0 { t.Fatalf("audit rows = %d", len(rows)) } } func TestEncodedPathMatches(t *testing.T) { t.Parallel() // Percent-encoded unreserved characters decode to the same path st := newRealStack(t, slog.LevelInfo) assertJSONResponse(t, doRequest(st.server, "GET", "/api/v1/%63ontainers", bearer(st.good)), 200, "[]") } func TestClientIP(t *testing.T) { t.Parallel() for in, want := range map[string]string{ "203.0.113.5:1234": "203.0.113.5", "[2001:db8::1]:443": "2001:db8::1", "[fe80::1%eth0]:8080": "fe80::1%eth0", "127.0.0.1:0": "127.0.0.1", "garbage": "garbage", "": "", "@": "@", } { if got := clientIP(in); got != want { t.Errorf("clientIP(%q) = %q, want %q", in, got, want) } } } func TestOriginIgnoresForwardingHeaders(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) h := bearer(st.good) h.Set("X-Forwarded-For", "1.2.3.4") h.Set("Forwarded", "for=5.6.7.8") h.Set("X-Real-IP", "9.9.9.9") h.Set("X-Client-IP", "8.8.8.8") h.Set("True-Client-IP", "7.7.7.7") h.Set("CF-Connecting-IP", "6.6.6.6") doRequest(st.server, "GET", "/api/v1/containers", h) rows := readOperations(t, st.audit) if len(rows) != 1 || rows[0].origin != "198.51.100.7" { t.Fatalf("origin = %+v", rows) } if strings.Contains(st.log.String(), "1.2.3.4") { t.Fatal("forwarded address logged as origin") } } func TestRequestLogLine(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) doRequest(st.server, "GET", "/api/v1/containers", nil) doRequest(st.server, "POST", "/api/v1/containers", nil) doRequest(st.server, "GET", "/missing%0Aline", nil) st.docker.err = errors.New("x") doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) out := st.log.String() for _, want := range []string{ `level=INFO msg="this request has been made" method=GET path=/api/v1/containers status=200 duration=`, `level=INFO msg="this request has been made" method=GET path=/api/v1/containers status=401 duration=`, `level=INFO msg="this request has been made" method=POST path=/api/v1/containers status=405 duration=`, `level=INFO msg="this request has been made" method=GET path="/missing\nline" status=404 duration=`, `level=INFO msg="this request has been made" method=GET path=/api/v1/containers status=500 duration=`, } { if !strings.Contains(out, want) { t.Errorf("missing %q in:\n%s", want, out) } } if c := strings.Count(out, "this request has been made"); c != 5 { t.Fatalf("request lines = %d", c) } if !strings.Contains(out, "origin=198.51.100.7\n") { t.Fatalf("origin missing:\n%s", out) } // INFO shows no ERROR-level noise other than the Docker failure if c := strings.Count(out, "level=ERROR"); c != 1 { t.Fatalf("error lines = %d", c) } } func TestNoSecretInLogsAtTrace(t *testing.T) { t.Parallel() st := newRealStack(t, LevelTrace) st.docker.containers = makeContainers(2) unknown := nearMiss(st.good) for _, h := range []http.Header{ bearer(st.good), bearer(st.old), bearer(unknown), bearer(""), {"Authorization": {"Basic " + st.good}}, {"Authorization": {"Bearer " + st.good, "Bearer " + st.good}}, } { doRequest(st.server, "GET", "/api/v1/containers", h) doRequest(st.server, "POST", "/api/v1/containers", h) } st.docker.err = errors.New("fail") doRequest(st.server, "GET", "/api/v1/containers", bearer(st.good)) out := st.log.String() for _, secret := range []string{st.good, st.old, unknown, st.good[:16], "Authorization", "Bearer"} { if strings.Contains(out, secret) { t.Fatalf("%q leaked into logs", secret) } } // TRACE covers the execution path for _, want := range []string{ `func=WebServer.logRequests`, `func=WebServer.handleContainers`, `func=authenticate`, `func=TokenStore.GetToken`, `func=AuditStore.RecordOperation`, `func=WebServer.handleMethodNotAllowed`, `msg="row read" table=tokens token_id=` + tokenID(st.good), `msg="row inserted" table=operations token_id=` + tokenID(st.good) + ` origin=198.51.100.7 operation="GET containers"`, `level=DEBUG msg="authentication result" result=ok token_id=` + tokenID(st.good), `level=DEBUG msg="audit row inserted" token_id=` + tokenID(st.good), `level=DEBUG msg="docker request started"`, `level=DEBUG msg="docker request finished" ok=true`, `level=DEBUG msg="docker request finished" ok=false`, } { if !strings.Contains(out, want) { t.Errorf("missing %q", want) } } } func TestHandlerOverRealHTTP(t *testing.T) { t.Parallel() st := newRealStack(t, slog.LevelInfo) srv := httptest.NewServer(st.server.HTTPServer.Handler) defer srv.Close() get := func(header http.Header) *http.Response { req, _ := http.NewRequest("GET", srv.URL+"/api/v1/containers", nil) req.Header = header resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } return resp } resp := get(bearer(st.good)) body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 200 || string(body) != "[]" || resp.Header.Get("Content-Type") != "application/json" { t.Fatalf("got %d %q", resp.StatusCode, body) } // Two Authorization header lines on the wire resp = get(http.Header{"Authorization": {"Bearer " + st.good, "Bearer " + st.good}}) resp.Body.Close() if resp.StatusCode != 401 { t.Fatalf("duplicate header status = %d", resp.StatusCode) } rows := readOperations(t, st.audit) if len(rows) != 1 || rows[0].origin != "127.0.0.1" { t.Fatalf("rows = %+v", rows) } } func TestStatusRecorder(t *testing.T) { t.Parallel() // Implicit 200 on Write, first WriteHeader wins, Unwrap exposes the writer inner := httptest.NewRecorder() rec := &statusRecorder{ResponseWriter: inner, status: http.StatusOK} rec.Write([]byte("x")) rec.WriteHeader(http.StatusTeapot) if rec.status != http.StatusOK { t.Fatalf("status = %d", rec.status) } if rec.Unwrap() != inner { t.Fatal("Unwrap") } rec2 := &statusRecorder{ResponseWriter: httptest.NewRecorder(), status: http.StatusOK} rec2.WriteHeader(http.StatusAccepted) rec2.WriteHeader(http.StatusTeapot) if rec2.status != http.StatusAccepted { t.Fatalf("status = %d", rec2.status) } // A handler writing nothing is logged as 200 log, buf := newTestLogger(slog.LevelInfo) s := &WebServer{log: log} h := s.logRequests(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil)) if !strings.Contains(buf.String(), "status=200") { t.Fatalf("log = %s", buf.String()) } }