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 }