github.com/argoproj/argo-events@v1.9.1/eventsources/sources/webhook/start.go (about)

     1  /*
     2  Copyright 2018 BlackRock, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhook
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"time"
    27  
    28  	"github.com/argoproj/argo-events/common"
    29  	"github.com/argoproj/argo-events/common/logging"
    30  	eventsourcecommon "github.com/argoproj/argo-events/eventsources/common"
    31  	"github.com/argoproj/argo-events/eventsources/common/webhook"
    32  	metrics "github.com/argoproj/argo-events/metrics"
    33  	apicommon "github.com/argoproj/argo-events/pkg/apis/common"
    34  	"github.com/argoproj/argo-events/pkg/apis/events"
    35  	"github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1"
    36  	"go.uber.org/zap"
    37  )
    38  
    39  var (
    40  	controller = webhook.NewController()
    41  )
    42  
    43  func init() {
    44  	go webhook.ProcessRouteStatus(controller)
    45  }
    46  
    47  // EventListener implements Eventing for webhook events
    48  type EventListener struct {
    49  	EventSourceName string
    50  	EventName       string
    51  	Webhook         v1alpha1.WebhookEventSource
    52  	Metrics         *metrics.Metrics
    53  }
    54  
    55  // GetEventSourceName returns name of event source
    56  func (el *EventListener) GetEventSourceName() string {
    57  	return el.EventSourceName
    58  }
    59  
    60  // GetEventName returns name of event
    61  func (el *EventListener) GetEventName() string {
    62  	return el.EventName
    63  }
    64  
    65  // GetEventSourceType return type of event server
    66  func (el *EventListener) GetEventSourceType() apicommon.EventSourceType {
    67  	return apicommon.WebhookEvent
    68  }
    69  
    70  // Router contains the configuration information for a route
    71  type Router struct {
    72  	// route contains information about a API endpoint
    73  	route *webhook.Route
    74  }
    75  
    76  // Implement Router
    77  // 1. GetRoute
    78  // 2. HandleRoute
    79  // 3. PostActivate
    80  // 4. PostDeactivate
    81  
    82  // GetRoute returns the route
    83  func (router *Router) GetRoute() *webhook.Route {
    84  	return router.route
    85  }
    86  
    87  // HandleRoute handles incoming requests on the route
    88  func (router *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) {
    89  	route := router.GetRoute()
    90  
    91  	logger := route.Logger.With(
    92  		logging.LabelEndpoint, route.Context.Endpoint,
    93  		logging.LabelPort, route.Context.Port,
    94  		logging.LabelHTTPMethod, route.Context.Method,
    95  	)
    96  
    97  	logger.Info("a request received, processing it...")
    98  
    99  	if !route.Active {
   100  		logger.Info("endpoint is not active, wont't process the request")
   101  		common.SendErrorResponse(writer, "endpoint is inactive")
   102  		return
   103  	}
   104  
   105  	if route.Context.Method != request.Method {
   106  		logger.Info("http method does not match")
   107  		common.SendErrorResponse(writer, "http method does not match")
   108  		return
   109  	}
   110  
   111  	defer func(start time.Time) {
   112  		route.Metrics.EventProcessingDuration(route.EventSourceName, route.EventName, float64(time.Since(start)/time.Millisecond))
   113  	}(time.Now())
   114  
   115  	body, err := GetBody(&writer, request, route, logger)
   116  	if err != nil {
   117  		logger.Errorw("failed to get body", zap.Error(err))
   118  		common.SendErrorResponse(writer, err.Error())
   119  		route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
   120  		return
   121  	}
   122  
   123  	payload := &events.WebhookEventData{
   124  		Header:   request.Header,
   125  		Body:     body,
   126  		Metadata: route.Context.Metadata,
   127  	}
   128  
   129  	data, err := json.Marshal(payload)
   130  	if err != nil {
   131  		logger.Errorw("failed to construct the event payload", zap.Error(err))
   132  		common.SendErrorResponse(writer, err.Error())
   133  		route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
   134  		return
   135  	}
   136  
   137  	logger.Info("dispatching event on route's data channel...")
   138  	route.DataCh <- data
   139  	logger.Info("successfully processed the request")
   140  	common.SendSuccessResponse(writer, "success")
   141  }
   142  
   143  // PostActivate performs operations once the route is activated and ready to consume requests
   144  func (router *Router) PostActivate() error {
   145  	return nil
   146  }
   147  
   148  // PostInactivate performs operations after the route is inactivated
   149  func (router *Router) PostInactivate() error {
   150  	return nil
   151  }
   152  
   153  // StartListening starts listening events
   154  func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
   155  	log := logging.FromContext(ctx).
   156  		With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName())
   157  	log.Info("started processing the webhook event source...")
   158  
   159  	route := webhook.NewRoute(&el.Webhook.WebhookContext, log, el.GetEventSourceName(), el.GetEventName(), el.Metrics)
   160  	return webhook.ManageRoute(ctx, &Router{
   161  		route: route,
   162  	}, controller, dispatch)
   163  }
   164  
   165  func GetBody(writer *http.ResponseWriter, request *http.Request, route *webhook.Route, logger *zap.SugaredLogger) (*json.RawMessage, error) {
   166  	switch request.Method {
   167  	case http.MethodGet:
   168  		body, _ := json.Marshal(request.URL.Query())
   169  		ret := json.RawMessage(body)
   170  		return &ret, nil
   171  	case http.MethodPost:
   172  		contentType := ""
   173  		if len(request.Header["Content-Type"]) > 0 {
   174  			contentType = request.Header["Content-Type"][0]
   175  		}
   176  
   177  		switch contentType {
   178  		case "application/x-www-form-urlencoded":
   179  			if err := request.ParseForm(); err != nil {
   180  				logger.Errorw("failed to parse form data", zap.Error(err))
   181  				common.SendInternalErrorResponse(*writer, err.Error())
   182  				route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
   183  				return nil, err
   184  			}
   185  			body, _ := json.Marshal(request.PostForm)
   186  			ret := json.RawMessage(body)
   187  			return &ret, nil
   188  		// default including "application/json" is parsing body as JSON
   189  		default:
   190  			request.Body = http.MaxBytesReader(*writer, request.Body, route.Context.GetMaxPayloadSize())
   191  			body, err := getRequestBody(request)
   192  			if err != nil {
   193  				logger.Errorw("failed to read request body", zap.Error(err))
   194  				common.SendErrorResponse(*writer, err.Error())
   195  				route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName)
   196  				return nil, err
   197  			}
   198  			ret := json.RawMessage(body)
   199  			return &ret, nil
   200  		}
   201  	default:
   202  		return nil, fmt.Errorf("unsupoorted method: %s", request.Method)
   203  	}
   204  }
   205  
   206  func getRequestBody(request *http.Request) ([]byte, error) {
   207  	// Read request payload
   208  	body, err := io.ReadAll(request.Body)
   209  	// Reset request.Body ReadCloser to prevent side-effect if re-read
   210  	request.Body = io.NopCloser(bytes.NewBuffer(body))
   211  	if err != nil {
   212  		return nil, fmt.Errorf("failed to parse request body, %w", err)
   213  	}
   214  	return body, nil
   215  }