forked from liping86/snshttp
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathhandler.go
127 lines (99 loc) · 2.85 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
124
125
126
127
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 message) error {
data, err := ioutil.ReadAll(reader)
if err != nil {
return err
}
err = json.Unmarshal(data, event)
if err != nil {
return err
}
if err = Verify(event); err != nil {
return err
}
return nil
}