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 }