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