123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 |
- package webhook
- import (
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "net/http"
- "time"
- )
- // Client handles webhook operations
- type Client struct {
- config Config
- httpClient *http.Client
- }
- // Payload represents the webhook payload
- type Payload struct {
- Email string `json:"email"`
- Domain string `json:"domain"`
- Action string `json:"action"`
- Timestamp time.Time `json:"timestamp"`
- }
- // New creates a new webhook client with the provided configuration
- func New(cfg Config) (*Client, error) {
- if cfg.BaseURL == "" || cfg.Domain == "" || cfg.SecretKey == "" {
- return nil, ErrInvalidConfig
- }
- // Apply defaults if not set
- if cfg.Timeout == 0 {
- cfg.Timeout = DefaultConfig().Timeout
- }
- if cfg.MaxRetries == 0 {
- cfg.MaxRetries = DefaultConfig().MaxRetries
- }
- if cfg.RetryBackoff == 0 {
- cfg.RetryBackoff = DefaultConfig().RetryBackoff
- }
- return &Client{
- config: cfg,
- httpClient: &http.Client{
- Timeout: cfg.Timeout,
- },
- }, nil
- }
- // Send sends a webhook with the provided email and action
- func (c *Client) Send(ctx context.Context, email, action string) error {
- payload := Payload{
- Email: email,
- Domain: c.config.Domain,
- Action: action,
- Timestamp: time.Now(),
- }
- payloadBytes, err := json.Marshal(payload)
- if err != nil {
- return NewError("marshal", ErrPayloadMarshall, err.Error())
- }
- // Calculate signature
- signature := c.calculateHMAC(payloadBytes)
- // Create request
- req, err := http.NewRequestWithContext(
- ctx,
- http.MethodPost,
- fmt.Sprintf("%s/webhook/user-sync", c.config.BaseURL),
- bytes.NewBuffer(payloadBytes),
- )
- if err != nil {
- return NewError("create_request", ErrRequestCreation, err.Error())
- }
- // Set headers
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-Webhook-Signature", signature)
- req.Header.Set("X-Webhook-Domain", c.config.Domain)
- // Send request with retries
- var lastErr error
- for retry := 0; retry < c.config.MaxRetries; retry++ {
- select {
- case <-ctx.Done():
- return ctx.Err()
- default:
- resp, err := c.httpClient.Do(req)
- if err != nil {
- lastErr = err
- time.Sleep(c.config.RetryBackoff * time.Duration(retry+1))
- continue
- }
- defer resp.Body.Close()
- if resp.StatusCode == http.StatusOK {
- return nil
- }
- if resp.StatusCode == http.StatusTooManyRequests {
- lastErr = ErrTooManyRequests
- time.Sleep(c.config.RetryBackoff * time.Duration(retry+1))
- continue
- }
- // Permanent failure
- if resp.StatusCode >= 400 && resp.StatusCode != http.StatusTooManyRequests {
- return NewHTTPError(
- "send",
- resp.StatusCode,
- ErrRequestFailed,
- "permanent failure",
- )
- }
- }
- }
- if lastErr != nil {
- return NewError("send", ErrMaxRetriesReached, lastErr.Error())
- }
- return NewError("send", ErrMaxRetriesReached, "all retry attempts failed")
- }
- // calculateHMAC calculates the HMAC signature for the payload
- func (c *Client) calculateHMAC(message []byte) string {
- mac := hmac.New(sha256.New, []byte(c.config.SecretKey))
- mac.Write(message)
- return hex.EncodeToString(mac.Sum(nil))
- }
- // Close closes the webhook client and its resources
- func (c *Client) Close() error {
- if c.httpClient != nil {
- if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
- transport.CloseIdleConnections()
- }
- c.httpClient = nil
- }
- return nil
- }
- // ValidateSignature validates the HMAC signature of a webhook payload
- func ValidateSignature(payload []byte, signature, secretKey string) bool {
- mac := hmac.New(sha256.New, []byte(secretKey))
- mac.Write(payload)
- expectedMAC := hex.EncodeToString(mac.Sum(nil))
- return hmac.Equal([]byte(signature), []byte(expectedMAC))
- }
|