Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mailgun: fix signature errors #7

Merged
merged 5 commits into from
Jun 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
14 changes: 13 additions & 1 deletion auth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,14 +410,19 @@ 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) // 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
}

tok := GetToken(req)
if tok == "" {
return false
}

// 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)
Expand All @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions smoketest/harness/harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type Harness struct {
tw *twServer
twS *httptest.Server

cfg config.Config

slack *slackServer
slackS *httptest.Server
slackApp mockslack.AppInfo
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
108 changes: 108 additions & 0 deletions smoketest/mailgunalerts_test.go
Original file line number Diff line number Diff line change
@@ -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
}
}