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