Files
headscale/hscontrol/app_test.go
T
Andrei Korviakov 7472e98f6c hscontrol: keep the ACME error body readable in acmeLogger
acmeLogger drained and closed the body of every ACME error response before
handing the response back to golang.org/x/crypto/acme. The client parses that
body to classify errors, so badNonce was no longer recognised: isBadNonce
returned false, clearNonces was never called and Client.post treated the 400 as
non-retriable.

One badNonce reply therefore stops certificate renewal for good. autocert
retries every 30-60 minutes, each attempt reuses a nonce stored from the
previous failed response, that nonce has already expired, and the loop repeats
until the process is restarted or the certificate expires.

Restore the body with a fresh reader after logging it.
2026-09-15 14:37:20 +02:00

54 lines
1.6 KiB
Go

package hscontrol
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSecurityHeaders(t *testing.T) {
handler := securityHeaders(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
rec := httptest.NewRecorder()
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/", nil)
handler.ServeHTTP(rec, req)
h := rec.Result().Header
assert.Equal(t, "DENY", h.Get("X-Frame-Options"))
assert.Equal(t, "frame-ancestors 'none'", h.Get("Content-Security-Policy"))
assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options"))
assert.Equal(t, "no-referrer", h.Get("Referrer-Policy"))
}
func TestACMELoggerKeepsErrorBodyReadable(t *testing.T) {
const problem = `{"type":"urn:ietf:params:acme:error:badNonce","detail":"JWS has an invalid anti-replay nonce","status":400}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(problem))
}))
defer server.Close()
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, nil)
require.NoError(t, err)
resp, err := (&acmeLogger{rt: http.DefaultTransport}).RoundTrip(req)
require.NoError(t, err)
defer resp.Body.Close()
// acme parses the body to classify errors such as badNonce, so it has to
// survive the logging middleware.
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.JSONEq(t, problem, string(body))
}