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