github.com/prebid/prebid-server/v2@v2.18.0/endpoints/events/vtrack.go (about) 1 package events 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/golang/glog" 13 "github.com/julienschmidt/httprouter" 14 accountService "github.com/prebid/prebid-server/v2/account" 15 "github.com/prebid/prebid-server/v2/analytics" 16 "github.com/prebid/prebid-server/v2/config" 17 "github.com/prebid/prebid-server/v2/errortypes" 18 "github.com/prebid/prebid-server/v2/metrics" 19 "github.com/prebid/prebid-server/v2/openrtb_ext" 20 "github.com/prebid/prebid-server/v2/prebid_cache_client" 21 "github.com/prebid/prebid-server/v2/stored_requests" 22 "github.com/prebid/prebid-server/v2/util/jsonutil" 23 ) 24 25 const ( 26 AccountParameter = "a" 27 IntegrationParameter = "int" 28 ImpressionCloseTag = "</Impression>" 29 ImpressionOpenTag = "<Impression>" 30 ) 31 32 type normalizeBidderName func(name string) (openrtb_ext.BidderName, bool) 33 34 type vtrackEndpoint struct { 35 Cfg *config.Configuration 36 Accounts stored_requests.AccountFetcher 37 BidderInfos config.BidderInfos 38 Cache prebid_cache_client.Client 39 MetricsEngine metrics.MetricsEngine 40 normalizeBidderName normalizeBidderName 41 } 42 43 type BidCacheRequest struct { 44 Puts []prebid_cache_client.Cacheable `json:"puts"` 45 } 46 47 type BidCacheResponse struct { 48 Responses []CacheObject `json:"responses"` 49 } 50 51 type CacheObject struct { 52 UUID string `json:"uuid"` 53 } 54 55 func NewVTrackEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, cache prebid_cache_client.Client, bidderInfos config.BidderInfos, me metrics.MetricsEngine) httprouter.Handle { 56 vte := &vtrackEndpoint{ 57 Cfg: cfg, 58 Accounts: accounts, 59 BidderInfos: bidderInfos, 60 Cache: cache, 61 MetricsEngine: me, 62 normalizeBidderName: openrtb_ext.NormalizeBidderName, 63 } 64 65 return vte.Handle 66 } 67 68 // /vtrack Handler 69 func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 70 71 // get account id from request parameter 72 accountId := getAccountId(r) 73 74 // account id is required 75 if accountId == "" { 76 w.WriteHeader(http.StatusBadRequest) 77 fmt.Fprintf(w, "Account '%s' is required query parameter and can't be empty", AccountParameter) 78 return 79 } 80 81 // get integration value from request parameter 82 integrationType, err := getIntegrationType(r) 83 if err != nil { 84 w.WriteHeader(http.StatusBadRequest) 85 fmt.Fprintf(w, "Invalid integration type: %s\n", err.Error()) 86 return 87 } 88 89 // parse puts request from request body 90 req, err := ParseVTrackRequest(r, v.Cfg.MaxRequestSize+1) 91 92 // check if there was any error while parsing puts request 93 if err != nil { 94 w.WriteHeader(http.StatusBadRequest) 95 fmt.Fprintf(w, "Invalid request: %s\n", err.Error()) 96 return 97 } 98 99 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(v.Cfg.VTrack.TimeoutMS)*time.Millisecond)) 100 defer cancel() 101 102 // get account details 103 account, errs := accountService.GetAccount(ctx, v.Cfg, v.Accounts, accountId, v.MetricsEngine) 104 if len(errs) > 0 { 105 status, messages := HandleAccountServiceErrors(errs) 106 w.WriteHeader(status) 107 108 for _, message := range messages { 109 fmt.Fprintf(w, "Invalid request: %s\n", message) 110 } 111 return 112 } 113 114 // insert impression tracking if account allows events and bidder allows VAST modification 115 if v.Cache != nil { 116 cachingResponse, errs := v.handleVTrackRequest(ctx, req, account, integrationType) 117 118 if len(errs) > 0 { 119 w.WriteHeader(http.StatusInternalServerError) 120 for _, err := range errs { 121 fmt.Fprintf(w, "Error(s) updating vast: %s\n", err.Error()) 122 123 return 124 } 125 } 126 127 d, err := jsonutil.Marshal(*cachingResponse) 128 129 if err != nil { 130 w.WriteHeader(http.StatusInternalServerError) 131 fmt.Fprintf(w, "Error serializing pbs cache response: %s\n", err.Error()) 132 133 return 134 } 135 136 w.Header().Add("Content-Type", "application/json") 137 w.WriteHeader(http.StatusOK) 138 w.Write(d) 139 140 return 141 } 142 143 w.WriteHeader(http.StatusInternalServerError) 144 w.Write([]byte("PBS Cache client is not configured")) 145 } 146 147 // GetVastUrlTracking creates a vast url tracking 148 func GetVastUrlTracking(externalUrl string, bidid string, bidder string, accountId string, timestamp int64, integration string) string { 149 150 eventReq := &analytics.EventRequest{ 151 Type: analytics.Imp, 152 BidID: bidid, 153 AccountID: accountId, 154 Bidder: bidder, 155 Timestamp: timestamp, 156 Format: analytics.Blank, 157 Integration: integration, 158 } 159 160 return EventRequestToUrl(externalUrl, eventReq) 161 } 162 163 // ParseVTrackRequest parses a BidCacheRequest from an HTTP Request 164 func ParseVTrackRequest(httpRequest *http.Request, maxRequestSize int64) (req *BidCacheRequest, err error) { 165 req = &BidCacheRequest{} 166 err = nil 167 168 // Pull the request body into a buffer, so we have it for later usage. 169 lr := &io.LimitedReader{ 170 R: httpRequest.Body, 171 N: maxRequestSize, 172 } 173 174 defer httpRequest.Body.Close() 175 requestJson, err := io.ReadAll(lr) 176 if err != nil { 177 return req, err 178 } 179 180 // Check if the request size was too large 181 if lr.N <= 0 { 182 err = &errortypes.BadInput{Message: fmt.Sprintf("request size exceeded max size of %d bytes", maxRequestSize-1)} 183 return req, err 184 } 185 186 if len(requestJson) == 0 { 187 err = &errortypes.BadInput{Message: "request body is empty"} 188 return req, err 189 } 190 191 if err := jsonutil.UnmarshalValid(requestJson, req); err != nil { 192 return req, err 193 } 194 195 for _, bcr := range req.Puts { 196 if bcr.BidID == "" { 197 err = error(&errortypes.BadInput{Message: "'bidid' is required field and can't be empty"}) 198 return req, err 199 } 200 201 if bcr.Bidder == "" { 202 err = error(&errortypes.BadInput{Message: "'bidder' is required field and can't be empty"}) 203 return req, err 204 } 205 } 206 207 return req, nil 208 } 209 210 // handleVTrackRequest handles a VTrack request 211 func (v *vtrackEndpoint) handleVTrackRequest(ctx context.Context, req *BidCacheRequest, account *config.Account, integration string) (*BidCacheResponse, []error) { 212 biddersAllowingVastUpdate := getBiddersAllowingVastUpdate(req, &v.BidderInfos, v.Cfg.VTrack.AllowUnknownBidder, v.normalizeBidderName) 213 // cache data 214 r, errs := v.cachePutObjects(ctx, req, biddersAllowingVastUpdate, account.ID, integration) 215 216 // handle pbs caching errors 217 if len(errs) != 0 { 218 glog.Errorf("Error(s) updating vast: %v", errs) 219 return nil, errs 220 } 221 222 // build response 223 response := &BidCacheResponse{ 224 Responses: []CacheObject{}, 225 } 226 227 for _, uuid := range r { 228 response.Responses = append(response.Responses, CacheObject{ 229 UUID: uuid, 230 }) 231 } 232 233 return response, nil 234 } 235 236 // cachePutObjects caches BidCacheRequest data 237 func (v *vtrackEndpoint) cachePutObjects(ctx context.Context, req *BidCacheRequest, biddersAllowingVastUpdate map[string]struct{}, accountId string, integration string) ([]string, []error) { 238 var cacheables []prebid_cache_client.Cacheable 239 240 for _, c := range req.Puts { 241 242 nc := &prebid_cache_client.Cacheable{ 243 Type: c.Type, 244 Data: c.Data, 245 TTLSeconds: c.TTLSeconds, 246 Key: c.Key, 247 } 248 249 if _, ok := biddersAllowingVastUpdate[c.Bidder]; ok && nc.Data != nil { 250 nc.Data = ModifyVastXmlJSON(v.Cfg.ExternalURL, nc.Data, c.BidID, c.Bidder, accountId, c.Timestamp, integration) 251 } 252 253 cacheables = append(cacheables, *nc) 254 } 255 256 return v.Cache.PutJson(ctx, cacheables) 257 } 258 259 // getBiddersAllowingVastUpdate returns a list of bidders that allow VAST XML modification 260 func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName normalizeBidderName) map[string]struct{} { 261 bl := map[string]struct{}{} 262 263 for _, bcr := range req.Puts { 264 if _, ok := bl[bcr.Bidder]; isAllowVastForBidder(bcr.Bidder, bidderInfos, allowUnknownBidder, normalizeBidderName) && !ok { 265 bl[bcr.Bidder] = struct{}{} 266 } 267 } 268 269 return bl 270 } 271 272 // isAllowVastForBidder checks if a bidder is active and allowed to modify vast xml data 273 func isAllowVastForBidder(bidder string, bidderInfos *config.BidderInfos, allowUnknownBidder bool, normalizeBidderName normalizeBidderName) bool { 274 // if bidder is active and isModifyingVastXmlAllowed is true 275 // check if bidder is configured 276 if normalizedBidder, ok := normalizeBidderName(bidder); ok { 277 if bidderInfos != nil { 278 if b, ok := (*bidderInfos)[normalizedBidder.String()]; ok { 279 return b.IsEnabled() && b.ModifyingVastXmlAllowed 280 } 281 } 282 } 283 284 return allowUnknownBidder 285 } 286 287 // getAccountId extracts an account id from an HTTP Request 288 func getAccountId(httpRequest *http.Request) string { 289 return httpRequest.URL.Query().Get(AccountParameter) 290 } 291 292 func getIntegrationType(httpRequest *http.Request) (string, error) { 293 integrationType := httpRequest.URL.Query().Get(IntegrationParameter) 294 err := validateIntegrationType(integrationType) 295 if err != nil { 296 return "", err 297 } 298 return integrationType, nil 299 } 300 301 // ModifyVastXmlString rewrites and returns the string vastXML and a flag indicating if it was modified 302 func ModifyVastXmlString(externalUrl, vast, bidid, bidder, accountID string, timestamp int64, integrationType string) (string, bool) { 303 ci := strings.Index(vast, ImpressionCloseTag) 304 305 // no impression tag - pass it as it is 306 if ci == -1 { 307 return vast, false 308 } 309 310 vastUrlTracking := GetVastUrlTracking(externalUrl, bidid, bidder, accountID, timestamp, integrationType) 311 impressionUrl := "<![CDATA[" + vastUrlTracking + "]]>" 312 oi := strings.Index(vast, ImpressionOpenTag) 313 314 if ci-oi == len(ImpressionOpenTag) { 315 return strings.Replace(vast, ImpressionOpenTag, ImpressionOpenTag+impressionUrl, 1), true 316 } 317 318 return strings.Replace(vast, ImpressionCloseTag, ImpressionCloseTag+ImpressionOpenTag+impressionUrl+ImpressionCloseTag, 1), true 319 } 320 321 // ModifyVastXmlJSON modifies BidCacheRequest element Vast XML data 322 func ModifyVastXmlJSON(externalUrl string, data json.RawMessage, bidid, bidder, accountId string, timestamp int64, integrationType string) json.RawMessage { 323 var vast string 324 if err := jsonutil.Unmarshal(data, &vast); err != nil { 325 // failed to decode json, fall back to string 326 vast = string(data) 327 } 328 vast, ok := ModifyVastXmlString(externalUrl, vast, bidid, bidder, accountId, timestamp, integrationType) 329 if !ok { 330 return data 331 } 332 return json.RawMessage(vast) 333 }