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

     1  package endpoints
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/julienschmidt/httprouter"
    13  	gpplib "github.com/prebid/go-gpp"
    14  	gppConstants "github.com/prebid/go-gpp/constants"
    15  	accountService "github.com/prebid/prebid-server/v2/account"
    16  	"github.com/prebid/prebid-server/v2/analytics"
    17  	"github.com/prebid/prebid-server/v2/config"
    18  	"github.com/prebid/prebid-server/v2/errortypes"
    19  	"github.com/prebid/prebid-server/v2/gdpr"
    20  	"github.com/prebid/prebid-server/v2/metrics"
    21  	"github.com/prebid/prebid-server/v2/openrtb_ext"
    22  	"github.com/prebid/prebid-server/v2/privacy"
    23  	gppPrivacy "github.com/prebid/prebid-server/v2/privacy/gpp"
    24  	"github.com/prebid/prebid-server/v2/stored_requests"
    25  	"github.com/prebid/prebid-server/v2/usersync"
    26  	"github.com/prebid/prebid-server/v2/util/httputil"
    27  	stringutil "github.com/prebid/prebid-server/v2/util/stringutil"
    28  )
    29  
    30  const (
    31  	chromeStr       = "Chrome/"
    32  	chromeiOSStr    = "CriOS/"
    33  	chromeMinVer    = 67
    34  	chromeStrLen    = len(chromeStr)
    35  	chromeiOSStrLen = len(chromeiOSStr)
    36  )
    37  
    38  const uidCookieName = "uids"
    39  
    40  func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, analyticsRunner analytics.Runner, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle {
    41  	encoder := usersync.Base64Encoder{}
    42  	decoder := usersync.Base64Decoder{}
    43  
    44  	return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    45  		so := analytics.SetUIDObject{
    46  			Status: http.StatusOK,
    47  			Errors: make([]error, 0),
    48  		}
    49  
    50  		defer analyticsRunner.LogSetUIDObject(&so)
    51  
    52  		cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie)
    53  		if !cookie.AllowSyncs() {
    54  			handleBadStatus(w, http.StatusUnauthorized, metrics.SetUidOptOut, nil, metricsEngine, &so)
    55  			return
    56  		}
    57  		usersync.SyncHostCookie(r, cookie, &cfg.HostCookie)
    58  
    59  		query := r.URL.Query()
    60  
    61  		syncer, bidderName, err := getSyncer(query, syncersByBidder)
    62  		if err != nil {
    63  			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidSyncerUnknown, err, metricsEngine, &so)
    64  			return
    65  		}
    66  		so.Bidder = syncer.Key()
    67  
    68  		responseFormat, err := getResponseFormat(query, syncer)
    69  		if err != nil {
    70  			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
    71  			return
    72  		}
    73  
    74  		accountID := query.Get("account")
    75  		if accountID == "" {
    76  			accountID = metrics.PublisherUnknown
    77  		}
    78  		account, fetchErrs := accountService.GetAccount(context.Background(), cfg, accountsFetcher, accountID, metricsEngine)
    79  		if len(fetchErrs) > 0 {
    80  			var metricValue metrics.SetUidStatus
    81  			err := combineErrors(fetchErrs)
    82  			switch err {
    83  			case errCookieSyncAccountBlocked:
    84  				metricValue = metrics.SetUidAccountBlocked
    85  			case errCookieSyncAccountConfigMalformed:
    86  				metricValue = metrics.SetUidAccountConfigMalformed
    87  			case errCookieSyncAccountInvalid:
    88  				metricValue = metrics.SetUidAccountInvalid
    89  			default:
    90  				metricValue = metrics.SetUidBadRequest
    91  			}
    92  			handleBadStatus(w, http.StatusBadRequest, metricValue, err, metricsEngine, &so)
    93  			return
    94  		}
    95  
    96  		activityControl := privacy.NewActivityControl(&account.Privacy)
    97  
    98  		gppSID, err := stringutil.StrToInt8Slice(query.Get("gpp_sid"))
    99  		if err != nil {
   100  			err := fmt.Errorf("invalid gpp_sid encoding, must be a csv list of integers")
   101  			w.WriteHeader(http.StatusBadRequest)
   102  			w.Write([]byte(err.Error()))
   103  			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
   104  			so.Errors = []error{err}
   105  			so.Status = http.StatusBadRequest
   106  			return
   107  		}
   108  
   109  		policies := privacy.Policies{
   110  			GPPSID: gppSID,
   111  		}
   112  
   113  		userSyncActivityAllowed := activityControl.Allow(privacy.ActivitySyncUser,
   114  			privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName},
   115  			privacy.NewRequestFromPolicies(policies))
   116  
   117  		if !userSyncActivityAllowed {
   118  			w.WriteHeader(http.StatusUnavailableForLegalReasons)
   119  			return
   120  		}
   121  
   122  		gdprRequestInfo, err := extractGDPRInfo(query)
   123  		if err != nil {
   124  			// Only exit if non-warning
   125  			if !errortypes.IsWarning(err) {
   126  				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
   127  				return
   128  			}
   129  		}
   130  
   131  		tcf2Cfg := tcf2CfgBuilder(cfg.GDPR.TCF2, account.GDPR)
   132  
   133  		if shouldReturn, status, body := preventSyncsGDPR(gdprRequestInfo, gdprPermsBuilder, tcf2Cfg); shouldReturn {
   134  			var metricValue metrics.SetUidStatus
   135  			switch status {
   136  			case http.StatusBadRequest:
   137  				metricValue = metrics.SetUidBadRequest
   138  			case http.StatusUnavailableForLegalReasons:
   139  				metricValue = metrics.SetUidGDPRHostCookieBlocked
   140  			}
   141  			handleBadStatus(w, status, metricValue, errors.New(body), metricsEngine, &so)
   142  			return
   143  		}
   144  
   145  		uid := query.Get("uid")
   146  		so.UID = uid
   147  
   148  		if uid == "" {
   149  			cookie.Unsync(syncer.Key())
   150  			metricsEngine.RecordSetUid(metrics.SetUidOK)
   151  			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidCleared)
   152  			so.Success = true
   153  		} else if err = cookie.Sync(syncer.Key(), uid); err == nil {
   154  			metricsEngine.RecordSetUid(metrics.SetUidOK)
   155  			metricsEngine.RecordSyncerSet(syncer.Key(), metrics.SyncerSetUidOK)
   156  			so.Success = true
   157  		}
   158  
   159  		setSiteCookie := siteCookieCheck(r.UserAgent())
   160  
   161  		// Priority Ejector Set Up
   162  		priorityEjector := &usersync.PriorityBidderEjector{PriorityGroups: cfg.UserSync.PriorityGroups, TieEjector: &usersync.OldestEjector{}, SyncersByBidder: syncersByBidder}
   163  		priorityEjector.IsSyncerPriority = isSyncerPriority(bidderName, cfg.UserSync.PriorityGroups)
   164  
   165  		// Write Cookie
   166  		encodedCookie, err := cookie.PrepareCookieForWrite(&cfg.HostCookie, encoder, priorityEjector)
   167  		if err != nil {
   168  			if err.Error() == errSyncerIsNotPriority.Error() {
   169  				w.WriteHeader(http.StatusOK)
   170  				w.Write([]byte("Warning: " + err.Error() + ", cookie not updated"))
   171  				so.Status = http.StatusOK
   172  				return
   173  			} else {
   174  				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
   175  				return
   176  			}
   177  		}
   178  		usersync.WriteCookie(w, encodedCookie, &cfg.HostCookie, setSiteCookie)
   179  
   180  		switch responseFormat {
   181  		case "i":
   182  			w.Header().Add("Content-Type", httputil.Pixel1x1PNG.ContentType)
   183  			w.Header().Add("Content-Length", strconv.Itoa(len(httputil.Pixel1x1PNG.Content)))
   184  			w.WriteHeader(http.StatusOK)
   185  			w.Write(httputil.Pixel1x1PNG.Content)
   186  		case "b":
   187  			w.Header().Add("Content-Type", "text/html")
   188  			w.Header().Add("Content-Length", "0")
   189  			w.WriteHeader(http.StatusOK)
   190  		}
   191  	})
   192  }
   193  
   194  // extractGDPRInfo looks for the GDPR consent string and GDPR signal in the GPP query params
   195  // first and the 'gdpr' and 'gdpr_consent' query params second. If found in both, throws a
   196  // warning. Can also throw a parsing or validation error
   197  func extractGDPRInfo(query url.Values) (reqInfo gdpr.RequestInfo, err error) {
   198  	reqInfo, err = parseGDPRFromGPP(query)
   199  	if err != nil {
   200  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
   201  	}
   202  
   203  	legacySignal, legacyConsent, err := parseLegacyGDPRFields(query, reqInfo.GDPRSignal, reqInfo.Consent)
   204  	isWarning := errortypes.IsWarning(err)
   205  
   206  	if err != nil && !isWarning {
   207  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
   208  	}
   209  
   210  	// If no GDPR data in the GPP fields, use legacy instead
   211  	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalAmbiguous {
   212  		reqInfo.GDPRSignal = legacySignal
   213  		reqInfo.Consent = legacyConsent
   214  	}
   215  
   216  	if reqInfo.Consent == "" && reqInfo.GDPRSignal == gdpr.SignalYes {
   217  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, errors.New("GDPR consent is required when gdpr signal equals 1")
   218  	}
   219  
   220  	return reqInfo, err
   221  }
   222  
   223  // parseGDPRFromGPP parses and validates the "gpp_sid" and "gpp" query fields.
   224  func parseGDPRFromGPP(query url.Values) (gdpr.RequestInfo, error) {
   225  	gdprSignal, err := parseSignalFromGppSidStr(query.Get("gpp_sid"))
   226  	if err != nil {
   227  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
   228  	}
   229  
   230  	gdprConsent, errs := parseConsentFromGppStr(query.Get("gpp"))
   231  	if len(errs) > 0 {
   232  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, errs[0]
   233  	}
   234  
   235  	return gdpr.RequestInfo{
   236  		Consent:    gdprConsent,
   237  		GDPRSignal: gdprSignal,
   238  	}, nil
   239  }
   240  
   241  // parseLegacyGDPRFields parses and validates the "gdpr" and "gdpr_consent" query fields which
   242  // are considered deprecated in favor of the "gpp" and "gpp_sid". The parsed and validated GDPR
   243  // values contained in "gpp" and "gpp_sid" are passed in the parameters gppGDPRSignal and
   244  // gppGDPRConsent. If the GPP parameters come with non-default values, this function discards
   245  // "gdpr" and "gdpr_consent" and returns a warning.
   246  func parseLegacyGDPRFields(query url.Values, gppGDPRSignal gdpr.Signal, gppGDPRConsent string) (gdpr.Signal, string, error) {
   247  	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
   248  	var gdprConsent string
   249  	var warning error
   250  
   251  	if gdprQuerySignal := query.Get("gdpr"); len(gdprQuerySignal) > 0 {
   252  		if gppGDPRSignal == gdpr.SignalAmbiguous {
   253  			switch gdprQuerySignal {
   254  			case "0":
   255  				fallthrough
   256  			case "1":
   257  				if zeroOrOne, err := strconv.Atoi(gdprQuerySignal); err == nil {
   258  					gdprSignal = gdpr.Signal(zeroOrOne)
   259  				}
   260  			default:
   261  				return gdpr.SignalAmbiguous, "", errors.New("the gdpr query param must be either 0 or 1. You gave " + gdprQuerySignal)
   262  			}
   263  		} else {
   264  			warning = &errortypes.Warning{
   265  				Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
   266  				WarningCode: errortypes.UnknownWarningCode,
   267  			}
   268  		}
   269  	}
   270  
   271  	if gdprLegacyConsent := query.Get("gdpr_consent"); len(gdprLegacyConsent) > 0 {
   272  		if len(gppGDPRConsent) > 0 {
   273  			warning = &errortypes.Warning{
   274  				Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
   275  				WarningCode: errortypes.UnknownWarningCode,
   276  			}
   277  		} else {
   278  			gdprConsent = gdprLegacyConsent
   279  		}
   280  	}
   281  	return gdprSignal, gdprConsent, warning
   282  }
   283  
   284  func parseSignalFromGppSidStr(strSID string) (gdpr.Signal, error) {
   285  	gdprSignal := gdpr.SignalAmbiguous
   286  
   287  	if len(strSID) > 0 {
   288  		gppSID, err := stringutil.StrToInt8Slice(strSID)
   289  		if err != nil {
   290  			return gdpr.SignalAmbiguous, fmt.Errorf("Error parsing gpp_sid %s", err.Error())
   291  		}
   292  
   293  		if len(gppSID) > 0 {
   294  			gdprSignal = gdpr.SignalNo
   295  			if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
   296  				gdprSignal = gdpr.SignalYes
   297  			}
   298  		}
   299  	}
   300  
   301  	return gdprSignal, nil
   302  }
   303  
   304  func parseConsentFromGppStr(gppQueryValue string) (string, []error) {
   305  	var gdprConsent string
   306  
   307  	if len(gppQueryValue) > 0 {
   308  		gpp, errs := gpplib.Parse(gppQueryValue)
   309  		if len(errs) > 0 {
   310  			return "", errs
   311  		}
   312  
   313  		if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
   314  			gdprConsent = gpp.Sections[i].GetValue()
   315  		}
   316  	}
   317  
   318  	return gdprConsent, nil
   319  }
   320  
   321  func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, string, error) {
   322  	bidder := query.Get("bidder")
   323  
   324  	if bidder == "" {
   325  		return nil, "", errors.New(`"bidder" query param is required`)
   326  	}
   327  
   328  	// case insensitive comparison
   329  	bidderNormalized, bidderFound := openrtb_ext.NormalizeBidderName(bidder)
   330  	if !bidderFound {
   331  		return nil, "", errors.New("The bidder name provided is not supported by Prebid Server")
   332  	}
   333  
   334  	syncer, syncerExists := syncersByBidder[bidderNormalized.String()]
   335  	if !syncerExists {
   336  		return nil, "", errors.New("The bidder name provided is not supported by Prebid Server")
   337  	}
   338  
   339  	return syncer, bidder, nil
   340  }
   341  
   342  func isSyncerPriority(bidderNameFromSyncerQuery string, priorityGroups [][]string) bool {
   343  	for _, group := range priorityGroups {
   344  		for _, bidder := range group {
   345  			if strings.EqualFold(bidderNameFromSyncerQuery, bidder) {
   346  				return true
   347  			}
   348  		}
   349  	}
   350  	return false
   351  }
   352  
   353  // getResponseFormat reads the format query parameter or falls back to the syncer's default.
   354  // Returns either "b" (iframe), "i" (redirect), or an empty string "" (legacy behavior of an
   355  // empty response body with no content type).
   356  func getResponseFormat(query url.Values, syncer usersync.Syncer) (string, error) {
   357  	format, formatProvided := query["f"]
   358  	formatEmpty := len(format) == 0 || format[0] == ""
   359  
   360  	if !formatProvided || formatEmpty {
   361  		switch syncer.DefaultResponseFormat() {
   362  		case usersync.SyncTypeIFrame:
   363  			return "b", nil
   364  		case usersync.SyncTypeRedirect:
   365  			return "i", nil
   366  		default:
   367  			return "", nil
   368  		}
   369  	}
   370  
   371  	if !strings.EqualFold(format[0], "b") && !strings.EqualFold(format[0], "i") {
   372  		return "", errors.New(`"f" query param is invalid. must be "b" or "i"`)
   373  	}
   374  	return strings.ToLower(format[0]), nil
   375  }
   376  
   377  // siteCookieCheck scans the input User Agent string to check if browser is Chrome and browser version is greater than the minimum version for adding the SameSite cookie attribute
   378  func siteCookieCheck(ua string) bool {
   379  	result := false
   380  
   381  	index := strings.Index(ua, chromeStr)
   382  	criOSIndex := strings.Index(ua, chromeiOSStr)
   383  	if index != -1 {
   384  		result = checkChromeBrowserVersion(ua, index, chromeStrLen)
   385  	} else if criOSIndex != -1 {
   386  		result = checkChromeBrowserVersion(ua, criOSIndex, chromeiOSStrLen)
   387  	}
   388  
   389  	return result
   390  }
   391  
   392  func checkChromeBrowserVersion(ua string, index int, chromeStrLength int) bool {
   393  	result := false
   394  	vIndex := index + chromeStrLength
   395  	dotIndex := strings.Index(ua[vIndex:], ".")
   396  	if dotIndex == -1 {
   397  		dotIndex = len(ua[vIndex:])
   398  	}
   399  	version, _ := strconv.Atoi(ua[vIndex : vIndex+dotIndex])
   400  	if version >= chromeMinVer {
   401  		result = true
   402  	}
   403  	return result
   404  }
   405  
   406  func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
   407  	perms := permsBuilder(tcf2Cfg, gdprRequestInfo)
   408  
   409  	allowed, err := perms.HostCookiesAllowed(context.Background())
   410  	if err != nil {
   411  		if _, ok := err.(*gdpr.ErrorMalformedConsent); ok {
   412  			return true, http.StatusBadRequest, "gdpr_consent was invalid. " + err.Error()
   413  		}
   414  
   415  		// We can't distinguish between requests for a new version of the global vendor list, and requests
   416  		// which are malformed (version number is much too large). Since we try to fetch new versions as we
   417  		// receive requests, PBS *should* self-correct quickly, allowing us to assume most of the errors
   418  		// caught here will be malformed strings.
   419  		return true, http.StatusBadRequest, "No global vendor list was available to interpret this consent string. If this is a new, valid version, it should become available soon."
   420  	}
   421  
   422  	if allowed {
   423  		return false, 0, ""
   424  	}
   425  
   426  	return true, http.StatusUnavailableForLegalReasons, "The gdpr_consent string prevents cookies from being saved"
   427  }
   428  
   429  func handleBadStatus(w http.ResponseWriter, status int, metricValue metrics.SetUidStatus, err error, me metrics.MetricsEngine, so *analytics.SetUIDObject) {
   430  	w.WriteHeader(status)
   431  	me.RecordSetUid(metricValue)
   432  	so.Status = status
   433  
   434  	if err != nil {
   435  		so.Errors = []error{err}
   436  		w.Write([]byte(err.Error()))
   437  	}
   438  }