check / check (push) Waiting to run
The access log, the rate-limit rejection lines, the CSRF warning and the receiver's "webhook request received" line now carry clientIP, the address the rate limiters key on, next to remoteIP, the connecting peer. Logging works it out once per request from the same code the rate limiters use and stores it on the request context for the other lines. The CSRF and receiver lines name the peer as remoteIP instead of remote_addr. The README documents the field and that it is only as trustworthy as TRUSTED_PROXIES. Model: opus-5-5
125 lines
3.0 KiB
Go
125 lines
3.0 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
)
|
|
|
|
// TestHandleWebhook_LogsClientNextToThePeer checks that the
|
|
// receiver's "webhook request received" line carries both addresses:
|
|
// remoteIP, the connecting peer, and clientIP, the client the access
|
|
// log attributes the request to.
|
|
func TestHandleWebhook_LogsClientNextToThePeer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// untrustedPeer is outside the trusted 10.0.0.0/8, so its
|
|
// X-Forwarded-For is ignored and it is the client.
|
|
const untrustedPeer = "192.0.2.10"
|
|
|
|
cases := map[string]struct {
|
|
peer string
|
|
wantRemote string
|
|
wantClient string
|
|
}{
|
|
"trusted proxy with a forwarded chain": {
|
|
peer: "10.0.0.1:44444",
|
|
wantRemote: "10.0.0.1",
|
|
wantClient: "198.51.100.7",
|
|
},
|
|
"untrusted peer": {
|
|
peer: untrustedPeer + ":5555",
|
|
wantRemote: untrustedPeer,
|
|
wantClient: untrustedPeer,
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
mw *middleware.Middleware
|
|
db *database.Database
|
|
)
|
|
|
|
app := newTestAppWithConfig(t, &config.Config{
|
|
DataDir: t.TempDir(),
|
|
TrustedProxies: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.0.0/8"),
|
|
},
|
|
}, &h, &mw, &db)
|
|
app.RequireStart()
|
|
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
buf := new(bytes.Buffer)
|
|
h.SetLogForTest(slog.New(slog.NewJSONHandler(buf, nil)))
|
|
|
|
webhook := seedWebhook(t, db)
|
|
seedEntrypoint(t, db, webhook.ID)
|
|
|
|
// Logging is what works the client address out, so the
|
|
// request goes through it as it does in production.
|
|
router := chi.NewRouter()
|
|
router.Use(mw.Logging())
|
|
router.Post("/h/{uuid}", h.HandleWebhook())
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), http.MethodPost,
|
|
"/h/ep-"+webhook.ID, strings.NewReader("{}"),
|
|
)
|
|
req.RemoteAddr = tc.peer
|
|
req.Header.Set("X-Forwarded-For", "198.51.100.7, 10.0.0.2")
|
|
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
line := receivedLine(t, buf)
|
|
assert.Equal(t, tc.wantRemote, line["remoteIP"])
|
|
assert.Equal(t, tc.wantClient, line["clientIP"])
|
|
})
|
|
}
|
|
}
|
|
|
|
// receivedLine returns the one "webhook request received" line in the
|
|
// captured JSON log.
|
|
func receivedLine(t *testing.T, buf *bytes.Buffer) map[string]any {
|
|
t.Helper()
|
|
|
|
var found []map[string]any
|
|
|
|
for line := range strings.SplitSeq(
|
|
strings.TrimSpace(buf.String()), "\n",
|
|
) {
|
|
var entry map[string]any
|
|
|
|
require.NoError(t, json.Unmarshal([]byte(line), &entry))
|
|
|
|
if entry["msg"] == "webhook request received" {
|
|
found = append(found, entry)
|
|
}
|
|
}
|
|
|
|
require.Len(t, found, 1)
|
|
|
|
return found[0]
|
|
}
|