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 }