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  }