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