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

     1  package adocean
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"math/rand"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"text/template"
    15  
    16  	"github.com/prebid/openrtb/v20/openrtb2"
    17  	"github.com/prebid/prebid-server/v2/adapters"
    18  	"github.com/prebid/prebid-server/v2/config"
    19  	"github.com/prebid/prebid-server/v2/errortypes"
    20  	"github.com/prebid/prebid-server/v2/macros"
    21  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    22  )
    23  
    24  const adapterVersion = "1.3.0"
    25  const maxUriLength = 8000
    26  const measurementCode = `
    27  	<script>
    28  		+function() {
    29  			var wu = "%s";
    30  			var su = "%s".replace(/\[TIMESTAMP\]/, Date.now());
    31  
    32  			if (wu && !(navigator.sendBeacon && navigator.sendBeacon(wu))) {
    33  				(new Image(1,1)).src = wu
    34  			}
    35  
    36  			if (su && !(navigator.sendBeacon && navigator.sendBeacon(su))) {
    37  				(new Image(1,1)).src = su
    38  			}
    39  		}();
    40  	</script>
    41  `
    42  
    43  type ResponseAdUnit struct {
    44  	ID       string `json:"id"`
    45  	CrID     string `json:"crid"`
    46  	Currency string `json:"currency"`
    47  	Price    string `json:"price"`
    48  	Width    string `json:"width"`
    49  	Height   string `json:"height"`
    50  	Code     string `json:"code"`
    51  	WinURL   string `json:"winUrl"`
    52  	StatsURL string `json:"statsUrl"`
    53  	Error    string `json:"error"`
    54  }
    55  
    56  type requestData struct {
    57  	Url        *url.URL
    58  	Headers    *http.Header
    59  	SlaveSizes map[string]string
    60  	ImpIDs     []string
    61  }
    62  
    63  // Builder builds a new instance of the AdOcean adapter for the given bidder with the given config.
    64  func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
    65  	endpointTemplate, err := template.New("endpointTemplate").Parse(config.Endpoint)
    66  	if err != nil {
    67  		return nil, errors.New("Unable to parse endpoint template")
    68  	}
    69  
    70  	whiteSpace := regexp.MustCompile(`\s+`)
    71  
    72  	bidder := &AdOceanAdapter{
    73  		endpointTemplate: endpointTemplate,
    74  		measurementCode:  whiteSpace.ReplaceAllString(measurementCode, " "),
    75  	}
    76  	return bidder, nil
    77  }
    78  
    79  type AdOceanAdapter struct {
    80  	endpointTemplate *template.Template
    81  	measurementCode  string
    82  }
    83  
    84  func (a *AdOceanAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
    85  	if len(request.Imp) == 0 {
    86  		return nil, []error{&errortypes.BadInput{
    87  			Message: "No impression in the bid request",
    88  		}}
    89  	}
    90  
    91  	consentString := ""
    92  	if request.User != nil {
    93  		var extUser openrtb_ext.ExtUser
    94  		if err := json.Unmarshal(request.User.Ext, &extUser); err == nil {
    95  			consentString = extUser.Consent
    96  		}
    97  	}
    98  
    99  	var reqCreationErrors []error
   100  	var err error
   101  	requestsData := make([]*requestData, 0, len(request.Imp))
   102  	for _, auction := range request.Imp {
   103  		requestsData, err = a.addNewBid(requestsData, &auction, request, consentString)
   104  		if err != nil {
   105  			reqCreationErrors = append(reqCreationErrors, err)
   106  		}
   107  	}
   108  
   109  	httpRequests := make([]*adapters.RequestData, 0, len(requestsData))
   110  	for _, requestData := range requestsData {
   111  		httpRequests = append(httpRequests, &adapters.RequestData{
   112  			Method:  "GET",
   113  			Uri:     requestData.Url.String(),
   114  			Headers: *requestData.Headers,
   115  			ImpIDs:  requestData.ImpIDs,
   116  		})
   117  	}
   118  
   119  	return httpRequests, reqCreationErrors
   120  }
   121  
   122  func (a *AdOceanAdapter) addNewBid(
   123  	requestsData []*requestData,
   124  	imp *openrtb2.Imp,
   125  	request *openrtb2.BidRequest,
   126  	consentString string,
   127  ) ([]*requestData, error) {
   128  	var bidderExt adapters.ExtImpBidder
   129  	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
   130  		return requestsData, &errortypes.BadInput{
   131  			Message: "Error parsing bidderExt object",
   132  		}
   133  	}
   134  
   135  	var adOceanExt openrtb_ext.ExtImpAdOcean
   136  	if err := json.Unmarshal(bidderExt.Bidder, &adOceanExt); err != nil {
   137  		return requestsData, &errortypes.BadInput{
   138  			Message: "Error parsing adOceanExt parameters",
   139  		}
   140  	}
   141  
   142  	if adOceanExt.EmitterPrefix == "" {
   143  		return requestsData, &errortypes.BadInput{
   144  			Message: "No emitterPrefix param",
   145  		}
   146  	}
   147  
   148  	addedToExistingRequest := addToExistingRequest(requestsData, &adOceanExt, imp, (request.Test == 1))
   149  	if addedToExistingRequest {
   150  		return requestsData, nil
   151  	}
   152  
   153  	slaveSizes := map[string]string{}
   154  	slaveSizes[adOceanExt.SlaveID] = getImpSizes(imp)
   155  
   156  	url, err := a.makeURL(&adOceanExt, imp, request, slaveSizes, consentString)
   157  	if err != nil {
   158  		return requestsData, err
   159  	}
   160  
   161  	requestsData = append(requestsData, &requestData{
   162  		Url:        url,
   163  		Headers:    a.formHeaders(request),
   164  		SlaveSizes: slaveSizes,
   165  		ImpIDs:     []string{imp.ID},
   166  	})
   167  
   168  	return requestsData, nil
   169  }
   170  
   171  func addToExistingRequest(requestsData []*requestData, newParams *openrtb_ext.ExtImpAdOcean, imp *openrtb2.Imp, testImp bool) bool {
   172  	auctionID := imp.ID
   173  
   174  	for _, requestData := range requestsData {
   175  		queryParams := requestData.Url.Query()
   176  		masterID := queryParams["id"][0]
   177  
   178  		if masterID == newParams.MasterID {
   179  			if _, has := requestData.SlaveSizes[newParams.SlaveID]; has {
   180  				continue
   181  			}
   182  
   183  			queryParams.Add("aid", newParams.SlaveID+":"+auctionID)
   184  			requestData.SlaveSizes[newParams.SlaveID] = getImpSizes(imp)
   185  			setSlaveSizesParam(&queryParams, requestData.SlaveSizes, testImp)
   186  
   187  			newUrl := *(requestData.Url)
   188  			newUrl.RawQuery = queryParams.Encode()
   189  			if len(newUrl.String()) < maxUriLength {
   190  				requestData.Url = &newUrl
   191  				requestData.ImpIDs = append(requestData.ImpIDs, auctionID)
   192  				return true
   193  			}
   194  
   195  			delete(requestData.SlaveSizes, newParams.SlaveID)
   196  		}
   197  	}
   198  
   199  	return false
   200  }
   201  
   202  func (a *AdOceanAdapter) makeURL(
   203  	params *openrtb_ext.ExtImpAdOcean,
   204  	imp *openrtb2.Imp,
   205  	request *openrtb2.BidRequest,
   206  	slaveSizes map[string]string,
   207  	consentString string,
   208  ) (*url.URL, error) {
   209  	endpointParams := macros.EndpointTemplateParams{Host: params.EmitterPrefix}
   210  	host, err := macros.ResolveMacros(a.endpointTemplate, endpointParams)
   211  	if err != nil {
   212  		return nil, &errortypes.BadInput{
   213  			Message: "Unable to parse endpoint url template: " + err.Error(),
   214  		}
   215  	}
   216  
   217  	endpointURL, err := url.Parse(host)
   218  	if err != nil {
   219  		return nil, &errortypes.BadInput{
   220  			Message: "Malformed URL: " + err.Error(),
   221  		}
   222  	}
   223  
   224  	randomizedPart := 10000000 + rand.Intn(99999999-10000000)
   225  	if request.Test == 1 {
   226  		randomizedPart = 10000000
   227  	}
   228  	endpointURL.Path = "/_" + strconv.Itoa(randomizedPart) + "/ad.json"
   229  
   230  	auctionID := imp.ID
   231  	queryParams := url.Values{}
   232  	queryParams.Add("pbsrv_v", adapterVersion)
   233  	queryParams.Add("id", params.MasterID)
   234  	queryParams.Add("nc", "1")
   235  	queryParams.Add("nosecure", "1")
   236  	queryParams.Add("aid", params.SlaveID+":"+auctionID)
   237  	if consentString != "" {
   238  		queryParams.Add("gdpr_consent", consentString)
   239  		queryParams.Add("gdpr", "1")
   240  	}
   241  	if request.User != nil && request.User.BuyerUID != "" {
   242  		queryParams.Add("hcuserid", request.User.BuyerUID)
   243  	}
   244  	if request.App != nil {
   245  		queryParams.Add("app", "1")
   246  		queryParams.Add("appname", request.App.Name)
   247  		queryParams.Add("appbundle", request.App.Bundle)
   248  		queryParams.Add("appdomain", request.App.Domain)
   249  	}
   250  	if request.Device != nil {
   251  		if request.Device.IFA != "" {
   252  			queryParams.Add("ifa", request.Device.IFA)
   253  		} else {
   254  			queryParams.Add("dpidmd5", request.Device.DPIDMD5)
   255  		}
   256  
   257  		queryParams.Add("devos", request.Device.OS)
   258  		queryParams.Add("devosv", request.Device.OSV)
   259  		queryParams.Add("devmodel", request.Device.Model)
   260  		queryParams.Add("devmake", request.Device.Make)
   261  	}
   262  
   263  	setSlaveSizesParam(&queryParams, slaveSizes, (request.Test == 1))
   264  	endpointURL.RawQuery = queryParams.Encode()
   265  
   266  	return endpointURL, nil
   267  }
   268  
   269  func (a *AdOceanAdapter) formHeaders(req *openrtb2.BidRequest) *http.Header {
   270  	headers := make(http.Header)
   271  	headers.Add("Content-Type", "application/json;charset=utf-8")
   272  	headers.Add("Accept", "application/json")
   273  
   274  	if req.Device != nil {
   275  		headers.Add("User-Agent", req.Device.UA)
   276  
   277  		if req.Device.IP != "" {
   278  			headers.Add("X-Forwarded-For", req.Device.IP)
   279  		} else if req.Device.IPv6 != "" {
   280  			headers.Add("X-Forwarded-For", req.Device.IPv6)
   281  		}
   282  	}
   283  
   284  	if req.Site != nil {
   285  		headers.Add("Referer", req.Site.Page)
   286  	}
   287  
   288  	return &headers
   289  }
   290  
   291  func getImpSizes(imp *openrtb2.Imp) string {
   292  	if imp.Banner == nil {
   293  		return ""
   294  	}
   295  
   296  	if len(imp.Banner.Format) > 0 {
   297  		sizes := make([]string, len(imp.Banner.Format))
   298  		for i, format := range imp.Banner.Format {
   299  			sizes[i] = strconv.FormatInt(format.W, 10) + "x" + strconv.FormatInt(format.H, 10)
   300  		}
   301  
   302  		return strings.Join(sizes, "_")
   303  	}
   304  
   305  	if imp.Banner.W != nil && imp.Banner.H != nil {
   306  		return strconv.FormatInt(*imp.Banner.W, 10) + "x" + strconv.FormatInt(*imp.Banner.H, 10)
   307  	}
   308  
   309  	return ""
   310  }
   311  
   312  func setSlaveSizesParam(queryParams *url.Values, slaveSizes map[string]string, orderByKey bool) {
   313  	sizeValues := make([]string, 0, len(slaveSizes))
   314  	slaveIDs := make([]string, 0, len(slaveSizes))
   315  	for k := range slaveSizes {
   316  		slaveIDs = append(slaveIDs, k)
   317  	}
   318  
   319  	if orderByKey {
   320  		sort.Strings(slaveIDs)
   321  	}
   322  
   323  	for _, slaveID := range slaveIDs {
   324  		sizes := slaveSizes[slaveID]
   325  		if sizes == "" {
   326  			continue
   327  		}
   328  
   329  		rawSlaveID := strings.Replace(slaveID, "adocean", "", 1)
   330  		sizeValues = append(sizeValues, rawSlaveID+"~"+sizes)
   331  	}
   332  
   333  	if len(sizeValues) > 0 {
   334  		queryParams.Set("aosspsizes", strings.Join(sizeValues, "-"))
   335  	}
   336  }
   337  
   338  func (a *AdOceanAdapter) MakeBids(
   339  	internalRequest *openrtb2.BidRequest,
   340  	externalRequest *adapters.RequestData,
   341  	response *adapters.ResponseData,
   342  ) (*adapters.BidderResponse, []error) {
   343  	if response.StatusCode != http.StatusOK {
   344  		return nil, []error{fmt.Errorf("Unexpected status code: %d. Network error?", response.StatusCode)}
   345  	}
   346  
   347  	requestURL, _ := url.Parse(externalRequest.Uri)
   348  	queryParams := requestURL.Query()
   349  	auctionIDs := queryParams["aid"]
   350  
   351  	bidResponses := make([]ResponseAdUnit, 0)
   352  	if err := json.Unmarshal(response.Body, &bidResponses); err != nil {
   353  		return nil, []error{err}
   354  	}
   355  
   356  	var parsedResponses = adapters.NewBidderResponseWithBidsCapacity(len(auctionIDs))
   357  	var parsingErrors []error
   358  	var slaveToAuctionIDMap = make(map[string]string, len(auctionIDs))
   359  
   360  	for _, auctionFullID := range auctionIDs {
   361  		auctionIDsSlice := strings.SplitN(auctionFullID, ":", 2)
   362  		slaveToAuctionIDMap[auctionIDsSlice[0]] = auctionIDsSlice[1]
   363  	}
   364  
   365  	for _, bid := range bidResponses {
   366  		if auctionID, found := slaveToAuctionIDMap[bid.ID]; found {
   367  			if bid.Error == "true" {
   368  				continue
   369  			}
   370  
   371  			price, _ := strconv.ParseFloat(bid.Price, 64)
   372  			width, _ := strconv.ParseInt(bid.Width, 10, 64)
   373  			height, _ := strconv.ParseInt(bid.Height, 10, 64)
   374  			adCode, err := a.prepareAdCodeForBid(bid)
   375  			if err != nil {
   376  				parsingErrors = append(parsingErrors, err)
   377  				continue
   378  			}
   379  
   380  			parsedResponses.Bids = append(parsedResponses.Bids, &adapters.TypedBid{
   381  				Bid: &openrtb2.Bid{
   382  					ID:    bid.ID,
   383  					ImpID: auctionID,
   384  					Price: price,
   385  					AdM:   adCode,
   386  					CrID:  bid.CrID,
   387  					W:     width,
   388  					H:     height,
   389  				},
   390  				BidType: openrtb_ext.BidTypeBanner,
   391  			})
   392  			parsedResponses.Currency = bid.Currency
   393  		}
   394  	}
   395  
   396  	return parsedResponses, parsingErrors
   397  }
   398  
   399  func (a *AdOceanAdapter) prepareAdCodeForBid(bid ResponseAdUnit) (string, error) {
   400  	sspCode, err := url.QueryUnescape(bid.Code)
   401  	if err != nil {
   402  		return "", err
   403  	}
   404  
   405  	adCode := fmt.Sprintf(a.measurementCode, bid.WinURL, bid.StatsURL) + sspCode
   406  
   407  	return adCode, nil
   408  }