route-proxy: the challenge path falls through for real
autocert's HTTPHandler answers 404 itself for a token it does not hold and never consults its fallback on the challenge path — the predecessor's exact fault, rediscovered live when Mailu's renewal died behind this proxy on cutover day. tokenOrRoute probes each authority against a buffered writer and hands a token none of them holds to plain routing, so a consumer's own ACME client answers its own challenge through an ordinary path-scoped route. Four tests pin it, including the cache-key shape a restart-surviving token actually has.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package main
|
||||
|
||||
// The challenge path falls through for real. autocert's own HTTPHandler answers 404 itself for a
|
||||
// token it does not hold and never consults its fallback on the challenge path — the
|
||||
// predecessor's fault, the edge owning /.well-known/acme-challenge outright, rediscovered live
|
||||
// when Mailu's renewal died behind this proxy on cutover day (2026-09-26). These tests pin the
|
||||
// three behaviours tokenOrRoute exists for.
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
func routedTo(t *testing.T, marker string) http.Handler {
|
||||
t.Helper()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write([]byte(marker)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestATokenNoAuthorityHoldsIsRoutedNot404d(t *testing.T) {
|
||||
m := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(t.TempDir())}
|
||||
h := tokenOrRoute(routedTo(t, "the workload answered"), m)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "http://mail.example/.well-known/acme-challenge/somebody-elses-token", nil))
|
||||
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "the workload answered" {
|
||||
t.Fatalf("a token no authority holds must reach plain routing; got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestATokenAManagerHoldsIsAnsweredByIt(t *testing.T) {
|
||||
// autocert reads a token it does not have in memory from its cache, under "<token>+http-01" —
|
||||
// which is also how a token would survive the manager restarting mid-issuance.
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "held-token+http-01"), []byte("the-key-authorization"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(dir)}
|
||||
h := tokenOrRoute(routedTo(t, "must not be reached"), m)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "http://mail.example/.well-known/acme-challenge/held-token", nil))
|
||||
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "the-key-authorization" {
|
||||
t.Fatalf("the manager holding a token answers it; got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondAuthorityIsProbedBeforeRouting(t *testing.T) {
|
||||
first := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(t.TempDir())}
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "internal-token+http-01"), []byte("internal-key"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(dir)}
|
||||
h := tokenOrRoute(routedTo(t, "must not be reached"), first, second)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "http://git.internal/.well-known/acme-challenge/internal-token", nil))
|
||||
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "internal-key" {
|
||||
t.Fatalf("the second authority's token is found by probing past the first; got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnOrdinaryPathNeverTouchesTheChallengeMachinery(t *testing.T) {
|
||||
m := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(t.TempDir())}
|
||||
h := tokenOrRoute(routedTo(t, "routed"), m)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "http://site.example/index.html", nil))
|
||||
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != "routed" {
|
||||
t.Fatalf("an ordinary path goes straight to routing; got %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user