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