github.com/prebid/prebid-server/v2@v2.18.0/endpoints/events/event.go (about)

     1  package events
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"time"
    11  	"unicode"
    12  
    13  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    14  
    15  	"github.com/julienschmidt/httprouter"
    16  	accountService "github.com/prebid/prebid-server/v2/account"
    17  	"github.com/prebid/prebid-server/v2/analytics"
    18  	"github.com/prebid/prebid-server/v2/config"
    19  	"github.com/prebid/prebid-server/v2/errortypes"
    20  	"github.com/prebid/prebid-server/v2/metrics"
    21  	"github.com/prebid/prebid-server/v2/privacy"
    22  	"github.com/prebid/prebid-server/v2/stored_requests"
    23  	"github.com/prebid/prebid-server/v2/util/httputil"
    24  )
    25  
    26  const (
    27  	// Required
    28  	TemplateUrl        = "%v/event?t=%v&b=%v&a=%v"
    29  	TypeParameter      = "t"
    30  	VTypeParameter     = "vtype"
    31  	BidIdParameter     = "b"
    32  	AccountIdParameter = "a"
    33  
    34  	// Optional
    35  	BidderParameter          = "bidder"
    36  	TimestampParameter       = "ts"
    37  	FormatParameter          = "f"
    38  	AnalyticsParameter       = "x"
    39  	IntegrationTypeParameter = "int"
    40  )
    41  
    42  const integrationParamMaxLength = 64
    43  
    44  type eventEndpoint struct {
    45  	Accounts      stored_requests.AccountFetcher
    46  	Analytics     analytics.Runner
    47  	Cfg           *config.Configuration
    48  	TrackingPixel *httputil.Pixel
    49  	MetricsEngine metrics.MetricsEngine
    50  }
    51  
    52  func NewEventEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, analytics analytics.Runner, me metrics.MetricsEngine) httprouter.Handle {
    53  	ee := &eventEndpoint{
    54  		Accounts:      accounts,
    55  		Analytics:     analytics,
    56  		Cfg:           cfg,
    57  		TrackingPixel: &httputil.Pixel1x1PNG,
    58  		MetricsEngine: me,
    59  	}
    60  
    61  	return ee.Handle
    62  }
    63  
    64  func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    65  	// parse event request from http req
    66  	eventRequest, errs := ParseEventRequest(r)
    67  
    68  	// handle possible parsing errors
    69  	if len(errs) > 0 {
    70  		w.WriteHeader(http.StatusBadRequest)
    71  
    72  		for _, err := range errs {
    73  			fmt.Fprintf(w, "invalid request: %s\n", err.Error())
    74  		}
    75  
    76  		return
    77  	}
    78  
    79  	// validate account id
    80  	accountId, err := checkRequiredParameter(r, AccountIdParameter)
    81  
    82  	if err != nil {
    83  		w.WriteHeader(http.StatusUnauthorized)
    84  		fmt.Fprintf(w, "Account '%s' is required query parameter and can't be empty", AccountIdParameter)
    85  		return
    86  	}
    87  	eventRequest.AccountID = accountId
    88  
    89  	if eventRequest.Analytics != analytics.Enabled {
    90  		w.WriteHeader(http.StatusNoContent)
    91  		return
    92  	}
    93  
    94  	ctx := context.Background()
    95  	if e.Cfg.Event.TimeoutMS > 0 {
    96  		var cancel context.CancelFunc
    97  		ctx, cancel = context.WithTimeout(ctx, time.Duration(e.Cfg.Event.TimeoutMS)*time.Millisecond)
    98  		defer cancel()
    99  	}
   100  
   101  	// get account details
   102  	account, errs := accountService.GetAccount(ctx, e.Cfg, e.Accounts, eventRequest.AccountID, e.MetricsEngine)
   103  	if len(errs) > 0 {
   104  		status, messages := HandleAccountServiceErrors(errs)
   105  		w.WriteHeader(status)
   106  
   107  		for _, message := range messages {
   108  			fmt.Fprintf(w, "Invalid request: %s\n", message)
   109  		}
   110  		return
   111  	}
   112  
   113  	// Check if events are enabled for the account
   114  	if !account.Events.Enabled {
   115  		w.WriteHeader(http.StatusUnauthorized)
   116  		fmt.Fprintf(w, "Account '%s' doesn't support events", eventRequest.AccountID)
   117  		return
   118  	}
   119  
   120  	activities := privacy.NewActivityControl(&account.Privacy)
   121  
   122  	// handle notification event
   123  	e.Analytics.LogNotificationEventObject(&analytics.NotificationEvent{
   124  		Request: eventRequest,
   125  		Account: account,
   126  	}, activities)
   127  
   128  	// Add tracking pixel if format == image
   129  	if eventRequest.Format == analytics.Image {
   130  		w.WriteHeader(http.StatusOK)
   131  		w.Header().Add("Content-Type", e.TrackingPixel.ContentType)
   132  		w.Write(e.TrackingPixel.Content)
   133  
   134  		return
   135  	}
   136  
   137  	w.WriteHeader(http.StatusNoContent)
   138  }
   139  
   140  // EventRequestToUrl converts an analytics.EventRequest to an URL
   141  func EventRequestToUrl(externalUrl string, request *analytics.EventRequest) string {
   142  	s := fmt.Sprintf(TemplateUrl, externalUrl, request.Type, request.BidID, request.AccountID)
   143  
   144  	return s + optionalParameters(request)
   145  }
   146  
   147  // ParseEventRequest parses an analytics.EventRequest from an Http request
   148  func ParseEventRequest(r *http.Request) (*analytics.EventRequest, []error) {
   149  	event := &analytics.EventRequest{}
   150  	var errs []error
   151  	// validate type
   152  	if err := readType(event, r); err != nil {
   153  		errs = append(errs, err)
   154  	}
   155  
   156  	if event.Type == analytics.Vast {
   157  		if err := readVType(event, r); err != nil {
   158  			errs = append(errs, err)
   159  		}
   160  	} else {
   161  		if t := r.URL.Query().Get(VTypeParameter); t != "" {
   162  			errs = append(errs, &errortypes.BadInput{Message: "parameter 'vtype' is only required for t=vast"})
   163  		}
   164  	}
   165  
   166  	// validate bidid
   167  	if bidid, err := checkRequiredParameter(r, BidIdParameter); err != nil {
   168  		errs = append(errs, err)
   169  	} else {
   170  		event.BidID = bidid
   171  	}
   172  
   173  	// validate timestamp (optional)
   174  	if err := readTimestamp(event, r); err != nil {
   175  		errs = append(errs, err)
   176  	}
   177  
   178  	// validate format (optional)
   179  	if err := readFormat(event, r); err != nil {
   180  		errs = append(errs, err)
   181  	}
   182  
   183  	// validate analytics (optional)
   184  	if err := readAnalytics(event, r); err != nil {
   185  		errs = append(errs, err)
   186  	}
   187  
   188  	if err := readIntegrationType(event, r); err != nil {
   189  		errs = append(errs, err)
   190  	}
   191  
   192  	// Bidder
   193  	bidderName := r.URL.Query().Get(BidderParameter)
   194  	if normalisedBidderName, ok := openrtb_ext.NormalizeBidderName(bidderName); ok {
   195  		bidderName = normalisedBidderName.String()
   196  	}
   197  
   198  	event.Bidder = bidderName
   199  
   200  	return event, errs
   201  }
   202  
   203  // HandleAccountServiceErrors handles account.GetAccount errors
   204  func HandleAccountServiceErrors(errs []error) (status int, messages []string) {
   205  	messages = []string{}
   206  	status = http.StatusBadRequest
   207  
   208  	for _, er := range errs {
   209  		if errors.Is(er, context.DeadlineExceeded) {
   210  			er = &errortypes.Timeout{
   211  				Message: er.Error(),
   212  			}
   213  		}
   214  
   215  		messages = append(messages, er.Error())
   216  
   217  		errCode := errortypes.ReadCode(er)
   218  
   219  		if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode {
   220  			status = http.StatusServiceUnavailable
   221  		}
   222  		if errCode == errortypes.MalformedAcctErrorCode {
   223  			status = http.StatusInternalServerError
   224  		}
   225  		if errCode == errortypes.TimeoutErrorCode && status == http.StatusBadRequest {
   226  			status = http.StatusGatewayTimeout
   227  		}
   228  	}
   229  
   230  	return
   231  }
   232  
   233  func optionalParameters(request *analytics.EventRequest) string {
   234  	r := url.Values{}
   235  
   236  	// timestamp
   237  	if request.Timestamp > 0 {
   238  		r.Add(TimestampParameter, strconv.FormatInt(request.Timestamp, 10))
   239  	}
   240  
   241  	// bidder
   242  	if request.Bidder != "" {
   243  		r.Add(BidderParameter, request.Bidder)
   244  	}
   245  
   246  	// format
   247  	switch request.Format {
   248  	case analytics.Blank:
   249  		r.Add(FormatParameter, string(analytics.Blank))
   250  	case analytics.Image:
   251  		r.Add(FormatParameter, string(analytics.Image))
   252  	}
   253  
   254  	//analytics
   255  	switch request.Analytics {
   256  	case analytics.Enabled:
   257  		r.Add(AnalyticsParameter, string(analytics.Enabled))
   258  	case analytics.Disabled:
   259  		r.Add(AnalyticsParameter, string(analytics.Disabled))
   260  	}
   261  
   262  	if request.Integration != "" {
   263  		r.Add(IntegrationTypeParameter, request.Integration)
   264  	}
   265  
   266  	opt := r.Encode()
   267  
   268  	if opt != "" {
   269  		return "&" + opt
   270  	}
   271  
   272  	return opt
   273  }
   274  
   275  // readType validates analytics.EventRequest type
   276  func readType(er *analytics.EventRequest, httpRequest *http.Request) error {
   277  	t, err := checkRequiredParameter(httpRequest, TypeParameter)
   278  
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	switch t {
   284  	case string(analytics.Imp):
   285  		er.Type = analytics.Imp
   286  		return nil
   287  	case string(analytics.Win):
   288  		er.Type = analytics.Win
   289  		return nil
   290  	case string(analytics.Vast):
   291  		er.Type = analytics.Vast
   292  		return nil
   293  	default:
   294  		return &errortypes.BadInput{Message: fmt.Sprintf("unknown type: '%s'", t)}
   295  	}
   296  }
   297  
   298  // readVType validates analytics.EventRequest vtype
   299  func readVType(er *analytics.EventRequest, httpRequest *http.Request) error {
   300  	vtype, err := checkRequiredParameter(httpRequest, VTypeParameter)
   301  
   302  	if err != nil {
   303  		return err
   304  	}
   305  
   306  	switch vtype {
   307  	case string(analytics.Start):
   308  		er.VType = analytics.Start
   309  	case string(analytics.FirstQuartile):
   310  		er.VType = analytics.FirstQuartile
   311  	case string(analytics.MidPoint):
   312  		er.VType = analytics.MidPoint
   313  	case string(analytics.ThirdQuartile):
   314  		er.VType = analytics.ThirdQuartile
   315  	case string(analytics.Complete):
   316  		er.VType = analytics.Complete
   317  	default:
   318  		return &errortypes.BadInput{Message: fmt.Sprintf("unknown vtype: '%s'", vtype)}
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  // readFormat validates analytics.EventRequest format attribute
   325  func readFormat(er *analytics.EventRequest, httpRequest *http.Request) error {
   326  	f := httpRequest.URL.Query().Get(FormatParameter)
   327  
   328  	if f != "" {
   329  		switch f {
   330  		case string(analytics.Blank):
   331  			er.Format = analytics.Blank
   332  			return nil
   333  		case string(analytics.Image):
   334  			er.Format = analytics.Image
   335  			return nil
   336  		default:
   337  			return &errortypes.BadInput{Message: fmt.Sprintf("unknown format: '%s'", f)}
   338  		}
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  // readAnalytics validates analytics.EventRequest analytics attribute
   345  func readAnalytics(er *analytics.EventRequest, httpRequest *http.Request) error {
   346  	a := httpRequest.URL.Query().Get(AnalyticsParameter)
   347  
   348  	if a != "" {
   349  		switch a {
   350  		case string(analytics.Enabled):
   351  			er.Analytics = analytics.Enabled
   352  			return nil
   353  		case string(analytics.Disabled):
   354  			er.Analytics = analytics.Disabled
   355  			return nil
   356  		default:
   357  			return &errortypes.BadInput{Message: fmt.Sprintf("unknown analytics: '%s'", a)}
   358  		}
   359  	}
   360  
   361  	er.Analytics = analytics.Enabled
   362  	return nil
   363  }
   364  
   365  // readTimestamp validates analytics.EventRequest timestamp attribute
   366  func readTimestamp(er *analytics.EventRequest, httpRequest *http.Request) error {
   367  	t := httpRequest.URL.Query().Get(TimestampParameter)
   368  
   369  	if t != "" {
   370  		ts, err := strconv.ParseInt(t, 10, 64)
   371  
   372  		if err != nil {
   373  			return &errortypes.BadInput{Message: fmt.Sprintf("invalid request: error parsing timestamp '%s'", t)}
   374  		}
   375  
   376  		er.Timestamp = ts
   377  		return nil
   378  	}
   379  
   380  	return nil
   381  }
   382  
   383  // checkRequiredParameter checks if http.Request contains all required parameters
   384  func checkRequiredParameter(httpRequest *http.Request, parameter string) (string, error) {
   385  	t := httpRequest.URL.Query().Get(parameter)
   386  
   387  	if t == "" {
   388  		return "", &errortypes.BadInput{Message: fmt.Sprintf("parameter '%s' is required", parameter)}
   389  	}
   390  
   391  	return t, nil
   392  }
   393  
   394  func readIntegrationType(er *analytics.EventRequest, httpRequest *http.Request) error {
   395  	integrationType := httpRequest.URL.Query().Get(IntegrationParameter)
   396  	err := validateIntegrationType(integrationType)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	er.Integration = integrationType
   401  	return nil
   402  }
   403  
   404  func validateIntegrationType(integrationType string) error {
   405  	if len(integrationType) > integrationParamMaxLength {
   406  		return errors.New("integration type length is too long")
   407  	}
   408  	for _, char := range integrationType {
   409  		if !unicode.IsDigit(char) && !unicode.IsLetter(char) && char != '-' && char != '_' {
   410  			return errors.New("integration type can only contain numbers, letters and these characters '-', '_'")
   411  		}
   412  	}
   413  	return nil
   414  }