github.com/prebid/prebid-server/v2@v2.18.0/usersync/cookie.go (about) 1 package usersync 2 3 import ( 4 "errors" 5 "net/http" 6 "time" 7 8 "github.com/prebid/prebid-server/v2/config" 9 "github.com/prebid/prebid-server/v2/openrtb_ext" 10 "github.com/prebid/prebid-server/v2/util/jsonutil" 11 ) 12 13 const uidCookieName = "uids" 14 15 // uidTTL is the default amount of time a uid stored within a cookie is considered valid. This is 16 // separate from the cookie ttl. 17 const uidTTL = 14 * 24 * time.Hour 18 19 // Cookie is the cookie used in Prebid Server. 20 // 21 // To get an instance of this from a request, use ReadCookie. 22 // To write an instance onto a response, use WriteCookie. 23 type Cookie struct { 24 uids map[string]UIDEntry 25 optOut bool 26 } 27 28 // UIDEntry bundles the UID with an Expiration date. 29 type UIDEntry struct { 30 // UID is the ID given to a user by a particular bidder 31 UID string `json:"uid"` 32 // Expires is the time at which this UID should no longer apply. 33 Expires time.Time `json:"expires"` 34 } 35 36 // NewCookie returns a new empty cookie. 37 func NewCookie() *Cookie { 38 return &Cookie{ 39 uids: make(map[string]UIDEntry), 40 } 41 } 42 43 // ReadCookie reads the cookie from the request 44 func ReadCookie(r *http.Request, decoder Decoder, host *config.HostCookie) *Cookie { 45 if hostOptOutCookie := checkHostCookieOptOut(r, host); hostOptOutCookie != nil { 46 return hostOptOutCookie 47 } 48 49 // Read cookie from request 50 cookieFromRequest, err := r.Cookie(uidCookieName) 51 if err != nil { 52 return NewCookie() 53 } 54 decodedCookie := decoder.Decode(cookieFromRequest.Value) 55 56 return decodedCookie 57 } 58 59 // PrepareCookieForWrite ejects UIDs as long as the cookie is too full 60 func (cookie *Cookie) PrepareCookieForWrite(cfg *config.HostCookie, encoder Encoder, ejector Ejector) (string, error) { 61 for len(cookie.uids) > 0 { 62 encodedCookie, err := encoder.Encode(cookie) 63 if err != nil { 64 return encodedCookie, nil 65 } 66 67 // Convert to HTTP Cookie to Get Size 68 httpCookie := &http.Cookie{ 69 Name: uidCookieName, 70 Value: encodedCookie, 71 Expires: time.Now().Add(cfg.TTLDuration()), 72 Path: "/", 73 } 74 cookieSize := len([]byte(httpCookie.String())) 75 76 isCookieTooBig := cookieSize > cfg.MaxCookieSizeBytes && cfg.MaxCookieSizeBytes > 0 77 if !isCookieTooBig { 78 return encodedCookie, nil 79 } else if len(cookie.uids) == 1 { 80 return "", errors.New("uid that's trying to be synced is bigger than MaxCookieSize") 81 } 82 83 uidToDelete, err := ejector.Choose(cookie.uids) 84 if err != nil { 85 return encodedCookie, err 86 } 87 delete(cookie.uids, uidToDelete) 88 } 89 return "", nil 90 } 91 92 // WriteCookie sets the prepared cookie onto the header 93 func WriteCookie(w http.ResponseWriter, encodedCookie string, cfg *config.HostCookie, setSiteCookie bool) { 94 ttl := cfg.TTLDuration() 95 96 httpCookie := &http.Cookie{ 97 Name: uidCookieName, 98 Value: encodedCookie, 99 Expires: time.Now().Add(ttl), 100 Path: "/", 101 } 102 103 if cfg.Domain != "" { 104 httpCookie.Domain = cfg.Domain 105 } 106 107 if setSiteCookie { 108 httpCookie.Secure = true 109 httpCookie.SameSite = http.SameSiteNoneMode 110 } 111 112 w.Header().Add("Set-Cookie", httpCookie.String()) 113 } 114 115 // Sync tries to set the UID for some syncer key. It returns an error if the set didn't happen. 116 func (cookie *Cookie) Sync(key string, uid string) error { 117 if !cookie.AllowSyncs() { 118 return errors.New("the user has opted out of prebid server cookie syncs") 119 } 120 121 if checkAudienceNetwork(key, uid) { 122 return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\"") 123 } 124 125 // Sync 126 cookie.uids[key] = UIDEntry{ 127 UID: uid, 128 Expires: time.Now().Add(uidTTL), 129 } 130 131 return nil 132 } 133 134 // SyncHostCookie syncs the request cookie with the host cookie 135 func SyncHostCookie(r *http.Request, requestCookie *Cookie, host *config.HostCookie) { 136 if uid, _, _ := requestCookie.GetUID(host.Family); uid == "" && host.CookieName != "" { 137 if hostCookie, err := r.Cookie(host.CookieName); err == nil { 138 requestCookie.Sync(host.Family, hostCookie.Value) 139 } 140 } 141 } 142 143 func checkHostCookieOptOut(r *http.Request, host *config.HostCookie) *Cookie { 144 if host.OptOutCookie.Name != "" { 145 optOutCookie, err := r.Cookie(host.OptOutCookie.Name) 146 if err == nil && optOutCookie.Value == host.OptOutCookie.Value { 147 hostOptOut := NewCookie() 148 hostOptOut.SetOptOut(true) 149 return hostOptOut 150 } 151 } 152 return nil 153 } 154 155 // AllowSyncs is true if the user lets bidders sync cookies, and false otherwise. 156 func (cookie *Cookie) AllowSyncs() bool { 157 return cookie != nil && !cookie.optOut 158 } 159 160 // SetOptOut is used to change whether or not we're allowed to sync cookies for this user. 161 func (cookie *Cookie) SetOptOut(optOut bool) { 162 cookie.optOut = optOut 163 164 if optOut { 165 cookie.uids = make(map[string]UIDEntry) 166 } 167 } 168 169 // GetUID Gets this user's ID for the given syncer key. 170 func (cookie *Cookie) GetUID(key string) (uid string, isUIDFound bool, isUIDActive bool) { 171 if cookie != nil { 172 if uid, ok := cookie.uids[key]; ok { 173 return uid.UID, true, time.Now().Before(uid.Expires) 174 } 175 } 176 return "", false, false 177 } 178 179 // GetUIDs returns this user's ID for all the bidders 180 func (cookie *Cookie) GetUIDs() map[string]string { 181 uids := make(map[string]string) 182 if cookie != nil { 183 // Extract just the uid for each bidder 184 for bidderName, uidWithExpiry := range cookie.uids { 185 uids[bidderName] = uidWithExpiry.UID 186 } 187 } 188 return uids 189 } 190 191 // Unsync removes the user's ID for the given syncer key from this cookie. 192 func (cookie *Cookie) Unsync(key string) { 193 delete(cookie.uids, key) 194 } 195 196 // HasLiveSync returns true if we have an active UID for the given syncer key, and false otherwise. 197 func (cookie *Cookie) HasLiveSync(key string) bool { 198 _, _, isLive := cookie.GetUID(key) 199 return isLive 200 } 201 202 // HasAnyLiveSyncs returns true if this cookie has at least one active sync. 203 func (cookie *Cookie) HasAnyLiveSyncs() bool { 204 now := time.Now() 205 if cookie != nil { 206 for _, value := range cookie.uids { 207 if now.Before(value.Expires) { 208 return true 209 } 210 } 211 } 212 return false 213 } 214 215 func checkAudienceNetwork(key string, uid string) bool { 216 return key == string(openrtb_ext.BidderAudienceNetwork) && uid == "0" 217 } 218 219 // cookieJson defines the JSON contract for the cookie data's storage format. 220 // 221 // This exists so that Cookie (which is public) can have private fields, and the rest of 222 // the code doesn't have to worry about the cookie data storage format. 223 type cookieJson struct { 224 UIDs map[string]UIDEntry `json:"tempUIDs,omitempty"` 225 OptOut bool `json:"optout,omitempty"` 226 } 227 228 func (cookie *Cookie) MarshalJSON() ([]byte, error) { // nosemgrep: marshal-json-pointer-receiver 229 return jsonutil.Marshal(cookieJson{ 230 UIDs: cookie.uids, 231 OptOut: cookie.optOut, 232 }) 233 } 234 235 func (cookie *Cookie) UnmarshalJSON(b []byte) error { 236 var cookieContract cookieJson 237 if err := jsonutil.Unmarshal(b, &cookieContract); err != nil { 238 return err 239 } 240 241 cookie.optOut = cookieContract.OptOut 242 243 if cookie.optOut { 244 cookie.uids = nil 245 } else { 246 cookie.uids = cookieContract.UIDs 247 } 248 249 if cookie.uids == nil { 250 cookie.uids = make(map[string]UIDEntry) 251 } 252 253 // Audience Network Handling 254 if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" { 255 delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork)) 256 } 257 258 return nil 259 }