github.com/prebid/prebid-server/v2@v2.18.0/adapters/sspBC/sspbc.go (about)

     1  package sspBC
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"html/template"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/prebid/openrtb/v20/openrtb2"
    14  	"github.com/prebid/prebid-server/v2/adapters"
    15  	"github.com/prebid/prebid-server/v2/config"
    16  	"github.com/prebid/prebid-server/v2/errortypes"
    17  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    18  )
    19  
    20  const (
    21  	adapterVersion              = "5.8"
    22  	impFallbackSize             = "1x1"
    23  	requestTypeStandard         = 1
    24  	requestTypeOneCode          = 2
    25  	requestTypeTest             = 3
    26  	prebidServerIntegrationType = "4"
    27  )
    28  
    29  var (
    30  	errSiteNill           = errors.New("site cannot be nill")
    31  	errImpNotFound        = errors.New("imp not found")
    32  	errNotSupportedFormat = errors.New("bid format is not supported")
    33  )
    34  
    35  // mcAd defines the MC payload for banner ads.
    36  type mcAd struct {
    37  	Id      string             `json:"id"`
    38  	Seat    string             `json:"seat"`
    39  	SeatBid []openrtb2.SeatBid `json:"seatbid"`
    40  }
    41  
    42  // adSlotData defines struct used for the oneCode detection.
    43  type adSlotData struct {
    44  	PbSlot string `json:"pbslot"`
    45  	PbSize string `json:"pbsize"`
    46  }
    47  
    48  // templatePayload represents the banner template payload.
    49  type templatePayload struct {
    50  	SiteId  string `json:"siteid"`
    51  	SlotId  string `json:"slotid"`
    52  	AdLabel string `json:"adlabel"`
    53  	PubId   string `json:"pubid"`
    54  	Page    string `json:"page"`
    55  	Referer string `json:"referer"`
    56  	McAd    mcAd   `json:"mcad"`
    57  	Inver   string `json:"inver"`
    58  }
    59  
    60  // requestImpExt represents the ext field of the request imp field.
    61  type requestImpExt struct {
    62  	Data adSlotData `json:"data"`
    63  }
    64  
    65  // responseExt represents ext data added by proxy.
    66  type responseExt struct {
    67  	AdLabel     string `json:"adlabel"`
    68  	PublisherId string `json:"pubid"`
    69  	SiteId      string `json:"siteid"`
    70  	SlotId      string `json:"slotid"`
    71  }
    72  
    73  type adapter struct {
    74  	endpoint       string
    75  	bannerTemplate *template.Template
    76  }
    77  
    78  // ---------------ADAPTER INTERFACE------------------
    79  // Builder builds a new instance of the sspBC adapter
    80  func Builder(_ openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    81  	// HTML template used to create banner ads
    82  	const bannerHTML = `<html><head><title></title><meta charset="UTF-8"><meta name="viewport" content="` +
    83  		`width=device-width, initial-scale=1.0"><style> body { background-color: transparent; margin: 0;` +
    84  		` padding: 0; }</style><script> window.rekid = {{.SiteId}}; window.slot = {{.SlotId}}; window.ad` +
    85  		`label = '{{.AdLabel}}'; window.pubid = '{{.PubId}}'; window.wp_sn = 'sspbc_go'; window.page = '` +
    86  		`{{.Page}}'; window.ref = '{{.Referer}}'; window.mcad = {{.McAd}}; window.in` +
    87  		`ver = '{{.Inver}}'; </script></head><body><div id="c"></div><script async c` +
    88  		`rossorigin nomodule src="//std.wpcdn.pl/wpjslib/wpjslib-inline.js" id="wpjslib"></script><scrip` +
    89  		`t async crossorigin type="module" src="//std.wpcdn.pl/wpjslib6/wpjslib-inline.js" id="wpjslib6"` +
    90  		`></script></body></html>`
    91  
    92  	bannerTemplate, err := template.New("banner").Parse(bannerHTML)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	bidder := &adapter{
    98  		endpoint:       config.Endpoint,
    99  		bannerTemplate: bannerTemplate,
   100  	}
   101  
   102  	return bidder, nil
   103  }
   104  
   105  func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
   106  	formattedRequest, err := formatSspBcRequest(request)
   107  	if err != nil {
   108  		return nil, []error{err}
   109  	}
   110  
   111  	requestJSON, err := json.Marshal(formattedRequest)
   112  	if err != nil {
   113  		return nil, []error{err}
   114  	}
   115  
   116  	requestURL, err := url.Parse(a.endpoint)
   117  	if err != nil {
   118  		return nil, []error{err}
   119  	}
   120  
   121  	// add query parameters to request
   122  	queryParams := requestURL.Query()
   123  	queryParams.Add("bdver", adapterVersion)
   124  	queryParams.Add("inver", prebidServerIntegrationType)
   125  	requestURL.RawQuery = queryParams.Encode()
   126  
   127  	requestData := &adapters.RequestData{
   128  		Method: http.MethodPost,
   129  		Uri:    requestURL.String(),
   130  		Body:   requestJSON,
   131  		ImpIDs: getImpIDs(formattedRequest.Imp),
   132  	}
   133  
   134  	return []*adapters.RequestData{requestData}, nil
   135  }
   136  
   137  func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, externalResponse *adapters.ResponseData) (*adapters.BidderResponse, []error) {
   138  	if externalResponse.StatusCode == http.StatusNoContent {
   139  		return nil, nil
   140  	}
   141  
   142  	if externalResponse.StatusCode != http.StatusOK {
   143  		err := &errortypes.BadServerResponse{
   144  			Message: fmt.Sprintf("Unexpected status code: %d.", externalResponse.StatusCode),
   145  		}
   146  		return nil, []error{err}
   147  	}
   148  
   149  	var response openrtb2.BidResponse
   150  	if err := json.Unmarshal(externalResponse.Body, &response); err != nil {
   151  		return nil, []error{err}
   152  	}
   153  
   154  	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(internalRequest.Imp))
   155  	bidResponse.Currency = response.Cur
   156  
   157  	var errors []error
   158  	for _, seatBid := range response.SeatBid {
   159  		for _, bid := range seatBid.Bid {
   160  			if err := a.impToBid(internalRequest, seatBid, bid, bidResponse); err != nil {
   161  				errors = append(errors, err)
   162  			}
   163  		}
   164  	}
   165  
   166  	return bidResponse, errors
   167  }
   168  
   169  func (a *adapter) impToBid(internalRequest *openrtb2.BidRequest, seatBid openrtb2.SeatBid, bid openrtb2.Bid,
   170  	bidResponse *adapters.BidderResponse) error {
   171  	var bidType openrtb_ext.BidType
   172  
   173  	/*
   174  	  Determine bid type
   175  	  At this moment we only check if bid contains Adm property
   176  
   177  	  Later updates will check for video & native data
   178  	*/
   179  	if bid.AdM != "" {
   180  		bidType = openrtb_ext.BidTypeBanner
   181  	}
   182  
   183  	/*
   184  	  Recover original ImpID
   185  	  (stored on request in TagID)
   186  	*/
   187  	impID, err := getOriginalImpID(bid.ImpID, internalRequest.Imp)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	bid.ImpID = impID
   192  
   193  	// read additional data from proxy
   194  	var bidDataExt responseExt
   195  	if err := json.Unmarshal(bid.Ext, &bidDataExt); err != nil {
   196  		return err
   197  	}
   198  	/*
   199  		use correct ad creation method for a detected bid type
   200  		right now, we are only creating banner ads
   201  		if type is not detected / supported, throw error
   202  	*/
   203  	if bidType != openrtb_ext.BidTypeBanner {
   204  		return errNotSupportedFormat
   205  	}
   206  
   207  	var adCreationError error
   208  	bid.AdM, adCreationError = a.createBannerAd(bid, bidDataExt, internalRequest, seatBid.Seat)
   209  	if adCreationError != nil {
   210  		return adCreationError
   211  	}
   212  	// append bid to responses
   213  	bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
   214  		Bid:     &bid,
   215  		BidType: bidType,
   216  	})
   217  
   218  	return nil
   219  }
   220  
   221  func getOriginalImpID(impID string, imps []openrtb2.Imp) (string, error) {
   222  	for _, imp := range imps {
   223  		if imp.ID == impID {
   224  			return imp.TagID, nil
   225  		}
   226  	}
   227  
   228  	return "", errImpNotFound
   229  }
   230  
   231  func (a *adapter) createBannerAd(bid openrtb2.Bid, ext responseExt, request *openrtb2.BidRequest, seat string) (string, error) {
   232  	if strings.Contains(bid.AdM, "<!--preformatted-->") {
   233  		// Banner ad is already formatted
   234  		return bid.AdM, nil
   235  	}
   236  
   237  	// create McAd payload
   238  	var mcad = mcAd{
   239  		Id:   request.ID,
   240  		Seat: seat,
   241  		SeatBid: []openrtb2.SeatBid{
   242  			{Bid: []openrtb2.Bid{bid}},
   243  		},
   244  	}
   245  
   246  	bannerData := &templatePayload{
   247  		SiteId:  ext.SiteId,
   248  		SlotId:  ext.SlotId,
   249  		AdLabel: ext.AdLabel,
   250  		PubId:   ext.PublisherId,
   251  		Page:    request.Site.Page,
   252  		Referer: request.Site.Ref,
   253  		McAd:    mcad,
   254  		Inver:   prebidServerIntegrationType,
   255  	}
   256  
   257  	var filledTemplate bytes.Buffer
   258  	if err := a.bannerTemplate.Execute(&filledTemplate, bannerData); err != nil {
   259  		return "", err
   260  	}
   261  
   262  	return filledTemplate.String(), nil
   263  }
   264  
   265  func getImpSize(imp openrtb2.Imp) string {
   266  	if imp.Banner == nil || len(imp.Banner.Format) == 0 {
   267  		return impFallbackSize
   268  	}
   269  
   270  	var (
   271  		areaMax int64
   272  		impSize = impFallbackSize
   273  	)
   274  
   275  	for _, size := range imp.Banner.Format {
   276  		area := size.W * size.H
   277  		if area > areaMax {
   278  			areaMax = area
   279  			impSize = fmt.Sprintf("%dx%d", size.W, size.H)
   280  		}
   281  	}
   282  
   283  	return impSize
   284  }
   285  
   286  // getBidParameters reads additional data for this imp (site id , placement id, test)
   287  // Errors in parameters do not break imp flow, and thus are not returned
   288  func getBidParameters(imp openrtb2.Imp) openrtb_ext.ExtImpSspbc {
   289  	var extBidder adapters.ExtImpBidder
   290  	var extSSP openrtb_ext.ExtImpSspbc
   291  
   292  	if err := json.Unmarshal(imp.Ext, &extBidder); err == nil {
   293  		_ = json.Unmarshal(extBidder.Bidder, &extSSP)
   294  	}
   295  
   296  	return extSSP
   297  }
   298  
   299  // getRequestType checks what kind of request we have. It can either be:
   300  // - a standard request, where all Imps have complete site / placement data
   301  // - a oneCodeRequest, where site / placement data has to be determined by server
   302  // - a test request, where server returns fixed example ads
   303  func getRequestType(request *openrtb2.BidRequest) int {
   304  	incompleteImps := 0
   305  
   306  	for _, imp := range request.Imp {
   307  		// Read data for this imp
   308  		extSSP := getBidParameters(imp)
   309  
   310  		if extSSP.IsTest != 0 {
   311  			return requestTypeTest
   312  		}
   313  
   314  		if extSSP.SiteId == "" || extSSP.Id == "" {
   315  			incompleteImps += 1
   316  		}
   317  	}
   318  
   319  	if incompleteImps > 0 {
   320  		return requestTypeOneCode
   321  	}
   322  
   323  	return requestTypeStandard
   324  }
   325  
   326  func formatSspBcRequest(request *openrtb2.BidRequest) (*openrtb2.BidRequest, error) {
   327  	if request.Site == nil {
   328  		return nil, errSiteNill
   329  	}
   330  
   331  	var siteID string
   332  
   333  	// determine what kind of request we are dealing with
   334  	requestType := getRequestType(request)
   335  
   336  	for i, imp := range request.Imp {
   337  		// read ext data for the impression
   338  		extSSP := getBidParameters(imp)
   339  
   340  		// store SiteID
   341  		if extSSP.SiteId != "" {
   342  			siteID = extSSP.SiteId
   343  		}
   344  
   345  		// save current imp.id (adUnit name) as imp.tagid
   346  		// we will recover it in makeBids
   347  		imp.TagID = imp.ID
   348  
   349  		// if there is a placement id, and this is not a oneCodeRequest, use it in imp.id
   350  		if extSSP.Id != "" && requestType != requestTypeOneCode {
   351  			imp.ID = extSSP.Id
   352  		}
   353  
   354  		// check imp size and update e.ext - send pbslot, pbsize
   355  		// inability to set bid.ext will cause request to be invalid
   356  		impSize := getImpSize(imp)
   357  		impExt := requestImpExt{
   358  			Data: adSlotData{
   359  				PbSlot: imp.TagID,
   360  				PbSize: impSize,
   361  			},
   362  		}
   363  
   364  		impExtJSON, err := json.Marshal(impExt)
   365  		if err != nil {
   366  			return nil, err
   367  		}
   368  		imp.Ext = impExtJSON
   369  		// save updated imp
   370  		request.Imp[i] = imp
   371  	}
   372  
   373  	siteCopy := *request.Site
   374  	request.Site = &siteCopy
   375  
   376  	/*
   377  		update site ID
   378  		for oneCode request it has to be blank
   379  		for other requests it should be equal to
   380  		SiteId from one of the bids
   381  	*/
   382  	if requestType == requestTypeOneCode || siteID == "" {
   383  		request.Site.ID = ""
   384  	} else {
   385  		request.Site.ID = siteID
   386  	}
   387  
   388  	// add domain info
   389  	if siteURL, err := url.Parse(request.Site.Page); err == nil {
   390  		request.Site.Domain = siteURL.Hostname()
   391  	}
   392  
   393  	// set TEST Flag
   394  	if requestType == requestTypeTest {
   395  		request.Test = 1
   396  	}
   397  
   398  	return request, nil
   399  }
   400  
   401  // getImpIDs uses imp.TagID instead of imp.ID as formattedRequest stores imp.ID in imp.TagID
   402  func getImpIDs(imps []openrtb2.Imp) []string {
   403  	impIDs := make([]string, len(imps))
   404  	for i := range imps {
   405  		impIDs[i] = imps[i].TagID
   406  	}
   407  	return impIDs
   408  }