github.com/argoproj/argo-events@v1.9.1/eventsources/sources/slack/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 slack 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "net/http" 26 "time" 27 28 "github.com/slack-go/slack" 29 "github.com/slack-go/slack/slackevents" 30 "go.uber.org/zap" 31 32 "github.com/argoproj/argo-events/common" 33 "github.com/argoproj/argo-events/common/logging" 34 eventsourcecommon "github.com/argoproj/argo-events/eventsources/common" 35 "github.com/argoproj/argo-events/eventsources/common/webhook" 36 "github.com/argoproj/argo-events/eventsources/sources" 37 metrics "github.com/argoproj/argo-events/metrics" 38 apicommon "github.com/argoproj/argo-events/pkg/apis/common" 39 "github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1" 40 ) 41 42 // controller controls the webhook operations 43 var ( 44 controller = webhook.NewController() 45 ) 46 47 // set up the activation and inactivation channels to control the state of routes. 48 func init() { 49 go webhook.ProcessRouteStatus(controller) 50 } 51 52 // EventListener implements Eventing for slack event source 53 type EventListener struct { 54 EventSourceName string 55 EventName string 56 SlackEventSource v1alpha1.SlackEventSource 57 Metrics *metrics.Metrics 58 } 59 60 // GetEventSourceName returns name of event source 61 func (el *EventListener) GetEventSourceName() string { 62 return el.EventSourceName 63 } 64 65 // GetEventName returns name of event 66 func (el *EventListener) GetEventName() string { 67 return el.EventName 68 } 69 70 // GetEventSourceType return type of event server 71 func (el *EventListener) GetEventSourceType() apicommon.EventSourceType { 72 return apicommon.SlackEvent 73 } 74 75 // Router contains information about a REST endpoint 76 type Router struct { 77 // route holds information to process an incoming request 78 route *webhook.Route 79 // slackEventSource is the event source which refers to configuration required to consume events from slack 80 slackEventSource *v1alpha1.SlackEventSource 81 // token is the slack token 82 token string 83 // refer to https://api.slack.com/docs/verifying-requests-from-slack 84 signingSecret string 85 } 86 87 // Implement Router 88 // 1. GetRoute 89 // 2. HandleRoute 90 // 3. PostActivate 91 // 4. PostDeactivate 92 93 // GetRoute returns the route 94 func (rc *Router) GetRoute() *webhook.Route { 95 return rc.route 96 } 97 98 // HandleRoute handles incoming requests on the route 99 func (rc *Router) HandleRoute(writer http.ResponseWriter, request *http.Request) { 100 route := rc.route 101 102 logger := route.Logger.With( 103 logging.LabelEndpoint, route.Context.Endpoint, 104 logging.LabelPort, route.Context.Port, 105 logging.LabelHTTPMethod, route.Context.Method, 106 ) 107 108 logger.Info("request a received, processing it...") 109 110 if !route.Active { 111 logger.Warn("endpoint is not active, won't process it") 112 common.SendErrorResponse(writer, "endpoint is inactive") 113 return 114 } 115 116 defer func(start time.Time) { 117 route.Metrics.EventProcessingDuration(route.EventSourceName, route.EventName, float64(time.Since(start)/time.Millisecond)) 118 }(time.Now()) 119 120 logger.Info("verifying the request...") 121 err := rc.verifyRequest(request) 122 if err != nil { 123 logger.Errorw("failed to validate the request", zap.Error(err)) 124 common.SendResponse(writer, http.StatusUnauthorized, err.Error()) 125 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 126 return 127 } 128 129 var data []byte 130 131 // Interactive element actions are always 132 // sent as application/x-www-form-urlencoded 133 // If request was generated by an interactive element or a slash command, it will be a POST form 134 if len(request.Header["Content-Type"]) > 0 && request.Header["Content-Type"][0] == "application/x-www-form-urlencoded" { 135 if err := request.ParseForm(); err != nil { 136 logger.Errorw("failed to parse form data", zap.Error(err)) 137 common.SendInternalErrorResponse(writer, err.Error()) 138 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 139 return 140 } 141 142 switch { 143 case request.PostForm.Get("payload") != "": 144 data, err = rc.handleInteraction(request) 145 if err != nil { 146 logger.Errorw("failed to process the interaction", zap.Error(err)) 147 common.SendInternalErrorResponse(writer, err.Error()) 148 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 149 return 150 } 151 152 case request.PostForm.Get("command") != "": 153 data, err = rc.handleSlashCommand(request) 154 if err != nil { 155 logger.Errorw("failed to process the slash command", zap.Error(err)) 156 common.SendInternalErrorResponse(writer, err.Error()) 157 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 158 return 159 } 160 161 default: 162 err = fmt.Errorf("could not determine slack type from form parameters") 163 logger.Errorw("failed to determine type of slack post", zap.Error(err)) 164 common.SendInternalErrorResponse(writer, err.Error()) 165 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 166 return 167 } 168 } else { 169 // If there's no payload in the post body, this is likely an 170 // Event API request. Parse and process if valid. 171 logger.Info("handling slack event...") 172 var response []byte 173 data, response, err = rc.handleEvent(request) 174 if err != nil { 175 logger.Errorw("failed to handle the event", zap.Error(err)) 176 common.SendInternalErrorResponse(writer, err.Error()) 177 route.Metrics.EventProcessingFailed(route.EventSourceName, route.EventName) 178 return 179 } 180 if response != nil { 181 writer.Header().Set("Content-Type", "text") 182 if _, err := writer.Write(response); err != nil { 183 logger.Errorw("failed to write the response for url verification", zap.Error(err)) 184 // don't return, we want to keep this running to give user chance to retry 185 } 186 } 187 } 188 189 if data != nil { 190 logger.Info("dispatching event on route's data channel...") 191 route.DataCh <- data 192 } 193 194 logger.Debug("request successfully processed") 195 common.SendSuccessResponse(writer, "success") 196 } 197 198 // PostActivate performs operations once the route is activated and ready to consume requests 199 func (rc *Router) PostActivate() error { 200 return nil 201 } 202 203 // PostInactivate performs operations after the route is inactivated 204 func (rc *Router) PostInactivate() error { 205 return nil 206 } 207 208 // handleEvent parse the slack notification and validates the event type 209 func (rc *Router) handleEvent(request *http.Request) ([]byte, []byte, error) { 210 var err error 211 var response []byte 212 var data []byte 213 body, err := rc.getRequestBody(request) 214 if err != nil { 215 return data, response, fmt.Errorf("failed to fetch request body, %w", err) 216 } 217 218 eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionVerifyToken(&slackevents.TokenComparator{VerificationToken: rc.token})) 219 if err != nil { 220 return data, response, fmt.Errorf("failed to extract event, %w", err) 221 } 222 223 if eventsAPIEvent.Type == slackevents.URLVerification { 224 var r *slackevents.ChallengeResponse 225 err = json.Unmarshal(body, &r) 226 if err != nil { 227 return data, response, fmt.Errorf("failed to verify the challenge, %w", err) 228 } 229 response = []byte(r.Challenge) 230 } 231 232 if eventsAPIEvent.Type == slackevents.CallbackEvent { 233 data, err = json.Marshal(&eventsAPIEvent.InnerEvent) 234 if err != nil { 235 return data, response, fmt.Errorf("failed to marshal event data, rejecting the event, %w", err) 236 } 237 } 238 239 return data, response, nil 240 } 241 242 func (rc *Router) handleInteraction(request *http.Request) ([]byte, error) { 243 payload := request.PostForm.Get("payload") 244 ie := &slack.InteractionCallback{} 245 err := json.Unmarshal([]byte(payload), ie) 246 if err != nil { 247 return nil, fmt.Errorf("failed to parse interaction event, %w", err) 248 } 249 250 data, err := json.Marshal(ie) 251 if err != nil { 252 return nil, fmt.Errorf("failed to marshal action data, %w", err) 253 } 254 255 return data, nil 256 } 257 258 func (rc *Router) handleSlashCommand(request *http.Request) ([]byte, error) { 259 command, err := slack.SlashCommandParse(request) 260 if err != nil { 261 return nil, fmt.Errorf("failed to parse command, %w", err) 262 } 263 264 data, err := json.Marshal(command) 265 if err != nil { 266 return nil, fmt.Errorf("failed to marshal command data, %w", err) 267 } 268 269 return data, nil 270 } 271 272 func (rc *Router) getRequestBody(request *http.Request) ([]byte, error) { 273 // Read request payload 274 body, err := io.ReadAll(io.LimitReader(request.Body, rc.route.Context.GetMaxPayloadSize())) 275 // Reset request.Body ReadCloser to prevent side-effect if re-read 276 request.Body = io.NopCloser(bytes.NewBuffer(body)) 277 if err != nil { 278 return nil, fmt.Errorf("failed to parse request body, %w", err) 279 } 280 return body, nil 281 } 282 283 // If a signing secret is provided, validate the request against the 284 // X-Slack-Signature header value. 285 // The signature is a hash generated as per Slack documentation at: 286 // https://api.slack.com/docs/verifying-requests-from-slack 287 func (rc *Router) verifyRequest(request *http.Request) error { 288 signingSecret := rc.signingSecret 289 if len(signingSecret) > 0 { 290 sv, err := slack.NewSecretsVerifier(request.Header, signingSecret) 291 if err != nil { 292 return fmt.Errorf("cannot create secrets verifier, %w", err) 293 } 294 295 // Read the request body 296 body, err := rc.getRequestBody(request) 297 if err != nil { 298 return err 299 } 300 301 _, err = sv.Write(body) 302 if err != nil { 303 return fmt.Errorf("error writing body: cannot verify signature, %w", err) 304 } 305 306 err = sv.Ensure() 307 if err != nil { 308 return fmt.Errorf("signature validation failed, %w", err) 309 } 310 } 311 return nil 312 } 313 314 // StartListening starts an event source 315 func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error { 316 log := logging.FromContext(ctx). 317 With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName()) 318 319 log.Info("started processing the Slack event source...") 320 defer sources.Recover(el.GetEventName()) 321 322 slackEventSource := &el.SlackEventSource 323 log.Info("retrieving the slack token...") 324 token, err := common.GetSecretFromVolume(slackEventSource.Token) 325 if err != nil { 326 return fmt.Errorf("failed to retrieve the token, %w", err) 327 } 328 329 log.Info("retrieving the signing secret...") 330 signingSecret, err := common.GetSecretFromVolume(slackEventSource.SigningSecret) 331 if err != nil { 332 return fmt.Errorf("failed to retrieve the signing secret, %w", err) 333 } 334 335 route := webhook.NewRoute(slackEventSource.Webhook, log, el.GetEventSourceName(), el.GetEventName(), el.Metrics) 336 337 return webhook.ManageRoute(ctx, &Router{ 338 route: route, 339 token: token, 340 signingSecret: signingSecret, 341 slackEventSource: slackEventSource, 342 }, controller, dispatch) 343 }