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  }