From 217b044bd24fb4d62fabeceb8646a5899cc4f9d4 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 5 Jun 2019 10:16:51 -0500 Subject: [PATCH 1/5] add mailgun config to harness --- smoketest/harness/harness.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/smoketest/harness/harness.go b/smoketest/harness/harness.go index 72e24e0459..bc0a0fb123 100644 --- a/smoketest/harness/harness.go +++ b/smoketest/harness/harness.go @@ -67,6 +67,8 @@ type Harness struct { tw *twServer twS *httptest.Server + cfg config.Config + slack *slackServer slackS *httptest.Server slackApp mockslack.AppInfo @@ -100,6 +102,10 @@ type Harness struct { authH *auth.Handler } +func (h *Harness) Config() config.Config { + return h.cfg +} + func runCmd(t *testing.T, c *exec.Cmd) { t.Helper() data, err := c.CombinedOutput() @@ -134,6 +140,7 @@ func NewHarnessDebugDB(t *testing.T, initSQL, migrationName string) *Harness { const ( twilioAuthToken = "11111111111111111111111111111111" twilioAccountSID = "AC00000000000000000000000000000000" + mailgunApiKey = "key-00000000000000000000000000000000" ) // NewStoppedHarness will create a NewHarness, but will not call Start. @@ -222,6 +229,11 @@ func (h *Harness) Start() { cfg.Twilio.AccountSID = twilioAccountSID cfg.Twilio.AuthToken = twilioAuthToken cfg.Twilio.FromNumber = h.phoneG.Get("twilio") + + cfg.Mailgun.Enable = true + cfg.Mailgun.APIKey = mailgunApiKey + cfg.Mailgun.EmailDomain = "smoketest.example.com" + h.cfg = cfg data, err := json.Marshal(cfg) if err != nil { h.t.Fatalf("failed to marshal config: %v", err) From 47613d4c72666c1c2c7148f28a1e9dd69186471a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 5 Jun 2019 10:20:58 -0500 Subject: [PATCH 2/5] Return 400 on form-parse errors (except mailgun) --- auth/handler.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/auth/handler.go b/auth/handler.go index aa55983cee..a1c248adae 100644 --- a/auth/handler.go +++ b/auth/handler.go @@ -410,6 +410,12 @@ func (h *Handler) setSessionCookie(w http.ResponseWriter, req *http.Request, val } func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next http.Handler) bool { + err := req.ParseMultipartForm(32 << 20) + if err != nil && err != http.ErrNotMultipart { + http.Error(w, err.Error(), 400) + return true + } + tok := GetToken(req) if tok == "" { return false @@ -417,7 +423,6 @@ func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next h // TODO: update once scopes are implemented ctx := req.Context() - var err error switch req.URL.Path { case "/v1/api/alerts", "/api/v2/generic/incoming": ctx, err = h.cfg.IntKeyStore.Authorize(ctx, tok, integrationkey.TypeGeneric) @@ -441,6 +446,13 @@ func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next h // Updating and clearing the session cookie is automatically handled. func (h *Handler) WrapHandler(wrapped http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/api/v2/mailgun/incoming" || req.URL.Path == "/v1/webhooks/mailgun" { + // Mailgun handles it's own auth and has special + // requirements on status codes, so we pass it through + // untouched. + wrapped.ServeHTTP(w, req) + return + } if h.authWithToken(w, req, wrapped) { return } From 5741be79b40a1d872323199e011d26b23d56bdfe Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 5 Jun 2019 10:24:51 -0500 Subject: [PATCH 3/5] add mailgun smoketest --- smoketest/mailgunalerts_test.go | 108 ++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 smoketest/mailgunalerts_test.go diff --git a/smoketest/mailgunalerts_test.go b/smoketest/mailgunalerts_test.go new file mode 100644 index 0000000000..c13b8ecd81 --- /dev/null +++ b/smoketest/mailgunalerts_test.go @@ -0,0 +1,108 @@ +package smoketest + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/target/goalert/smoketest/harness" +) + +// TestMailgunAlerts tests that GoAlert responds and +// processes Mailgun requests appropriately. +func TestMailgunAlerts(t *testing.T) { + t.Parallel() + + sql := ` + insert into users (id, name, email) + values + ({{uuid "user"}}, 'bob', 'joe'); + insert into user_contact_methods (id, user_id, name, type, value) + values + ({{uuid "cm1"}}, {{uuid "user"}}, 'personal', 'SMS', {{phone "1"}}); + + insert into user_notification_rules (user_id, contact_method_id, delay_minutes) + values + ({{uuid "user"}}, {{uuid "cm1"}}, 0); + + insert into escalation_policies (id, name) + values + ({{uuid "eid"}}, 'esc policy'); + insert into escalation_policy_steps (id, escalation_policy_id) + values + ({{uuid "esid"}}, {{uuid "eid"}}); + insert into escalation_policy_actions (escalation_policy_step_id, user_id) + values + ({{uuid "esid"}}, {{uuid "user"}}); + + insert into services (id, escalation_policy_id, name) + values + ({{uuid "sid"}}, {{uuid "eid"}}, 'service'); + + insert into integration_keys (id, type, service_id, name) + values + ({{uuid "intkey"}}, 'email', {{uuid "sid"}}, 'intkey'); +` + h := harness.NewHarness(t, sql, "trigger-config-sync") + defer h.Close() + + cfg := h.Config() + + v := make(url.Values) + v.Set("recipient", h.UUID("intkey")+"@"+cfg.Mailgun.EmailDomain) + v.Set("from", "foo@example.com") + v.Set("subject", "test alert") + v.Set("body-plain", "details") + + timestamp := time.Now().Format(time.RFC3339) + token := "some-token" + v.Set("timestamp", timestamp) + v.Set("token", token) + + hm := hmac.New(sha256.New, []byte(cfg.Mailgun.APIKey)) + io.WriteString(hm, timestamp) + io.WriteString(hm, token) + calculatedSignature := hm.Sum(nil) + + v.Set("signature", hex.EncodeToString(calculatedSignature)) + + resp, err := http.PostForm(h.URL()+"/api/v2/mailgun/incoming", v) + assert.Nil(t, err) + if !assert.Equal(t, 200, resp.StatusCode, "create alert (v2 URL)") { + return + } + + h.Twilio().Device(h.Phone("1")).ExpectSMS("test alert") + + v.Set("subject", "second alert") + resp, err = http.PostForm(h.URL()+"/v1/webhooks/mailgun", v) + assert.Nil(t, err) + if !assert.Equal(t, 200, resp.StatusCode, "create alert (v1 URL)") { + return + } + + h.Twilio().Device(h.Phone("1")).ExpectSMS("second alert") + + v.Set("body-plain", strings.Repeat("too big", 1<<20)) // ~7MiB + + resp, err = http.PostForm(h.URL()+"/api/v2/mailgun/incoming", v) + assert.Nil(t, err) + if !assert.Equal(t, 406, resp.StatusCode, "reject large bodies with 406 (v2 URL)") { + return + } + + v.Set("body-plain", strings.Repeat("too big", 1<<20)) // ~7MiB + + resp, err = http.PostForm(h.URL()+"/v1/webhooks/mailgun", v) + assert.Nil(t, err) + if !assert.Equal(t, 406, resp.StatusCode, "reject large bodies with 406 (v1 URL)") { + return + } +} From a6c798caf8eb093f27b1571cbc5aa038cef78676 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 5 Jun 2019 10:27:47 -0500 Subject: [PATCH 4/5] increase max req. body size 32KiB -> 256KiB --- app/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/cmd.go b/app/cmd.go index be2b2ecf99..8aeca4a264 100644 --- a/app/cmd.go +++ b/app/cmd.go @@ -527,7 +527,7 @@ func init() { RootCmd.Flags().Int("db-max-open", 15, "Max open DB connections.") RootCmd.Flags().Int("db-max-idle", 5, "Max idle DB connections.") - RootCmd.Flags().Int64("max-request-body-bytes", 32768, "Max body size for all incoming requests (in bytes). Set to 0 to disable limit.") + RootCmd.Flags().Int64("max-request-body-bytes", 256*1024, "Max body size for all incoming requests (in bytes). Set to 0 to disable limit.") RootCmd.Flags().Int("max-request-header-bytes", 4096, "Max header size for all incoming requests (in bytes). Set to 0 to disable limit.") RootCmd.Flags().String("github-base-url", "", "Base URL for GitHub auth and API calls.") From 86e3a8d2ec8c9cba61518b2bafdc0ae477097fb1 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Wed, 5 Jun 2019 10:38:23 -0500 Subject: [PATCH 5/5] Comment for chosen max memory value --- auth/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/handler.go b/auth/handler.go index a1c248adae..08049b55ce 100644 --- a/auth/handler.go +++ b/auth/handler.go @@ -410,7 +410,7 @@ func (h *Handler) setSessionCookie(w http.ResponseWriter, req *http.Request, val } func (h *Handler) authWithToken(w http.ResponseWriter, req *http.Request, next http.Handler) bool { - err := req.ParseMultipartForm(32 << 20) + err := req.ParseMultipartForm(32 << 20) // 32<<20 (32MiB) value is the `defaultMaxMemory` used in the net/http package when `req.FormValue` is called if err != nil && err != http.ErrNotMultipart { http.Error(w, err.Error(), 400) return true