// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package acmehttp import ( "net/http" "net/http/httptest" "os" "path/filepath" "testing" ) func serve(t *testing.T, redirectTo string) (http.Handler, string) { t.Helper() root := t.TempDir() if err := EnsureWebroot(root); err != nil { t.Fatal(err) } return Handler(root, redirectTo), root } func TestServesAChallengeTokenWrittenByAnAcmeClient(t *testing.T) { h, root := serve(t, "https://admin.example.net") // Exactly what `lego --http.webroot` writes. token := "abc-123_XYZ" want := "abc-123_XYZ.keyauthorization-part" if err := os.WriteFile(filepath.Join(root, ".well-known", "acme-challenge", token), []byte(want), 0o644); err != nil { t.Fatal(err) } rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", ChallengePath+token, nil)) if rec.Code != 200 { t.Fatalf("challenge not served: %d", rec.Code) } if rec.Body.String() != want { t.Fatalf("body = %q, want %q", rec.Body.String(), want) } } // The token comes from the network and is used to build a path. Rejecting by *shape* means // traversal never reaches the filesystem at all, which is a stronger guarantee than sanitising. func TestTraversalNeverTouchesTheFilesystem(t *testing.T) { h, root := serve(t, "https://admin.example.net") secret := filepath.Join(filepath.Dir(root), "secret.txt") if err := os.WriteFile(secret, []byte("do not serve me"), 0o600); err != nil { t.Fatal(err) } for _, bad := range []string{ "../secret.txt", "..%2Fsecret.txt", "../../etc/passwd", "..", ".", "a/b", "tok%20en", // a space arrives percent-encoded; a literal one is not a valid request line } { rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", ChallengePath+bad, nil)) if rec.Code == 200 && rec.Body.String() == "do not serve me" { t.Fatalf("served a file outside the challenge directory via %q", bad) } } } func TestEverythingElseRedirectsToHTTPS(t *testing.T) { h, _ := serve(t, "https://admin.example.net") rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", "/devices?page=2", nil)) if rec.Code != http.StatusPermanentRedirect { t.Fatalf("code = %d, want 308", rec.Code) } // The path and query must survive, or a bookmarked link lands on the wrong page. if got := rec.Header().Get("Location"); got != "https://admin.example.net/devices?page=2" { t.Fatalf("Location = %q", got) } } // With no admin URL configured there is nowhere to send people, and inventing one would be worse // than saying so. func TestNoRedirectTargetIsHonest(t *testing.T) { h, _ := serve(t, "") rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("code = %d, want 404", rec.Code) } } func TestMissingTokenIsNotFound(t *testing.T) { h, _ := serve(t, "https://admin.example.net") rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest("GET", ChallengePath+"never-written", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("code = %d, want 404", rec.Code) } }