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 }