github.com/prebid/prebid-server@v0.275.0/adapters/audienceNetwork/facebook.go (about) 1 package audienceNetwork 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "net/http" 11 "strings" 12 13 "github.com/buger/jsonparser" 14 "github.com/prebid/openrtb/v19/openrtb2" 15 16 "github.com/prebid/prebid-server/adapters" 17 "github.com/prebid/prebid-server/config" 18 "github.com/prebid/prebid-server/errortypes" 19 "github.com/prebid/prebid-server/openrtb_ext" 20 "github.com/prebid/prebid-server/util/jsonutil" 21 "github.com/prebid/prebid-server/util/maputil" 22 ) 23 24 var supportedBannerHeights = map[int64]struct{}{ 25 50: {}, 26 250: {}, 27 } 28 29 type FacebookAdapter struct { 30 URI string 31 platformID string 32 appSecret string 33 } 34 35 type facebookAdMarkup struct { 36 BidID string `json:"bid_id"` 37 } 38 39 type facebookReqExt struct { 40 PlatformID string `json:"platformid"` 41 AuthID string `json:"authentication_id"` 42 } 43 44 func (this *FacebookAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) { 45 if len(request.Imp) == 0 { 46 return nil, []error{&errortypes.BadInput{ 47 Message: "No impressions provided", 48 }} 49 } 50 51 if request.User == nil || request.User.BuyerUID == "" { 52 return nil, []error{&errortypes.BadInput{ 53 Message: "Missing bidder token in 'user.buyeruid'", 54 }} 55 } 56 57 if request.Site != nil { 58 return nil, []error{&errortypes.BadInput{ 59 Message: "Site impressions are not supported.", 60 }} 61 } 62 63 return this.buildRequests(request) 64 } 65 66 func (this *FacebookAdapter) buildRequests(request *openrtb2.BidRequest) ([]*adapters.RequestData, []error) { 67 // Documentation suggests bid request splitting by impression so that each 68 // request only represents a single impression 69 reqs := make([]*adapters.RequestData, 0, len(request.Imp)) 70 headers := http.Header{} 71 var errs []error 72 73 headers.Add("Content-Type", "application/json;charset=utf-8") 74 headers.Add("Accept", "application/json") 75 headers.Add("X-Fb-Pool-Routing-Token", request.User.BuyerUID) 76 77 for _, imp := range request.Imp { 78 // Make a copy of the request so that we don't change the original request which 79 // is shared across multiple threads 80 fbreq := *request 81 fbreq.Imp = []openrtb2.Imp{imp} 82 83 if err := this.modifyRequest(&fbreq); err != nil { 84 errs = append(errs, err) 85 continue 86 } 87 88 body, err := json.Marshal(&fbreq) 89 if err != nil { 90 errs = append(errs, err) 91 continue 92 } 93 94 body, err = modifyImpCustom(body, &fbreq.Imp[0]) 95 if err != nil { 96 errs = append(errs, err) 97 continue 98 } 99 100 body, err = jsonutil.DropElement(body, "consented_providers_settings") 101 if err != nil { 102 errs = append(errs, err) 103 return reqs, errs 104 } 105 106 reqs = append(reqs, &adapters.RequestData{ 107 Method: "POST", 108 Uri: this.URI, 109 Body: body, 110 Headers: headers, 111 }) 112 } 113 114 return reqs, errs 115 } 116 117 // The authentication ID is a sha256 hmac hash encoded as a hex string, based on 118 // the app secret and the ID of the bid request 119 func (this *FacebookAdapter) makeAuthID(req *openrtb2.BidRequest) string { 120 h := hmac.New(sha256.New, []byte(this.appSecret)) 121 h.Write([]byte(req.ID)) 122 123 return hex.EncodeToString(h.Sum(nil)) 124 } 125 126 func (this *FacebookAdapter) modifyRequest(out *openrtb2.BidRequest) error { 127 if len(out.Imp) != 1 { 128 panic("each bid request to facebook should only have a single impression") 129 } 130 131 imp := &out.Imp[0] 132 plmtId, pubId, err := this.extractPlacementAndPublisher(imp) 133 if err != nil { 134 return err 135 } 136 137 // Every outgoing FAN request has a single impression, so we can safely use the unique 138 // impression ID as the FAN request ID. We need to make sure that we update the request 139 // ID *BEFORE* we generate the auth ID since its a hash based on the request ID 140 out.ID = imp.ID 141 142 reqExt := facebookReqExt{ 143 PlatformID: this.platformID, 144 AuthID: this.makeAuthID(out), 145 } 146 147 if out.Ext, err = json.Marshal(reqExt); err != nil { 148 return err 149 } 150 151 imp.TagID = pubId + "_" + plmtId 152 imp.Ext = nil 153 154 if out.App != nil { 155 app := *out.App 156 app.Publisher = &openrtb2.Publisher{ID: pubId} 157 out.App = &app 158 } 159 160 if err = this.modifyImp(imp); err != nil { 161 return err 162 } 163 164 return nil 165 } 166 167 func (this *FacebookAdapter) modifyImp(out *openrtb2.Imp) error { 168 impType := resolveImpType(out) 169 170 if out.Instl == 1 && impType != openrtb_ext.BidTypeBanner { 171 return &errortypes.BadInput{ 172 Message: fmt.Sprintf("imp #%s: interstitial imps are only supported for banner", out.ID), 173 } 174 } 175 176 if impType == openrtb_ext.BidTypeBanner { 177 bannerCopy := *out.Banner 178 out.Banner = &bannerCopy 179 180 if out.Instl == 1 { 181 out.Banner.W = openrtb2.Int64Ptr(0) 182 out.Banner.H = openrtb2.Int64Ptr(0) 183 out.Banner.Format = nil 184 return nil 185 } 186 187 if out.Banner.H == nil { 188 for _, f := range out.Banner.Format { 189 if _, ok := supportedBannerHeights[f.H]; ok { 190 h := f.H 191 out.Banner.H = &h 192 break 193 } 194 } 195 if out.Banner.H == nil { 196 return &errortypes.BadInput{ 197 Message: fmt.Sprintf("imp #%s: banner height required", out.ID), 198 } 199 } 200 } 201 202 if _, ok := supportedBannerHeights[*out.Banner.H]; !ok { 203 return &errortypes.BadInput{ 204 Message: fmt.Sprintf("imp #%s: only banner heights 50 and 250 are supported", out.ID), 205 } 206 } 207 208 out.Banner.W = openrtb2.Int64Ptr(-1) 209 out.Banner.Format = nil 210 } 211 212 return nil 213 } 214 215 func (this *FacebookAdapter) extractPlacementAndPublisher(out *openrtb2.Imp) (string, string, error) { 216 var bidderExt adapters.ExtImpBidder 217 if err := json.Unmarshal(out.Ext, &bidderExt); err != nil { 218 return "", "", &errortypes.BadInput{ 219 Message: err.Error(), 220 } 221 } 222 223 var fbExt openrtb_ext.ExtImpFacebook 224 if err := json.Unmarshal(bidderExt.Bidder, &fbExt); err != nil { 225 return "", "", &errortypes.BadInput{ 226 Message: err.Error(), 227 } 228 } 229 230 if fbExt.PlacementId == "" { 231 return "", "", &errortypes.BadInput{ 232 Message: "Missing placementId param", 233 } 234 } 235 236 placementID := fbExt.PlacementId 237 publisherID := fbExt.PublisherId 238 239 // Support the legacy path with the caller was expected to pass in just placementId 240 // which was an underscore concatenated string with the publisherId and placementId. 241 // The new path for callers is to pass in the placementId and publisherId independently 242 // and the below code will prefix the placementId that we pass to FAN with the publisherId 243 // so that we can abstract the implementation details from the caller 244 toks := strings.Split(placementID, "_") 245 if len(toks) == 1 { 246 if publisherID == "" { 247 return "", "", &errortypes.BadInput{ 248 Message: "Missing publisherId param", 249 } 250 } 251 252 return placementID, publisherID, nil 253 } else if len(toks) == 2 { 254 publisherID = toks[0] 255 placementID = toks[1] 256 } else { 257 return "", "", &errortypes.BadInput{ 258 Message: fmt.Sprintf("Invalid placementId param '%s' and publisherId param '%s'", placementID, publisherID), 259 } 260 } 261 262 return placementID, publisherID, nil 263 } 264 265 // modifyImpCustom modifies the impression after it's marshalled to add a non-openrtb field. 266 func modifyImpCustom(jsonData []byte, imp *openrtb2.Imp) ([]byte, error) { 267 impType := resolveImpType(imp) 268 269 // we only need to modify video and native impressions 270 if impType != openrtb_ext.BidTypeVideo && impType != openrtb_ext.BidTypeNative { 271 return jsonData, nil 272 } 273 274 var jsonMap map[string]interface{} 275 if err := json.Unmarshal(jsonData, &jsonMap); err != nil { 276 return jsonData, err 277 } 278 279 var impMap map[string]interface{} 280 if impSlice, ok := maputil.ReadEmbeddedSlice(jsonMap, "imp"); !ok { 281 return jsonData, errors.New("unable to find imp in json data") 282 } else if len(impSlice) == 0 { 283 return jsonData, errors.New("unable to find imp[0] in json data") 284 } else if impMap, ok = impSlice[0].(map[string]interface{}); !ok { 285 return jsonData, errors.New("unexpected type for imp[0] found in json data") 286 } 287 288 switch impType { 289 case openrtb_ext.BidTypeVideo: 290 videoMap, ok := maputil.ReadEmbeddedMap(impMap, "video") 291 if !ok { 292 return jsonData, errors.New("unable to find imp[0].video in json data") 293 } 294 295 // the openrtb library omits video.w/h if set to zero, so we need to force set those 296 // fields to zero post-serialization for the time being 297 videoMap["w"] = json.RawMessage("0") 298 videoMap["h"] = json.RawMessage("0") 299 300 case openrtb_ext.BidTypeNative: 301 nativeMap, ok := maputil.ReadEmbeddedMap(impMap, "native") 302 if !ok { 303 return jsonData, errors.New("unable to find imp[0].video in json data") 304 } 305 306 // Set w/h to -1 for native impressions based on the facebook native spec. 307 // We have to set this post-serialization since these fields are not included 308 // in the OpenRTB 2.5 spec. 309 nativeMap["w"] = json.RawMessage("-1") 310 nativeMap["h"] = json.RawMessage("-1") 311 312 // The FAN adserver does not expect the native request payload, all that information 313 // is derived server side based on the placement ID. We need to remove these pieces of 314 // information manually since OpenRTB (and thus mxmCherry) never omit native.request 315 delete(nativeMap, "ver") 316 delete(nativeMap, "request") 317 } 318 319 if jsonReEncoded, err := json.Marshal(jsonMap); err == nil { 320 return jsonReEncoded, nil 321 } else { 322 return nil, fmt.Errorf("unable to encode json data (%v)", err) 323 } 324 } 325 326 func (this *FacebookAdapter) MakeBids(request *openrtb2.BidRequest, adapterRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) { 327 if response.StatusCode == http.StatusNoContent { 328 return nil, nil 329 } 330 331 if response.StatusCode != http.StatusOK { 332 msg := response.Headers.Get("x-fb-an-errors") 333 return nil, []error{&errortypes.BadInput{ 334 Message: fmt.Sprintf("Unexpected status code %d with error message '%s'", response.StatusCode, msg), 335 }} 336 } 337 338 var bidResp openrtb2.BidResponse 339 if err := json.Unmarshal(response.Body, &bidResp); err != nil { 340 return nil, []error{err} 341 } 342 343 out := adapters.NewBidderResponseWithBidsCapacity(4) 344 var errs []error 345 346 for _, seatbid := range bidResp.SeatBid { 347 for i := range seatbid.Bid { 348 bid := seatbid.Bid[i] 349 350 if bid.AdM == "" { 351 errs = append(errs, &errortypes.BadServerResponse{ 352 Message: fmt.Sprintf("Bid %s missing 'adm'", bid.ID), 353 }) 354 continue 355 } 356 357 var obj facebookAdMarkup 358 if err := json.Unmarshal([]byte(bid.AdM), &obj); err != nil { 359 errs = append(errs, &errortypes.BadServerResponse{ 360 Message: err.Error(), 361 }) 362 continue 363 } 364 365 if obj.BidID == "" { 366 errs = append(errs, &errortypes.BadServerResponse{ 367 Message: fmt.Sprintf("bid %s missing 'bid_id' in 'adm'", bid.ID), 368 }) 369 continue 370 } 371 372 bid.AdID = obj.BidID 373 bid.CrID = obj.BidID 374 375 out.Bids = append(out.Bids, &adapters.TypedBid{ 376 Bid: &bid, 377 BidType: resolveBidType(&bid, request), 378 }) 379 } 380 } 381 382 return out, errs 383 } 384 385 func resolveBidType(bid *openrtb2.Bid, req *openrtb2.BidRequest) openrtb_ext.BidType { 386 for _, imp := range req.Imp { 387 if bid.ImpID == imp.ID { 388 return resolveImpType(&imp) 389 } 390 } 391 392 panic(fmt.Sprintf("Invalid bid imp ID %s does not match any imp IDs from the original bid request", bid.ImpID)) 393 } 394 395 func resolveImpType(imp *openrtb2.Imp) openrtb_ext.BidType { 396 if imp.Banner != nil { 397 return openrtb_ext.BidTypeBanner 398 } 399 400 if imp.Video != nil { 401 return openrtb_ext.BidTypeVideo 402 } 403 404 if imp.Audio != nil { 405 return openrtb_ext.BidTypeAudio 406 } 407 408 if imp.Native != nil { 409 return openrtb_ext.BidTypeNative 410 } 411 412 // Required to satisfy compiler. Not reachable in practice due to validations performed in PBS-Core. 413 return openrtb_ext.BidTypeBanner 414 } 415 416 // Builder builds a new instance of Facebook's Audience Network adapter for the given bidder with the given config. 417 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) { 418 if config.PlatformID == "" { 419 return nil, errors.New("PartnerID is not configured. Did you set adapters.facebook.platform_id in the app config?") 420 } 421 422 if config.AppSecret == "" { 423 return nil, errors.New("AppSecret is not configured. Did you set adapters.facebook.app_secret in the app config?") 424 } 425 426 bidder := &FacebookAdapter{ 427 URI: config.Endpoint, 428 platformID: config.PlatformID, 429 appSecret: config.AppSecret, 430 } 431 return bidder, nil 432 } 433 434 func (fa *FacebookAdapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error) { 435 var ( 436 rID string 437 pubID string 438 err error 439 ) 440 441 // Note, the facebook adserver can only handle single impression requests, so we have to split multi-imp requests into 442 // multiple request. In order to ensure that every split request has a unique ID, the split request IDs are set to the 443 // corresponding imp's ID 444 rID, err = jsonparser.GetString(req.Body, "id") 445 if err != nil { 446 return &adapters.RequestData{}, []error{err} 447 } 448 449 // The publisher ID is expected in the app object 450 pubID, err = jsonparser.GetString(req.Body, "app", "publisher", "id") 451 if err != nil { 452 return &adapters.RequestData{}, []error{ 453 errors.New("path app.publisher.id not found in the request"), 454 } 455 } 456 457 uri := fmt.Sprintf("https://www.facebook.com/audiencenetwork/nurl/?partner=%s&app=%s&auction=%s&ortb_loss_code=2", fa.platformID, pubID, rID) 458 timeoutReq := adapters.RequestData{ 459 Method: "GET", 460 Uri: uri, 461 Body: nil, 462 Headers: http.Header{}, 463 } 464 465 return &timeoutReq, nil 466 }