@@ -0,0 +1,115 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"source.offmarket.win/aleagle/eaglecast/models"
|
||||
)
|
||||
|
||||
type sendgridNotif struct {
|
||||
Email string `json:"email"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Event string `json:"event"`
|
||||
BounceClassification string `json:"bounce_classification"`
|
||||
|
||||
// SendGrid flattens all X-headers and adds them to the bounce
|
||||
// event notification.
|
||||
CampaignUUID string `json:"XEagleCastCampaign"`
|
||||
}
|
||||
|
||||
// Sendgrid handles Sendgrid/SNS webhook notifications including confirming SNS topic subscription
|
||||
// requests and bounce notifications.
|
||||
type Sendgrid struct {
|
||||
pubKey *ecdsa.PublicKey
|
||||
}
|
||||
|
||||
// NewSendgrid returns a new Sendgrid instance.
|
||||
func NewSendgrid(key string) (*Sendgrid, error) {
|
||||
// Get the certificate from the key.
|
||||
sigB, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pubKey, err := x509.ParsePKIXPublicKey(sigB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Sendgrid{pubKey: pubKey.(*ecdsa.PublicKey)}, nil
|
||||
}
|
||||
|
||||
// ProcessBounce processes Sendgrid bounce notifications and returns one or more Bounce objects.
|
||||
func (s *Sendgrid) ProcessBounce(sig, timestamp string, b []byte) ([]models.Bounce, error) {
|
||||
if err := s.verifyNotif(sig, timestamp, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var notifs []sendgridNotif
|
||||
if err := json.Unmarshal(b, ¬ifs); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshalling Sendgrid notification: %v", err)
|
||||
}
|
||||
|
||||
out := make([]models.Bounce, 0, len(notifs))
|
||||
for _, n := range notifs {
|
||||
if n.Event != "bounce" {
|
||||
continue
|
||||
}
|
||||
|
||||
typ := models.BounceTypeHard
|
||||
if n.BounceClassification == "technical" || n.BounceClassification == "content" {
|
||||
typ = models.BounceTypeSoft
|
||||
}
|
||||
|
||||
tstamp := time.Unix(n.Timestamp, 0)
|
||||
bn := models.Bounce{
|
||||
CampaignUUID: n.CampaignUUID,
|
||||
Email: strings.ToLower(n.Email),
|
||||
Type: typ,
|
||||
Meta: json.RawMessage(b),
|
||||
Source: "sendgrid",
|
||||
CreatedAt: tstamp,
|
||||
}
|
||||
out = append(out, bn)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// verifyNotif verifies the signature on a notification payload.
|
||||
func (s *Sendgrid) verifyNotif(sig, timestamp string, b []byte) error {
|
||||
sigB, err := base64.StdEncoding.DecodeString(sig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ecdsaSig := struct {
|
||||
R *big.Int
|
||||
S *big.Int
|
||||
}{}
|
||||
|
||||
if _, err := asn1.Unmarshal(sigB, &ecdsaSig); err != nil {
|
||||
return fmt.Errorf("error asn1 unmarshal of signature: %v", err)
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(timestamp))
|
||||
h.Write(b)
|
||||
hash := h.Sum(nil)
|
||||
|
||||
if !ecdsa.Verify(s.pubKey, hash, ecdsaSig.R, ecdsaSig.S) {
|
||||
return errors.New("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user