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  }