github.com/prebid/prebid-server/v2@v2.18.0/endpoints/openrtb2/amp_auction.go (about) 1 package openrtb2 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "net/http" 10 "net/url" 11 "strings" 12 "time" 13 14 "github.com/buger/jsonparser" 15 "github.com/golang/glog" 16 "github.com/julienschmidt/httprouter" 17 "github.com/prebid/openrtb/v20/openrtb2" 18 "github.com/prebid/openrtb/v20/openrtb3" 19 "github.com/prebid/prebid-server/v2/hooks/hookexecution" 20 "github.com/prebid/prebid-server/v2/ortb" 21 "github.com/prebid/prebid-server/v2/util/uuidutil" 22 jsonpatch "gopkg.in/evanphx/json-patch.v4" 23 24 accountService "github.com/prebid/prebid-server/v2/account" 25 "github.com/prebid/prebid-server/v2/amp" 26 "github.com/prebid/prebid-server/v2/analytics" 27 "github.com/prebid/prebid-server/v2/config" 28 "github.com/prebid/prebid-server/v2/errortypes" 29 "github.com/prebid/prebid-server/v2/exchange" 30 "github.com/prebid/prebid-server/v2/gdpr" 31 "github.com/prebid/prebid-server/v2/hooks" 32 "github.com/prebid/prebid-server/v2/metrics" 33 "github.com/prebid/prebid-server/v2/openrtb_ext" 34 "github.com/prebid/prebid-server/v2/privacy" 35 "github.com/prebid/prebid-server/v2/stored_requests" 36 "github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher" 37 "github.com/prebid/prebid-server/v2/stored_responses" 38 "github.com/prebid/prebid-server/v2/usersync" 39 "github.com/prebid/prebid-server/v2/util/iputil" 40 "github.com/prebid/prebid-server/v2/util/jsonutil" 41 "github.com/prebid/prebid-server/v2/version" 42 ) 43 44 const defaultAmpRequestTimeoutMillis = 900 45 46 var nilBody []byte = nil 47 48 type AmpResponse struct { 49 Targeting map[string]string `json:"targeting"` 50 ORTB2 ORTB2 `json:"ortb2"` 51 } 52 53 type ORTB2 struct { 54 Ext openrtb_ext.ExtBidResponse `json:"ext"` 55 } 56 57 // NewAmpEndpoint modifies the OpenRTB endpoint to handle AMP requests. This will basically modify the parsing 58 // of the request, and the return value, using the OpenRTB machinery to handle everything in between. 59 func NewAmpEndpoint( 60 uuidGenerator uuidutil.UUIDGenerator, 61 ex exchange.Exchange, 62 validator openrtb_ext.BidderParamValidator, 63 requestsById stored_requests.Fetcher, 64 accounts stored_requests.AccountFetcher, 65 cfg *config.Configuration, 66 metricsEngine metrics.MetricsEngine, 67 analyticsRunner analytics.Runner, 68 disabledBidders map[string]string, 69 defReqJSON []byte, 70 bidderMap map[string]openrtb_ext.BidderName, 71 storedRespFetcher stored_requests.Fetcher, 72 hookExecutionPlanBuilder hooks.ExecutionPlanBuilder, 73 tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed, 74 ) (httprouter.Handle, error) { 75 76 if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || metricsEngine == nil { 77 return nil, errors.New("NewAmpEndpoint requires non-nil arguments.") 78 } 79 80 defRequest := len(defReqJSON) > 0 81 82 ipValidator := iputil.PublicNetworkIPValidator{ 83 IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed, 84 IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed, 85 } 86 87 return httprouter.Handle((&endpointDeps{ 88 uuidGenerator, 89 ex, 90 validator, 91 requestsById, 92 empty_fetcher.EmptyFetcher{}, 93 accounts, 94 cfg, 95 metricsEngine, 96 analyticsRunner, 97 disabledBidders, 98 defRequest, 99 defReqJSON, 100 bidderMap, 101 nil, 102 nil, 103 ipValidator, 104 storedRespFetcher, 105 hookExecutionPlanBuilder, 106 tmaxAdjustments, 107 openrtb_ext.NormalizeBidderName, 108 }).AmpAuction), nil 109 110 } 111 112 func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 113 // Prebid Server interprets request.tmax to be the maximum amount of time that a caller is willing 114 // to wait for bids. However, tmax may be defined in the Stored Request data. 115 // 116 // If so, then the trip to the backend might use a significant amount of this time. 117 // We can respect timeouts more accurately if we note the *real* start time, and use it 118 // to compute the auction timeout. 119 start := time.Now() 120 121 hookExecutor := hookexecution.NewHookExecutor(deps.hookExecutionPlanBuilder, hookexecution.EndpointAmp, deps.metricsEngine) 122 123 ao := analytics.AmpObject{ 124 Status: http.StatusOK, 125 Errors: make([]error, 0), 126 StartTime: start, 127 } 128 129 // Set this as an AMP request in Metrics. 130 131 labels := metrics.Labels{ 132 Source: metrics.DemandWeb, 133 RType: metrics.ReqTypeAMP, 134 PubID: metrics.PublisherUnknown, 135 CookieFlag: metrics.CookieFlagUnknown, 136 RequestStatus: metrics.RequestStatusOK, 137 } 138 activityControl := privacy.ActivityControl{} 139 140 defer func() { 141 deps.metricsEngine.RecordRequest(labels) 142 deps.metricsEngine.RecordRequestTime(labels, time.Since(start)) 143 deps.analytics.LogAmpObject(&ao, activityControl) 144 }() 145 146 // Add AMP headers 147 origin := r.FormValue("__amp_source_origin") 148 if len(origin) == 0 { 149 // Just to be safe 150 origin = r.Header.Get("Origin") 151 ao.Origin = origin 152 } 153 154 // Headers "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", 155 // and "Access-Control-Allow-Credentials" are handled in CORS middleware 156 w.Header().Set("AMP-Access-Control-Allow-Source-Origin", origin) 157 w.Header().Set("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin") 158 w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver)) 159 setBrowsingTopicsHeader(w, r) 160 161 // There is no body for AMP requests, so we pass a nil body and ignore the return value. 162 _, rejectErr := hookExecutor.ExecuteEntrypointStage(r, nilBody) 163 reqWrapper, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, errL := deps.parseAmpRequest(r) 164 ao.Errors = append(ao.Errors, errL...) 165 // Process reject after parsing amp request, so we can use reqWrapper. 166 // There is no body for AMP requests, so we pass a nil body and ignore the return value. 167 if rejectErr != nil { 168 labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, nil, labels, ao, nil) 169 return 170 } 171 172 if errortypes.ContainsFatalError(errL) { 173 w.WriteHeader(http.StatusBadRequest) 174 for _, err := range errortypes.FatalOnly(errL) { 175 fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) 176 } 177 labels.RequestStatus = metrics.RequestStatusBadInput 178 return 179 } 180 181 ao.RequestWrapper = reqWrapper 182 183 ctx := context.Background() 184 var cancel context.CancelFunc 185 if reqWrapper.TMax > 0 { 186 ctx, cancel = context.WithDeadline(ctx, start.Add(time.Duration(reqWrapper.TMax)*time.Millisecond)) 187 } else { 188 ctx, cancel = context.WithDeadline(ctx, start.Add(time.Duration(defaultAmpRequestTimeoutMillis)*time.Millisecond)) 189 } 190 defer cancel() 191 192 // Read UserSyncs/Cookie from Request 193 usersyncs := usersync.ReadCookie(r, usersync.Base64Decoder{}, &deps.cfg.HostCookie) 194 usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie) 195 if usersyncs.HasAnyLiveSyncs() { 196 labels.CookieFlag = metrics.CookieFlagYes 197 } else { 198 labels.CookieFlag = metrics.CookieFlagNo 199 } 200 201 labels.PubID = getAccountID(reqWrapper.Site.Publisher) 202 // Look up account now that we have resolved the pubID value 203 account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine) 204 if len(acctIDErrs) > 0 { 205 // best attempt to rebuild the request for analytics. we're already in an error state, so ignoring a 206 // potential error from this call 207 reqWrapper.RebuildRequest() 208 209 errL = append(errL, acctIDErrs...) 210 httpStatus := http.StatusBadRequest 211 metricsStatus := metrics.RequestStatusBadInput 212 for _, er := range errL { 213 errCode := errortypes.ReadCode(er) 214 if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.AccountDisabledErrorCode { 215 httpStatus = http.StatusServiceUnavailable 216 metricsStatus = metrics.RequestStatusBlacklisted 217 break 218 } 219 if errCode == errortypes.MalformedAcctErrorCode { 220 httpStatus = http.StatusInternalServerError 221 metricsStatus = metrics.RequestStatusAccountConfigErr 222 break 223 } 224 } 225 w.WriteHeader(httpStatus) 226 labels.RequestStatus = metricsStatus 227 for _, err := range errortypes.FatalOnly(errL) { 228 fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) 229 } 230 ao.Errors = append(ao.Errors, acctIDErrs...) 231 return 232 } 233 234 // Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers). 235 if errs := deps.setFieldsImplicitly(r, reqWrapper, account); len(errs) > 0 { 236 errL = append(errL, errs...) 237 } 238 239 hasStoredResponses := len(storedAuctionResponses) > 0 240 errs := deps.validateRequest(account, r, reqWrapper, true, hasStoredResponses, storedBidResponses, false) 241 errL = append(errL, errs...) 242 ao.Errors = append(ao.Errors, errs...) 243 if errortypes.ContainsFatalError(errs) { 244 w.WriteHeader(http.StatusBadRequest) 245 for _, err := range errortypes.FatalOnly(errs) { 246 fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) 247 } 248 labels.RequestStatus = metrics.RequestStatusBadInput 249 return 250 } 251 252 tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR) 253 254 activityControl = privacy.NewActivityControl(&account.Privacy) 255 256 hookExecutor.SetActivityControl(activityControl) 257 hookExecutor.SetAccount(account) 258 259 secGPC := r.Header.Get("Sec-GPC") 260 261 auctionRequest := &exchange.AuctionRequest{ 262 BidRequestWrapper: reqWrapper, 263 Account: *account, 264 UserSyncs: usersyncs, 265 RequestType: labels.RType, 266 StartTime: start, 267 LegacyLabels: labels, 268 GlobalPrivacyControlHeader: secGPC, 269 StoredAuctionResponses: storedAuctionResponses, 270 StoredBidResponses: storedBidResponses, 271 BidderImpReplaceImpID: bidderImpReplaceImp, 272 PubID: labels.PubID, 273 HookExecutor: hookExecutor, 274 QueryParams: r.URL.Query(), 275 TCF2Config: tcf2Config, 276 Activities: activityControl, 277 TmaxAdjustments: deps.tmaxAdjustments, 278 } 279 280 auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, nil) 281 defer func() { 282 if !auctionRequest.BidderResponseStartTime.IsZero() { 283 deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime)) 284 } 285 }() 286 var response *openrtb2.BidResponse 287 if auctionResponse != nil { 288 response = auctionResponse.BidResponse 289 } 290 ao.SeatNonBid = auctionResponse.GetSeatNonBid() 291 ao.AuctionResponse = response 292 rejectErr, isRejectErr := hookexecution.CastRejectErr(err) 293 if err != nil && !isRejectErr { 294 w.WriteHeader(http.StatusInternalServerError) 295 fmt.Fprintf(w, "Critical error while running the auction: %v", err) 296 glog.Errorf("/openrtb2/amp Critical error: %v", err) 297 ao.Status = http.StatusInternalServerError 298 ao.Errors = append(ao.Errors, err) 299 return 300 } 301 302 // hold auction rebuilds the request wrapper first thing, so there is likely 303 // no work to do here, but added a rebuild just in case this behavior changes. 304 if err := reqWrapper.RebuildRequest(); err != nil { 305 w.WriteHeader(http.StatusInternalServerError) 306 fmt.Fprintf(w, "Critical error while running the auction: %v", err) 307 glog.Errorf("/openrtb2/amp Critical error: %v", err) 308 ao.Status = http.StatusInternalServerError 309 ao.Errors = append(ao.Errors, err) 310 return 311 } 312 313 if isRejectErr { 314 labels, ao = rejectAmpRequest(*rejectErr, w, hookExecutor, reqWrapper, account, labels, ao, errL) 315 return 316 } 317 318 labels, ao = sendAmpResponse(w, hookExecutor, auctionResponse, reqWrapper, account, labels, ao, errL) 319 } 320 321 func rejectAmpRequest( 322 rejectErr hookexecution.RejectError, 323 w http.ResponseWriter, 324 hookExecutor hookexecution.HookStageExecutor, 325 reqWrapper *openrtb_ext.RequestWrapper, 326 account *config.Account, 327 labels metrics.Labels, 328 ao analytics.AmpObject, 329 errs []error, 330 ) (metrics.Labels, analytics.AmpObject) { 331 response := &openrtb2.BidResponse{NBR: openrtb3.NoBidReason(rejectErr.NBR).Ptr()} 332 ao.AuctionResponse = response 333 ao.Errors = append(ao.Errors, rejectErr) 334 335 return sendAmpResponse(w, hookExecutor, &exchange.AuctionResponse{BidResponse: response}, reqWrapper, account, labels, ao, errs) 336 } 337 338 func sendAmpResponse( 339 w http.ResponseWriter, 340 hookExecutor hookexecution.HookStageExecutor, 341 auctionResponse *exchange.AuctionResponse, 342 reqWrapper *openrtb_ext.RequestWrapper, 343 account *config.Account, 344 labels metrics.Labels, 345 ao analytics.AmpObject, 346 errs []error, 347 ) (metrics.Labels, analytics.AmpObject) { 348 var response *openrtb2.BidResponse 349 if auctionResponse != nil { 350 response = auctionResponse.BidResponse 351 } 352 hookExecutor.ExecuteAuctionResponseStage(response) 353 // Need to extract the targeting parameters from the response, as those are all that 354 // go in the AMP response 355 targets := map[string]string{} 356 byteCache := []byte("\"hb_cache_id") 357 if response != nil { 358 for _, seatBids := range response.SeatBid { 359 for _, bid := range seatBids.Bid { 360 if bytes.Contains(bid.Ext, byteCache) { 361 // Looking for cache_id to be set, as this should only be set on winning bids (or 362 // deal bids), and AMP can only deliver cached ads in any case. 363 // Note, this could cause issues if a targeting key value starts with "hb_cache_id", 364 // but this is a very unlikely corner case. Doing this so we can catch "hb_cache_id" 365 // and "hb_cache_id_{deal}", which allows for deal support in AMP. 366 bidExt := &openrtb_ext.ExtBid{} 367 err := jsonutil.Unmarshal(bid.Ext, bidExt) 368 if err != nil { 369 w.WriteHeader(http.StatusInternalServerError) 370 fmt.Fprintf(w, "Critical error while unpacking AMP targets: %v", err) 371 glog.Errorf("/openrtb2/amp Critical error unpacking targets: %v", err) 372 ao.Errors = append(ao.Errors, fmt.Errorf("Critical error while unpacking AMP targets: %v", err)) 373 ao.Status = http.StatusInternalServerError 374 return labels, ao 375 } 376 for key, value := range bidExt.Prebid.Targeting { 377 targets[key] = value 378 } 379 } 380 } 381 } 382 } 383 384 // Extract global targeting 385 var extResponse openrtb_ext.ExtBidResponse 386 eRErr := jsonutil.Unmarshal(response.Ext, &extResponse) 387 if eRErr != nil { 388 ao.Errors = append(ao.Errors, fmt.Errorf("AMP response: failed to unpack OpenRTB response.ext, debug info cannot be forwarded: %v", eRErr)) 389 } 390 // Extract global targeting 391 extPrebid := extResponse.Prebid 392 if extPrebid != nil { 393 for key, value := range extPrebid.Targeting { 394 _, exists := targets[key] 395 if !exists { 396 targets[key] = value 397 } 398 } 399 } 400 // Now JSONify the targets for the AMP response. 401 ampResponse := AmpResponse{Targeting: targets} 402 ao, ampResponse.ORTB2.Ext = getExtBidResponse(hookExecutor, auctionResponse, reqWrapper, account, ao, errs) 403 404 ao.AmpTargetingValues = targets 405 406 // Fixes #231 407 enc := json.NewEncoder(w) // nosemgrep: json-encoder-needs-type 408 enc.SetEscapeHTML(false) 409 // Explicitly set content type to text/plain, which had previously been 410 // the implied behavior from the time the project was launched. 411 // It's unclear why text/plain was chosen or if it was an oversight, 412 // nevertheless we will keep it as such for compatibility reasons. 413 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 414 415 // If an error happens when encoding the response, there isn't much we can do. 416 // If we've sent _any_ bytes, then Go would have sent the 200 status code first. 417 // That status code can't be un-sent... so the best we can do is log the error. 418 if err := enc.Encode(ampResponse); err != nil { 419 labels.RequestStatus = metrics.RequestStatusNetworkErr 420 ao.Errors = append(ao.Errors, fmt.Errorf("/openrtb2/amp Failed to send response: %v", err)) 421 } 422 423 return labels, ao 424 } 425 426 func getExtBidResponse( 427 hookExecutor hookexecution.HookStageExecutor, 428 auctionResponse *exchange.AuctionResponse, 429 reqWrapper *openrtb_ext.RequestWrapper, 430 account *config.Account, 431 ao analytics.AmpObject, 432 errs []error, 433 ) (analytics.AmpObject, openrtb_ext.ExtBidResponse) { 434 var response *openrtb2.BidResponse 435 if auctionResponse != nil { 436 response = auctionResponse.BidResponse 437 } 438 // Extract any errors 439 var extResponse openrtb_ext.ExtBidResponse 440 eRErr := jsonutil.Unmarshal(response.Ext, &extResponse) 441 if eRErr != nil { 442 ao.Errors = append(ao.Errors, fmt.Errorf("AMP response: failed to unpack OpenRTB response.ext, debug info cannot be forwarded: %v", eRErr)) 443 } 444 445 warnings := extResponse.Warnings 446 if warnings == nil { 447 warnings = make(map[openrtb_ext.BidderName][]openrtb_ext.ExtBidderMessage) 448 } 449 for _, v := range errortypes.WarningOnly(errs) { 450 if errortypes.ReadScope(v) == errortypes.ScopeDebug && !(reqWrapper != nil && reqWrapper.Test == 1) { 451 continue 452 } 453 bidderErr := openrtb_ext.ExtBidderMessage{ 454 Code: errortypes.ReadCode(v), 455 Message: v.Error(), 456 } 457 warnings[openrtb_ext.BidderReservedGeneral] = append(warnings[openrtb_ext.BidderReservedGeneral], bidderErr) 458 } 459 460 extBidResponse := openrtb_ext.ExtBidResponse{ 461 Errors: extResponse.Errors, 462 Warnings: warnings, 463 } 464 465 // add debug information if requested 466 if reqWrapper != nil { 467 if reqWrapper.Test == 1 && eRErr == nil { 468 if extResponse.Debug != nil { 469 extBidResponse.Debug = extResponse.Debug 470 } else { 471 glog.Errorf("Test set on request but debug not present in response.") 472 ao.Errors = append(ao.Errors, fmt.Errorf("test set on request but debug not present in response")) 473 } 474 } 475 476 stageOutcomes := hookExecutor.GetOutcomes() 477 ao.HookExecutionOutcome = stageOutcomes 478 modules, warns, err := hookexecution.GetModulesJSON(stageOutcomes, reqWrapper.BidRequest, account) 479 if err != nil { 480 err := fmt.Errorf("Failed to get modules outcome: %s", err) 481 glog.Errorf(err.Error()) 482 ao.Errors = append(ao.Errors, err) 483 } else if modules != nil { 484 extBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{Modules: modules} 485 } 486 487 if len(warns) > 0 { 488 ao.Errors = append(ao.Errors, warns...) 489 } 490 } 491 492 setSeatNonBid(&extBidResponse, reqWrapper, auctionResponse) 493 494 return ao, extBidResponse 495 } 496 497 // parseRequest turns the HTTP request into an OpenRTB request. 498 // If the errors list is empty, then the returned request will be valid according to the OpenRTB 2.5 spec. 499 // In case of "strong recommendations" in the spec, it tends to be restrictive. If a better workaround is 500 // possible, it will return errors with messages that suggest improvements. 501 // 502 // If the errors list has at least one element, then no guarantees are made about the returned request. 503 func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImp stored_responses.BidderImpReplaceImpID, errs []error) { 504 // Load the stored request for the AMP ID. 505 reqNormal, storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, e := deps.loadRequestJSONForAmp(httpRequest) 506 if errs = append(errs, e...); errortypes.ContainsFatalError(errs) { 507 return 508 } 509 510 // move to using the request wrapper 511 req = &openrtb_ext.RequestWrapper{BidRequest: reqNormal} 512 513 // Need to ensure cache and targeting are turned on 514 e = initAmpTargetingAndCache(req) 515 if errs = append(errs, e...); errortypes.ContainsFatalError(errs) { 516 return 517 } 518 519 if err := ortb.SetDefaults(req); err != nil { 520 errs = append(errs, err) 521 return 522 } 523 524 return 525 } 526 527 // Load the stored OpenRTB request for an incoming AMP request, or return the errors found. 528 func (deps *endpointDeps) loadRequestJSONForAmp(httpRequest *http.Request) (req *openrtb2.BidRequest, storedAuctionResponses stored_responses.ImpsWithBidResponses, storedBidResponses stored_responses.ImpBidderStoredResp, bidderImpReplaceImp stored_responses.BidderImpReplaceImpID, errs []error) { 529 req = &openrtb2.BidRequest{} 530 errs = nil 531 532 ampParams, err := amp.ParseParams(httpRequest) 533 if err != nil { 534 return nil, nil, nil, nil, []error{err} 535 } 536 537 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond) 538 defer cancel() 539 540 storedRequests, _, errs := deps.storedReqFetcher.FetchRequests(ctx, []string{ampParams.StoredRequestID}, nil) 541 if len(errs) > 0 { 542 return nil, nil, nil, nil, errs 543 } 544 if len(storedRequests) == 0 { 545 errs = []error{fmt.Errorf("No AMP config found for tag_id '%s'", ampParams.StoredRequestID)} 546 return 547 } 548 549 // The fetched config becomes the entire OpenRTB request 550 requestJSON := storedRequests[ampParams.StoredRequestID] 551 if err := jsonutil.UnmarshalValid(requestJSON, req); err != nil { 552 errs = []error{err} 553 return 554 } 555 556 storedAuctionResponses, storedBidResponses, bidderImpReplaceImp, errs = stored_responses.ProcessStoredResponses(ctx, &openrtb_ext.RequestWrapper{BidRequest: req}, deps.storedRespFetcher) 557 if err != nil { 558 errs = []error{err} 559 return 560 } 561 562 if deps.cfg.GenerateRequestID || req.ID == "{{UUID}}" { 563 newBidRequestId, err := deps.uuidGenerator.Generate() 564 if err != nil { 565 errs = []error{err} 566 return 567 } 568 req.ID = newBidRequestId 569 } 570 571 if ampParams.Debug { 572 req.Test = 1 573 } 574 575 // Two checks so users know which way the Imp check failed. 576 if len(req.Imp) == 0 { 577 errs = []error{fmt.Errorf("data for tag_id='%s' does not define the required imp array", ampParams.StoredRequestID)} 578 return 579 } 580 if len(req.Imp) > 1 { 581 errs = []error{fmt.Errorf("data for tag_id '%s' includes %d imp elements. Only one is allowed", ampParams.StoredRequestID, len(req.Imp))} 582 return 583 } 584 585 if req.App != nil { 586 errs = []error{errors.New("request.app must not exist in AMP stored requests.")} 587 return 588 } 589 590 // Force HTTPS as AMP requires it, but pubs can forget to set it. 591 if req.Imp[0].Secure == nil { 592 secure := int8(1) 593 req.Imp[0].Secure = &secure 594 } else { 595 *req.Imp[0].Secure = 1 596 } 597 598 errs = deps.overrideWithParams(ampParams, req) 599 return 600 } 601 602 func (deps *endpointDeps) overrideWithParams(ampParams amp.Params, req *openrtb2.BidRequest) []error { 603 if req.Site == nil { 604 req.Site = &openrtb2.Site{} 605 } 606 607 // Override the stored request sizes with AMP ones, if they exist. 608 if req.Imp[0].Banner != nil { 609 if format := makeFormatReplacement(ampParams.Size); len(format) != 0 { 610 req.Imp[0].Banner.Format = format 611 } else if ampParams.Size.Width != 0 { 612 setWidths(req.Imp[0].Banner.Format, ampParams.Size.Width) 613 } else if ampParams.Size.Height != 0 { 614 setHeights(req.Imp[0].Banner.Format, ampParams.Size.Height) 615 } 616 } 617 618 if ampParams.CanonicalURL != "" { 619 req.Site.Page = ampParams.CanonicalURL 620 // Fixes #683 621 if parsedURL, err := url.Parse(ampParams.CanonicalURL); err == nil { 622 domain := parsedURL.Host 623 if colonIndex := strings.LastIndex(domain, ":"); colonIndex != -1 { 624 domain = domain[:colonIndex] 625 } 626 req.Site.Domain = domain 627 } 628 } 629 630 setAmpExtDirect(req.Site, "1") 631 632 setEffectiveAmpPubID(req, ampParams.Account) 633 634 if ampParams.Slot != "" { 635 req.Imp[0].TagID = ampParams.Slot 636 } 637 638 if err := setConsentedProviders(req, ampParams); err != nil { 639 return []error{err} 640 } 641 642 policyWriter, policyWriterErr := amp.ReadPolicy(ampParams, deps.cfg.GDPR.Enabled) 643 if policyWriterErr != nil { 644 return []error{policyWriterErr} 645 } 646 if err := policyWriter.Write(req); err != nil { 647 return []error{err} 648 } 649 650 if ampParams.Timeout != nil { 651 req.TMax = int64(*ampParams.Timeout) - deps.cfg.AMPTimeoutAdjustment 652 } 653 654 var errors []error 655 if warn := setTargeting(req, ampParams.Targeting); warn != nil { 656 errors = append(errors, warn) 657 } 658 659 if err := setTrace(req, ampParams.Trace); err != nil { 660 return append(errors, err) 661 } 662 663 return errors 664 } 665 666 // setConsentedProviders sets the addtl_consent value to user.ext.ConsentedProvidersSettings.consented_providers 667 // in its orginal Google Additional Consent string format and user.ext.consented_providers_settings.consented_providers 668 // that is an array of ints that contains the elements found in addtl_consent 669 func setConsentedProviders(req *openrtb2.BidRequest, ampParams amp.Params) error { 670 if len(ampParams.AdditionalConsent) > 0 { 671 reqWrap := &openrtb_ext.RequestWrapper{BidRequest: req} 672 673 userExt, err := reqWrap.GetUserExt() 674 if err != nil { 675 return err 676 } 677 678 // Parse addtl_consent, that is supposed to come formatted as a Google Additional Consent string, into array of ints 679 consentedProvidersList := openrtb_ext.ParseConsentedProvidersString(ampParams.AdditionalConsent) 680 681 // Set user.ext.consented_providers_settings.consented_providers if elements where found 682 if len(consentedProvidersList) > 0 { 683 cps := userExt.GetConsentedProvidersSettingsOut() 684 if cps == nil { 685 cps = &openrtb_ext.ConsentedProvidersSettingsOut{} 686 } 687 cps.ConsentedProvidersList = append(cps.ConsentedProvidersList, consentedProvidersList...) 688 userExt.SetConsentedProvidersSettingsOut(cps) 689 } 690 691 // Copy addtl_consent into user.ext.ConsentedProvidersSettings.consented_providers as is 692 cps := userExt.GetConsentedProvidersSettingsIn() 693 if cps == nil { 694 cps = &openrtb_ext.ConsentedProvidersSettingsIn{} 695 } 696 cps.ConsentedProvidersString = ampParams.AdditionalConsent 697 userExt.SetConsentedProvidersSettingsIn(cps) 698 699 if err := reqWrap.RebuildRequest(); err != nil { 700 return err 701 } 702 } 703 return nil 704 } 705 706 // setTargeting merges "targeting" to imp[0].ext.data 707 func setTargeting(req *openrtb2.BidRequest, targeting string) error { 708 if len(targeting) == 0 { 709 return nil 710 } 711 712 targetingData := exchange.WrapJSONInData([]byte(targeting)) 713 714 if len(req.Imp[0].Ext) > 0 { 715 newImpExt, err := jsonpatch.MergePatch(req.Imp[0].Ext, targetingData) 716 if err != nil { 717 warn := errortypes.Warning{ 718 WarningCode: errortypes.BadInputErrorCode, 719 Message: fmt.Sprintf("unable to merge imp.ext with targeting data, check targeting data is correct: %s", err.Error()), 720 } 721 722 return &warn 723 } 724 req.Imp[0].Ext = newImpExt 725 return nil 726 } 727 728 req.Imp[0].Ext = targetingData 729 return nil 730 } 731 732 func makeFormatReplacement(size amp.Size) []openrtb2.Format { 733 var formats []openrtb2.Format 734 if size.OverrideWidth != 0 && size.OverrideHeight != 0 { 735 formats = []openrtb2.Format{{ 736 W: size.OverrideWidth, 737 H: size.OverrideHeight, 738 }} 739 } else if size.OverrideWidth != 0 && size.Height != 0 { 740 formats = []openrtb2.Format{{ 741 W: size.OverrideWidth, 742 H: size.Height, 743 }} 744 } else if size.Width != 0 && size.OverrideHeight != 0 { 745 formats = []openrtb2.Format{{ 746 W: size.Width, 747 H: size.OverrideHeight, 748 }} 749 } else if size.Width != 0 && size.Height != 0 { 750 formats = []openrtb2.Format{{ 751 W: size.Width, 752 H: size.Height, 753 }} 754 } 755 756 return append(formats, size.Multisize...) 757 } 758 759 func setWidths(formats []openrtb2.Format, width int64) { 760 for i := 0; i < len(formats); i++ { 761 formats[i].W = width 762 } 763 } 764 765 func setHeights(formats []openrtb2.Format, height int64) { 766 for i := 0; i < len(formats); i++ { 767 formats[i].H = height 768 } 769 } 770 771 // AMP won't function unless ext.prebid.targeting and ext.prebid.cache.bids are defined. 772 // If the user didn't include them, default those here. 773 func initAmpTargetingAndCache(req *openrtb_ext.RequestWrapper) []error { 774 extRequest, err := req.GetRequestExt() 775 if err != nil { 776 return []error{err} 777 } 778 779 prebid := extRequest.GetPrebid() 780 prebidModified := false 781 782 // create prebid object if missing 783 if prebid == nil { 784 prebid = &openrtb_ext.ExtRequestPrebid{} 785 } 786 787 // create targeting object if missing 788 if prebid.Targeting == nil { 789 prebid.Targeting = &openrtb_ext.ExtRequestTargeting{} 790 prebidModified = true 791 } 792 793 // create cache object if missing 794 if prebid.Cache == nil { 795 prebid.Cache = &openrtb_ext.ExtRequestPrebidCache{} 796 prebidModified = true 797 } 798 if prebid.Cache.Bids == nil { 799 prebid.Cache.Bids = &openrtb_ext.ExtRequestPrebidCacheBids{} 800 prebidModified = true 801 } 802 803 if prebidModified { 804 extRequest.SetPrebid(prebid) 805 } 806 return nil 807 } 808 809 func setAmpExtDirect(site *openrtb2.Site, value string) { 810 if len(site.Ext) > 0 { 811 if _, dataType, _, _ := jsonparser.Get(site.Ext, "amp"); dataType == jsonparser.NotExist { 812 if val, err := jsonparser.Set(site.Ext, []byte(value), "amp"); err == nil { 813 site.Ext = val 814 } 815 } 816 } else { 817 site.Ext = json.RawMessage(`{"amp":` + value + `}`) 818 } 819 } 820 821 // Sets the effective publisher ID for amp request 822 func setEffectiveAmpPubID(req *openrtb2.BidRequest, account string) { 823 // ACCOUNT_ID is the unresolved macro name and should be ignored. 824 if account == "" || account == "ACCOUNT_ID" { 825 return 826 } 827 828 var pub *openrtb2.Publisher 829 if req.App != nil { 830 if req.App.Publisher == nil { 831 req.App.Publisher = &openrtb2.Publisher{} 832 } 833 pub = req.App.Publisher 834 } else if req.Site != nil { 835 if req.Site.Publisher == nil { 836 req.Site.Publisher = &openrtb2.Publisher{} 837 } 838 pub = req.Site.Publisher 839 } 840 841 if pub.ID == "" { 842 pub.ID = account 843 } 844 } 845 846 func setTrace(req *openrtb2.BidRequest, value string) error { 847 if value == "" { 848 return nil 849 } 850 851 ext, err := jsonutil.Marshal(map[string]map[string]string{"prebid": {"trace": value}}) 852 if err != nil { 853 return err 854 } 855 856 if len(req.Ext) > 0 { 857 ext, err = jsonpatch.MergePatch(req.Ext, ext) 858 if err != nil { 859 return err 860 } 861 } 862 req.Ext = ext 863 864 return nil 865 } 866 867 // setSeatNonBid populates bidresponse.ext.prebid.seatnonbid if bidrequest.ext.prebid.returnallbidstatus is true 868 func setSeatNonBid(finalExtBidResponse *openrtb_ext.ExtBidResponse, request *openrtb_ext.RequestWrapper, auctionResponse *exchange.AuctionResponse) bool { 869 if finalExtBidResponse == nil || auctionResponse == nil || request == nil { 870 return false 871 } 872 reqExt, err := request.GetRequestExt() 873 if err != nil { 874 return false 875 } 876 prebid := reqExt.GetPrebid() 877 if prebid == nil || !prebid.ReturnAllBidStatus { 878 return false 879 } 880 if finalExtBidResponse.Prebid == nil { 881 finalExtBidResponse.Prebid = &openrtb_ext.ExtResponsePrebid{} 882 } 883 finalExtBidResponse.Prebid.SeatNonBid = auctionResponse.GetSeatNonBid() 884 return true 885 }