forked from TykTechnologies/tyk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
event_handler_webhooks.go
245 lines (206 loc) · 6.88 KB
/
event_handler_webhooks.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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/config"
)
type WebHookRequestMethod string
const (
WH_GET WebHookRequestMethod = "GET"
WH_PUT WebHookRequestMethod = "PUT"
WH_POST WebHookRequestMethod = "POST"
WH_DELETE WebHookRequestMethod = "DELETE"
WH_PATCH WebHookRequestMethod = "PATCH"
// Define the Event Handler name so we can register it
EH_WebHook apidef.TykEventHandlerName = "eh_web_hook_handler"
)
// WebHookHandler is an event handler that triggers web hooks
type WebHookHandler struct {
conf config.WebHookHandlerConf
template *template.Template
store StorageHandler
}
// Not Pretty, but will avoi dmillions of connections
var WebHookRedisStorePointer *RedisClusterStorageManager
// GetRedisInterfacePointer creates a reference to a redis connection pool that can be shared across all webhook instances
func GetRedisInterfacePointer() *RedisClusterStorageManager {
if WebHookRedisStorePointer == nil {
WebHookRedisStorePointer = &RedisClusterStorageManager{KeyPrefix: "webhook.cache."}
WebHookRedisStorePointer.Connect()
}
return WebHookRedisStorePointer
}
// createConfigObject by default tyk will provide a ma[string]interface{} type as a conf, converting it
// specifically here makes it easier to handle, only happens once, so not a massive issue, but not pretty
func (w *WebHookHandler) createConfigObject(handlerConf interface{}) (config.WebHookHandlerConf, error) {
newConf := config.WebHookHandlerConf{}
asJSON, _ := json.Marshal(handlerConf)
if err := json.Unmarshal(asJSON, &newConf); err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Format of webhook configuration is incorrect: ", err)
return newConf, err
}
return newConf, nil
}
// Init enables the init of event handler instances when they are created on ApiSpec creation
func (w *WebHookHandler) Init(handlerConf interface{}) error {
var err error
w.conf, err = w.createConfigObject(handlerConf)
if err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Problem getting configuration, skipping. ", err)
return err
}
// Get a storage reference
w.store = GetRedisInterfacePointer()
// Pre-load template on init
var webHookTemplate *template.Template
var templateLoaded bool
if w.conf.TemplatePath != "" {
webHookTemplate, err = template.ParseFiles(w.conf.TemplatePath)
if err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
"target": w.conf.TargetPath,
}).Warning("Custom template load failure, using default: ", err)
defaultPath := filepath.Join(globalConf.TemplatePath, "default_webhook.json")
webHookTemplate, _ = template.ParseFiles(defaultPath)
templateLoaded = true
}
}
if w.conf.TemplatePath == "" && templateLoaded {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
"target": w.conf.TargetPath,
}).Info("Loading default template.")
defaultPath := filepath.Join(globalConf.TemplatePath, "default_webhook.json")
webHookTemplate, _ = template.ParseFiles(defaultPath)
}
w.template = webHookTemplate
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Debug("Timeout set to: ", w.conf.EventTimeout)
if !w.checkURL(w.conf.TargetPath) {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Init failed for this webhook, invalid URL, URL must be absolute")
}
return nil
}
// hookFired checks if an event has been fired within the EventTimeout setting
func (w *WebHookHandler) WasHookFired(checksum string) bool {
if _, err := w.store.GetKey(checksum); err != nil {
// Key not found, so hook is in limit
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Debug("Event can fire, no duplicates found")
return false
}
return true
}
// setHookFired will create an expiring key for the checksum of the event
func (w *WebHookHandler) setHookFired(checksum string) {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Debug("Setting Webhook Checksum: ", checksum)
w.store.SetKey(checksum, "1", w.conf.EventTimeout)
}
func (w *WebHookHandler) getRequestMethod(m string) WebHookRequestMethod {
upper := WebHookRequestMethod(strings.ToUpper(m))
switch upper {
case WH_GET, WH_PUT, WH_POST, WH_DELETE, WH_PATCH:
return upper
default:
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Warning("Method must be one of GET, PUT, POST, DELETE or PATCH, defaulting to GET")
return WH_GET
}
}
func (w *WebHookHandler) checkURL(r string) bool {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Debug("Checking URL: ", r)
if _, err := url.ParseRequestURI(r); err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Failed to parse URL! ", err, r)
return false
}
return true
}
func (w *WebHookHandler) GetChecksum(reqBody string) (string, error) {
// We do this twice because fuck it.
localRequest, _ := http.NewRequest(string(w.getRequestMethod(w.conf.Method)), w.conf.TargetPath, strings.NewReader(reqBody))
h := md5.New()
localRequest.Write(h)
return hex.EncodeToString(h.Sum(nil)), nil
}
func (w *WebHookHandler) BuildRequest(reqBody string) (*http.Request, error) {
req, err := http.NewRequest(string(w.getRequestMethod(w.conf.Method)), w.conf.TargetPath, strings.NewReader(reqBody))
if err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Failed to create request object: ", err)
return nil, err
}
req.Header.Set("User-Agent", "Tyk-Hookshot")
for key, val := range w.conf.HeaderList {
req.Header.Set(key, val)
}
return req, nil
}
func (w *WebHookHandler) CreateBody(em config.EventMessage) (string, error) {
var reqBody bytes.Buffer
w.template.Execute(&reqBody, em)
return reqBody.String(), nil
}
// HandleEvent will be fired when the event handler instance is found in an APISpec EventPaths object during a request chain
func (w *WebHookHandler) HandleEvent(em config.EventMessage) {
// Inject event message into template, render to string
reqBody, _ := w.CreateBody(em)
// Construct request (method, body, params)
req, err := w.BuildRequest(reqBody)
if err != nil {
return
}
// Generate signature for request
reqChecksum, _ := w.GetChecksum(reqBody)
// Check request velocity for this hook (wasHookFired())
if w.WasHookFired(reqChecksum) {
return
}
// Fire web hook routine (setHookFired())
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error("Webhook request failed: ", err)
} else {
defer resp.Body.Close()
content, err := ioutil.ReadAll(resp.Body)
if err == nil {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Debug(string(content))
} else {
log.WithFields(logrus.Fields{
"prefix": "webhooks",
}).Error(err)
}
}
w.setHookFired(reqChecksum)
}