github.com/prebid/prebid-server/v2@v2.18.0/endpoints/openrtb2/video_auction.go (about) 1 package openrtb2 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "regexp" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/buger/jsonparser" 16 "github.com/gofrs/uuid" 17 "github.com/golang/glog" 18 "github.com/julienschmidt/httprouter" 19 "github.com/prebid/openrtb/v20/openrtb2" 20 "github.com/prebid/prebid-server/v2/hooks" 21 "github.com/prebid/prebid-server/v2/hooks/hookexecution" 22 "github.com/prebid/prebid-server/v2/ortb" 23 "github.com/prebid/prebid-server/v2/privacy" 24 jsonpatch "gopkg.in/evanphx/json-patch.v4" 25 26 accountService "github.com/prebid/prebid-server/v2/account" 27 "github.com/prebid/prebid-server/v2/analytics" 28 "github.com/prebid/prebid-server/v2/config" 29 "github.com/prebid/prebid-server/v2/errortypes" 30 "github.com/prebid/prebid-server/v2/exchange" 31 "github.com/prebid/prebid-server/v2/metrics" 32 "github.com/prebid/prebid-server/v2/openrtb_ext" 33 "github.com/prebid/prebid-server/v2/prebid_cache_client" 34 "github.com/prebid/prebid-server/v2/stored_requests" 35 "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" 36 "github.com/prebid/prebid-server/v2/usersync" 37 "github.com/prebid/prebid-server/v2/util/iputil" 38 "github.com/prebid/prebid-server/v2/util/jsonutil" 39 "github.com/prebid/prebid-server/v2/util/ptrutil" 40 "github.com/prebid/prebid-server/v2/util/uuidutil" 41 "github.com/prebid/prebid-server/v2/version" 42 ) 43 44 var defaultRequestTimeout int64 = 5000 45 46 func NewVideoEndpoint( 47 uuidGenerator uuidutil.UUIDGenerator, 48 ex exchange.Exchange, 49 validator openrtb_ext.BidderParamValidator, 50 requestsById stored_requests.Fetcher, 51 videoFetcher stored_requests.Fetcher, 52 accounts stored_requests.AccountFetcher, 53 cfg *config.Configuration, 54 met metrics.MetricsEngine, 55 analyticsRunner analytics.Runner, 56 disabledBidders map[string]string, 57 defReqJSON []byte, 58 bidderMap map[string]openrtb_ext.BidderName, 59 cache prebid_cache_client.Client, 60 tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, 61 ) (httprouter.Handle, error) { 62 63 if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil { 64 return nil, errors.New("NewVideoEndpoint requires non-nil arguments.") 65 } 66 67 defRequest := len(defReqJSON) > 0 68 69 ipValidator := iputil.PublicNetworkIPValidator{ 70 IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, 71 IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, 72 } 73 74 videoEndpointRegexp := regexp.MustCompile(`[<>]`) 75 76 return httprouter.Handle((&endpointDeps{ 77 uuidGenerator, 78 ex, 79 validator, 80 requestsById, 81 videoFetcher, 82 accounts, 83 cfg, 84 met, 85 analyticsRunner, 86 disabledBidders, 87 defRequest, 88 defReqJSON, 89 bidderMap, 90 cache, 91 videoEndpointRegexp, 92 ipValidator, 93 empty_fetcher.EmptyFetcher{}, 94 hooks.EmptyPlanBuilder{}, 95 tmaxAdjustments, 96 openrtb_ext.NormalizeBidderName}).VideoAuctionEndpoint), nil 97 } 98 99 /* 100 1. Parse "storedrequestid" field from simplified endpoint request body. 101 2. If config flag to require that field is set (which it will be for us) and this field is not given then error out here. 102 3. Load the stored request JSON for the given storedrequestid, if the id was invalid then error out here. 103 4. Use "json-patch" 3rd party library to merge the request body JSON data into the stored request JSON data. 104 5. Unmarshal the merged JSON data into a Go structure. 105 6. Add fields from merged JSON data that correspond to an OpenRTB request into the OpenRTB bid request we are building. 106 a. Unmarshal certain OpenRTB defined structs directly into the OpenRTB bid request. 107 b. In cases where customized logic is needed just copy/fill the fields in directly. 108 7. Call setFieldsImplicitly from auction.go to get basic data from the HTTP request into an OpenRTB bid request to start building the OpenRTB bid request. 109 8. Loop through ad pods to build array of Imps into OpenRTB request, for each pod: 110 a. Load the stored impression to use as the basis for impressions generated for this pod from the configid field. 111 b. NumImps = adpoddurationsec / MIN_VALUE(allowedDurations) 112 c. Build impression array for this pod: 113 I.Create array of NumImps entries initialized to the base impression loaded from the configid. 114 1. If requireexactdurations = true, iterate over allowdDurations and for (NumImps / len(allowedDurations)) number of Imps set minduration = maxduration = allowedDurations[i] 115 2. If requireexactdurations = false, set maxduration = MAX_VALUE(allowedDurations) 116 II. Set Imp.id field to "podX_Y" where X is the pod index and Y is the impression index within this pod. 117 d. Append impressions for this pod to the overall list of impressions in the OpenRTB bid request. 118 9. Call validateRequest() function from auction.go to validate the generated request. 119 10. Call HoldAuction() function to run the auction for the OpenRTB bid request that was built in the previous step. 120 11. Build proper response format. 121 */ 122 func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 123 start := time.Now() 124 125 vo := analytics.VideoObject{ 126 Status: http.StatusOK, 127 Errors: make([]error, 0), 128 StartTime: start, 129 } 130 131 labels := metrics.Labels{ 132 Source: metrics.DemandUnknown, 133 RType: metrics.ReqTypeVideo, 134 PubID: metrics.PublisherUnknown, 135 CookieFlag: metrics.CookieFlagUnknown, 136 RequestStatus: metrics.RequestStatusOK, 137 } 138 139 debugQuery := r.URL.Query().Get("debug") 140 cacheTTL := int64(3600) 141 if deps.cfg.CacheURL.DefaultTTLs.Video > 0 { 142 cacheTTL = int64(deps.cfg.CacheURL.DefaultTTLs.Video) 143 } 144 debugLog := exchange.DebugLog{ 145 Enabled: strings.EqualFold(debugQuery, "true"), 146 CacheType: prebid_cache_client.TypeXML, 147 TTL: cacheTTL, 148 Regexp: deps.debugLogRegexp, 149 DebugOverride: exchange.IsDebugOverrideEnabled(r.Header.Get(exchange.DebugOverrideHeader), deps.cfg.Debug.OverrideToken), 150 } 151 debugLog.DebugEnabledOrOverridden = debugLog.Enabled || debugLog.DebugOverride 152 153 activityControl := privacy.ActivityControl{} 154 155 defer func() { 156 if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil { 157 err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) 158 if err != nil { 159 vo.Errors = append(vo.Errors, err) 160 } 161 } 162 deps.metricsEngine.RecordRequest(labels) 163 deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) 164 deps.analytics.LogVideoObject(&vo, activityControl) 165 }() 166 167 w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) 168 setBrowsingTopicsHeader(w, r) 169 170 lr := &io.LimitedReader{ 171 R: r.Body, 172 N: deps.cfg.MaxRequestSize, 173 } 174 requestJson, err := io.ReadAll(lr) 175 if err != nil { 176 handleError(&labels, w, []error{err}, &vo, &debugLog) 177 return 178 } 179 180 resolvedRequest := requestJson 181 if debugLog.DebugEnabledOrOverridden { 182 debugLog.Data.Request = string(requestJson) 183 if headerBytes, err := jsonutil.Marshal(r.Header); err == nil { 184 debugLog.Data.Headers = string(headerBytes) 185 } else { 186 debugLog.Data.Headers = fmt.Sprintf("Unable to marshal headers data: %s", err.Error()) 187 } 188 } 189 190 //load additional data - stored simplified req 191 storedRequestId, err := getVideoStoredRequestId(requestJson) 192 193 if err != nil { 194 if deps.cfg.VideoStoredRequestRequired { 195 handleError(&labels, w, []error{err}, &vo, &debugLog) 196 return 197 } 198 } else { 199 storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId) 200 if len(errs) > 0 { 201 handleError(&labels, w, errs, &vo, &debugLog) 202 return 203 } 204 205 //merge incoming req with stored video req 206 resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson) 207 if err != nil { 208 handleError(&labels, w, []error{err}, &vo, &debugLog) 209 return 210 } 211 } 212 //unmarshal and validate combined result 213 videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest, r.Header) 214 if len(errL) > 0 { 215 handleError(&labels, w, errL, &vo, &debugLog) 216 return 217 } 218 219 vo.VideoRequest = videoBidReq 220 221 var bidReq = &openrtb2.BidRequest{} 222 if deps.defaultRequest { 223 if err := jsonutil.UnmarshalValid(deps.defReqJSON, bidReq); err != nil { 224 err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err) 225 handleError(&labels, w, []error{err}, &vo, &debugLog) 226 return 227 } 228 } 229 230 //create full open rtb req from full video request 231 mergeData(videoBidReq, bidReq) 232 // If debug query param is set, force the response to enable test flag 233 if debugLog.DebugEnabledOrOverridden { 234 bidReq.Test = 1 235 } 236 237 initialPodNumber := len(videoBidReq.PodConfig.Pods) 238 if len(podErrors) > 0 { 239 //remove incorrect pods 240 videoBidReq = cleanupVideoBidRequest(videoBidReq, podErrors) 241 } 242 243 //create impressions array 244 imps, podErrors := deps.createImpressions(videoBidReq, podErrors) 245 246 if len(podErrors) == initialPodNumber { 247 resPodErr := make([]string, 0) 248 for _, podEr := range podErrors { 249 resPodErr = append(resPodErr, strings.Join(podEr.ErrMsgs, ", ")) 250 } 251 err := fmt.Errorf("all pods are incorrect: %s", strings.Join(resPodErr, "; ")) 252 errL = append(errL, err) 253 handleError(&labels, w, errL, &vo, &debugLog) 254 return 255 } 256 257 bidReq.Imp = imps 258 bidReq.ID = "bid_id" //TODO: look at prebid.js 259 260 // all code after this line should use the bidReqWrapper instead of bidReq directly 261 bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq} 262 263 if err := ortb.SetDefaults(bidReqWrapper); err != nil { 264 handleError(&labels, w, errL, &vo, &debugLog) 265 return 266 } 267 268 ctx := context.Background() 269 timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(bidReqWrapper.TMax) * time.Millisecond) 270 if timeout > 0 { 271 var cancel context.CancelFunc 272 ctx, cancel = context.WithDeadline(ctx, start.Add(timeout)) 273 defer cancel() 274 } 275 276 // Read Usersyncs/Cookie 277 decoder := usersync.Base64Decoder{} 278 usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie) 279 usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie) 280 281 if bidReqWrapper.App != nil { 282 labels.Source = metrics.DemandApp 283 labels.PubID = getAccountID(bidReqWrapper.App.Publisher) 284 } else { // both bidReqWrapper.App == nil and bidReqWrapper.Site != nil are true 285 labels.Source = metrics.DemandWeb 286 if usersyncs.HasAnyLiveSyncs() { 287 labels.CookieFlag = metrics.CookieFlagYes 288 } else { 289 labels.CookieFlag = metrics.CookieFlagNo 290 } 291 labels.PubID = getAccountID(bidReqWrapper.Site.Publisher) 292 } 293 294 // Look up account now that we have resolved the pubID value 295 account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine) 296 if len(acctIDErrs) > 0 { 297 handleError(&labels, w, acctIDErrs, &vo, &debugLog) 298 return 299 } 300 301 // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). 302 if errs := deps.setFieldsImplicitly(r, bidReqWrapper, account); len(errs) > 0 { 303 errL = append(errL, errs...) 304 } 305 306 errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false) 307 errL = append(errL, errs...) 308 if errortypes.ContainsFatalError(errL) { 309 handleError(&labels, w, errL, &vo, &debugLog) 310 return 311 } 312 313 activityControl = privacy.NewActivityControl(&account.Privacy) 314 315 warnings := errortypes.WarningOnly(errL) 316 317 secGPC := r.Header.Get("Sec-GPC") 318 auctionRequest := &exchange.AuctionRequest{ 319 BidRequestWrapper: bidReqWrapper, 320 Account: *account, 321 UserSyncs: usersyncs, 322 RequestType: labels.RType, 323 StartTime: start, 324 LegacyLabels: labels, 325 Warnings: warnings, 326 GlobalPrivacyControlHeader: secGPC, 327 PubID: labels.PubID, 328 HookExecutor: hookexecution.EmptyHookExecutor{}, 329 TmaxAdjustments: deps.tmaxAdjustments, 330 Activities: activityControl, 331 } 332 333 auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog) 334 defer func() { 335 if !auctionRequest.BidderResponseStartTime.IsZero() { 336 deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime)) 337 } 338 }() 339 vo.RequestWrapper = bidReqWrapper 340 var response *openrtb2.BidResponse 341 if auctionResponse != nil { 342 response = auctionResponse.BidResponse 343 } 344 vo.Response = response 345 vo.SeatNonBid = auctionResponse.GetSeatNonBid() 346 if err != nil { 347 errL := []error{err} 348 handleError(&labels, w, errL, &vo, &debugLog) 349 return 350 } 351 352 //build simplified response 353 bidResp, err := buildVideoResponse(response, podErrors) 354 if err != nil { 355 errL := []error{err} 356 handleError(&labels, w, errL, &vo, &debugLog) 357 return 358 } 359 if bidReq.Test == 1 { 360 err = setSeatNonBidRaw(bidReqWrapper, auctionResponse) 361 if err != nil { 362 glog.Errorf("Error setting seat non-bid: %v", err) 363 } 364 bidResp.Ext = response.Ext 365 } 366 367 if len(bidResp.AdPods) == 0 && debugLog.DebugEnabledOrOverridden { 368 err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors) 369 if err != nil { 370 vo.Errors = append(vo.Errors, err) 371 } else { 372 bidResp.AdPods = append(bidResp.AdPods, &openrtb_ext.AdPod{ 373 Targeting: []openrtb_ext.VideoTargeting{ 374 { 375 HbCacheID: debugLog.CacheKey, 376 }, 377 }, 378 }) 379 } 380 } 381 382 vo.VideoResponse = bidResp 383 384 resp, err := jsonutil.Marshal(bidResp) 385 if err != nil { 386 errL := []error{err} 387 handleError(&labels, w, errL, &vo, &debugLog) 388 return 389 } 390 391 w.Header().Set("Content-Type", "application/json") 392 w.Write(resp) 393 } 394 395 func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) *openrtb_ext.BidRequestVideo { 396 for i := len(podErrors) - 1; i >= 0; i-- { 397 videoReq.PodConfig.Pods = append(videoReq.PodConfig.Pods[:podErrors[i].PodIndex], videoReq.PodConfig.Pods[podErrors[i].PodIndex+1:]...) 398 } 399 return videoReq 400 } 401 402 func handleError(labels *metrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject, debugLog *exchange.DebugLog) { 403 if debugLog != nil && debugLog.DebugEnabledOrOverridden { 404 if rawUUID, err := uuid.NewV4(); err == nil { 405 debugLog.CacheKey = rawUUID.String() 406 } 407 errL = append(errL, fmt.Errorf("[Debug cache ID: %s]", debugLog.CacheKey)) 408 } 409 labels.RequestStatus = metrics.RequestStatusErr 410 var errors string 411 var status int = http.StatusInternalServerError 412 for _, er := range errL { 413 erVal := errortypes.ReadCode(er) 414 if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode { 415 status = http.StatusServiceUnavailable 416 labels.RequestStatus = metrics.RequestStatusBlacklisted 417 break 418 } else if erVal == errortypes.AcctRequiredErrorCode { 419 status = http.StatusBadRequest 420 labels.RequestStatus = metrics.RequestStatusBadInput 421 break 422 } else if erVal == errortypes.MalformedAcctErrorCode { 423 status = http.StatusInternalServerError 424 labels.RequestStatus = metrics.RequestStatusAccountConfigErr 425 break 426 } 427 errors = fmt.Sprintf("%s %s", errors, er.Error()) 428 } 429 w.WriteHeader(status) 430 vo.Status = status 431 fmt.Fprintf(w, "Critical error while running the video endpoint: %v", errors) 432 glog.Errorf("/openrtb2/video Critical error: %v", errors) 433 vo.Errors = append(vo.Errors, errL...) 434 } 435 436 func (deps *endpointDeps) createImpressions(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) ([]openrtb2.Imp, []PodError) { 437 videoDur := videoReq.PodConfig.DurationRangeSec 438 minDuration, maxDuration := minMax(videoDur) 439 reqExactDur := videoReq.PodConfig.RequireExactDuration 440 videoData := videoReq.Video 441 442 finalImpsArray := make([]openrtb2.Imp, 0) 443 for ind, pod := range videoReq.PodConfig.Pods { 444 445 //load stored impression 446 storedImpressionId := string(pod.ConfigId) 447 storedImp, errs := deps.loadStoredImp(storedImpressionId) 448 if errs != nil { 449 err := fmt.Sprintf("unable to load configid %s, Pod id: %d", storedImpressionId, pod.PodId) 450 podErr := PodError{} 451 podErr.PodId = pod.PodId 452 podErr.PodIndex = ind 453 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 454 podErrors = append(podErrors, podErr) 455 continue 456 } 457 458 numImps := pod.AdPodDurationSec / minDuration 459 if reqExactDur { 460 // In case of impressions number is less than durations array, we bump up impressions number up to duration array size 461 // with this handler we will have one impression per specified duration 462 numImps = max(numImps, len(videoDur)) 463 } 464 impDivNumber := numImps / len(videoDur) 465 466 impsArray := make([]openrtb2.Imp, numImps) 467 for impInd := range impsArray { 468 newImp := createImpressionTemplate(storedImp, videoData) 469 impsArray[impInd] = newImp 470 if reqExactDur { 471 //floor := int(math.Floor(ind/impDivNumber)) 472 durationIndex := impInd / impDivNumber 473 if durationIndex > len(videoDur)-1 { 474 durationIndex = len(videoDur) - 1 475 } 476 impsArray[impInd].Video.MaxDuration = int64(videoDur[durationIndex]) 477 impsArray[impInd].Video.MinDuration = int64(videoDur[durationIndex]) 478 //fmt.Println("Imp ind ", impInd, "duration ", videoDur[durationIndex]) 479 } else { 480 impsArray[impInd].Video.MaxDuration = int64(maxDuration) 481 } 482 483 impsArray[impInd].ID = fmt.Sprintf("%d_%d", pod.PodId, impInd) 484 } 485 finalImpsArray = append(finalImpsArray, impsArray...) 486 487 } 488 return finalImpsArray, podErrors 489 } 490 491 func max(a, b int) int { 492 if a > b { 493 return a 494 } 495 return b 496 } 497 498 func createImpressionTemplate(imp openrtb2.Imp, video *openrtb2.Video) openrtb2.Imp { 499 //for every new impression we need to have it's own copy of video object, because we customize it in further processing 500 newVideo := *video 501 imp.Video = &newVideo 502 return imp 503 } 504 505 func (deps *endpointDeps) loadStoredImp(storedImpId string) (openrtb2.Imp, []error) { 506 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond) 507 defer cancel() 508 509 impr := openrtb2.Imp{} 510 _, imp, err := deps.storedReqFetcher.FetchRequests(ctx, []string{}, []string{storedImpId}) 511 if err != nil { 512 return impr, err 513 } 514 515 if err := jsonutil.UnmarshalValid(imp[storedImpId], &impr); err != nil { 516 return impr, []error{err} 517 } 518 return impr, nil 519 } 520 521 func minMax(array []int) (int, int) { 522 var max = array[0] 523 var min = array[0] 524 for _, value := range array { 525 if max < value { 526 max = value 527 } 528 if min > value { 529 min = value 530 } 531 } 532 return min, max 533 } 534 535 func buildVideoResponse(bidresponse *openrtb2.BidResponse, podErrors []PodError) (*openrtb_ext.BidResponseVideo, error) { 536 537 adPods := make([]*openrtb_ext.AdPod, 0) 538 anyBidsReturned := false 539 for _, seatBid := range bidresponse.SeatBid { 540 for _, bid := range seatBid.Bid { 541 anyBidsReturned = true 542 543 var tempRespBidExt openrtb_ext.ExtBid 544 if err := jsonutil.UnmarshalValid(bid.Ext, &tempRespBidExt); err != nil { 545 return nil, err 546 } 547 if tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)] == "" { 548 continue 549 } 550 551 impId := bid.ImpID 552 podNum := strings.Split(impId, "_")[0] 553 podId, _ := strconv.ParseInt(podNum, 0, 64) 554 555 videoTargeting := openrtb_ext.VideoTargeting{ 556 HbPb: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbpbConstantKey, seatBid.Seat)], 557 HbPbCatDur: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbCategoryDurationKey, seatBid.Seat)], 558 HbCacheID: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)], 559 HbDeal: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbDealIDConstantKey, seatBid.Seat)], 560 } 561 562 adPod := findAdPod(podId, adPods) 563 if adPod == nil { 564 adPod = &openrtb_ext.AdPod{ 565 PodId: podId, 566 Targeting: make([]openrtb_ext.VideoTargeting, 0), 567 } 568 adPods = append(adPods, adPod) 569 } 570 adPod.Targeting = append(adPod.Targeting, videoTargeting) 571 572 } 573 } 574 575 //check if there are any bids in response. 576 //if there are no bids - empty response should be returned, no cache errors 577 if len(adPods) == 0 && anyBidsReturned { 578 //means there is a global cache error, we need to reject all bids 579 err := errors.New("caching failed for all bids") 580 return nil, err 581 } 582 583 // If there were incorrect pods, we put them back to response with error message 584 if len(podErrors) > 0 { 585 for _, podEr := range podErrors { 586 adPodEr := &openrtb_ext.AdPod{ 587 PodId: int64(podEr.PodId), 588 Errors: podEr.ErrMsgs, 589 } 590 adPods = append(adPods, adPodEr) 591 } 592 } 593 594 return &openrtb_ext.BidResponseVideo{AdPods: adPods}, nil 595 } 596 597 func formatTargetingKey(key openrtb_ext.TargetingKey, bidderName string) string { 598 fullKey := fmt.Sprintf("%s_%s", string(key), bidderName) 599 if len(fullKey) > exchange.MaxKeyLength { 600 return string(fullKey[0:exchange.MaxKeyLength]) 601 } 602 return fullKey 603 } 604 605 func findAdPod(podInd int64, pods []*openrtb_ext.AdPod) *openrtb_ext.AdPod { 606 for _, pod := range pods { 607 if pod.PodId == podInd { 608 return pod 609 } 610 } 611 return nil 612 } 613 614 func (deps *endpointDeps) loadStoredVideoRequest(ctx context.Context, storedRequestId string) ([]byte, []error) { 615 storedRequests, _, errs := deps.videoFetcher.FetchRequests(ctx, []string{storedRequestId}, []string{}) 616 jsonString := storedRequests[storedRequestId] 617 return jsonString, errs 618 } 619 620 func getVideoStoredRequestId(request []byte) (string, error) { 621 value, dataType, _, err := jsonparser.Get(request, "storedrequestid") 622 if dataType != jsonparser.String || err != nil { 623 return "", &errortypes.BadInput{Message: "Unable to find required stored request id"} 624 } 625 return string(value), nil 626 } 627 628 func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb2.BidRequest) error { 629 if videoRequest.Site != nil { 630 bidRequest.Site = videoRequest.Site 631 bidRequest.Site.Content = &videoRequest.Content 632 } 633 634 if videoRequest.App != nil { 635 bidRequest.App = videoRequest.App 636 bidRequest.App.Content = &videoRequest.Content 637 } 638 639 bidRequest.Device = &videoRequest.Device 640 bidRequest.User = videoRequest.User 641 642 if len(videoRequest.BCat) != 0 { 643 bidRequest.BCat = videoRequest.BCat 644 } 645 646 if len(videoRequest.BAdv) != 0 { 647 bidRequest.BAdv = videoRequest.BAdv 648 } 649 650 bidExt, err := createBidExtension(videoRequest) 651 if err != nil { 652 return err 653 } 654 if len(bidExt) > 0 { 655 bidRequest.Ext = bidExt 656 } 657 658 bidRequest.Test = videoRequest.Test 659 660 if videoRequest.TMax == 0 { 661 bidRequest.TMax = defaultRequestTimeout 662 } else { 663 bidRequest.TMax = videoRequest.TMax 664 } 665 666 if videoRequest.Regs != nil { 667 bidRequest.Regs = videoRequest.Regs 668 } 669 670 return nil 671 } 672 673 func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, error) { 674 var inclBrandCat *openrtb_ext.ExtIncludeBrandCategory 675 if videoRequest.IncludeBrandCategory != nil { 676 inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{ 677 PrimaryAdServer: videoRequest.IncludeBrandCategory.PrimaryAdserver, 678 Publisher: videoRequest.IncludeBrandCategory.Publisher, 679 WithCategory: true, 680 TranslateCategories: videoRequest.IncludeBrandCategory.TranslateCategories, 681 } 682 } else { 683 inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{ 684 WithCategory: false, 685 } 686 } 687 688 var durationRangeSec []int 689 if !videoRequest.PodConfig.RequireExactDuration { 690 durationRangeSec = videoRequest.PodConfig.DurationRangeSec 691 } 692 693 targeting := openrtb_ext.ExtRequestTargeting{ 694 PriceGranularity: videoRequest.PriceGranularity, 695 IncludeBrandCategory: inclBrandCat, 696 DurationRangeSec: durationRangeSec, 697 IncludeBidderKeys: ptrutil.ToPtr(true), 698 AppendBidderNames: videoRequest.AppendBidderNames, 699 } 700 701 vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{} 702 cache := openrtb_ext.ExtRequestPrebidCache{ 703 VastXML: &vastXml, 704 } 705 706 prebid := openrtb_ext.ExtRequestPrebid{ 707 Cache: &cache, 708 Targeting: &targeting, 709 SupportDeals: videoRequest.SupportDeals, 710 } 711 extReq := openrtb_ext.ExtRequest{Prebid: prebid} 712 713 return jsonutil.Marshal(extReq) 714 } 715 716 func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) { 717 req = &openrtb_ext.BidRequestVideo{} 718 719 if err := jsonutil.UnmarshalValid(request, &req); err != nil { 720 errs = []error{err} 721 return 722 } 723 724 //if Device.UA is not present in request body, init it with user-agent from request header if it's present 725 if req.Device.UA == "" { 726 ua := headers.Get("User-Agent") 727 728 //Check UA is encoded. Without it the `+` character would get changed to a space if not actually encoded 729 if strings.ContainsAny(ua, "%") { 730 var err error 731 req.Device.UA, err = url.QueryUnescape(ua) 732 if err != nil { 733 req.Device.UA = ua 734 } 735 } else { 736 req.Device.UA = ua 737 } 738 } 739 740 errL, podErrors := deps.validateVideoRequest(req) 741 if len(errL) > 0 { 742 errs = append(errs, errL...) 743 } 744 return 745 } 746 747 type PodError struct { 748 PodId int 749 PodIndex int 750 ErrMsgs []string 751 } 752 753 func (deps *endpointDeps) validateVideoRequest(req *openrtb_ext.BidRequestVideo) ([]error, []PodError) { 754 errL := []error{} 755 756 if deps.cfg.VideoStoredRequestRequired && req.StoredRequestId == "" { 757 err := errors.New("request missing required field: storedrequestid") 758 errL = append(errL, err) 759 } 760 if len(req.PodConfig.DurationRangeSec) == 0 { 761 err := errors.New("request missing required field: PodConfig.DurationRangeSec") 762 errL = append(errL, err) 763 } 764 if isZeroOrNegativeDuration(req.PodConfig.DurationRangeSec) { 765 err := errors.New("duration array cannot contain negative or zero values") 766 errL = append(errL, err) 767 } 768 if len(req.PodConfig.Pods) == 0 { 769 err := errors.New("request missing required field: PodConfig.Pods") 770 errL = append(errL, err) 771 } 772 podErrors := make([]PodError, 0) 773 podIdsSet := make(map[int]bool) 774 for ind, pod := range req.PodConfig.Pods { 775 podErr := PodError{} 776 777 if podIdsSet[pod.PodId] { 778 err := fmt.Sprintf("request duplicated required field: PodConfig.Pods.PodId, Pod id: %d", pod.PodId) 779 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 780 } else { 781 podIdsSet[pod.PodId] = true 782 } 783 if pod.PodId <= 0 { 784 err := fmt.Sprintf("request missing required field: PodConfig.Pods.PodId, Pod index: %d", ind) 785 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 786 } 787 if pod.AdPodDurationSec == 0 { 788 err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.AdPodDurationSec, Pod index: %d", ind) 789 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 790 } 791 if pod.AdPodDurationSec < 0 { 792 err := fmt.Sprintf("request incorrect required field: PodConfig.Pods.AdPodDurationSec is negative, Pod index: %d", ind) 793 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 794 } 795 if pod.ConfigId == "" { 796 err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.ConfigId, Pod index: %d", ind) 797 podErr.ErrMsgs = append(podErr.ErrMsgs, err) 798 } 799 if len(podErr.ErrMsgs) > 0 { 800 podErr.PodId = pod.PodId 801 podErr.PodIndex = ind 802 podErrors = append(podErrors, podErr) 803 } 804 } 805 if req.App == nil && req.Site == nil { 806 err := errors.New("request missing required field: site or app") 807 errL = append(errL, err) 808 } else if req.App != nil && req.Site != nil { 809 err := errors.New("request.site or request.app must be defined, but not both") 810 errL = append(errL, err) 811 } else if req.Site != nil && req.Site.ID == "" && req.Site.Page == "" { 812 err := errors.New("request.site missing required field: id or page") 813 errL = append(errL, err) 814 } else if req.App != nil { 815 if req.App.ID != "" { 816 if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found { 817 err := &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)} 818 errL = append(errL, err) 819 return errL, podErrors 820 } 821 } else { 822 if req.App.Bundle == "" { 823 err := errors.New("request.app missing required field: id or bundle") 824 errL = append(errL, err) 825 } 826 } 827 } 828 829 if req.Video != nil { 830 if len(req.Video.MIMEs) == 0 { 831 err := errors.New("request missing required field: Video.Mimes") 832 errL = append(errL, err) 833 } else { 834 mimes := make([]string, 0, len(req.Video.MIMEs)) 835 for _, mime := range req.Video.MIMEs { 836 if mime != "" { 837 mimes = append(mimes, mime) 838 } 839 } 840 if len(mimes) == 0 { 841 err := errors.New("request missing required field: Video.Mimes, mime types contains empty strings only") 842 errL = append(errL, err) 843 } 844 req.Video.MIMEs = mimes 845 } 846 847 if len(req.Video.Protocols) == 0 { 848 err := errors.New("request missing required field: Video.Protocols") 849 errL = append(errL, err) 850 } 851 } else { 852 err := errors.New("request missing required field: Video") 853 errL = append(errL, err) 854 } 855 856 return errL, podErrors 857 } 858 859 func isZeroOrNegativeDuration(duration []int) bool { 860 for _, value := range duration { 861 if value <= 0 { 862 return true 863 } 864 } 865 return false 866 }