package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const ToleranceSeconds = 5 * 60
func verifySettlxWebhook(rawBody []byte, signatureHeader, secret string) bool {
if signatureHeader == "" {
return false
}
var timestamp int64
var v1Sigs []string
for _, part := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "t":
ts, err := strconv.ParseInt(kv[1], 10, 64)
if err == nil {
timestamp = ts
}
case "v1":
v1Sigs = append(v1Sigs, kv[1])
}
}
if timestamp == 0 || len(v1Sigs) == 0 {
return false
}
// Reject if outside tolerance window (replay protection)
diff := time.Now().Unix() - timestamp
if diff < 0 {
diff = -diff
}
if diff > ToleranceSeconds {
return false
}
// Recompute signature: HMAC over `<timestamp>.<rawBody>`
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", timestamp)
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
expectedBytes := []byte(expected)
for _, candidate := range v1Sigs {
if hmac.Equal([]byte(candidate), expectedBytes) {
return true
}
}
return false
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
if !verifySettlxWebhook(
body,
r.Header.Get("X-Webhook-Signature"),
os.Getenv("SETTLX_WEBHOOK_SECRET"),
) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
var event map[string]interface{}
json.Unmarshal(body, &event)
// Process event...
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"received":true}`)
}