diff --git a/README.md b/README.md index 2afda084..cb985f03 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,7 @@ func main() { // You can find the Private API Key in your Account Menu, under "Settings": // (https://app.mailgun.com/app/account/security) mg := mailgun.NewMailgun("your-domain.com", "private-api-key") + mg.SetWebhookSigningKey("webhook-signing-key") http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/domains_test.go b/domains_test.go index 03bfba46..9cfb0993 100644 --- a/domains_test.go +++ b/domains_test.go @@ -11,8 +11,9 @@ import ( ) const ( - testDomain = "mailgun.test" - testKey = "api-fake-key" + testDomain = "mailgun.test" + testKey = "api-fake-key" + testWebhookSigningKey = "webhook-signing-key" ) func TestListDomains(t *testing.T) { diff --git a/examples/examples.go b/examples/examples.go index c3f04f8a..e1ac95fd 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -904,8 +904,9 @@ func UpdateWebhook(domain, apiKey string) error { return mg.UpdateWebhook(ctx, "clicked", []string{"https://your_domain.com/clicked"}) } -func VerifyWebhookSignature(domain, apiKey, timestamp, token, signature string) (bool, error) { +func VerifyWebhookSignature(domain, apiKey, webhookSigningKey, timestamp, token, signature string) (bool, error) { mg := mailgun.NewMailgun(domain, apiKey) + mg.SetWebhookSigningKey(webhookSigningKey) return mg.VerifyWebhookSignature(mailgun.Signature{ TimeStamp: timestamp, diff --git a/mailgun.go b/mailgun.go index c77616b7..584baa3a 100644 --- a/mailgun.go +++ b/mailgun.go @@ -6,11 +6,11 @@ // For further information please see the Mailgun documentation at // http://documentation.mailgun.com/ // -// Original Author: Michael Banzon -// Contributions: Samuel A. Falvo II -// Derrick J. Wippler +// Original Author: Michael Banzon +// Contributions: Samuel A. Falvo II +// Derrick J. Wippler // -// Examples +// # Examples // // All functions and method have a corresponding test, so if you don't find an // example for a function you'd like to know more about, please check for a @@ -18,7 +18,7 @@ // welcome as well. Feel free to submit a pull request or open a Github issue // if you cannot find an example to suit your needs. // -// List iterators +// # List iterators // // Most methods that begin with `List` return an iterator which simplfies // paging through large result sets returned by the mailgun API. Most `List` @@ -28,23 +28,22 @@ // // For example, the following iterates over all pages of events 100 items at a time // -// mg := mailgun.NewMailgun("your-domain.com", "your-api-key") -// it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100}) +// mg := mailgun.NewMailgun("your-domain.com", "your-api-key") +// it := mg.ListEvents(&mailgun.ListEventOptions{Limit: 100}) // -// // The entire operation should not take longer than 30 seconds -// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) -// defer cancel() +// // The entire operation should not take longer than 30 seconds +// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) +// defer cancel() // -// // For each page of 100 events -// var page []mailgun.Event -// for it.Next(ctx, &page) { -// for _, e := range page { -// // Do something with 'e' -// } -// } +// // For each page of 100 events +// var page []mailgun.Event +// for it.Next(ctx, &page) { +// for _, e := range page { +// // Do something with 'e' +// } +// } // -// -// License +// # License // // Copyright (c) 2013-2019, Michael Banzon. // All rights reserved. @@ -270,12 +269,13 @@ type Mailgun interface { // MailgunImpl bundles data needed by a large number of methods in order to interact with the Mailgun API. // Colloquially, we refer to instances of this structure as "clients." type MailgunImpl struct { - apiBase string - domain string - apiKey string - client *http.Client - baseURL string - overrideHeaders map[string]string + apiBase string + domain string + apiKey string + webhookSigningKey string + client *http.Client + baseURL string + overrideHeaders map[string]string mu sync.RWMutex capturedCurlOutput string @@ -292,7 +292,7 @@ func NewMailgun(domain, apiKey string) *MailgunImpl { } // NewMailgunFromEnv returns a new Mailgun client using the environment variables -// MG_API_KEY, MG_DOMAIN, and MG_URL +// MG_API_KEY, MG_DOMAIN, MG_URL, and MG_WEBHOOK_SIGNING_KEY func NewMailgunFromEnv() (*MailgunImpl, error) { apiKey := os.Getenv("MG_API_KEY") if apiKey == "" { @@ -310,6 +310,11 @@ func NewMailgunFromEnv() (*MailgunImpl, error) { mg.SetAPIBase(url) } + webhookSigningKey := os.Getenv("MG_WEBHOOK_SIGNING_KEY") + if webhookSigningKey != "" { + mg.SetWebhookSigningKey(webhookSigningKey) + } + return mg, nil } @@ -338,6 +343,16 @@ func (mg *MailgunImpl) SetClient(c *http.Client) { mg.client = c } +// WebhookSigningKey returns the webhook signing key configured for this client +func (mg *MailgunImpl) WebhookSigningKey() string { + return mg.webhookSigningKey +} + +// SetWebhookSigningKey updates the webhook signing key for this client +func (mg *MailgunImpl) SetWebhookSigningKey(webhookSigningKey string) { + mg.webhookSigningKey = webhookSigningKey +} + // SetOnBehalfOfSubaccount sets X-Mailgun-On-Behalf-Of header to SUBACCOUNT_ACCOUNT_ID in order to perform API request // on behalf of subaccount. func (mg *MailgunImpl) SetOnBehalfOfSubaccount(subaccountId string) { @@ -350,14 +365,15 @@ func (mg *MailgunImpl) RemoveOnBehalfOfSubaccount() { } // SetAPIBase updates the API Base URL for this client. -// // For EU Customers -// mg.SetAPIBase(mailgun.APIBaseEU) // -// // For US Customers -// mg.SetAPIBase(mailgun.APIBaseUS) +// // For EU Customers +// mg.SetAPIBase(mailgun.APIBaseEU) +// +// // For US Customers +// mg.SetAPIBase(mailgun.APIBaseUS) // -// // Set a custom base API -// mg.SetAPIBase("https://localhost/v3") +// // Set a custom base API +// mg.SetAPIBase("https://localhost/v3") func (mg *MailgunImpl) SetAPIBase(address string) { mg.apiBase = address } diff --git a/webhooks.go b/webhooks.go index 175df421..18cecbe0 100644 --- a/webhooks.go +++ b/webhooks.go @@ -123,7 +123,12 @@ type WebhookPayload struct { // Use this method to parse the webhook signature given as JSON in the webhook response func (mg *MailgunImpl) VerifyWebhookSignature(sig Signature) (verified bool, err error) { - h := hmac.New(sha256.New, []byte(mg.APIKey())) + key := mg.WebhookSigningKey() + // For backwards compatibility, fall back to the api key + if key == "" { + key = mg.APIKey() + } + h := hmac.New(sha256.New, []byte(key)) _, err = io.WriteString(h, sig.TimeStamp) if err != nil { @@ -149,7 +154,12 @@ func (mg *MailgunImpl) VerifyWebhookSignature(sig Signature) (verified bool, err // Deprecated: Please use the VerifyWebhookSignature() to parse the latest // version of WebHooks from mailgun func (mg *MailgunImpl) VerifyWebhookRequest(req *http.Request) (verified bool, err error) { - h := hmac.New(sha256.New, []byte(mg.APIKey())) + key := mg.WebhookSigningKey() + // For backwards compatibility, fall back to the api key + if key == "" { + key = mg.APIKey() + } + h := hmac.New(sha256.New, []byte(key)) _, err = io.WriteString(h, req.FormValue("timestamp")) if err != nil { diff --git a/webhooks_test.go b/webhooks_test.go index 0dcfcaf2..add8f8df 100644 --- a/webhooks_test.go +++ b/webhooks_test.go @@ -80,9 +80,10 @@ var signedTests = []bool{ func TestVerifyWebhookSignature(t *testing.T) { mg := mailgun.NewMailgun(testDomain, testKey) + mg.SetWebhookSigningKey(testWebhookSigningKey) for _, v := range signedTests { - fields := getSignatureFields(mg.APIKey(), v) + fields := getSignatureFields(mg.WebhookSigningKey(), v) sig := mailgun.Signature{ TimeStamp: fields["timestamp"], Token: fields["token"], @@ -100,9 +101,10 @@ func TestVerifyWebhookSignature(t *testing.T) { func TestVerifyWebhookRequest_Form(t *testing.T) { mg := mailgun.NewMailgun(testDomain, testKey) + mg.SetWebhookSigningKey(testWebhookSigningKey) for _, v := range signedTests { - fields := getSignatureFields(mg.APIKey(), v) + fields := getSignatureFields(mg.WebhookSigningKey(), v) req := buildFormRequest(fields) verified, err := mg.VerifyWebhookRequest(req) @@ -116,9 +118,10 @@ func TestVerifyWebhookRequest_Form(t *testing.T) { func TestVerifyWebhookRequest_MultipartForm(t *testing.T) { mg := mailgun.NewMailgun(testDomain, testKey) + mg.SetWebhookSigningKey(testWebhookSigningKey) for _, v := range signedTests { - fields := getSignatureFields(mg.APIKey(), v) + fields := getSignatureFields(mg.WebhookSigningKey(), v) req := buildMultipartFormRequest(fields) verified, err := mg.VerifyWebhookRequest(req)