github.com/prebid/prebid-server@v0.275.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/account"
    16  	"github.com/prebid/prebid-server/analytics"
    17  	"github.com/prebid/prebid-server/config"
    18  	"github.com/prebid/prebid-server/errortypes"
    19  	"github.com/prebid/prebid-server/gdpr"
    20  	"github.com/prebid/prebid-server/metrics"
    21  	"github.com/prebid/prebid-server/privacy"
    22  	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
    23  	"github.com/prebid/prebid-server/stored_requests"
    24  	"github.com/prebid/prebid-server/usersync"
    25  	"github.com/prebid/prebid-server/util/httputil"
    26  	stringutil "github.com/prebid/prebid-server/util/stringutil"
    27  )
    28  
    29  const (
    30  	chromeStr       = "Chrome/"
    31  	chromeiOSStr    = "CriOS/"
    32  	chromeMinVer    = 67
    33  	chromeStrLen    = len(chromeStr)
    34  	chromeiOSStrLen = len(chromeiOSStr)
    35  )
    36  
    37  const uidCookieName = "uids"
    38  
    39  func NewSetUIDEndpoint(cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, pbsanalytics analytics.PBSAnalyticsModule, accountsFetcher stored_requests.AccountFetcher, metricsEngine metrics.MetricsEngine) httprouter.Handle {
    40  	encoder := usersync.Base64Encoder{}
    41  	decoder := usersync.Base64Decoder{}
    42  
    43  	return httprouter.Handle(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    44  		so := analytics.SetUIDObject{
    45  			Status: http.StatusOK,
    46  			Errors: make([]error, 0),
    47  		}
    48  
    49  		defer pbsanalytics.LogSetUIDObject(&so)
    50  
    51  		cookie := usersync.ReadCookie(r, decoder, &cfg.HostCookie)
    52  		if !cookie.AllowSyncs() {
    53  			handleBadStatus(w, http.StatusUnauthorized, metrics.SetUidOptOut, nil, metricsEngine, &so)
    54  			return
    55  		}
    56  		usersync.SyncHostCookie(r, cookie, &cfg.HostCookie)
    57  
    58  		query := r.URL.Query()
    59  
    60  		syncer, bidderName, err := getSyncer(query, syncersByBidder)
    61  		if err != nil {
    62  			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidSyncerUnknown, err, metricsEngine, &so)
    63  			return
    64  		}
    65  		so.Bidder = syncer.Key()
    66  
    67  		responseFormat, err := getResponseFormat(query, syncer)
    68  		if err != nil {
    69  			handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
    70  			return
    71  		}
    72  
    73  		accountID := query.Get("account")
    74  		if accountID == "" {
    75  			accountID = metrics.PublisherUnknown
    76  		}
    77  		account, fetchErrs := accountService.GetAccount(context.Background(), cfg, accountsFetcher, accountID, metricsEngine)
    78  		if len(fetchErrs) > 0 {
    79  			var metricValue metrics.SetUidStatus
    80  			err := combineErrors(fetchErrs)
    81  			switch err {
    82  			case errCookieSyncAccountBlocked:
    83  				metricValue = metrics.SetUidAccountBlocked
    84  			case errCookieSyncAccountConfigMalformed:
    85  				metricValue = metrics.SetUidAccountConfigMalformed
    86  			case errCookieSyncAccountInvalid:
    87  				metricValue = metrics.SetUidAccountInvalid
    88  			default:
    89  				metricValue = metrics.SetUidBadRequest
    90  			}
    91  			handleBadStatus(w, http.StatusBadRequest, metricValue, err, metricsEngine, &so)
    92  			return
    93  		}
    94  
    95  		activityControl := privacy.NewActivityControl(&account.Privacy)
    96  
    97  		gppSID, err := stringutil.StrToInt8Slice(query.Get("gpp_sid"))
    98  		if err != nil {
    99  			err := fmt.Errorf("invalid gpp_sid encoding, must be a csv list of integers")
   100  			w.WriteHeader(http.StatusBadRequest)
   101  			w.Write([]byte(err.Error()))
   102  			metricsEngine.RecordSetUid(metrics.SetUidBadRequest)
   103  			so.Errors = []error{err}
   104  			so.Status = http.StatusBadRequest
   105  			return
   106  		}
   107  
   108  		policies := privacy.Policies{
   109  			GPPSID: gppSID,
   110  		}
   111  
   112  		userSyncActivityAllowed := activityControl.Allow(privacy.ActivitySyncUser,
   113  			privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidderName},
   114  			privacy.NewRequestFromPolicies(policies))
   115  
   116  		if !userSyncActivityAllowed {
   117  			w.WriteHeader(http.StatusUnavailableForLegalReasons)
   118  			return
   119  		}
   120  
   121  		gdprRequestInfo, err := extractGDPRInfo(query)
   122  		if err != nil {
   123  			// Only exit if non-warning
   124  			if !errortypes.IsWarning(err) {
   125  				handleBadStatus(w, http.StatusBadRequest, metrics.SetUidBadRequest, err, metricsEngine, &so)
   126  				return
   127  			}
   128  			w.Write([]byte("Warning: " + err.Error()))
   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  	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
   226  	var gdprConsent string = ""
   227  	var err error
   228  
   229  	gdprSignal, err = parseSignalFromGppSidStr(query.Get("gpp_sid"))
   230  	if err != nil {
   231  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
   232  	}
   233  
   234  	gdprConsent, err = parseConsentFromGppStr(query.Get("gpp"))
   235  	if err != nil {
   236  		return gdpr.RequestInfo{GDPRSignal: gdpr.SignalAmbiguous}, err
   237  	}
   238  
   239  	return gdpr.RequestInfo{
   240  		Consent:    gdprConsent,
   241  		GDPRSignal: gdprSignal,
   242  	}, nil
   243  }
   244  
   245  // parseLegacyGDPRFields parses and validates the "gdpr" and "gdpr_consent" query fields which
   246  // are considered deprecated in favor of the "gpp" and "gpp_sid". The parsed and validated GDPR
   247  // values contained in "gpp" and "gpp_sid" are passed in the parameters gppGDPRSignal and
   248  // gppGDPRConsent. If the GPP parameters come with non-default values, this function discards
   249  // "gdpr" and "gdpr_consent" and returns a warning.
   250  func parseLegacyGDPRFields(query url.Values, gppGDPRSignal gdpr.Signal, gppGDPRConsent string) (gdpr.Signal, string, error) {
   251  	var gdprSignal gdpr.Signal = gdpr.SignalAmbiguous
   252  	var gdprConsent string
   253  	var warning error
   254  
   255  	if gdprQuerySignal := query.Get("gdpr"); len(gdprQuerySignal) > 0 {
   256  		if gppGDPRSignal == gdpr.SignalAmbiguous {
   257  			switch gdprQuerySignal {
   258  			case "0":
   259  				fallthrough
   260  			case "1":
   261  				if zeroOrOne, err := strconv.Atoi(gdprQuerySignal); err == nil {
   262  					gdprSignal = gdpr.Signal(zeroOrOne)
   263  				}
   264  			default:
   265  				return gdpr.SignalAmbiguous, "", errors.New("the gdpr query param must be either 0 or 1. You gave " + gdprQuerySignal)
   266  			}
   267  		} else {
   268  			warning = &errortypes.Warning{
   269  				Message:     "'gpp_sid' signal value will be used over the one found in the deprecated 'gdpr' field.",
   270  				WarningCode: errortypes.UnknownWarningCode,
   271  			}
   272  		}
   273  	}
   274  
   275  	if gdprLegacyConsent := query.Get("gdpr_consent"); len(gdprLegacyConsent) > 0 {
   276  		if len(gppGDPRConsent) > 0 {
   277  			warning = &errortypes.Warning{
   278  				Message:     "'gpp' value will be used over the one found in the deprecated 'gdpr_consent' field.",
   279  				WarningCode: errortypes.UnknownWarningCode,
   280  			}
   281  		} else {
   282  			gdprConsent = gdprLegacyConsent
   283  		}
   284  	}
   285  	return gdprSignal, gdprConsent, warning
   286  }
   287  
   288  func parseSignalFromGppSidStr(strSID string) (gdpr.Signal, error) {
   289  	gdprSignal := gdpr.SignalAmbiguous
   290  
   291  	if len(strSID) > 0 {
   292  		gppSID, err := stringutil.StrToInt8Slice(strSID)
   293  		if err != nil {
   294  			return gdpr.SignalAmbiguous, fmt.Errorf("Error parsing gpp_sid %s", err.Error())
   295  		}
   296  
   297  		if len(gppSID) > 0 {
   298  			gdprSignal = gdpr.SignalNo
   299  			if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
   300  				gdprSignal = gdpr.SignalYes
   301  			}
   302  		}
   303  	}
   304  
   305  	return gdprSignal, nil
   306  }
   307  
   308  func parseConsentFromGppStr(gppQueryValue string) (string, error) {
   309  	var gdprConsent string
   310  
   311  	if len(gppQueryValue) > 0 {
   312  		gpp, err := gpplib.Parse(gppQueryValue)
   313  		if err != nil {
   314  			return "", err
   315  		}
   316  
   317  		if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
   318  			gdprConsent = gpp.Sections[i].GetValue()
   319  		}
   320  	}
   321  
   322  	return gdprConsent, nil
   323  }
   324  
   325  func getSyncer(query url.Values, syncersByBidder map[string]usersync.Syncer) (usersync.Syncer, string, error) {
   326  	bidder := query.Get("bidder")
   327  
   328  	if bidder == "" {
   329  		return nil, "", errors.New(`"bidder" query param is required`)
   330  	}
   331  
   332  	syncer, syncerExists := syncersByBidder[bidder]
   333  	if !syncerExists {
   334  		return nil, "", errors.New("The bidder name provided is not supported by Prebid Server")
   335  	}
   336  
   337  	return syncer, bidder, nil
   338  }
   339  
   340  func isSyncerPriority(bidderNameFromSyncerQuery string, priorityGroups [][]string) bool {
   341  	for _, group := range priorityGroups {
   342  		for _, bidder := range group {
   343  			if bidderNameFromSyncerQuery == bidder {
   344  				return true
   345  			}
   346  		}
   347  	}
   348  	return false
   349  }
   350  
   351  // getResponseFormat reads the format query parameter or falls back to the syncer's default.
   352  // Returns either "b" (iframe), "i" (redirect), or an empty string "" (legacy behavior of an
   353  // empty response body with no content type).
   354  func getResponseFormat(query url.Values, syncer usersync.Syncer) (string, error) {
   355  	format, formatProvided := query["f"]
   356  	formatEmpty := len(format) == 0 || format[0] == ""
   357  
   358  	if !formatProvided || formatEmpty {
   359  		switch syncer.DefaultSyncType() {
   360  		case usersync.SyncTypeIFrame:
   361  			return "b", nil
   362  		case usersync.SyncTypeRedirect:
   363  			return "i", nil
   364  		default:
   365  			return "", nil
   366  		}
   367  	}
   368  
   369  	if !strings.EqualFold(format[0], "b") && !strings.EqualFold(format[0], "i") {
   370  		return "", errors.New(`"f" query param is invalid. must be "b" or "i"`)
   371  	}
   372  	return strings.ToLower(format[0]), nil
   373  }
   374  
   375  // 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
   376  func siteCookieCheck(ua string) bool {
   377  	result := false
   378  
   379  	index := strings.Index(ua, chromeStr)
   380  	criOSIndex := strings.Index(ua, chromeiOSStr)
   381  	if index != -1 {
   382  		result = checkChromeBrowserVersion(ua, index, chromeStrLen)
   383  	} else if criOSIndex != -1 {
   384  		result = checkChromeBrowserVersion(ua, criOSIndex, chromeiOSStrLen)
   385  	}
   386  
   387  	return result
   388  }
   389  
   390  func checkChromeBrowserVersion(ua string, index int, chromeStrLength int) bool {
   391  	result := false
   392  	vIndex := index + chromeStrLength
   393  	dotIndex := strings.Index(ua[vIndex:], ".")
   394  	if dotIndex == -1 {
   395  		dotIndex = len(ua[vIndex:])
   396  	}
   397  	version, _ := strconv.Atoi(ua[vIndex : vIndex+dotIndex])
   398  	if version >= chromeMinVer {
   399  		result = true
   400  	}
   401  	return result
   402  }
   403  
   404  func preventSyncsGDPR(gdprRequestInfo gdpr.RequestInfo, permsBuilder gdpr.PermissionsBuilder, tcf2Cfg gdpr.TCF2ConfigReader) (shouldReturn bool, status int, body string) {
   405  	perms := permsBuilder(tcf2Cfg, gdprRequestInfo)
   406  
   407  	allowed, err := perms.HostCookiesAllowed(context.Background())
   408  	if err != nil {
   409  		if _, ok := err.(*gdpr.ErrorMalformedConsent); ok {
   410  			return true, http.StatusBadRequest, "gdpr_consent was invalid. " + err.Error()
   411  		}
   412  
   413  		// We can't distinguish between requests for a new version of the global vendor list, and requests
   414  		// which are malformed (version number is much too large). Since we try to fetch new versions as we
   415  		// receive requests, PBS *should* self-correct quickly, allowing us to assume most of the errors
   416  		// caught here will be malformed strings.
   417  		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."
   418  	}
   419  
   420  	if allowed {
   421  		return false, 0, ""
   422  	}
   423  
   424  	return true, http.StatusUnavailableForLegalReasons, "The gdpr_consent string prevents cookies from being saved"
   425  }
   426  
   427  func handleBadStatus(w http.ResponseWriter, status int, metricValue metrics.SetUidStatus, err error, me metrics.MetricsEngine, so *analytics.SetUIDObject) {
   428  	w.WriteHeader(status)
   429  	me.RecordSetUid(metricValue)
   430  	so.Status = status
   431  
   432  	if err != nil {
   433  		so.Errors = []error{err}
   434  		w.Write([]byte(err.Error()))
   435  	}
   436  }