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 }