/
abuse_protection.go
128 lines (108 loc) · 3.54 KB
/
abuse_protection.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
128
/*
Copyright 2021 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/
package http
import (
"context"
cecontext "github.com/cloudevents/sdk-go/v2/context"
"go.uber.org/zap"
"net/http"
"strconv"
"strings"
"time"
)
type WebhookConfig struct {
AllowedMethods []string // defaults to POST
AllowedRate *int
AutoACKCallback bool
AllowedOrigins []string
}
const (
DefaultAllowedRate = 1000
DefaultTimeout = time.Second * 600
)
// TODO: implement rate limiting.
// Throttling is indicated by requests being rejected using HTTP status code 429 Too Many Requests.
// TODO: use this if Webhook Request Origin has been turned on.
// Inbound requests should be rejected if Allowed Origins is required by SDK.
func (p *Protocol) OptionsHandler(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodOptions || p.WebhookConfig == nil {
rw.WriteHeader(http.StatusMethodNotAllowed)
return
}
headers := make(http.Header)
// The spec does not say we need to validate the origin, just the request origin.
// After the handshake, we will validate the origin.
if origin, ok := p.ValidateRequestOrigin(req); !ok {
rw.WriteHeader(http.StatusBadRequest)
return
} else {
headers.Set("WebHook-Allowed-Origin", origin)
}
allowedRateRequired := false
if _, ok := req.Header[http.CanonicalHeaderKey("WebHook-Request-Rate")]; ok {
// must send WebHook-Allowed-Rate
allowedRateRequired = true
}
if p.WebhookConfig.AllowedRate != nil {
headers.Set("WebHook-Allowed-Rate", strconv.Itoa(*p.WebhookConfig.AllowedRate))
} else if allowedRateRequired {
headers.Set("WebHook-Allowed-Rate", strconv.Itoa(DefaultAllowedRate))
}
if len(p.WebhookConfig.AllowedMethods) > 0 {
headers.Set("Allow", strings.Join(p.WebhookConfig.AllowedMethods, ", "))
} else {
headers.Set("Allow", http.MethodPost)
}
cb := req.Header.Get("WebHook-Request-Callback")
if cb != "" {
if p.WebhookConfig.AutoACKCallback {
go func() {
reqAck, err := http.NewRequest(http.MethodPost, cb, nil)
if err != nil {
cecontext.LoggerFrom(req.Context()).Errorw("OPTIONS handler failed to create http request attempting to ack callback.", zap.Error(err), zap.String("callback", cb))
return
}
// Write out the headers.
for k := range headers {
reqAck.Header.Set(k, headers.Get(k))
}
_, err = http.DefaultClient.Do(reqAck)
if err != nil {
cecontext.LoggerFrom(req.Context()).Errorw("OPTIONS handler failed to ack callback.", zap.Error(err), zap.String("callback", cb))
return
}
}()
return
} else {
cecontext.LoggerFrom(req.Context()).Infof("ACTION REQUIRED: Please validate web hook request callback: %q", cb)
// TODO: what to do pending https://github.com/cloudevents/spec/issues/617
return
}
}
// Write out the headers.
for k := range headers {
rw.Header().Set(k, headers.Get(k))
}
}
func (p *Protocol) ValidateRequestOrigin(req *http.Request) (string, bool) {
return p.validateOrigin(req.Header.Get("WebHook-Request-Origin"))
}
func (p *Protocol) ValidateOrigin(req *http.Request) (string, bool) {
return p.validateOrigin(req.Header.Get("Origin"))
}
func (p *Protocol) validateOrigin(ro string) (string, bool) {
cecontext.LoggerFrom(context.TODO()).Infow("Validating origin.", zap.String("origin", ro))
for _, ao := range p.WebhookConfig.AllowedOrigins {
if ao == "*" {
return ao, true
}
// TODO: it is not clear what the rules for allowed hosts are.
// Need to find docs for this. For now, test for prefix.
if strings.HasPrefix(ro, ao) {
return ao, true
}
}
return ro, false
}