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  }