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 }