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 }