github.com/prebid/prebid-server/v2@v2.18.0/endpoints/openrtb2/video_auction.go (about)

     1  package openrtb2
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/buger/jsonparser"
    16  	"github.com/gofrs/uuid"
    17  	"github.com/golang/glog"
    18  	"github.com/julienschmidt/httprouter"
    19  	"github.com/prebid/openrtb/v20/openrtb2"
    20  	"github.com/prebid/prebid-server/v2/hooks"
    21  	"github.com/prebid/prebid-server/v2/hooks/hookexecution"
    22  	"github.com/prebid/prebid-server/v2/ortb"
    23  	"github.com/prebid/prebid-server/v2/privacy"
    24  	jsonpatch "gopkg.in/evanphx/json-patch.v4"
    25  
    26  	accountService "github.com/prebid/prebid-server/v2/account"
    27  	"github.com/prebid/prebid-server/v2/analytics"
    28  	"github.com/prebid/prebid-server/v2/config"
    29  	"github.com/prebid/prebid-server/v2/errortypes"
    30  	"github.com/prebid/prebid-server/v2/exchange"
    31  	"github.com/prebid/prebid-server/v2/metrics"
    32  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    33  	"github.com/prebid/prebid-server/v2/prebid_cache_client"
    34  	"github.com/prebid/prebid-server/v2/stored_requests"
    35  	"github.com/prebid/prebid-server/v2/stored_requests/backends/empty_fetcher"
    36  	"github.com/prebid/prebid-server/v2/usersync"
    37  	"github.com/prebid/prebid-server/v2/util/iputil"
    38  	"github.com/prebid/prebid-server/v2/util/jsonutil"
    39  	"github.com/prebid/prebid-server/v2/util/ptrutil"
    40  	"github.com/prebid/prebid-server/v2/util/uuidutil"
    41  	"github.com/prebid/prebid-server/v2/version"
    42  )
    43  
    44  var defaultRequestTimeout int64 = 5000
    45  
    46  func NewVideoEndpoint(
    47  	uuidGenerator uuidutil.UUIDGenerator,
    48  	ex exchange.Exchange,
    49  	validator openrtb_ext.BidderParamValidator,
    50  	requestsById stored_requests.Fetcher,
    51  	videoFetcher stored_requests.Fetcher,
    52  	accounts stored_requests.AccountFetcher,
    53  	cfg *config.Configuration,
    54  	met metrics.MetricsEngine,
    55  	analyticsRunner analytics.Runner,
    56  	disabledBidders map[string]string,
    57  	defReqJSON []byte,
    58  	bidderMap map[string]openrtb_ext.BidderName,
    59  	cache prebid_cache_client.Client,
    60  	tmaxAdjustments *exchange.TmaxAdjustmentsPreprocessed,
    61  ) (httprouter.Handle, error) {
    62  
    63  	if ex == nil || validator == nil || requestsById == nil || accounts == nil || cfg == nil || met == nil {
    64  		return nil, errors.New("NewVideoEndpoint requires non-nil arguments.")
    65  	}
    66  
    67  	defRequest := len(defReqJSON) > 0
    68  
    69  	ipValidator := iputil.PublicNetworkIPValidator{
    70  		IPv4PrivateNetworks: cfg.RequestValidation.IPv4PrivateNetworksParsed,
    71  		IPv6PrivateNetworks: cfg.RequestValidation.IPv6PrivateNetworksParsed,
    72  	}
    73  
    74  	videoEndpointRegexp := regexp.MustCompile(`[<>]`)
    75  
    76  	return httprouter.Handle((&endpointDeps{
    77  		uuidGenerator,
    78  		ex,
    79  		validator,
    80  		requestsById,
    81  		videoFetcher,
    82  		accounts,
    83  		cfg,
    84  		met,
    85  		analyticsRunner,
    86  		disabledBidders,
    87  		defRequest,
    88  		defReqJSON,
    89  		bidderMap,
    90  		cache,
    91  		videoEndpointRegexp,
    92  		ipValidator,
    93  		empty_fetcher.EmptyFetcher{},
    94  		hooks.EmptyPlanBuilder{},
    95  		tmaxAdjustments,
    96  		openrtb_ext.NormalizeBidderName}).VideoAuctionEndpoint), nil
    97  }
    98  
    99  /*
   100   1. Parse "storedrequestid" field from simplified endpoint request body.
   101   2. If config flag to require that field is set (which it will be for us) and this field is not given then error out here.
   102   3. Load the stored request JSON for the given storedrequestid, if the id was invalid then error out here.
   103   4. Use "json-patch" 3rd party library to merge the request body JSON data into the stored request JSON data.
   104   5. Unmarshal the merged JSON data into a Go structure.
   105   6. Add fields from merged JSON data that correspond to an OpenRTB request into the OpenRTB bid request we are building.
   106      a. Unmarshal certain OpenRTB defined structs directly into the OpenRTB bid request.
   107      b. In cases where customized logic is needed just copy/fill the fields in directly.
   108   7. Call setFieldsImplicitly from auction.go to get basic data from the HTTP request into an OpenRTB bid request to start building the OpenRTB bid request.
   109   8. Loop through ad pods to build array of Imps into OpenRTB request, for each pod:
   110      a. Load the stored impression to use as the basis for impressions generated for this pod from the configid field.
   111      b. NumImps = adpoddurationsec / MIN_VALUE(allowedDurations)
   112      c. Build impression array for this pod:
   113      I.Create array of NumImps entries initialized to the base impression loaded from the configid.
   114   1. If requireexactdurations = true, iterate over allowdDurations and for (NumImps / len(allowedDurations)) number of Imps set minduration = maxduration = allowedDurations[i]
   115   2. If requireexactdurations = false, set maxduration = MAX_VALUE(allowedDurations)
   116      II. Set Imp.id field to "podX_Y" where X is the pod index and Y is the impression index within this pod.
   117      d. Append impressions for this pod to the overall list of impressions in the OpenRTB bid request.
   118   9. Call validateRequest() function from auction.go to validate the generated request.
   119   10. Call HoldAuction() function to run the auction for the OpenRTB bid request that was built in the previous step.
   120   11. Build proper response format.
   121  */
   122  func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
   123  	start := time.Now()
   124  
   125  	vo := analytics.VideoObject{
   126  		Status:    http.StatusOK,
   127  		Errors:    make([]error, 0),
   128  		StartTime: start,
   129  	}
   130  
   131  	labels := metrics.Labels{
   132  		Source:        metrics.DemandUnknown,
   133  		RType:         metrics.ReqTypeVideo,
   134  		PubID:         metrics.PublisherUnknown,
   135  		CookieFlag:    metrics.CookieFlagUnknown,
   136  		RequestStatus: metrics.RequestStatusOK,
   137  	}
   138  
   139  	debugQuery := r.URL.Query().Get("debug")
   140  	cacheTTL := int64(3600)
   141  	if deps.cfg.CacheURL.DefaultTTLs.Video > 0 {
   142  		cacheTTL = int64(deps.cfg.CacheURL.DefaultTTLs.Video)
   143  	}
   144  	debugLog := exchange.DebugLog{
   145  		Enabled:       strings.EqualFold(debugQuery, "true"),
   146  		CacheType:     prebid_cache_client.TypeXML,
   147  		TTL:           cacheTTL,
   148  		Regexp:        deps.debugLogRegexp,
   149  		DebugOverride: exchange.IsDebugOverrideEnabled(r.Header.Get(exchange.DebugOverrideHeader), deps.cfg.Debug.OverrideToken),
   150  	}
   151  	debugLog.DebugEnabledOrOverridden = debugLog.Enabled || debugLog.DebugOverride
   152  
   153  	activityControl := privacy.ActivityControl{}
   154  
   155  	defer func() {
   156  		if len(debugLog.CacheKey) > 0 && vo.VideoResponse == nil {
   157  			err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors)
   158  			if err != nil {
   159  				vo.Errors = append(vo.Errors, err)
   160  			}
   161  		}
   162  		deps.metricsEngine.RecordRequest(labels)
   163  		deps.metricsEngine.RecordRequestTime(labels, time.Since(start))
   164  		deps.analytics.LogVideoObject(&vo, activityControl)
   165  	}()
   166  
   167  	w.Header().Set("X-Prebid", version.BuildXPrebidHeader(version.Ver))
   168  	setBrowsingTopicsHeader(w, r)
   169  
   170  	lr := &io.LimitedReader{
   171  		R: r.Body,
   172  		N: deps.cfg.MaxRequestSize,
   173  	}
   174  	requestJson, err := io.ReadAll(lr)
   175  	if err != nil {
   176  		handleError(&labels, w, []error{err}, &vo, &debugLog)
   177  		return
   178  	}
   179  
   180  	resolvedRequest := requestJson
   181  	if debugLog.DebugEnabledOrOverridden {
   182  		debugLog.Data.Request = string(requestJson)
   183  		if headerBytes, err := jsonutil.Marshal(r.Header); err == nil {
   184  			debugLog.Data.Headers = string(headerBytes)
   185  		} else {
   186  			debugLog.Data.Headers = fmt.Sprintf("Unable to marshal headers data: %s", err.Error())
   187  		}
   188  	}
   189  
   190  	//load additional data - stored simplified req
   191  	storedRequestId, err := getVideoStoredRequestId(requestJson)
   192  
   193  	if err != nil {
   194  		if deps.cfg.VideoStoredRequestRequired {
   195  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   196  			return
   197  		}
   198  	} else {
   199  		storedRequest, errs := deps.loadStoredVideoRequest(context.Background(), storedRequestId)
   200  		if len(errs) > 0 {
   201  			handleError(&labels, w, errs, &vo, &debugLog)
   202  			return
   203  		}
   204  
   205  		//merge incoming req with stored video req
   206  		resolvedRequest, err = jsonpatch.MergePatch(storedRequest, requestJson)
   207  		if err != nil {
   208  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   209  			return
   210  		}
   211  	}
   212  	//unmarshal and validate combined result
   213  	videoBidReq, errL, podErrors := deps.parseVideoRequest(resolvedRequest, r.Header)
   214  	if len(errL) > 0 {
   215  		handleError(&labels, w, errL, &vo, &debugLog)
   216  		return
   217  	}
   218  
   219  	vo.VideoRequest = videoBidReq
   220  
   221  	var bidReq = &openrtb2.BidRequest{}
   222  	if deps.defaultRequest {
   223  		if err := jsonutil.UnmarshalValid(deps.defReqJSON, bidReq); err != nil {
   224  			err = fmt.Errorf("Invalid JSON in Default Request Settings: %s", err)
   225  			handleError(&labels, w, []error{err}, &vo, &debugLog)
   226  			return
   227  		}
   228  	}
   229  
   230  	//create full open rtb req from full video request
   231  	mergeData(videoBidReq, bidReq)
   232  	// If debug query param is set, force the response to enable test flag
   233  	if debugLog.DebugEnabledOrOverridden {
   234  		bidReq.Test = 1
   235  	}
   236  
   237  	initialPodNumber := len(videoBidReq.PodConfig.Pods)
   238  	if len(podErrors) > 0 {
   239  		//remove incorrect pods
   240  		videoBidReq = cleanupVideoBidRequest(videoBidReq, podErrors)
   241  	}
   242  
   243  	//create impressions array
   244  	imps, podErrors := deps.createImpressions(videoBidReq, podErrors)
   245  
   246  	if len(podErrors) == initialPodNumber {
   247  		resPodErr := make([]string, 0)
   248  		for _, podEr := range podErrors {
   249  			resPodErr = append(resPodErr, strings.Join(podEr.ErrMsgs, ", "))
   250  		}
   251  		err := fmt.Errorf("all pods are incorrect: %s", strings.Join(resPodErr, "; "))
   252  		errL = append(errL, err)
   253  		handleError(&labels, w, errL, &vo, &debugLog)
   254  		return
   255  	}
   256  
   257  	bidReq.Imp = imps
   258  	bidReq.ID = "bid_id" //TODO: look at prebid.js
   259  
   260  	// all code after this line should use the bidReqWrapper instead of bidReq directly
   261  	bidReqWrapper := &openrtb_ext.RequestWrapper{BidRequest: bidReq}
   262  
   263  	if err := ortb.SetDefaults(bidReqWrapper); err != nil {
   264  		handleError(&labels, w, errL, &vo, &debugLog)
   265  		return
   266  	}
   267  
   268  	ctx := context.Background()
   269  	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(bidReqWrapper.TMax) * time.Millisecond)
   270  	if timeout > 0 {
   271  		var cancel context.CancelFunc
   272  		ctx, cancel = context.WithDeadline(ctx, start.Add(timeout))
   273  		defer cancel()
   274  	}
   275  
   276  	// Read Usersyncs/Cookie
   277  	decoder := usersync.Base64Decoder{}
   278  	usersyncs := usersync.ReadCookie(r, decoder, &deps.cfg.HostCookie)
   279  	usersync.SyncHostCookie(r, usersyncs, &deps.cfg.HostCookie)
   280  
   281  	if bidReqWrapper.App != nil {
   282  		labels.Source = metrics.DemandApp
   283  		labels.PubID = getAccountID(bidReqWrapper.App.Publisher)
   284  	} else { // both bidReqWrapper.App == nil and bidReqWrapper.Site != nil are true
   285  		labels.Source = metrics.DemandWeb
   286  		if usersyncs.HasAnyLiveSyncs() {
   287  			labels.CookieFlag = metrics.CookieFlagYes
   288  		} else {
   289  			labels.CookieFlag = metrics.CookieFlagNo
   290  		}
   291  		labels.PubID = getAccountID(bidReqWrapper.Site.Publisher)
   292  	}
   293  
   294  	// Look up account now that we have resolved the pubID value
   295  	account, acctIDErrs := accountService.GetAccount(ctx, deps.cfg, deps.accounts, labels.PubID, deps.metricsEngine)
   296  	if len(acctIDErrs) > 0 {
   297  		handleError(&labels, w, acctIDErrs, &vo, &debugLog)
   298  		return
   299  	}
   300  
   301  	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
   302  	if errs := deps.setFieldsImplicitly(r, bidReqWrapper, account); len(errs) > 0 {
   303  		errL = append(errL, errs...)
   304  	}
   305  
   306  	errs := deps.validateRequest(account, r, bidReqWrapper, false, false, nil, false)
   307  	errL = append(errL, errs...)
   308  	if errortypes.ContainsFatalError(errL) {
   309  		handleError(&labels, w, errL, &vo, &debugLog)
   310  		return
   311  	}
   312  
   313  	activityControl = privacy.NewActivityControl(&account.Privacy)
   314  
   315  	warnings := errortypes.WarningOnly(errL)
   316  
   317  	secGPC := r.Header.Get("Sec-GPC")
   318  	auctionRequest := &exchange.AuctionRequest{
   319  		BidRequestWrapper:          bidReqWrapper,
   320  		Account:                    *account,
   321  		UserSyncs:                  usersyncs,
   322  		RequestType:                labels.RType,
   323  		StartTime:                  start,
   324  		LegacyLabels:               labels,
   325  		Warnings:                   warnings,
   326  		GlobalPrivacyControlHeader: secGPC,
   327  		PubID:                      labels.PubID,
   328  		HookExecutor:               hookexecution.EmptyHookExecutor{},
   329  		TmaxAdjustments:            deps.tmaxAdjustments,
   330  		Activities:                 activityControl,
   331  	}
   332  
   333  	auctionResponse, err := deps.ex.HoldAuction(ctx, auctionRequest, &debugLog)
   334  	defer func() {
   335  		if !auctionRequest.BidderResponseStartTime.IsZero() {
   336  			deps.metricsEngine.RecordOverheadTime(metrics.MakeAuctionResponse, time.Since(auctionRequest.BidderResponseStartTime))
   337  		}
   338  	}()
   339  	vo.RequestWrapper = bidReqWrapper
   340  	var response *openrtb2.BidResponse
   341  	if auctionResponse != nil {
   342  		response = auctionResponse.BidResponse
   343  	}
   344  	vo.Response = response
   345  	vo.SeatNonBid = auctionResponse.GetSeatNonBid()
   346  	if err != nil {
   347  		errL := []error{err}
   348  		handleError(&labels, w, errL, &vo, &debugLog)
   349  		return
   350  	}
   351  
   352  	//build simplified response
   353  	bidResp, err := buildVideoResponse(response, podErrors)
   354  	if err != nil {
   355  		errL := []error{err}
   356  		handleError(&labels, w, errL, &vo, &debugLog)
   357  		return
   358  	}
   359  	if bidReq.Test == 1 {
   360  		err = setSeatNonBidRaw(bidReqWrapper, auctionResponse)
   361  		if err != nil {
   362  			glog.Errorf("Error setting seat non-bid: %v", err)
   363  		}
   364  		bidResp.Ext = response.Ext
   365  	}
   366  
   367  	if len(bidResp.AdPods) == 0 && debugLog.DebugEnabledOrOverridden {
   368  		err := debugLog.PutDebugLogError(deps.cache, deps.cfg.CacheURL.ExpectedTimeMillis, vo.Errors)
   369  		if err != nil {
   370  			vo.Errors = append(vo.Errors, err)
   371  		} else {
   372  			bidResp.AdPods = append(bidResp.AdPods, &openrtb_ext.AdPod{
   373  				Targeting: []openrtb_ext.VideoTargeting{
   374  					{
   375  						HbCacheID: debugLog.CacheKey,
   376  					},
   377  				},
   378  			})
   379  		}
   380  	}
   381  
   382  	vo.VideoResponse = bidResp
   383  
   384  	resp, err := jsonutil.Marshal(bidResp)
   385  	if err != nil {
   386  		errL := []error{err}
   387  		handleError(&labels, w, errL, &vo, &debugLog)
   388  		return
   389  	}
   390  
   391  	w.Header().Set("Content-Type", "application/json")
   392  	w.Write(resp)
   393  }
   394  
   395  func cleanupVideoBidRequest(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) *openrtb_ext.BidRequestVideo {
   396  	for i := len(podErrors) - 1; i >= 0; i-- {
   397  		videoReq.PodConfig.Pods = append(videoReq.PodConfig.Pods[:podErrors[i].PodIndex], videoReq.PodConfig.Pods[podErrors[i].PodIndex+1:]...)
   398  	}
   399  	return videoReq
   400  }
   401  
   402  func handleError(labels *metrics.Labels, w http.ResponseWriter, errL []error, vo *analytics.VideoObject, debugLog *exchange.DebugLog) {
   403  	if debugLog != nil && debugLog.DebugEnabledOrOverridden {
   404  		if rawUUID, err := uuid.NewV4(); err == nil {
   405  			debugLog.CacheKey = rawUUID.String()
   406  		}
   407  		errL = append(errL, fmt.Errorf("[Debug cache ID: %s]", debugLog.CacheKey))
   408  	}
   409  	labels.RequestStatus = metrics.RequestStatusErr
   410  	var errors string
   411  	var status int = http.StatusInternalServerError
   412  	for _, er := range errL {
   413  		erVal := errortypes.ReadCode(er)
   414  		if erVal == errortypes.BlacklistedAppErrorCode || erVal == errortypes.AccountDisabledErrorCode {
   415  			status = http.StatusServiceUnavailable
   416  			labels.RequestStatus = metrics.RequestStatusBlacklisted
   417  			break
   418  		} else if erVal == errortypes.AcctRequiredErrorCode {
   419  			status = http.StatusBadRequest
   420  			labels.RequestStatus = metrics.RequestStatusBadInput
   421  			break
   422  		} else if erVal == errortypes.MalformedAcctErrorCode {
   423  			status = http.StatusInternalServerError
   424  			labels.RequestStatus = metrics.RequestStatusAccountConfigErr
   425  			break
   426  		}
   427  		errors = fmt.Sprintf("%s %s", errors, er.Error())
   428  	}
   429  	w.WriteHeader(status)
   430  	vo.Status = status
   431  	fmt.Fprintf(w, "Critical error while running the video endpoint: %v", errors)
   432  	glog.Errorf("/openrtb2/video Critical error: %v", errors)
   433  	vo.Errors = append(vo.Errors, errL...)
   434  }
   435  
   436  func (deps *endpointDeps) createImpressions(videoReq *openrtb_ext.BidRequestVideo, podErrors []PodError) ([]openrtb2.Imp, []PodError) {
   437  	videoDur := videoReq.PodConfig.DurationRangeSec
   438  	minDuration, maxDuration := minMax(videoDur)
   439  	reqExactDur := videoReq.PodConfig.RequireExactDuration
   440  	videoData := videoReq.Video
   441  
   442  	finalImpsArray := make([]openrtb2.Imp, 0)
   443  	for ind, pod := range videoReq.PodConfig.Pods {
   444  
   445  		//load stored impression
   446  		storedImpressionId := string(pod.ConfigId)
   447  		storedImp, errs := deps.loadStoredImp(storedImpressionId)
   448  		if errs != nil {
   449  			err := fmt.Sprintf("unable to load configid %s, Pod id: %d", storedImpressionId, pod.PodId)
   450  			podErr := PodError{}
   451  			podErr.PodId = pod.PodId
   452  			podErr.PodIndex = ind
   453  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   454  			podErrors = append(podErrors, podErr)
   455  			continue
   456  		}
   457  
   458  		numImps := pod.AdPodDurationSec / minDuration
   459  		if reqExactDur {
   460  			// In case of impressions number is less than durations array, we bump up impressions number up to duration array size
   461  			// with this handler we will have one impression per specified duration
   462  			numImps = max(numImps, len(videoDur))
   463  		}
   464  		impDivNumber := numImps / len(videoDur)
   465  
   466  		impsArray := make([]openrtb2.Imp, numImps)
   467  		for impInd := range impsArray {
   468  			newImp := createImpressionTemplate(storedImp, videoData)
   469  			impsArray[impInd] = newImp
   470  			if reqExactDur {
   471  				//floor := int(math.Floor(ind/impDivNumber))
   472  				durationIndex := impInd / impDivNumber
   473  				if durationIndex > len(videoDur)-1 {
   474  					durationIndex = len(videoDur) - 1
   475  				}
   476  				impsArray[impInd].Video.MaxDuration = int64(videoDur[durationIndex])
   477  				impsArray[impInd].Video.MinDuration = int64(videoDur[durationIndex])
   478  				//fmt.Println("Imp ind  ", impInd, "duration ", videoDur[durationIndex])
   479  			} else {
   480  				impsArray[impInd].Video.MaxDuration = int64(maxDuration)
   481  			}
   482  
   483  			impsArray[impInd].ID = fmt.Sprintf("%d_%d", pod.PodId, impInd)
   484  		}
   485  		finalImpsArray = append(finalImpsArray, impsArray...)
   486  
   487  	}
   488  	return finalImpsArray, podErrors
   489  }
   490  
   491  func max(a, b int) int {
   492  	if a > b {
   493  		return a
   494  	}
   495  	return b
   496  }
   497  
   498  func createImpressionTemplate(imp openrtb2.Imp, video *openrtb2.Video) openrtb2.Imp {
   499  	//for every new impression we need to have it's own copy of video object, because we customize it in further processing
   500  	newVideo := *video
   501  	imp.Video = &newVideo
   502  	return imp
   503  }
   504  
   505  func (deps *endpointDeps) loadStoredImp(storedImpId string) (openrtb2.Imp, []error) {
   506  	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(deps.cfg.StoredRequestsTimeout)*time.Millisecond)
   507  	defer cancel()
   508  
   509  	impr := openrtb2.Imp{}
   510  	_, imp, err := deps.storedReqFetcher.FetchRequests(ctx, []string{}, []string{storedImpId})
   511  	if err != nil {
   512  		return impr, err
   513  	}
   514  
   515  	if err := jsonutil.UnmarshalValid(imp[storedImpId], &impr); err != nil {
   516  		return impr, []error{err}
   517  	}
   518  	return impr, nil
   519  }
   520  
   521  func minMax(array []int) (int, int) {
   522  	var max = array[0]
   523  	var min = array[0]
   524  	for _, value := range array {
   525  		if max < value {
   526  			max = value
   527  		}
   528  		if min > value {
   529  			min = value
   530  		}
   531  	}
   532  	return min, max
   533  }
   534  
   535  func buildVideoResponse(bidresponse *openrtb2.BidResponse, podErrors []PodError) (*openrtb_ext.BidResponseVideo, error) {
   536  
   537  	adPods := make([]*openrtb_ext.AdPod, 0)
   538  	anyBidsReturned := false
   539  	for _, seatBid := range bidresponse.SeatBid {
   540  		for _, bid := range seatBid.Bid {
   541  			anyBidsReturned = true
   542  
   543  			var tempRespBidExt openrtb_ext.ExtBid
   544  			if err := jsonutil.UnmarshalValid(bid.Ext, &tempRespBidExt); err != nil {
   545  				return nil, err
   546  			}
   547  			if tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)] == "" {
   548  				continue
   549  			}
   550  
   551  			impId := bid.ImpID
   552  			podNum := strings.Split(impId, "_")[0]
   553  			podId, _ := strconv.ParseInt(podNum, 0, 64)
   554  
   555  			videoTargeting := openrtb_ext.VideoTargeting{
   556  				HbPb:       tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbpbConstantKey, seatBid.Seat)],
   557  				HbPbCatDur: tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbCategoryDurationKey, seatBid.Seat)],
   558  				HbCacheID:  tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbVastCacheKey, seatBid.Seat)],
   559  				HbDeal:     tempRespBidExt.Prebid.Targeting[formatTargetingKey(openrtb_ext.HbDealIDConstantKey, seatBid.Seat)],
   560  			}
   561  
   562  			adPod := findAdPod(podId, adPods)
   563  			if adPod == nil {
   564  				adPod = &openrtb_ext.AdPod{
   565  					PodId:     podId,
   566  					Targeting: make([]openrtb_ext.VideoTargeting, 0),
   567  				}
   568  				adPods = append(adPods, adPod)
   569  			}
   570  			adPod.Targeting = append(adPod.Targeting, videoTargeting)
   571  
   572  		}
   573  	}
   574  
   575  	//check if there are any bids in response.
   576  	//if there are no bids - empty response should be returned, no cache errors
   577  	if len(adPods) == 0 && anyBidsReturned {
   578  		//means there is a global cache error, we need to reject all bids
   579  		err := errors.New("caching failed for all bids")
   580  		return nil, err
   581  	}
   582  
   583  	// If there were incorrect pods, we put them back to response with error message
   584  	if len(podErrors) > 0 {
   585  		for _, podEr := range podErrors {
   586  			adPodEr := &openrtb_ext.AdPod{
   587  				PodId:  int64(podEr.PodId),
   588  				Errors: podEr.ErrMsgs,
   589  			}
   590  			adPods = append(adPods, adPodEr)
   591  		}
   592  	}
   593  
   594  	return &openrtb_ext.BidResponseVideo{AdPods: adPods}, nil
   595  }
   596  
   597  func formatTargetingKey(key openrtb_ext.TargetingKey, bidderName string) string {
   598  	fullKey := fmt.Sprintf("%s_%s", string(key), bidderName)
   599  	if len(fullKey) > exchange.MaxKeyLength {
   600  		return string(fullKey[0:exchange.MaxKeyLength])
   601  	}
   602  	return fullKey
   603  }
   604  
   605  func findAdPod(podInd int64, pods []*openrtb_ext.AdPod) *openrtb_ext.AdPod {
   606  	for _, pod := range pods {
   607  		if pod.PodId == podInd {
   608  			return pod
   609  		}
   610  	}
   611  	return nil
   612  }
   613  
   614  func (deps *endpointDeps) loadStoredVideoRequest(ctx context.Context, storedRequestId string) ([]byte, []error) {
   615  	storedRequests, _, errs := deps.videoFetcher.FetchRequests(ctx, []string{storedRequestId}, []string{})
   616  	jsonString := storedRequests[storedRequestId]
   617  	return jsonString, errs
   618  }
   619  
   620  func getVideoStoredRequestId(request []byte) (string, error) {
   621  	value, dataType, _, err := jsonparser.Get(request, "storedrequestid")
   622  	if dataType != jsonparser.String || err != nil {
   623  		return "", &errortypes.BadInput{Message: "Unable to find required stored request id"}
   624  	}
   625  	return string(value), nil
   626  }
   627  
   628  func mergeData(videoRequest *openrtb_ext.BidRequestVideo, bidRequest *openrtb2.BidRequest) error {
   629  	if videoRequest.Site != nil {
   630  		bidRequest.Site = videoRequest.Site
   631  		bidRequest.Site.Content = &videoRequest.Content
   632  	}
   633  
   634  	if videoRequest.App != nil {
   635  		bidRequest.App = videoRequest.App
   636  		bidRequest.App.Content = &videoRequest.Content
   637  	}
   638  
   639  	bidRequest.Device = &videoRequest.Device
   640  	bidRequest.User = videoRequest.User
   641  
   642  	if len(videoRequest.BCat) != 0 {
   643  		bidRequest.BCat = videoRequest.BCat
   644  	}
   645  
   646  	if len(videoRequest.BAdv) != 0 {
   647  		bidRequest.BAdv = videoRequest.BAdv
   648  	}
   649  
   650  	bidExt, err := createBidExtension(videoRequest)
   651  	if err != nil {
   652  		return err
   653  	}
   654  	if len(bidExt) > 0 {
   655  		bidRequest.Ext = bidExt
   656  	}
   657  
   658  	bidRequest.Test = videoRequest.Test
   659  
   660  	if videoRequest.TMax == 0 {
   661  		bidRequest.TMax = defaultRequestTimeout
   662  	} else {
   663  		bidRequest.TMax = videoRequest.TMax
   664  	}
   665  
   666  	if videoRequest.Regs != nil {
   667  		bidRequest.Regs = videoRequest.Regs
   668  	}
   669  
   670  	return nil
   671  }
   672  
   673  func createBidExtension(videoRequest *openrtb_ext.BidRequestVideo) ([]byte, error) {
   674  	var inclBrandCat *openrtb_ext.ExtIncludeBrandCategory
   675  	if videoRequest.IncludeBrandCategory != nil {
   676  		inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{
   677  			PrimaryAdServer:     videoRequest.IncludeBrandCategory.PrimaryAdserver,
   678  			Publisher:           videoRequest.IncludeBrandCategory.Publisher,
   679  			WithCategory:        true,
   680  			TranslateCategories: videoRequest.IncludeBrandCategory.TranslateCategories,
   681  		}
   682  	} else {
   683  		inclBrandCat = &openrtb_ext.ExtIncludeBrandCategory{
   684  			WithCategory: false,
   685  		}
   686  	}
   687  
   688  	var durationRangeSec []int
   689  	if !videoRequest.PodConfig.RequireExactDuration {
   690  		durationRangeSec = videoRequest.PodConfig.DurationRangeSec
   691  	}
   692  
   693  	targeting := openrtb_ext.ExtRequestTargeting{
   694  		PriceGranularity:     videoRequest.PriceGranularity,
   695  		IncludeBrandCategory: inclBrandCat,
   696  		DurationRangeSec:     durationRangeSec,
   697  		IncludeBidderKeys:    ptrutil.ToPtr(true),
   698  		AppendBidderNames:    videoRequest.AppendBidderNames,
   699  	}
   700  
   701  	vastXml := openrtb_ext.ExtRequestPrebidCacheVAST{}
   702  	cache := openrtb_ext.ExtRequestPrebidCache{
   703  		VastXML: &vastXml,
   704  	}
   705  
   706  	prebid := openrtb_ext.ExtRequestPrebid{
   707  		Cache:        &cache,
   708  		Targeting:    &targeting,
   709  		SupportDeals: videoRequest.SupportDeals,
   710  	}
   711  	extReq := openrtb_ext.ExtRequest{Prebid: prebid}
   712  
   713  	return jsonutil.Marshal(extReq)
   714  }
   715  
   716  func (deps *endpointDeps) parseVideoRequest(request []byte, headers http.Header) (req *openrtb_ext.BidRequestVideo, errs []error, podErrors []PodError) {
   717  	req = &openrtb_ext.BidRequestVideo{}
   718  
   719  	if err := jsonutil.UnmarshalValid(request, &req); err != nil {
   720  		errs = []error{err}
   721  		return
   722  	}
   723  
   724  	//if Device.UA is not present in request body, init it with user-agent from request header if it's present
   725  	if req.Device.UA == "" {
   726  		ua := headers.Get("User-Agent")
   727  
   728  		//Check UA is encoded. Without it the `+` character would get changed to a space if not actually encoded
   729  		if strings.ContainsAny(ua, "%") {
   730  			var err error
   731  			req.Device.UA, err = url.QueryUnescape(ua)
   732  			if err != nil {
   733  				req.Device.UA = ua
   734  			}
   735  		} else {
   736  			req.Device.UA = ua
   737  		}
   738  	}
   739  
   740  	errL, podErrors := deps.validateVideoRequest(req)
   741  	if len(errL) > 0 {
   742  		errs = append(errs, errL...)
   743  	}
   744  	return
   745  }
   746  
   747  type PodError struct {
   748  	PodId    int
   749  	PodIndex int
   750  	ErrMsgs  []string
   751  }
   752  
   753  func (deps *endpointDeps) validateVideoRequest(req *openrtb_ext.BidRequestVideo) ([]error, []PodError) {
   754  	errL := []error{}
   755  
   756  	if deps.cfg.VideoStoredRequestRequired && req.StoredRequestId == "" {
   757  		err := errors.New("request missing required field: storedrequestid")
   758  		errL = append(errL, err)
   759  	}
   760  	if len(req.PodConfig.DurationRangeSec) == 0 {
   761  		err := errors.New("request missing required field: PodConfig.DurationRangeSec")
   762  		errL = append(errL, err)
   763  	}
   764  	if isZeroOrNegativeDuration(req.PodConfig.DurationRangeSec) {
   765  		err := errors.New("duration array cannot contain negative or zero values")
   766  		errL = append(errL, err)
   767  	}
   768  	if len(req.PodConfig.Pods) == 0 {
   769  		err := errors.New("request missing required field: PodConfig.Pods")
   770  		errL = append(errL, err)
   771  	}
   772  	podErrors := make([]PodError, 0)
   773  	podIdsSet := make(map[int]bool)
   774  	for ind, pod := range req.PodConfig.Pods {
   775  		podErr := PodError{}
   776  
   777  		if podIdsSet[pod.PodId] {
   778  			err := fmt.Sprintf("request duplicated required field: PodConfig.Pods.PodId, Pod id: %d", pod.PodId)
   779  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   780  		} else {
   781  			podIdsSet[pod.PodId] = true
   782  		}
   783  		if pod.PodId <= 0 {
   784  			err := fmt.Sprintf("request missing required field: PodConfig.Pods.PodId, Pod index: %d", ind)
   785  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   786  		}
   787  		if pod.AdPodDurationSec == 0 {
   788  			err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.AdPodDurationSec, Pod index: %d", ind)
   789  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   790  		}
   791  		if pod.AdPodDurationSec < 0 {
   792  			err := fmt.Sprintf("request incorrect required field: PodConfig.Pods.AdPodDurationSec is negative, Pod index: %d", ind)
   793  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   794  		}
   795  		if pod.ConfigId == "" {
   796  			err := fmt.Sprintf("request missing or incorrect required field: PodConfig.Pods.ConfigId, Pod index: %d", ind)
   797  			podErr.ErrMsgs = append(podErr.ErrMsgs, err)
   798  		}
   799  		if len(podErr.ErrMsgs) > 0 {
   800  			podErr.PodId = pod.PodId
   801  			podErr.PodIndex = ind
   802  			podErrors = append(podErrors, podErr)
   803  		}
   804  	}
   805  	if req.App == nil && req.Site == nil {
   806  		err := errors.New("request missing required field: site or app")
   807  		errL = append(errL, err)
   808  	} else if req.App != nil && req.Site != nil {
   809  		err := errors.New("request.site or request.app must be defined, but not both")
   810  		errL = append(errL, err)
   811  	} else if req.Site != nil && req.Site.ID == "" && req.Site.Page == "" {
   812  		err := errors.New("request.site missing required field: id or page")
   813  		errL = append(errL, err)
   814  	} else if req.App != nil {
   815  		if req.App.ID != "" {
   816  			if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found {
   817  				err := &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)}
   818  				errL = append(errL, err)
   819  				return errL, podErrors
   820  			}
   821  		} else {
   822  			if req.App.Bundle == "" {
   823  				err := errors.New("request.app missing required field: id or bundle")
   824  				errL = append(errL, err)
   825  			}
   826  		}
   827  	}
   828  
   829  	if req.Video != nil {
   830  		if len(req.Video.MIMEs) == 0 {
   831  			err := errors.New("request missing required field: Video.Mimes")
   832  			errL = append(errL, err)
   833  		} else {
   834  			mimes := make([]string, 0, len(req.Video.MIMEs))
   835  			for _, mime := range req.Video.MIMEs {
   836  				if mime != "" {
   837  					mimes = append(mimes, mime)
   838  				}
   839  			}
   840  			if len(mimes) == 0 {
   841  				err := errors.New("request missing required field: Video.Mimes, mime types contains empty strings only")
   842  				errL = append(errL, err)
   843  			}
   844  			req.Video.MIMEs = mimes
   845  		}
   846  
   847  		if len(req.Video.Protocols) == 0 {
   848  			err := errors.New("request missing required field: Video.Protocols")
   849  			errL = append(errL, err)
   850  		}
   851  	} else {
   852  		err := errors.New("request missing required field: Video")
   853  		errL = append(errL, err)
   854  	}
   855  
   856  	return errL, podErrors
   857  }
   858  
   859  func isZeroOrNegativeDuration(duration []int) bool {
   860  	for _, value := range duration {
   861  		if value <= 0 {
   862  			return true
   863  		}
   864  	}
   865  	return false
   866  }