github.com/prebid/prebid-server/v2@v2.18.0/adapters/ix/ix.go (about) 1 package ix 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "sort" 8 "strings" 9 10 "github.com/prebid/prebid-server/v2/adapters" 11 "github.com/prebid/prebid-server/v2/config" 12 "github.com/prebid/prebid-server/v2/errortypes" 13 "github.com/prebid/prebid-server/v2/openrtb_ext" 14 "github.com/prebid/prebid-server/v2/util/ptrutil" 15 "github.com/prebid/prebid-server/v2/version" 16 17 "github.com/prebid/openrtb/v20/native1" 18 native1response "github.com/prebid/openrtb/v20/native1/response" 19 "github.com/prebid/openrtb/v20/openrtb2" 20 ) 21 22 type IxAdapter struct { 23 URI string 24 } 25 26 type ExtRequest struct { 27 Prebid *openrtb_ext.ExtRequestPrebid `json:"prebid"` 28 SChain *openrtb2.SupplyChain `json:"schain,omitempty"` 29 IxDiag *IxDiag `json:"ixdiag,omitempty"` 30 } 31 32 type IxDiag struct { 33 PbsV string `json:"pbsv,omitempty"` 34 PbjsV string `json:"pbjsv,omitempty"` 35 MultipleSiteIds string `json:"multipleSiteIds,omitempty"` 36 } 37 38 type auctionConfig struct { 39 BidId string `json:"bidId,omitempty"` 40 Config json.RawMessage `json:"config,omitempty"` 41 } 42 43 type ixRespExt struct { 44 AuctionConfig []auctionConfig `json:"protectedAudienceAuctionConfigs,omitempty"` 45 } 46 47 func (a *IxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 48 requests := make([]*adapters.RequestData, 0, len(request.Imp)) 49 errs := make([]error, 0) 50 51 headers := http.Header{ 52 "Content-Type": {"application/json;charset=utf-8"}, 53 "Accept": {"application/json"}} 54 55 uniqueSiteIDs := make(map[string]struct{}) 56 filteredImps := make([]openrtb2.Imp, 0, len(request.Imp)) 57 requestCopy := *request 58 59 ixDiag := &IxDiag{} 60 61 for _, imp := range requestCopy.Imp { 62 var err error 63 ixExt, err := unmarshalToIxExt(&imp) 64 65 if err != nil { 66 errs = append(errs, err) 67 continue 68 } 69 70 if err = parseSiteId(ixExt, uniqueSiteIDs); err != nil { 71 errs = append(errs, err) 72 continue 73 } 74 75 if err := moveSid(&imp, ixExt); err != nil { 76 errs = append(errs, err) 77 } 78 79 if imp.Banner != nil { 80 bannerCopy := *imp.Banner 81 82 if len(bannerCopy.Format) == 0 && bannerCopy.W != nil && bannerCopy.H != nil { 83 bannerCopy.Format = []openrtb2.Format{{W: *bannerCopy.W, H: *bannerCopy.H}} 84 } 85 86 if len(bannerCopy.Format) == 1 { 87 bannerCopy.W = ptrutil.ToPtr(bannerCopy.Format[0].W) 88 bannerCopy.H = ptrutil.ToPtr(bannerCopy.Format[0].H) 89 } 90 imp.Banner = &bannerCopy 91 } 92 filteredImps = append(filteredImps, imp) 93 } 94 requestCopy.Imp = filteredImps 95 96 setPublisherId(&requestCopy, uniqueSiteIDs, ixDiag) 97 98 err := setIxDiagIntoExtRequest(&requestCopy, ixDiag) 99 if err != nil { 100 errs = append(errs, err) 101 } 102 103 if len(requestCopy.Imp) != 0 { 104 if requestData, err := createRequestData(a, &requestCopy, &headers); err == nil { 105 requests = append(requests, requestData) 106 } else { 107 errs = append(errs, err) 108 } 109 } 110 111 return requests, errs 112 } 113 114 func setPublisherId(requestCopy *openrtb2.BidRequest, uniqueSiteIDs map[string]struct{}, ixDiag *IxDiag) { 115 siteIDs := make([]string, 0, len(uniqueSiteIDs)) 116 for key := range uniqueSiteIDs { 117 siteIDs = append(siteIDs, key) 118 } 119 if requestCopy.Site != nil { 120 site := *requestCopy.Site 121 if site.Publisher == nil { 122 site.Publisher = &openrtb2.Publisher{} 123 } else { 124 publisher := *site.Publisher 125 site.Publisher = &publisher 126 } 127 if len(siteIDs) == 1 { 128 site.Publisher.ID = siteIDs[0] 129 } 130 requestCopy.Site = &site 131 } 132 133 if requestCopy.App != nil { 134 app := *requestCopy.App 135 136 if app.Publisher == nil { 137 app.Publisher = &openrtb2.Publisher{} 138 } else { 139 publisher := *app.Publisher 140 app.Publisher = &publisher 141 } 142 if len(siteIDs) == 1 { 143 app.Publisher.ID = siteIDs[0] 144 } 145 requestCopy.App = &app 146 } 147 148 if len(siteIDs) > 1 { 149 // Sorting siteIDs for predictable output as Go maps don't guarantee order 150 sort.Strings(siteIDs) 151 multipleSiteIDs := strings.Join(siteIDs, ", ") 152 ixDiag.MultipleSiteIds = multipleSiteIDs 153 } 154 } 155 156 func unmarshalToIxExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpIx, error) { 157 var bidderExt adapters.ExtImpBidder 158 if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil { 159 return nil, err 160 } 161 162 var ixExt openrtb_ext.ExtImpIx 163 if err := json.Unmarshal(bidderExt.Bidder, &ixExt); err != nil { 164 return nil, err 165 } 166 167 return &ixExt, nil 168 } 169 170 func parseSiteId(ixExt *openrtb_ext.ExtImpIx, uniqueSiteIDs map[string]struct{}) error { 171 if ixExt == nil { 172 return fmt.Errorf("Nil Ix Ext") 173 } 174 if ixExt.SiteId != "" { 175 uniqueSiteIDs[ixExt.SiteId] = struct{}{} 176 } 177 return nil 178 } 179 180 func createRequestData(a *IxAdapter, request *openrtb2.BidRequest, headers *http.Header) (*adapters.RequestData, error) { 181 body, err := json.Marshal(request) 182 return &adapters.RequestData{ 183 Method: "POST", 184 Uri: a.URI, 185 Body: body, 186 Headers: *headers, 187 ImpIDs: openrtb_ext.GetImpIDs(request.Imp), 188 }, err 189 } 190 191 func (a *IxAdapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 192 switch { 193 case response.StatusCode == http.StatusNoContent: 194 return nil, nil 195 case response.StatusCode == http.StatusBadRequest: 196 return nil, []error{&errortypes.BadInput{ 197 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 198 }} 199 case response.StatusCode != http.StatusOK: 200 return nil, []error{&errortypes.BadServerResponse{ 201 Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode), 202 }} 203 } 204 205 var bidResponse openrtb2.BidResponse 206 if err := json.Unmarshal(response.Body, &bidResponse); err != nil { 207 return nil, []error{&errortypes.BadServerResponse{ 208 Message: fmt.Sprintf("JSON parsing error: %v", err), 209 }} 210 } 211 212 // Store media type per impression in a map for later use to set in bid.ext.prebid.type 213 // Won't work for multiple bid case with a multi-format ad unit. We expect to get type from exchange on such case. 214 impMediaTypeReq := map[string]openrtb_ext.BidType{} 215 for _, imp := range internalRequest.Imp { 216 if imp.Banner != nil { 217 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeBanner 218 } else if imp.Video != nil { 219 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeVideo 220 } else if imp.Native != nil { 221 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeNative 222 } else if imp.Audio != nil { 223 impMediaTypeReq[imp.ID] = openrtb_ext.BidTypeAudio 224 } 225 } 226 227 // capacity 0 will make channel unbuffered 228 bidderResponse := adapters.NewBidderResponseWithBidsCapacity(0) 229 bidderResponse.Currency = bidResponse.Cur 230 231 var errs []error 232 233 for _, seatBid := range bidResponse.SeatBid { 234 for i := range seatBid.Bid { 235 bid := seatBid.Bid[i] 236 237 bidType, err := getMediaTypeForBid(bid, impMediaTypeReq) 238 if err != nil { 239 errs = append(errs, err) 240 continue 241 } 242 243 var bidExtVideo *openrtb_ext.ExtBidPrebidVideo 244 var bidExt openrtb_ext.ExtBid 245 if bidType == openrtb_ext.BidTypeVideo { 246 unmarshalExtErr := json.Unmarshal(bid.Ext, &bidExt) 247 if unmarshalExtErr == nil && bidExt.Prebid != nil && bidExt.Prebid.Video != nil { 248 bidExtVideo = &openrtb_ext.ExtBidPrebidVideo{ 249 Duration: bidExt.Prebid.Video.Duration, 250 } 251 if len(bid.Cat) == 0 { 252 bid.Cat = []string{bidExt.Prebid.Video.PrimaryCategory} 253 } 254 } 255 } 256 257 var bidNative1v1 *Native11Wrapper 258 if bidType == openrtb_ext.BidTypeNative { 259 err := json.Unmarshal([]byte(bid.AdM), &bidNative1v1) 260 if err == nil && len(bidNative1v1.Native.EventTrackers) > 0 { 261 mergeNativeImpTrackers(&bidNative1v1.Native) 262 if json, err := marshalJsonWithoutUnicode(bidNative1v1); err == nil { 263 bid.AdM = string(json) 264 } 265 } 266 } 267 268 var bidNative1v2 *native1response.Response 269 if bidType == openrtb_ext.BidTypeNative { 270 err := json.Unmarshal([]byte(bid.AdM), &bidNative1v2) 271 if err == nil && len(bidNative1v2.EventTrackers) > 0 { 272 mergeNativeImpTrackers(bidNative1v2) 273 if json, err := marshalJsonWithoutUnicode(bidNative1v2); err == nil { 274 bid.AdM = string(json) 275 } 276 } 277 } 278 279 bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{ 280 Bid: &bid, 281 BidType: bidType, 282 BidVideo: bidExtVideo, 283 }) 284 } 285 } 286 287 if bidResponse.Ext != nil { 288 var bidRespExt ixRespExt 289 if err := json.Unmarshal(bidResponse.Ext, &bidRespExt); err != nil { 290 return nil, append(errs, err) 291 } 292 293 if bidRespExt.AuctionConfig != nil { 294 bidderResponse.FledgeAuctionConfigs = make([]*openrtb_ext.FledgeAuctionConfig, 0, len(bidRespExt.AuctionConfig)) 295 for _, config := range bidRespExt.AuctionConfig { 296 if config.Config != nil { 297 fledgeAuctionConfig := &openrtb_ext.FledgeAuctionConfig{ 298 ImpId: config.BidId, 299 Config: config.Config, 300 } 301 bidderResponse.FledgeAuctionConfigs = append(bidderResponse.FledgeAuctionConfigs, fledgeAuctionConfig) 302 } 303 } 304 } 305 } 306 307 return bidderResponse, errs 308 } 309 310 func getMediaTypeForBid(bid openrtb2.Bid, impMediaTypeReq map[string]openrtb_ext.BidType) (openrtb_ext.BidType, error) { 311 switch bid.MType { 312 case openrtb2.MarkupBanner: 313 return openrtb_ext.BidTypeBanner, nil 314 case openrtb2.MarkupVideo: 315 return openrtb_ext.BidTypeVideo, nil 316 case openrtb2.MarkupAudio: 317 return openrtb_ext.BidTypeAudio, nil 318 case openrtb2.MarkupNative: 319 return openrtb_ext.BidTypeNative, nil 320 } 321 322 if bid.Ext != nil { 323 var bidExt openrtb_ext.ExtBid 324 err := json.Unmarshal(bid.Ext, &bidExt) 325 if err == nil && bidExt.Prebid != nil { 326 prebidType := string(bidExt.Prebid.Type) 327 if prebidType != "" { 328 return openrtb_ext.ParseBidType(prebidType) 329 } 330 } 331 } 332 333 if bidType, ok := impMediaTypeReq[bid.ImpID]; ok { 334 return bidType, nil 335 } else { 336 return "", fmt.Errorf("unmatched impression id: %s", bid.ImpID) 337 } 338 } 339 340 // Builder builds a new instance of the Ix adapter for the given bidder with the given config. 341 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 342 bidder := &IxAdapter{ 343 URI: config.Endpoint, 344 } 345 return bidder, nil 346 } 347 348 // native 1.2 to 1.1 tracker compatibility handling 349 350 type Native11Wrapper struct { 351 Native native1response.Response `json:"native,omitempty"` 352 } 353 354 func mergeNativeImpTrackers(bidNative *native1response.Response) { 355 356 // create unique list of imp pixels urls from `imptrackers` and `eventtrackers` 357 uniqueImpPixels := map[string]struct{}{} 358 for _, v := range bidNative.ImpTrackers { 359 uniqueImpPixels[v] = struct{}{} 360 } 361 362 for _, v := range bidNative.EventTrackers { 363 if v.Event == native1.EventTypeImpression && v.Method == native1.EventTrackingMethodImage { 364 uniqueImpPixels[v.URL] = struct{}{} 365 } 366 } 367 368 // rewrite `imptrackers` with new deduped list of imp pixels 369 bidNative.ImpTrackers = make([]string, 0) 370 for k := range uniqueImpPixels { 371 bidNative.ImpTrackers = append(bidNative.ImpTrackers, k) 372 } 373 374 // sort so tests pass correctly 375 sort.Strings(bidNative.ImpTrackers) 376 } 377 378 func marshalJsonWithoutUnicode(v interface{}) (string, error) { 379 // json.Marshal uses HTMLEscape for strings inside JSON which affects URLs 380 // this is a problem with Native responses that embed JSON within JSON 381 // a custom encoder can be used to disable this encoding. 382 // https://pkg.go.dev/encoding/json#Marshal 383 // https://pkg.go.dev/encoding/json#Encoder.SetEscapeHTML 384 sb := &strings.Builder{} 385 encoder := json.NewEncoder(sb) 386 encoder.SetEscapeHTML(false) 387 if err := encoder.Encode(v); err != nil { 388 return "", err 389 } 390 // json.Encode also writes a newline, need to remove 391 // https://pkg.go.dev/encoding/json#Encoder.Encode 392 return strings.TrimSuffix(sb.String(), "\n"), nil 393 } 394 395 func setIxDiagIntoExtRequest(request *openrtb2.BidRequest, ixDiag *IxDiag) error { 396 extRequest := &ExtRequest{} 397 if request.Ext != nil { 398 if err := json.Unmarshal(request.Ext, &extRequest); err != nil { 399 return err 400 } 401 } 402 403 if extRequest.Prebid != nil && extRequest.Prebid.Channel != nil { 404 ixDiag.PbjsV = extRequest.Prebid.Channel.Version 405 } 406 // Slice commit hash out of version 407 if strings.Contains(version.Ver, "-") { 408 ixDiag.PbsV = version.Ver[:strings.Index(version.Ver, "-")] 409 } else if version.Ver != "" { 410 ixDiag.PbsV = version.Ver 411 } 412 413 // Only set request.ext if ixDiag is not empty 414 if *ixDiag != (IxDiag{}) { 415 extRequest := &ExtRequest{} 416 if request.Ext != nil { 417 if err := json.Unmarshal(request.Ext, &extRequest); err != nil { 418 return err 419 } 420 } 421 extRequest.IxDiag = ixDiag 422 extRequestJson, err := json.Marshal(extRequest) 423 if err != nil { 424 return err 425 } 426 request.Ext = extRequestJson 427 } 428 return nil 429 } 430 431 // moves sid from imp[].ext.bidder.sid to imp[].ext.sid 432 func moveSid(imp *openrtb2.Imp, ixExt *openrtb_ext.ExtImpIx) error { 433 if ixExt == nil { 434 return fmt.Errorf("Nil Ix Ext") 435 } 436 437 if ixExt.Sid != "" { 438 var m map[string]interface{} 439 if err := json.Unmarshal(imp.Ext, &m); err != nil { 440 return err 441 } 442 m["sid"] = ixExt.Sid 443 ext, err := json.Marshal(m) 444 if err != nil { 445 return err 446 } 447 imp.Ext = ext 448 } 449 return nil 450 }