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  }