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  }