-
Notifications
You must be signed in to change notification settings - Fork 760
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bid Response Validation #2450
Bid Response Validation #2450
Changes from 6 commits
5adc1aa
1f314fa
cda79ba
f9ce0aa
594cae2
72961e9
97f3541
56e4d99
d60533c
308ae18
677626a
f824a89
197ee65
812e532
761247f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -100,7 +100,8 @@ type Configuration struct { | |
// Refers to main.go `configFileName` constant | ||
BidderInfos BidderInfos `mapstructure:"adapters"` | ||
// Hooks provides a way to specify hook execution plan for specific endpoints and stages | ||
Hooks Hooks `mapstructure:"hooks"` | ||
Hooks Hooks `mapstructure:"hooks"` | ||
Validations Validations `mapstructure:"validations"` | ||
} | ||
|
||
const MIN_COOKIE_SIZE_BYTES = 500 | ||
|
@@ -629,6 +630,19 @@ type TimeoutNotification struct { | |
FailOnly bool `mapstructure:"fail_only"` | ||
} | ||
|
||
type Validations struct { | ||
BannerCreativeMaxSize string `mapstructure:"banner_creative_max_size" json:"banner_creative_max_size"` | ||
SecureMarkup string `mapstructure:"secure_markup" json:"secure_markup"` | ||
MaxCreativeWidth int64 `mapstructure:"max_creative_width" json:"max_creative_width"` | ||
MaxCreativeHeight int64 `mapstructure:"max_creative_height" json:"max_creative_height"` | ||
} | ||
|
||
const ( | ||
ValidationEnforce string = "enforce" | ||
ValidationWarn string = "warn" | ||
ValidationSkip string = "skip" | ||
) | ||
|
||
func (cfg *TimeoutNotification) validate(errs []error) []error { | ||
if cfg.SamplingRate < 0.0 || cfg.SamplingRate > 1.0 { | ||
errs = append(errs, fmt.Errorf("debug.timeout_notification.sampling_rate must be positive and not greater than 1.0. Got %f", cfg.SamplingRate)) | ||
|
@@ -819,6 +833,10 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) { | |
v.SetDefault("host_cookie.ttl_days", 90) | ||
v.SetDefault("host_cookie.max_cookie_size_bytes", 0) | ||
v.SetDefault("host_schain_node", nil) | ||
v.SetDefault("validations.banner_creative_max_size", "skip") | ||
v.SetDefault("validations.secure_markup", "skip") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably use |
||
v.SetDefault("validations.max_creative_size.height", 0) | ||
v.SetDefault("validations.max_creative_size.width", 0) | ||
v.SetDefault("http_client.max_connections_per_host", 0) // unlimited | ||
v.SetDefault("http_client.max_idle_connections", 400) | ||
v.SetDefault("http_client.max_idle_connections_per_host", 10) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,23 +53,24 @@ type IdFetcher interface { | |
} | ||
|
||
type exchange struct { | ||
adapterMap map[openrtb_ext.BidderName]AdaptedBidder | ||
bidderInfo config.BidderInfos | ||
bidderToSyncerKey map[string]string | ||
me metrics.MetricsEngine | ||
cache prebid_cache_client.Client | ||
cacheTime time.Duration | ||
gdprPermsBuilder gdpr.PermissionsBuilder | ||
tcf2ConfigBuilder gdpr.TCF2ConfigBuilder | ||
currencyConverter *currency.RateConverter | ||
externalURL string | ||
gdprDefaultValue gdpr.Signal | ||
privacyConfig config.Privacy | ||
categoriesFetcher stored_requests.CategoryFetcher | ||
bidIDGenerator BidIDGenerator | ||
hostSChainNode *openrtb2.SupplyChainNode | ||
adsCertSigner adscert.Signer | ||
server config.Server | ||
adapterMap map[openrtb_ext.BidderName]AdaptedBidder | ||
bidderInfo config.BidderInfos | ||
bidderToSyncerKey map[string]string | ||
me metrics.MetricsEngine | ||
cache prebid_cache_client.Client | ||
cacheTime time.Duration | ||
gdprPermsBuilder gdpr.PermissionsBuilder | ||
tcf2ConfigBuilder gdpr.TCF2ConfigBuilder | ||
currencyConverter *currency.RateConverter | ||
externalURL string | ||
gdprDefaultValue gdpr.Signal | ||
privacyConfig config.Privacy | ||
categoriesFetcher stored_requests.CategoryFetcher | ||
bidIDGenerator BidIDGenerator | ||
hostSChainNode *openrtb2.SupplyChainNode | ||
adsCertSigner adscert.Signer | ||
server config.Server | ||
bidValidationEnforcement config.Validations | ||
} | ||
|
||
// Container to pass out response ext data from the GetAllBids goroutines back into the main thread | ||
|
@@ -145,10 +146,11 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid | |
GDPR: cfg.GDPR, | ||
LMT: cfg.LMT, | ||
}, | ||
bidIDGenerator: &bidIDGenerator{cfg.GenerateBidID}, | ||
hostSChainNode: cfg.HostSChainNode, | ||
adsCertSigner: adsCertSigner, | ||
server: config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter}, | ||
bidIDGenerator: &bidIDGenerator{cfg.GenerateBidID}, | ||
hostSChainNode: cfg.HostSChainNode, | ||
adsCertSigner: adsCertSigner, | ||
server: config.Server{ExternalUrl: cfg.ExternalURL, GvlID: cfg.GDPR.HostVendorID, DataCenter: cfg.DataCenter}, | ||
bidValidationEnforcement: cfg.Validations, | ||
} | ||
} | ||
|
||
|
@@ -394,8 +396,10 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog * | |
bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral] = append(bidResponseExt.Warnings[openrtb_ext.BidderReservedGeneral], generalWarning) | ||
} | ||
|
||
e.bidValidationEnforcement = setBidValidationStatus(e.bidValidationEnforcement, r.Account.Validations) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: Maybe we can simplify
|
||
|
||
// Build the response | ||
return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, errs) | ||
return e.buildBidResponse(ctx, liveAdapters, adapterBids, r.BidRequestWrapper.BidRequest, adapterExtra, auc, bidResponseExt, cacheInstructions.returnCreative, r.ImpExtInfoMap, r.PubID, errs) | ||
} | ||
|
||
func (e *exchange) parseGDPRDefaultValue(bidRequest *openrtb2.BidRequest) gdpr.Signal { | ||
|
@@ -693,7 +697,7 @@ func errsToBidderWarnings(errs []error) []openrtb_ext.ExtBidderMessage { | |
} | ||
|
||
// This piece takes all the bids supplied by the adapters and crafts an openRTB response to send back to the requester | ||
func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, errList []error) (*openrtb2.BidResponse, error) { | ||
func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ext.BidderName, adapterSeatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, bidRequest *openrtb2.BidRequest, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, bidResponseExt *openrtb_ext.ExtBidResponse, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, pubID string, errList []error) (*openrtb2.BidResponse, error) { | ||
bidResponse := new(openrtb2.BidResponse) | ||
var err error | ||
|
||
|
@@ -709,7 +713,7 @@ func (e *exchange) buildBidResponse(ctx context.Context, liveAdapters []openrtb_ | |
for a, adapterSeatBids := range adapterSeatBids { | ||
//while processing every single bib, do we need to handle categories here? | ||
if adapterSeatBids != nil && len(adapterSeatBids.bids) > 0 { | ||
sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap) | ||
sb := e.makeSeatBid(adapterSeatBids, a, adapterExtra, auc, returnCreative, impExtInfoMap, bidResponseExt, pubID) | ||
seatBids = append(seatBids, *sb) | ||
bidResponse.Cur = adapterSeatBids.currency | ||
} | ||
|
@@ -1009,26 +1013,42 @@ func (e *exchange) makeExtBidResponse(adapterBids map[openrtb_ext.BidderName]*pb | |
|
||
// Return an openrtb seatBid for a bidder | ||
// BuildBidResponse is responsible for ensuring nil bid seatbids are not included | ||
func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo) *openrtb2.SeatBid { | ||
func (e *exchange) makeSeatBid(adapterBid *pbsOrtbSeatBid, adapter openrtb_ext.BidderName, adapterExtra map[openrtb_ext.BidderName]*seatResponseExtra, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, pubID string) *openrtb2.SeatBid { | ||
seatBid := &openrtb2.SeatBid{ | ||
Seat: adapter.String(), | ||
Group: 0, // Prebid cannot support roadblocking | ||
} | ||
|
||
var errList []error | ||
seatBid.Bid, errList = e.makeBid(adapterBid.bids, auc, returnCreative, impExtInfoMap) | ||
seatBid.Bid, errList = e.makeBid(adapterBid.bids, auc, returnCreative, impExtInfoMap, bidResponseExt, adapter, pubID) | ||
if len(errList) > 0 { | ||
adapterExtra[adapter].Errors = append(adapterExtra[adapter].Errors, errsToBidderErrors(errList)...) | ||
} | ||
|
||
return seatBid | ||
} | ||
|
||
func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo) ([]openrtb2.Bid, []error) { | ||
func (e *exchange) makeBid(bids []*pbsOrtbBid, auc *auction, returnCreative bool, impExtInfoMap map[string]ImpExtInfo, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string) ([]openrtb2.Bid, []error) { | ||
result := make([]openrtb2.Bid, 0, len(bids)) | ||
errs := make([]error, 0, 1) | ||
|
||
for _, bid := range bids { | ||
if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationEnforce && bid.bidType == openrtb_ext.BidTypeBanner { | ||
if !e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID) { | ||
continue // Don't add bid to result | ||
} | ||
} else if e.bidValidationEnforcement.BannerCreativeMaxSize == config.ValidationWarn && bid.bidType == openrtb_ext.BidTypeBanner { | ||
e.validateBannerCreativeSize(bid, bidResponseExt, adapter, pubID) | ||
} | ||
|
||
if e.bidValidationEnforcement.SecureMarkup == config.ValidationEnforce && (bid.bidType == openrtb_ext.BidTypeBanner || bid.bidType == openrtb_ext.BidTypeVideo) { | ||
if !e.validateBidAdM(bid, bidResponseExt, adapter, pubID) { | ||
continue // Don't add bid to result | ||
} | ||
} else if e.bidValidationEnforcement.SecureMarkup == config.ValidationWarn && (bid.bidType == openrtb_ext.BidTypeBanner || bid.bidType == openrtb_ext.BidTypeVideo) { | ||
e.validateBidAdM(bid, bidResponseExt, adapter, pubID) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Under both
Under the assumptions above, the
But it's up to you. Let me know your thoughts There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I appreciate this suggestion, but I think I prefer the current way it's structured. It feels a bit more readable to me, but I can also bring up this suggestion at team time. |
||
|
||
bidExtPrebid := &openrtb_ext.ExtBidPrebid{ | ||
DealPriority: bid.dealPriority, | ||
DealTierSatisfied: bid.dealTierSatisfied, | ||
|
@@ -1242,3 +1262,51 @@ func isAdsCertEnabled(experiment *openrtb_ext.Experiment, info config.BidderInfo | |
bidderAdsCertEnabled := info.Experiment.AdsCert.Enabled | ||
return requestAdsCertEnabled && bidderAdsCertEnabled | ||
} | ||
|
||
func (e exchange) validateBannerCreativeSize(bid *pbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string) bool { | ||
if bid.bid.W > e.bidValidationEnforcement.MaxCreativeWidth || bid.bid.H > e.bidValidationEnforcement.MaxCreativeHeight { | ||
// Add error to debug array | ||
bidCreativeMaxSizeError := openrtb_ext.ExtBidderMessage{ | ||
Code: errortypes.BadInputErrorCode, | ||
Message: "bidResponse rejected: size WxH", | ||
} | ||
bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidCreativeMaxSizeError) | ||
|
||
// Log Metrics | ||
e.me.RecordBidValidationCreativeSizeError(adapter, pubID) | ||
|
||
return false | ||
} | ||
return true | ||
} | ||
|
||
func (e exchange) validateBidAdM(bid *pbsOrtbBid, bidResponseExt *openrtb_ext.ExtBidResponse, adapter openrtb_ext.BidderName, pubID string) bool { | ||
invalidAdM := []string{"http:", "http%3A"} | ||
requiredAdM := []string{"https:", "https%3A"} | ||
|
||
if (strings.Contains(bid.bid.AdM, invalidAdM[0]) || strings.Contains(bid.bid.AdM, invalidAdM[1])) && (!strings.Contains(bid.bid.AdM, requiredAdM[0]) || !strings.Contains(bid.bid.AdM, requiredAdM[1])) { | ||
// Add error to debug array | ||
bidSecureMarkupError := openrtb_ext.ExtBidderMessage{ | ||
Code: errortypes.BadInputErrorCode, | ||
Message: "bidResponse rejected: insecure creative in secure context", | ||
} | ||
bidResponseExt.Errors[adapter] = append(bidResponseExt.Errors[adapter], bidSecureMarkupError) | ||
|
||
// Log Metrics | ||
e.me.RecordBidValidationSecureMarkupError(adapter, pubID) | ||
|
||
return false | ||
} | ||
return true | ||
} | ||
|
||
func setBidValidationStatus(host config.Validations, account config.Validations) config.Validations { | ||
finalEnforcement := host | ||
if len(account.BannerCreativeMaxSize) > 0 { | ||
finalEnforcement.BannerCreativeMaxSize = account.BannerCreativeMaxSize | ||
} | ||
if len(account.SecureMarkup) > 0 { | ||
finalEnforcement.SecureMarkup = account.SecureMarkup | ||
} | ||
return finalEnforcement | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
unit tests pass. Unless you need to check values specifically and they can have other values from "enforce", "warn" and "skip" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that these validations only validate the bid response, unless we plan to include more validations in this field in the future, should we rename to
bidResponseValidations
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
II think we should keep the name
Validations
as that is what was specified in the issue along with what’s present in the Java version