forked from deadmanssnitch/snshttp
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathhandler.go
123 lines (96 loc) · 2.8 KB
/
handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package snshttp
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"time"
)
const (
// requestTimeout is how long Amazon will wait from a response from the
// server before considering the request failed. This does not appear to be
// configurable, see the documentation below.
//
// https://docs.aws.amazon.com/sns/latest/dg/DeliveryPolicies.html#delivery-policy-maximum-receive-rate
requestTimeout = 15 * time.Second
)
type handler struct {
handler EventHandler
credentials *authOption
}
// New creates a http.Handler for receiving webhooks from an Amazon SNS
// subscription and dispatching them to the EventHandler. Options are applied
// in the order they're provided and may clobber previous options.
func New(eventHandler EventHandler, opts ...Option) http.Handler {
handler := &handler{
handler: eventHandler,
}
for _, opt := range opts {
opt.apply(handler)
}
return handler
}
func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
var err error
if !h.credentials.Check(req) {
resp.Header().Set("WWW-Authenticate", `Basic realm="ses"`)
http.Error(resp, "Unauthorized", http.StatusUnauthorized)
return
}
// Amazon will consider a request failed if it takes longer than 15 seconds
// to execute. This does not appear to be configurable.
ctx, cancel := context.WithTimeout(req.Context(), requestTimeout)
defer cancel()
// Wrap the Request with the timeout so it's enforced when reading the body.
req = req.WithContext(ctx)
// Use the Type header so we can avoid parsing the body unless we know it's
// an event we support.
switch req.Header.Get("X-Amz-Sns-Message-Type") {
// Notifications should be the most common case and switch statements are
// checked in definition order.
case "Notification":
event := &Notification{}
err = readEvent(req.Body, event)
if err != nil {
break
}
err = h.handler.Notification(ctx, event)
case "SubscriptionConfirmation":
event := &SubscriptionConfirmation{}
err = readEvent(req.Body, event)
if err != nil {
break
}
err = h.handler.SubscriptionConfirmation(ctx, event)
case "UnsubscribeConfirmation":
event := &UnsubscribeConfirmation{}
err = readEvent(req.Body, event)
if err != nil {
break
}
err = h.handler.UnsubscribeConfirmation(ctx, event)
// Amazon (or someone else?) sent an unknown type
default:
http.NotFound(resp, req)
return
}
if err != nil {
http.Error(resp, err.Error(), http.StatusInternalServerError)
return
}
// Success! Signals Amazon to mark message as received.
resp.WriteHeader(http.StatusNoContent)
}
// readEvent reads and parses
func readEvent(reader io.Reader, event interface{}) error {
data, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
err = json.Unmarshal(data, event)
if err != nil {
return err
}
return nil
}