package webhook import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net" "net/http" "syscall" "time" ) // Attempts is how many times a delivery is tried. Chapter 23.4. const Attempts = 3 // Client refuses a denied address, whatever a name resolved to or a redirect asks for. func Client() *http.Client { // Checked in the dialer, after resolving and before connecting, closing the window between. dialer := &net.Dialer{ Timeout: 10 * time.Second, Control: func(network, address string, c syscall.RawConn) error { return AllowedAddr(address) }, } return &http.Client{ Timeout: 30 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { // Re-check every redirect, because a pass that then hops to 169.254.169.254 is the attack. if len(via) >= 5 { return fmt.Errorf("too many redirects") } return Allowed(req.Context(), req.URL.String()) }, Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { if err := AllowedAddr(addr); err != nil { return nil, err } return dialer.DialContext(ctx, network, addr) }, }, } } // Sign is HMAC-SHA256, with the config naming a secret whose value lives on the server. func Sign(secret string, body []byte) string { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) return "sha256=" + hex.EncodeToString(mac.Sum(nil)) } // Deliver posts one event, retrying with backoff up to attempts times. func Deliver(ctx context.Context, url, secret string, payload any, attempts int) error { body, err := json.Marshal(payload) if err != nil { return err } if err := Allowed(ctx, url); err != nil { return err } client := Client() var last error for attempt := range max(attempts, 1) { if attempt > 0 { select { case <-ctx.Done(): return ctx.Err() case <-time.After(time.Duration(1<