github.com/prebid/prebid-server@v0.275.0/endpoints/cookie_sync.go (about) 1 package endpoints 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "strconv" 11 "strings" 12 13 "github.com/golang/glog" 14 "github.com/julienschmidt/httprouter" 15 gpplib "github.com/prebid/go-gpp" 16 gppConstants "github.com/prebid/go-gpp/constants" 17 accountService "github.com/prebid/prebid-server/account" 18 "github.com/prebid/prebid-server/analytics" 19 "github.com/prebid/prebid-server/config" 20 "github.com/prebid/prebid-server/errortypes" 21 "github.com/prebid/prebid-server/gdpr" 22 "github.com/prebid/prebid-server/macros" 23 "github.com/prebid/prebid-server/metrics" 24 "github.com/prebid/prebid-server/openrtb_ext" 25 "github.com/prebid/prebid-server/privacy" 26 "github.com/prebid/prebid-server/privacy/ccpa" 27 gppPrivacy "github.com/prebid/prebid-server/privacy/gpp" 28 "github.com/prebid/prebid-server/stored_requests" 29 "github.com/prebid/prebid-server/usersync" 30 stringutil "github.com/prebid/prebid-server/util/stringutil" 31 ) 32 33 var ( 34 errCookieSyncOptOut = errors.New("User has opted out") 35 errCookieSyncBody = errors.New("Failed to read request body") 36 errCookieSyncGDPRConsentMissing = errors.New("gdpr_consent is required if gdpr=1") 37 errCookieSyncGDPRConsentMissingSignalAmbiguous = errors.New("gdpr_consent is required. gdpr is not specified and is assumed to be 1 by the server. set gdpr=0 to exempt this request") 38 errCookieSyncInvalidBiddersType = errors.New("invalid bidders type. must either be a string '*' or a string array of bidders") 39 errCookieSyncAccountBlocked = errors.New("account is disabled, please reach out to the prebid server host") 40 errCookieSyncAccountConfigMalformed = errors.New("account config is malformed and could not be read") 41 errCookieSyncAccountInvalid = errors.New("account must be valid if provided, please reach out to the prebid server host") 42 errSyncerIsNotPriority = errors.New("syncer key is not a priority, and there are only priority elements left") 43 ) 44 45 var cookieSyncBidderFilterAllowAll = usersync.NewUniformBidderFilter(usersync.BidderFilterModeInclude) 46 47 func NewCookieSyncEndpoint( 48 syncersByBidder map[string]usersync.Syncer, 49 config *config.Configuration, 50 gdprPermsBuilder gdpr.PermissionsBuilder, 51 tcf2CfgBuilder gdpr.TCF2ConfigBuilder, 52 metrics metrics.MetricsEngine, 53 pbsAnalytics analytics.PBSAnalyticsModule, 54 accountsFetcher stored_requests.AccountFetcher, 55 bidders map[string]openrtb_ext.BidderName) HTTPRouterHandler { 56 57 bidderHashSet := make(map[string]struct{}, len(bidders)) 58 for _, bidder := range bidders { 59 bidderHashSet[string(bidder)] = struct{}{} 60 } 61 62 return &cookieSyncEndpoint{ 63 chooser: usersync.NewChooser(syncersByBidder), 64 config: config, 65 privacyConfig: usersyncPrivacyConfig{ 66 gdprConfig: config.GDPR, 67 gdprPermissionsBuilder: gdprPermsBuilder, 68 tcf2ConfigBuilder: tcf2CfgBuilder, 69 ccpaEnforce: config.CCPA.Enforce, 70 bidderHashSet: bidderHashSet, 71 }, 72 metrics: metrics, 73 pbsAnalytics: pbsAnalytics, 74 accountsFetcher: accountsFetcher, 75 } 76 } 77 78 type cookieSyncEndpoint struct { 79 chooser usersync.Chooser 80 config *config.Configuration 81 privacyConfig usersyncPrivacyConfig 82 metrics metrics.MetricsEngine 83 pbsAnalytics analytics.PBSAnalyticsModule 84 accountsFetcher stored_requests.AccountFetcher 85 } 86 87 func (c *cookieSyncEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 88 request, privacyMacros, err := c.parseRequest(r) 89 if err != nil { 90 c.writeParseRequestErrorMetrics(err) 91 c.handleError(w, err, http.StatusBadRequest) 92 return 93 } 94 decoder := usersync.Base64Decoder{} 95 96 cookie := usersync.ReadCookie(r, decoder, &c.config.HostCookie) 97 usersync.SyncHostCookie(r, cookie, &c.config.HostCookie) 98 99 result := c.chooser.Choose(request, cookie) 100 switch result.Status { 101 case usersync.StatusBlockedByUserOptOut: 102 c.metrics.RecordCookieSync(metrics.CookieSyncOptOut) 103 c.handleError(w, errCookieSyncOptOut, http.StatusUnauthorized) 104 case usersync.StatusBlockedByGDPR: 105 c.metrics.RecordCookieSync(metrics.CookieSyncGDPRHostCookieBlocked) 106 c.handleResponse(w, request.SyncTypeFilter, cookie, privacyMacros, nil) 107 case usersync.StatusOK: 108 c.metrics.RecordCookieSync(metrics.CookieSyncOK) 109 c.writeSyncerMetrics(result.BiddersEvaluated) 110 c.handleResponse(w, request.SyncTypeFilter, cookie, privacyMacros, result.SyncersChosen) 111 } 112 } 113 114 func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, macros.UserSyncPrivacy, error) { 115 defer r.Body.Close() 116 body, err := io.ReadAll(r.Body) 117 if err != nil { 118 return usersync.Request{}, macros.UserSyncPrivacy{}, errCookieSyncBody 119 } 120 121 request := cookieSyncRequest{} 122 if err := json.Unmarshal(body, &request); err != nil { 123 return usersync.Request{}, macros.UserSyncPrivacy{}, fmt.Errorf("JSON parsing failed: %s", err.Error()) 124 } 125 126 if request.Account == "" { 127 request.Account = metrics.PublisherUnknown 128 } 129 account, fetchErrs := accountService.GetAccount(context.Background(), c.config, c.accountsFetcher, request.Account, c.metrics) 130 if len(fetchErrs) > 0 { 131 return usersync.Request{}, macros.UserSyncPrivacy{}, combineErrors(fetchErrs) 132 } 133 134 request = c.setLimit(request, account.CookieSync) 135 request = c.setCooperativeSync(request, account.CookieSync) 136 137 privacyMacros, gdprSignal, privacyPolicies, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue) 138 if err != nil { 139 return usersync.Request{}, macros.UserSyncPrivacy{}, err 140 } 141 142 ccpaParsedPolicy := ccpa.ParsedPolicy{} 143 if request.USPrivacy != "" { 144 parsedPolicy, err := ccpa.Policy{Consent: request.USPrivacy}.Parse(c.privacyConfig.bidderHashSet) 145 if err != nil { 146 privacyMacros.USPrivacy = "" 147 } 148 if c.privacyConfig.ccpaEnforce { 149 ccpaParsedPolicy = parsedPolicy 150 } 151 } 152 153 activityControl := privacy.NewActivityControl(&account.Privacy) 154 155 syncTypeFilter, err := parseTypeFilter(request.FilterSettings) 156 if err != nil { 157 return usersync.Request{}, macros.UserSyncPrivacy{}, err 158 } 159 160 gdprRequestInfo := gdpr.RequestInfo{ 161 Consent: privacyMacros.GDPRConsent, 162 GDPRSignal: gdprSignal, 163 } 164 165 tcf2Cfg := c.privacyConfig.tcf2ConfigBuilder(c.privacyConfig.gdprConfig.TCF2, account.GDPR) 166 gdprPerms := c.privacyConfig.gdprPermissionsBuilder(tcf2Cfg, gdprRequestInfo) 167 168 rx := usersync.Request{ 169 Bidders: request.Bidders, 170 Cooperative: usersync.Cooperative{ 171 Enabled: (request.CooperativeSync != nil && *request.CooperativeSync) || (request.CooperativeSync == nil && c.config.UserSync.Cooperative.EnabledByDefault), 172 PriorityGroups: c.config.UserSync.PriorityGroups, 173 }, 174 Limit: request.Limit, 175 Privacy: usersyncPrivacy{ 176 gdprPermissions: gdprPerms, 177 ccpaParsedPolicy: ccpaParsedPolicy, 178 activityControl: activityControl, 179 activityRequest: privacy.NewRequestFromPolicies(privacyPolicies), 180 }, 181 SyncTypeFilter: syncTypeFilter, 182 } 183 return rx, privacyMacros, nil 184 } 185 186 func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (macros.UserSyncPrivacy, gdpr.Signal, privacy.Policies, error) { 187 // GDPR 188 gppSID, err := stringutil.StrToInt8Slice(request.GPPSID) 189 if err != nil { 190 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err 191 } 192 193 gdprSignal, gdprString, err := extractGDPRSignal(request.GDPR, gppSID) 194 if err != nil { 195 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err 196 } 197 198 var gpp gpplib.GppContainer 199 if len(request.GPP) > 0 { 200 var err error 201 gpp, err = gpplib.Parse(request.GPP) 202 if err != nil { 203 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err 204 } 205 } 206 207 gdprConsent := request.GDPRConsent 208 if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 { 209 gdprConsent = gpp.Sections[i].GetValue() 210 } 211 212 if gdprConsent == "" { 213 if gdprSignal == gdpr.SignalYes { 214 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, errCookieSyncGDPRConsentMissing 215 } 216 217 if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, usersyncDefaultGDPRValue) == gdpr.SignalYes { 218 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, errCookieSyncGDPRConsentMissingSignalAmbiguous 219 } 220 } 221 222 // CCPA 223 ccpaString, err := ccpa.SelectCCPAConsent(request.USPrivacy, gpp, gppSID) 224 if err != nil { 225 return macros.UserSyncPrivacy{}, gdpr.SignalNo, privacy.Policies{}, err 226 } 227 228 privacyMacros := macros.UserSyncPrivacy{ 229 GDPR: gdprString, 230 GDPRConsent: gdprConsent, 231 USPrivacy: ccpaString, 232 GPP: request.GPP, 233 GPPSID: request.GPPSID, 234 } 235 236 privacyPolicies := privacy.Policies{ 237 GPPSID: gppSID, 238 } 239 240 return privacyMacros, gdprSignal, privacyPolicies, nil 241 } 242 243 func extractGDPRSignal(requestGDPR *int, gppSID []int8) (gdpr.Signal, string, error) { 244 if len(gppSID) > 0 { 245 if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) { 246 return gdpr.SignalYes, strconv.Itoa(int(gdpr.SignalYes)), nil 247 } 248 return gdpr.SignalNo, strconv.Itoa(int(gdpr.SignalNo)), nil 249 } 250 251 if requestGDPR == nil { 252 return gdpr.SignalAmbiguous, "", nil 253 } 254 255 gdprSignal, err := gdpr.IntSignalParse(*requestGDPR) 256 if err != nil { 257 return gdpr.SignalAmbiguous, strconv.Itoa(*requestGDPR), err 258 } 259 return gdprSignal, strconv.Itoa(*requestGDPR), nil 260 } 261 262 func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) { 263 switch err { 264 case errCookieSyncAccountBlocked: 265 c.metrics.RecordCookieSync(metrics.CookieSyncAccountBlocked) 266 case errCookieSyncAccountConfigMalformed: 267 c.metrics.RecordCookieSync(metrics.CookieSyncAccountConfigMalformed) 268 case errCookieSyncAccountInvalid: 269 c.metrics.RecordCookieSync(metrics.CookieSyncAccountInvalid) 270 default: 271 c.metrics.RecordCookieSync(metrics.CookieSyncBadRequest) 272 } 273 } 274 275 func (c *cookieSyncEndpoint) setLimit(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest { 276 if request.Limit <= 0 && cookieSyncConfig.DefaultLimit != nil { 277 request.Limit = *cookieSyncConfig.DefaultLimit 278 } 279 if cookieSyncConfig.MaxLimit != nil && (request.Limit <= 0 || request.Limit > *cookieSyncConfig.MaxLimit) { 280 request.Limit = *cookieSyncConfig.MaxLimit 281 } 282 if request.Limit < 0 { 283 request.Limit = 0 284 } 285 286 return request 287 } 288 289 func (c *cookieSyncEndpoint) setCooperativeSync(request cookieSyncRequest, cookieSyncConfig config.CookieSync) cookieSyncRequest { 290 if request.CooperativeSync == nil && cookieSyncConfig.DefaultCoopSync != nil { 291 request.CooperativeSync = cookieSyncConfig.DefaultCoopSync 292 } 293 294 return request 295 } 296 297 func parseTypeFilter(request *cookieSyncRequestFilterSettings) (usersync.SyncTypeFilter, error) { 298 syncTypeFilter := usersync.SyncTypeFilter{ 299 IFrame: cookieSyncBidderFilterAllowAll, 300 Redirect: cookieSyncBidderFilterAllowAll, 301 } 302 303 if request != nil { 304 if filter, err := parseBidderFilter(request.IFrame); err == nil { 305 syncTypeFilter.IFrame = filter 306 } else { 307 return usersync.SyncTypeFilter{}, fmt.Errorf("error parsing filtersettings.iframe: %v", err) 308 } 309 310 if filter, err := parseBidderFilter(request.Redirect); err == nil { 311 syncTypeFilter.Redirect = filter 312 } else { 313 return usersync.SyncTypeFilter{}, fmt.Errorf("error parsing filtersettings.image: %v", err) 314 } 315 } 316 317 return syncTypeFilter, nil 318 } 319 320 func parseBidderFilter(filter *cookieSyncRequestFilter) (usersync.BidderFilter, error) { 321 if filter == nil { 322 return cookieSyncBidderFilterAllowAll, nil 323 } 324 325 var mode usersync.BidderFilterMode 326 switch filter.Mode { 327 case "include": 328 mode = usersync.BidderFilterModeInclude 329 case "exclude": 330 mode = usersync.BidderFilterModeExclude 331 default: 332 return nil, fmt.Errorf("invalid filter value '%s'. must be either 'include' or 'exclude'", filter.Mode) 333 } 334 335 switch v := filter.Bidders.(type) { 336 case string: 337 if v == "*" { 338 return usersync.NewUniformBidderFilter(mode), nil 339 } 340 return nil, fmt.Errorf("invalid bidders value `%s`. must either be '*' or a string array", v) 341 case []interface{}: 342 bidders := make([]string, len(v)) 343 for i, x := range v { 344 if bidder, ok := x.(string); ok { 345 bidders[i] = bidder 346 } else { 347 return nil, errCookieSyncInvalidBiddersType 348 } 349 } 350 return usersync.NewSpecificBidderFilter(bidders, mode), nil 351 default: 352 return nil, errCookieSyncInvalidBiddersType 353 } 354 } 355 356 func (c *cookieSyncEndpoint) handleError(w http.ResponseWriter, err error, httpStatus int) { 357 http.Error(w, err.Error(), httpStatus) 358 c.pbsAnalytics.LogCookieSyncObject(&analytics.CookieSyncObject{ 359 Status: httpStatus, 360 Errors: []error{err}, 361 BidderStatus: []*analytics.CookieSyncBidder{}, 362 }) 363 } 364 365 func combineErrors(errs []error) error { 366 var errorStrings []string 367 for _, err := range errs { 368 // preserve knowledge of special account errors 369 switch errortypes.ReadCode(err) { 370 case errortypes.BlacklistedAcctErrorCode: 371 return errCookieSyncAccountBlocked 372 case errortypes.AcctRequiredErrorCode: 373 return errCookieSyncAccountInvalid 374 case errortypes.MalformedAcctErrorCode: 375 return errCookieSyncAccountConfigMalformed 376 } 377 378 errorStrings = append(errorStrings, err.Error()) 379 } 380 combinedErrors := strings.Join(errorStrings, " ") 381 return errors.New(combinedErrors) 382 } 383 384 func (c *cookieSyncEndpoint) writeSyncerMetrics(biddersEvaluated []usersync.BidderEvaluation) { 385 for _, bidder := range biddersEvaluated { 386 switch bidder.Status { 387 case usersync.StatusOK: 388 c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncOK) 389 case usersync.StatusBlockedByGDPR: 390 c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncPrivacyBlocked) 391 case usersync.StatusBlockedByCCPA: 392 c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncPrivacyBlocked) 393 case usersync.StatusAlreadySynced: 394 c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncAlreadySynced) 395 case usersync.StatusTypeNotSupported: 396 c.metrics.RecordSyncerRequest(bidder.SyncerKey, metrics.SyncerCookieSyncTypeNotSupported) 397 } 398 } 399 } 400 401 func (c *cookieSyncEndpoint) handleResponse(w http.ResponseWriter, tf usersync.SyncTypeFilter, co *usersync.Cookie, m macros.UserSyncPrivacy, s []usersync.SyncerChoice) { 402 status := "no_cookie" 403 if co.HasAnyLiveSyncs() { 404 status = "ok" 405 } 406 407 response := cookieSyncResponse{ 408 Status: status, 409 BidderStatus: make([]cookieSyncResponseBidder, 0, len(s)), 410 } 411 412 for _, syncerChoice := range s { 413 syncTypes := tf.ForBidder(syncerChoice.Bidder) 414 sync, err := syncerChoice.Syncer.GetSync(syncTypes, m) 415 if err != nil { 416 glog.Errorf("Failed to get usersync info for %s: %v", syncerChoice.Bidder, err) 417 continue 418 } 419 420 response.BidderStatus = append(response.BidderStatus, cookieSyncResponseBidder{ 421 BidderCode: syncerChoice.Bidder, 422 NoCookie: true, 423 UsersyncInfo: cookieSyncResponseSync{ 424 URL: sync.URL, 425 Type: string(sync.Type), 426 SupportCORS: sync.SupportCORS, 427 }, 428 }) 429 } 430 431 c.pbsAnalytics.LogCookieSyncObject(&analytics.CookieSyncObject{ 432 Status: http.StatusOK, 433 BidderStatus: mapBidderStatusToAnalytics(response.BidderStatus), 434 }) 435 436 w.Header().Set("Content-Type", "application/json; charset=utf-8") 437 enc := json.NewEncoder(w) 438 enc.SetEscapeHTML(false) 439 enc.Encode(response) 440 } 441 442 func mapBidderStatusToAnalytics(from []cookieSyncResponseBidder) []*analytics.CookieSyncBidder { 443 to := make([]*analytics.CookieSyncBidder, len(from)) 444 for i, b := range from { 445 to[i] = &analytics.CookieSyncBidder{ 446 BidderCode: b.BidderCode, 447 NoCookie: b.NoCookie, 448 UsersyncInfo: &analytics.UsersyncInfo{ 449 URL: b.UsersyncInfo.URL, 450 Type: b.UsersyncInfo.Type, 451 SupportCORS: b.UsersyncInfo.SupportCORS, 452 }, 453 } 454 } 455 return to 456 } 457 458 type cookieSyncRequest struct { 459 Bidders []string `json:"bidders"` 460 GDPR *int `json:"gdpr"` 461 GDPRConsent string `json:"gdpr_consent"` 462 USPrivacy string `json:"us_privacy"` 463 Limit int `json:"limit"` 464 GPP string `json:"gpp"` 465 GPPSID string `json:"gpp_sid"` 466 CooperativeSync *bool `json:"coopSync"` 467 FilterSettings *cookieSyncRequestFilterSettings `json:"filterSettings"` 468 Account string `json:"account"` 469 } 470 471 type cookieSyncRequestFilterSettings struct { 472 IFrame *cookieSyncRequestFilter `json:"iframe"` 473 Redirect *cookieSyncRequestFilter `json:"image"` 474 } 475 476 type cookieSyncRequestFilter struct { 477 Bidders interface{} `json:"bidders"` 478 Mode string `json:"filter"` 479 } 480 481 type cookieSyncResponse struct { 482 Status string `json:"status"` 483 BidderStatus []cookieSyncResponseBidder `json:"bidder_status"` 484 } 485 486 type cookieSyncResponseBidder struct { 487 BidderCode string `json:"bidder"` 488 NoCookie bool `json:"no_cookie,omitempty"` 489 UsersyncInfo cookieSyncResponseSync `json:"usersync,omitempty"` 490 } 491 492 type cookieSyncResponseSync struct { 493 URL string `json:"url,omitempty"` 494 Type string `json:"type,omitempty"` 495 SupportCORS bool `json:"supportCORS,omitempty"` 496 } 497 498 type usersyncPrivacyConfig struct { 499 gdprConfig config.GDPR 500 gdprPermissionsBuilder gdpr.PermissionsBuilder 501 tcf2ConfigBuilder gdpr.TCF2ConfigBuilder 502 ccpaEnforce bool 503 bidderHashSet map[string]struct{} 504 } 505 506 type usersyncPrivacy struct { 507 gdprPermissions gdpr.Permissions 508 ccpaParsedPolicy ccpa.ParsedPolicy 509 activityControl privacy.ActivityControl 510 activityRequest privacy.ActivityRequest 511 } 512 513 func (p usersyncPrivacy) GDPRAllowsHostCookie() bool { 514 allowCookie, err := p.gdprPermissions.HostCookiesAllowed(context.Background()) 515 return err == nil && allowCookie 516 } 517 518 func (p usersyncPrivacy) GDPRAllowsBidderSync(bidder string) bool { 519 allowSync, err := p.gdprPermissions.BidderSyncAllowed(context.Background(), openrtb_ext.BidderName(bidder)) 520 return err == nil && allowSync 521 } 522 523 func (p usersyncPrivacy) CCPAAllowsBidderSync(bidder string) bool { 524 enforce := p.ccpaParsedPolicy.CanEnforce() && p.ccpaParsedPolicy.ShouldEnforce(bidder) 525 return !enforce 526 } 527 528 func (p usersyncPrivacy) ActivityAllowsUserSync(bidder string) bool { 529 return p.activityControl.Allow( 530 privacy.ActivitySyncUser, 531 privacy.Component{Type: privacy.ComponentTypeBidder, Name: bidder}, 532 p.activityRequest) 533 }