github.com/Tyktechnologies/tyk@v2.9.5+incompatible/gateway/event_handler_webhooks.go (about) 1 package gateway 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/hex" 7 "encoding/json" 8 "html/template" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/sirupsen/logrus" 17 18 "github.com/TykTechnologies/tyk/apidef" 19 "github.com/TykTechnologies/tyk/config" 20 "github.com/TykTechnologies/tyk/headers" 21 "github.com/TykTechnologies/tyk/storage" 22 ) 23 24 type WebHookRequestMethod string 25 26 const ( 27 WH_GET WebHookRequestMethod = "GET" 28 WH_PUT WebHookRequestMethod = "PUT" 29 WH_POST WebHookRequestMethod = "POST" 30 WH_DELETE WebHookRequestMethod = "DELETE" 31 WH_PATCH WebHookRequestMethod = "PATCH" 32 33 // Define the Event Handler name so we can register it 34 EH_WebHook apidef.TykEventHandlerName = "eh_web_hook_handler" 35 ) 36 37 // WebHookHandler is an event handler that triggers web hooks 38 type WebHookHandler struct { 39 conf config.WebHookHandlerConf 40 template *template.Template // non-nil if Init is run without error 41 store storage.Handler 42 43 contentType string 44 dashboardService DashboardServiceSender 45 } 46 47 // createConfigObject by default tyk will provide a map[string]interface{} type as a conf, converting it 48 // specifically here makes it easier to handle, only happens once, so not a massive issue, but not pretty 49 func (w *WebHookHandler) createConfigObject(handlerConf interface{}) (config.WebHookHandlerConf, error) { 50 newConf := config.WebHookHandlerConf{} 51 52 asJSON, _ := json.Marshal(handlerConf) 53 if err := json.Unmarshal(asJSON, &newConf); err != nil { 54 log.WithFields(logrus.Fields{ 55 "prefix": "webhooks", 56 }).Error("Format of webhook configuration is incorrect: ", err) 57 return newConf, err 58 } 59 60 return newConf, nil 61 } 62 63 // Init enables the init of event handler instances when they are created on ApiSpec creation 64 func (w *WebHookHandler) Init(handlerConf interface{}) error { 65 var err error 66 w.conf, err = w.createConfigObject(handlerConf) 67 if err != nil { 68 log.WithFields(logrus.Fields{ 69 "prefix": "webhooks", 70 }).Error("Problem getting configuration, skipping. ", err) 71 return err 72 } 73 74 w.store = &storage.RedisCluster{KeyPrefix: "webhook.cache."} 75 w.store.Connect() 76 77 // Pre-load template on init 78 if w.conf.TemplatePath != "" { 79 w.template, err = template.ParseFiles(w.conf.TemplatePath) 80 if err != nil { 81 log.WithFields(logrus.Fields{ 82 "prefix": "webhooks", 83 "target": w.conf.TargetPath, 84 }).Warning("Custom template load failure, using default: ", err) 85 } 86 87 if strings.HasSuffix(w.conf.TemplatePath, ".json") { 88 w.contentType = headers.ApplicationJSON 89 } 90 } 91 92 // We use the default if TemplatePath was empty or if we failed 93 // to load it. 94 if w.template == nil { 95 log.WithFields(logrus.Fields{ 96 "prefix": "webhooks", 97 "target": w.conf.TargetPath, 98 }).Info("Loading default template.") 99 defaultPath := filepath.Join(config.Global().TemplatePath, "default_webhook.json") 100 w.template, err = template.ParseFiles(defaultPath) 101 if err != nil { 102 log.WithFields(logrus.Fields{ 103 "prefix": "webhooks", 104 }).Error("Could not load the default template: ", err) 105 return err 106 } 107 w.contentType = headers.ApplicationJSON 108 } 109 110 log.WithFields(logrus.Fields{ 111 "prefix": "webhooks", 112 }).Debug("Timeout set to: ", w.conf.EventTimeout) 113 114 if !w.checkURL(w.conf.TargetPath) { 115 log.WithFields(logrus.Fields{ 116 "prefix": "webhooks", 117 }).Error("Init failed for this webhook, invalid URL, URL must be absolute") 118 } 119 120 if config.Global().UseDBAppConfigs { 121 dashboardServiceInit() 122 w.dashboardService = DashService 123 } 124 125 return nil 126 } 127 128 // hookFired checks if an event has been fired within the EventTimeout setting 129 func (w *WebHookHandler) WasHookFired(checksum string) bool { 130 if _, err := w.store.GetKey(checksum); err != nil { 131 // Key not found, so hook is in limit 132 log.WithFields(logrus.Fields{ 133 "prefix": "webhooks", 134 }).Debug("Event can fire, no duplicates found") 135 return false 136 } 137 138 return true 139 } 140 141 // setHookFired will create an expiring key for the checksum of the event 142 func (w *WebHookHandler) setHookFired(checksum string) { 143 log.WithFields(logrus.Fields{ 144 "prefix": "webhooks", 145 }).Debug("Setting Webhook Checksum: ", checksum) 146 w.store.SetKey(checksum, "1", w.conf.EventTimeout) 147 } 148 149 func (w *WebHookHandler) getRequestMethod(m string) WebHookRequestMethod { 150 upper := WebHookRequestMethod(strings.ToUpper(m)) 151 switch upper { 152 case WH_GET, WH_PUT, WH_POST, WH_DELETE, WH_PATCH: 153 return upper 154 default: 155 log.WithFields(logrus.Fields{ 156 "prefix": "webhooks", 157 }).Warning("Method must be one of GET, PUT, POST, DELETE or PATCH, defaulting to GET") 158 return WH_GET 159 } 160 } 161 162 func (w *WebHookHandler) checkURL(r string) bool { 163 log.WithFields(logrus.Fields{ 164 "prefix": "webhooks", 165 }).Debug("Checking URL: ", r) 166 if _, err := url.ParseRequestURI(r); err != nil { 167 log.WithFields(logrus.Fields{ 168 "prefix": "webhooks", 169 }).Error("Failed to parse URL! ", err, r) 170 return false 171 } 172 return true 173 } 174 175 func (w *WebHookHandler) Checksum(reqBody string) (string, error) { 176 // We do this twice because fuck it. 177 localRequest, _ := http.NewRequest(string(w.getRequestMethod(w.conf.Method)), w.conf.TargetPath, strings.NewReader(reqBody)) 178 h := md5.New() 179 localRequest.Write(h) 180 return hex.EncodeToString(h.Sum(nil)), nil 181 } 182 183 func (w *WebHookHandler) BuildRequest(reqBody string) (*http.Request, error) { 184 req, err := http.NewRequest(string(w.getRequestMethod(w.conf.Method)), w.conf.TargetPath, strings.NewReader(reqBody)) 185 if err != nil { 186 log.WithFields(logrus.Fields{ 187 "prefix": "webhooks", 188 }).Error("Failed to create request object: ", err) 189 return nil, err 190 } 191 192 req.Header.Set(headers.UserAgent, headers.TykHookshot) 193 194 for key, val := range w.conf.HeaderList { 195 req.Header.Set(key, val) 196 } 197 198 if req.Header.Get(headers.ContentType) == "" { 199 req.Header.Set(headers.ContentType, w.contentType) 200 } 201 202 return req, nil 203 } 204 205 func (w *WebHookHandler) CreateBody(em config.EventMessage) (string, error) { 206 var reqBody bytes.Buffer 207 w.template.Execute(&reqBody, em) 208 209 return reqBody.String(), nil 210 } 211 212 // HandleEvent will be fired when the event handler instance is found in an APISpec EventPaths object during a request chain 213 func (w *WebHookHandler) HandleEvent(em config.EventMessage) { 214 215 // Inject event message into template, render to string 216 reqBody, _ := w.CreateBody(em) 217 218 // Construct request (method, body, params) 219 req, err := w.BuildRequest(reqBody) 220 if err != nil { 221 return 222 } 223 224 // Generate signature for request 225 reqChecksum, _ := w.Checksum(reqBody) 226 227 // Check request velocity for this hook (wasHookFired()) 228 if w.WasHookFired(reqChecksum) { 229 return 230 } 231 232 cli := &http.Client{Timeout: 30 * time.Second} 233 234 resp, err := cli.Do(req) 235 if err != nil { 236 log.WithFields(logrus.Fields{ 237 "prefix": "webhooks", 238 }).Error("Webhook request failed: ", err) 239 } else { 240 defer resp.Body.Close() 241 if resp.StatusCode >= 200 && resp.StatusCode < 300 { 242 content, err := ioutil.ReadAll(resp.Body) 243 if err == nil { 244 log.WithFields(logrus.Fields{ 245 "prefix": "webhooks", 246 "responseCode": resp.StatusCode, 247 }).Debug(string(content)) 248 } else { 249 log.WithFields(logrus.Fields{ 250 "prefix": "webhooks", 251 }).Error(err) 252 } 253 254 } else { 255 log.WithFields(logrus.Fields{ 256 "prefix": "webhooks", 257 "responseCode": resp.StatusCode, 258 }).Error("Request to webhook failed") 259 } 260 } 261 262 if w.dashboardService != nil && em.Type == EventTriggerExceeded { 263 w.dashboardService.NotifyDashboardOfEvent(em.Meta) 264 } 265 266 w.setHookFired(reqChecksum) 267 }