diff --git a/adapters/appnexus_test.go b/adapters/appnexus_test.go index 4fb1e9c8e11..bf5adeef38c 100644 --- a/adapters/appnexus_test.go +++ b/adapters/appnexus_test.go @@ -308,10 +308,10 @@ func TestAppNexusBasicResponse(t *testing.T) { req.Header.Add("User-Agent", andata.deviceUA) req.Header.Add("X-Real-IP", andata.deviceIP) - pc := pbs.ParseUIDCookie(req) - pc.UIDs["adnxs"] = andata.buyerUID + pc := pbs.ParseUserSyncMapFromRequest(req) + pc.TrySync("adnxs", andata.buyerUID) fakewriter := httptest.NewRecorder() - pbs.SetUIDCookie(fakewriter, pc) + pc.SetCookieOnResponse(fakewriter, "") req.Header.Add("Cookie", fakewriter.Header().Get("Set-Cookie")) cacheClient, _ := dummycache.New() diff --git a/adapters/facebook_test.go b/adapters/facebook_test.go index 671b4821689..3bbd6e76d20 100644 --- a/adapters/facebook_test.go +++ b/adapters/facebook_test.go @@ -212,10 +212,10 @@ func TestFacebookBasicResponse(t *testing.T) { req.Header.Add("User-Agent", fbdata.deviceUA) req.Header.Add("X-Real-IP", fbdata.deviceIP) - pc := pbs.ParseUIDCookie(req) - pc.UIDs["audienceNetwork"] = fbdata.buyerUID + pc := pbs.ParseUserSyncMapFromRequest(req) + pc.TrySync("audienceNetwork", fbdata.buyerUID) fakewriter := httptest.NewRecorder() - pbs.SetUIDCookie(fakewriter, pc) + pc.SetCookieOnResponse(fakewriter, "") req.Header.Add("Cookie", fakewriter.Header().Get("Set-Cookie")) cacheClient, _ := dummycache.New() diff --git a/adapters/pulsepoint_test.go b/adapters/pulsepoint_test.go index ff27c6dad97..93a29e005ae 100644 --- a/adapters/pulsepoint_test.go +++ b/adapters/pulsepoint_test.go @@ -264,10 +264,10 @@ func SampleRequest(numberOfImpressions int, t *testing.T) *pbs.PBSRequest { // setup a http request httpReq := httptest.NewRequest("POST", CreateService(BidOnTags("")).Server.URL, body) httpReq.Header.Add("Referer", "http://news.pub/topnews") - pc := pbs.ParseUIDCookie(httpReq) - pc.UIDs["pulsepoint"] = "pulsepointUser123" + pc := pbs.ParseUserSyncMapFromRequest(httpReq) + pc.TrySync("pulsepoint", "pulsepointUser123") fakewriter := httptest.NewRecorder() - pbs.SetUIDCookie(fakewriter, pc) + pc.SetCookieOnResponse(fakewriter, "") httpReq.Header.Add("Cookie", fakewriter.Header().Get("Set-Cookie")) // parse the http request cacheClient, _ := dummycache.New() diff --git a/adapters/rubicon_test.go b/adapters/rubicon_test.go index 1f82970f091..03ac5a2ce3f 100644 --- a/adapters/rubicon_test.go +++ b/adapters/rubicon_test.go @@ -259,10 +259,10 @@ func TestRubiconBasicResponse(t *testing.T) { req.Header.Add("User-Agent", rubidata.deviceUA) req.Header.Add("X-Real-IP", rubidata.deviceIP) - pc := pbs.ParseUIDCookie(req) - pc.UIDs["rubicon"] = rubidata.buyerUID + pc := pbs.ParseUserSyncMapFromRequest(req) + pc.TrySync("rubicon", rubidata.buyerUID) fakewriter := httptest.NewRecorder() - pbs.SetUIDCookie(fakewriter, pc) + pc.SetCookieOnResponse(fakewriter, "") req.Header.Add("Cookie", fakewriter.Header().Get("Set-Cookie")) cacheClient, _ := dummycache.New() diff --git a/pbs/pbsrequest.go b/pbs/pbsrequest.go index 449d3dd7b7b..8347f408bce 100644 --- a/pbs/pbsrequest.go +++ b/pbs/pbsrequest.go @@ -83,10 +83,10 @@ type PBSRequest struct { Device *openrtb.Device `json:"device"` // internal - Bidders []*PBSBidder `json:"-"` - UserIDs map[string]string `json:"-"` - Url string `json:"-"` - Domain string `json:"-"` + Bidders []*PBSBidder `json:"-"` + SyncMap UserSyncMap `json:"-"` + Url string `json:"-"` + Domain string `json:"-"` Start time.Time } @@ -129,12 +129,12 @@ func ParsePBSRequest(r *http.Request, cache cache.Cache) (*PBSRequest, error) { // use client-side data for web requests if pbsReq.App == nil { - pc := ParseUIDCookie(r) - pbsReq.UserIDs = pc.UIDs + pc := ParseUserSyncMapFromRequest(r) + pbsReq.SyncMap = pc // this would be for the shared adnxs.com domain if anid, err := r.Cookie("uuid2"); err == nil { - pbsReq.UserIDs["adnxs"] = anid.Value + pbsReq.SyncMap.TrySync("adnxs", anid.Value) } pbsReq.Device.UA = r.Header.Get("User-Agent") @@ -162,6 +162,8 @@ func ParsePBSRequest(r *http.Request, cache cache.Cache) (*PBSRequest, error) { if err != nil { return nil, fmt.Errorf("Invalid URL '%s': %v", url.Host, err) } + } else { + pbsReq.SyncMap = NewSyncMap() } if r.FormValue("debug") == "1" { @@ -229,8 +231,9 @@ func (req PBSRequest) Elapsed() int { return int(time.Since(req.Start) / 1000000) } -func (req PBSRequest) GetUserID(BidderCode string) string { - if uid, ok := req.UserIDs[BidderCode]; ok { +func (req *PBSRequest) GetUserID(BidderCode string) string { + if req.SyncMap != nil { + uid, _ := req.SyncMap.GetUID(BidderCode) return uid } return "" diff --git a/pbs/usersync.go b/pbs/usersync.go index c7ae10a45d8..aee7cfce8c1 100644 --- a/pbs/usersync.go +++ b/pbs/usersync.go @@ -10,33 +10,86 @@ import ( "strings" "time" + "errors" "github.com/golang/glog" "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-server/ssl" - metrics "github.com/rcrowley/go-metrics" + "github.com/rcrowley/go-metrics" ) -var cookie_domain string -var external_url string -var recaptcha_secret string +// Recaptcha code from https://github.com/haisum/recaptcha/blob/master/recaptcha.go +const RECAPTCHA_URL = "https://www.google.com/recaptcha/api/siteverify" +const COOKIE_NAME = "uids" -type PBSCookie struct { - UIDs map[string]string `json:"uids,omitempty"` - OptOut bool `json:"optout,omitempty"` - Birthday *time.Time `json:"bday,omitempty"` +const ( + USERSYNC_OPT_OUT = "usersync.opt_outs" + USERSYNC_BAD_REQUEST = "usersync.bad_requests" + USERSYNC_SUCCESS = "usersync.%s.sets" +) + +type UserSyncDeps struct { + Cookie_domain string + External_url string + Recaptcha_secret string + Metrics metrics.Registry +} + +// UserSyncMap is cookie which stores the user sync info for all of our bidders. +// +// To get an instance of this from a request, use ParseUserSyncMapFromRequest. +// To write an instance onto a response, use SetCookieOnResponse. +type UserSyncMap interface { + // AllowSyncs is true if the user lets bidders sync cookies, and false otherwise. + AllowSyncs() bool + // SetPreference is used to change whether or not we're allowed to sync cookies for this user. + SetPreference(allow bool) + // Gets an HTTP cookie containing all the data from this UserSyncMap. This is a snapshot--not a live view. + ToHTTPCookie() *http.Cookie + // SetCookieOnResponse is a shortcut for "ToHTTPCookie(); cookie.setDomain(domain); setCookie(w, cookie)" + SetCookieOnResponse(w http.ResponseWriter, domain string) + // GetUID Gets this user's ID for the given family, if present. If not present, this returns ("", false). + GetUID(familyName string) (string, bool) + // Unsync removes the user's ID for the given family from this cookie. + Unsync(familyName string) + // TrySync tries to set the UID for some family name. It returns an error if the set didn't happen. + TrySync(familyName string, uid string) error + // HasSync returns true if we have a UID for the given family, and false otherwise. + HasSync(familyName string) bool + // SyncCount returns the number of families which have UIDs for this user. + SyncCount() int +} + +// ParseUserSyncMapFromRequest parses the UserSyncMap from an HTTP Request. +func ParseUserSyncMapFromRequest(r *http.Request) UserSyncMap { + cookie, err := r.Cookie(COOKIE_NAME) + if err != nil { + return NewSyncMap() + } + + return ParseUserSyncMap(cookie) +} + +// ParseUserSyncMap parses the UserSync cookie from a raw HTTP cookie. +func ParseUserSyncMap(cookie *http.Cookie) UserSyncMap { + return parseCookieImpl(cookie) } -func ParseUIDCookie(r *http.Request) *PBSCookie { - t := time.Now() - pc := PBSCookie{ +// NewSyncMap returns an empty UserSyncMap +func NewSyncMap() UserSyncMap { + return &cookieImpl{ UIDs: make(map[string]string), - Birthday: &t, + Birthday: timestamp(), } +} - cookie, err := r.Cookie("uids") - if err != nil { - return &pc +// parseCookieImpl parses the cookieImpl from a raw HTTP cookie. +// This exists for testing. Callers should use ParseUserSyncMap. +func parseCookieImpl(cookie *http.Cookie) *cookieImpl { + pc := cookieImpl{ + UIDs: make(map[string]string), + Birthday: timestamp(), } + j, err := base64.URLEncoding.DecodeString(cookie.Value) if err != nil { // corrupted cookie; we should reset @@ -50,27 +103,92 @@ func ParseUIDCookie(r *http.Request) *PBSCookie { if pc.OptOut || pc.UIDs == nil { pc.UIDs = make(map[string]string) // empty map } + + // Facebook sends us a sentinel value of 0 if the user isn't logged in. + // As a result, we've stored "0" as the UID for many users in the audienceNetwork so far. + // Since users log in and out of facebook all the time, this will cause re-sync attempts until + // we get a non-zero value. + if pc.UIDs["audienceNetwork"] == "0" { + delete(pc.UIDs, "audienceNetwork") + } + return &pc } -func SetUIDCookie(w http.ResponseWriter, pc *PBSCookie) { - j, _ := json.Marshal(pc) +type cookieImpl struct { + UIDs map[string]string `json:"uids,omitempty"` + OptOut bool `json:"optout,omitempty"` + Birthday *time.Time `json:"bday,omitempty"` +} + +func (cookie *cookieImpl) AllowSyncs() bool { + return !cookie.OptOut +} + +func (cookie *cookieImpl) SetPreference(allow bool) { + if allow { + cookie.OptOut = false + } else { + cookie.OptOut = true + cookie.UIDs = make(map[string]string) + } +} + +func (cookie *cookieImpl) ToHTTPCookie() *http.Cookie { + j, _ := json.Marshal(cookie) b64 := base64.URLEncoding.EncodeToString(j) - hc := http.Cookie{ - Name: "uids", + return &http.Cookie{ + Name: COOKIE_NAME, Value: b64, Expires: time.Now().Add(180 * 24 * time.Hour), } - if cookie_domain != "" { - hc.Domain = cookie_domain +} + +func (cookie *cookieImpl) GetUID(familyName string) (string, bool) { + uid, ok := cookie.UIDs[familyName] + return uid, ok +} + +func (cookie *cookieImpl) SetCookieOnResponse(w http.ResponseWriter, domain string) { + httpCookie := cookie.ToHTTPCookie() + if domain != "" { + httpCookie.Domain = domain } - http.SetCookie(w, &hc) + http.SetCookie(w, httpCookie) } -func GetUIDs(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pc := ParseUIDCookie(r) - SetUIDCookie(w, pc) +func (cookie *cookieImpl) Unsync(familyName string) { + delete(cookie.UIDs, familyName) +} + +func (cookie *cookieImpl) HasSync(familyName string) bool { + _, ok := cookie.UIDs[familyName] + return ok +} + +func (cookie *cookieImpl) SyncCount() int { + return len(cookie.UIDs) +} + +func (cookie *cookieImpl) TrySync(familyName string, uid string) error { + if !cookie.AllowSyncs() { + return errors.New("The user has opted out of prebid server cookieImpl syncs.") + } + + // At the moment, Facebook calls /setuid with a UID of 0 if the user isn't logged into Facebook. + // They shouldn't be sending us a sentinel value... but since they are, we're refusing to save that ID. + if familyName == "audienceNetwork" && uid == "0" { + return errors.New("audienceNetwork uses a UID of 0 as \"not yet recognized\".") + } + + cookie.UIDs[familyName] = uid + return nil +} + +func (deps *UserSyncDeps) GetUIDs(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pc := ParseUserSyncMapFromRequest(r) + pc.SetCookieOnResponse(w, deps.Cookie_domain) json.NewEncoder(w).Encode(pc) return } @@ -89,10 +207,11 @@ func getRawQueryMap(query string) map[string]string { return m } -func SetUID(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - pc := ParseUIDCookie(r) - if pc.OptOut { +func (deps *UserSyncDeps) SetUID(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + pc := ParseUserSyncMapFromRequest(r) + if !pc.AllowSyncs() { w.WriteHeader(http.StatusUnauthorized) + metrics.GetOrRegisterMeter(USERSYNC_OPT_OUT, deps.Metrics).Mark(1) return } @@ -100,21 +219,24 @@ func SetUID(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { bidder := query["bidder"] if bidder == "" { w.WriteHeader(http.StatusBadRequest) + metrics.GetOrRegisterMeter(USERSYNC_BAD_REQUEST, deps.Metrics).Mark(1) return } uid := query["uid"] + var err error = nil if uid == "" { - delete(pc.UIDs, bidder) + pc.Unsync(bidder) } else { - pc.UIDs[bidder] = uid + err = pc.TrySync(bidder, uid) } - SetUIDCookie(w, pc) -} + if err == nil { + metrics.GetOrRegisterMeter(fmt.Sprintf(USERSYNC_SUCCESS, bidder), deps.Metrics).Mark(1) + } -// Recaptcha code from https://github.com/haisum/recaptcha/blob/master/recaptcha.go -var recaptchaURL = "https://www.google.com/recaptcha/api/siteverify" + pc.SetCookieOnResponse(w, deps.Cookie_domain) +} // Struct for parsing json in google's response type googleResponse struct { @@ -122,7 +244,7 @@ type googleResponse struct { ErrorCodes []string `json:"error-codes"` } -func VerifyRecaptcha(response string) error { +func (deps *UserSyncDeps) VerifyRecaptcha(response string) error { ts := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: ssl.GetRootCAPool()}, } @@ -130,8 +252,8 @@ func VerifyRecaptcha(response string) error { client := &http.Client{ Transport: ts, } - resp, err := client.PostForm(recaptchaURL, - url.Values{"secret": {recaptcha_secret}, "response": {response}}) + resp, err := client.PostForm(RECAPTCHA_URL, + url.Values{"secret": {deps.Recaptcha_secret}, "response": {response}}) if err != nil { return err } @@ -146,31 +268,26 @@ func VerifyRecaptcha(response string) error { return nil } -func OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (deps *UserSyncDeps) OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { optout := r.FormValue("optout") rr := r.FormValue("g-recaptcha-response") if rr == "" { - http.Redirect(w, r, fmt.Sprintf("%s/static/optout.html", external_url), 301) + http.Redirect(w, r, fmt.Sprintf("%s/static/optout.html", deps.External_url), 301) return } - err := VerifyRecaptcha(rr) + err := deps.VerifyRecaptcha(rr) if err != nil { glog.Infof("Optout failed recaptcha: %v", err) w.WriteHeader(http.StatusUnauthorized) return } - pc := ParseUIDCookie(r) - if optout == "" { - pc.OptOut = false - } else { - pc.OptOut = true - pc.UIDs = nil - } + pc := ParseUserSyncMapFromRequest(r) + pc.SetPreference(optout == "") - SetUIDCookie(w, pc) + pc.SetCookieOnResponse(w, deps.Cookie_domain) if optout == "" { http.Redirect(w, r, "https://ib.adnxs.com/optin", 301) } else { @@ -178,15 +295,7 @@ func OptOut(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { } } -// split this for testability -func InitUsersyncHandlers(router *httprouter.Router, metricsRegistry metrics.Registry, cdomain string, - xternal_url string, captcha_secret string) { - cookie_domain = cdomain - external_url = xternal_url - recaptcha_secret = captcha_secret - - router.GET("/getuids", GetUIDs) - router.GET("/setuid", SetUID) - router.POST("/optout", OptOut) - router.GET("/optout", OptOut) +func timestamp() *time.Time { + birthday := time.Now() + return &birthday } diff --git a/pbs/usersync_test.go b/pbs/usersync_test.go new file mode 100644 index 00000000000..ed9740dfee6 --- /dev/null +++ b/pbs/usersync_test.go @@ -0,0 +1,231 @@ +package pbs + +import ( + "encoding/base64" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOptOutCookie(t *testing.T) { + cookie := &cookieImpl{ + UIDs: make(map[string]string), + OptOut: true, + Birthday: timestamp(), + } + ensureConsistency(t, cookie) +} + +func TestEmptyOptOutCookie(t *testing.T) { + cookie := &cookieImpl{ + UIDs: make(map[string]string), + OptOut: true, + Birthday: timestamp(), + } + ensureConsistency(t, cookie) +} + +func TestEmptyCookie(t *testing.T) { + cookie := &cookieImpl{ + UIDs: make(map[string]string, 0), + OptOut: false, + Birthday: timestamp(), + } + ensureConsistency(t, cookie) +} + +func TestCookieWithData(t *testing.T) { + cookie := &cookieImpl{ + UIDs: map[string]string{ + "adnxs": "123", + "audienceNetwork": "456", + }, + OptOut: false, + Birthday: timestamp(), + } + ensureConsistency(t, cookie) +} + +func TestRejectAudienceNetworkCookie(t *testing.T) { + raw := &cookieImpl{ + UIDs: map[string]string{ + "audienceNetwork": "0", + }, + OptOut: false, + Birthday: timestamp(), + } + parsed := ParseUserSyncMap(raw.ToHTTPCookie()) + if parsed.HasSync("audienceNetwork") { + t.Errorf("Cookie serializing and deserializing should delete audienceNetwork values of 0") + } + + err := parsed.TrySync("audienceNetwork", "0") + if err == nil { + t.Errorf("Cookie should reject audienceNetwork values of 0.") + } + if parsed.HasSync("audienceNetwork") { + t.Errorf("Cookie The cookie should have rejected the audienceNetwork sync.") + } +} + +func TestOptOutReset(t *testing.T) { + cookie := &cookieImpl{ + UIDs: map[string]string{ + "adnxs": "123", + "audienceNetwork": "456", + }, + OptOut: false, + Birthday: timestamp(), + } + + cookie.SetPreference(false) + if cookie.AllowSyncs() { + t.Error("After SetPreference(false), a cookie should not allow more user syncs.") + } + ensureConsistency(t, cookie) +} + +func TestOptIn(t *testing.T) { + cookie := &cookieImpl{ + UIDs: make(map[string]string), + OptOut: true, + Birthday: timestamp(), + } + + cookie.SetPreference(true) + if !cookie.AllowSyncs() { + t.Error("After SetPreference(true), a cookie should allow more user syncs.") + } + ensureConsistency(t, cookie) +} + +func TestParseCorruptedCookie(t *testing.T) { + raw := http.Cookie{ + Name: "uids", + Value: "bad base64 encoding", + } + parsed := ParseUserSyncMap(&raw) + ensureEmptyMap(t, parsed) +} + +func TestParseCorruptedCookieJSON(t *testing.T) { + cookieData := base64.URLEncoding.EncodeToString([]byte("bad json")) + raw := http.Cookie{ + Name: "uids", + Value: cookieData, + } + parsed := ParseUserSyncMap(&raw) + ensureEmptyMap(t, parsed) +} + +func TestParseNilSyncMap(t *testing.T) { + cookieJSON := "{\"bday\":123,\"optout\":true}" + cookieData := base64.URLEncoding.EncodeToString([]byte(cookieJSON)) + raw := http.Cookie{ + Name: COOKIE_NAME, + Value: cookieData, + } + parsed := ParseUserSyncMap(&raw) + ensureEmptyMap(t, parsed) + ensureConsistency(t, parsed) +} + +func writeThenRead(t *testing.T, cookie UserSyncMap) UserSyncMap { + w := httptest.NewRecorder() + cookie.SetCookieOnResponse(w, "mock-domain") + writtenCookie := w.HeaderMap.Get("Set-Cookie") + + header := http.Header{} + header.Add("Cookie", writtenCookie) + request := http.Request{Header: header} + return ParseUserSyncMapFromRequest(&request) +} + +func TestCookieReadWrite(t *testing.T) { + cookie := &cookieImpl{ + UIDs: map[string]string{ + "adnxs": "123", + "audienceNetwork": "456", + }, + OptOut: false, + Birthday: timestamp(), + } + + received := writeThenRead(t, cookie) + uid, exists := received.GetUID("adnxs") + if !exists || uid != "123" { + t.Errorf("Received cookie should have the adnxs ID=123. Got %s", uid) + } + uid, exists = received.GetUID("audienceNetwork") + if !exists || uid != "456" { + t.Errorf("Received cookie should have the audienceNetwork ID=456. Got %s", uid) + } + if received.SyncCount() != 2 { + t.Errorf("Expected 2 user syncs. Got %d", received.SyncCount()) + } +} + +func ensureEmptyMap(t *testing.T, cookie UserSyncMap) { + if !cookie.AllowSyncs() { + t.Error("Empty cookies should allow user syncs.") + } + if cookie.SyncCount() != 0 { + t.Errorf("Empty cookies shouldn't have any user syncs. Found %d.", cookie.SyncCount()) + } +} + +func ensureConsistency(t *testing.T, cookie UserSyncMap) { + if cookie.AllowSyncs() { + err := cookie.TrySync("pulsepoint", "1") + if err != nil { + t.Errorf("Cookie sync should succeed if the user has opted in.") + } + if !cookie.HasSync("pulsepoint") { + t.Errorf("The cookieImpl should have a usersync after a successful call to TrySync") + } + savedUID, hadSync := cookie.GetUID("pulsepoint") + if !hadSync { + t.Error("The GetUID function should return true when it has a sync. Got false") + } + if savedUID != "1" { + t.Errorf("The cookieImpl isn't saving syncs correctly. Expected %s, got %s", "1", savedUID) + } + cookie.Unsync("pulsepoint") + if cookie.HasSync("pulsepoint") { + t.Errorf("The cookieImpl should not have have a usersync after a call to Unsync") + } + if value, hadValue := cookie.GetUID("pulsepoint"); value != "" || hadValue { + t.Error("cookieImpl.GetUID() should return empty strings if it doesn't have a sync") + } + } else { + if cookie.SyncCount() != 0 { + t.Errorf("If the user opted out, the cookieImpl should have no user syncs. Got %d", cookie.SyncCount()) + } + + err := cookie.TrySync("adnxs", "123") + if err == nil { + t.Error("TrySync should fail if the user has opted out of cookieImpl syncs, but it succeeded.") + } + } + + cookieImpl := parseCookieImpl(cookie.ToHTTPCookie()) + if cookieImpl.OptOut == cookie.AllowSyncs() { + t.Error("The cookieImpl interface shouldn't let modifications happen if the user has opted out") + } + if cookie.SyncCount() != len(cookieImpl.UIDs) { + t.Errorf("Incorrect sync count. Expected %d, got %d", len(cookieImpl.UIDs), cookie.SyncCount()) + } + + for family, uid := range cookieImpl.UIDs { + if !cookie.HasSync(family) { + t.Errorf("Cookie is missing sync for family %s", family) + } + savedUID, hadSync := cookie.GetUID(family) + if !hadSync { + t.Error("The GetUID function should return true when it has a sync. Got false") + } + if savedUID != uid { + t.Errorf("Wrong UID saved for family %s. Expected %s, got %s", family, uid, savedUID) + } + } +} diff --git a/pbs_light.go b/pbs_light.go index 62f41635252..2ef42d420f4 100644 --- a/pbs_light.go +++ b/pbs_light.go @@ -160,8 +160,8 @@ type cookieSyncResponse struct { func cookieSync(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { mCookieSyncMeter.Mark(1) - cookies := pbs.ParseUIDCookie(r) - if cookies.OptOut { + userSyncCookie := pbs.ParseUserSyncMapFromRequest(r) + if !userSyncCookie.AllowSyncs() { http.Error(w, "User has opted out", http.StatusUnauthorized) return } @@ -180,7 +180,7 @@ func cookieSync(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { UUID: csReq.UUID, BidderStatus: make([]*pbs.PBSBidder, 0, len(csReq.Bidders)), } - if _, err := r.Cookie("uuid2"); (requireUUID2 && err != nil) || len(cookies.UIDs) == 0 { + if _, err := r.Cookie("uuid2"); (requireUUID2 && err != nil) || userSyncCookie.SyncCount() == 0 { csResp.Status = "no_cookie" } else { csResp.Status = "ok" @@ -188,7 +188,7 @@ func cookieSync(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { for _, bidder := range csReq.Bidders { if ex, ok := exchanges[bidder]; ok { - if _, ok := cookies.UIDs[ex.FamilyName()]; !ok { + if !userSyncCookie.HasSync(ex.FamilyName()) { b := pbs.PBSBidder{ BidderCode: bidder, NoCookie: true, @@ -229,7 +229,7 @@ func auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { status := "OK" if pbs_req.App != nil { mAppRequestMeter.Mark(1) - } else if len(pbs_req.UserIDs) == 0 { + } else if pbs_req.SyncMap.SyncCount() == 0 { mNoCookieMeter.Mark(1) if isSafari { mSafariNoCookieMeter.Mark(1) @@ -244,7 +244,7 @@ func auction(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { Expires: time.Now().Add(180 * 24 * time.Hour), } http.SetCookie(w, &c) - pbs_req.UserIDs["adnxs"] = uuid2 + pbs_req.SyncMap.TrySync("adnxs", uuid2) } } @@ -716,7 +716,17 @@ func serve(cfg *config.Configuration) error { router.GET("/ip", getIP) router.ServeFiles("/static/*filepath", http.Dir("static")) - pbs.InitUsersyncHandlers(router, metricsRegistry, cfg.CookieDomain, cfg.ExternalURL, cfg.RecaptchaSecret) + userSyncDeps := &pbs.UserSyncDeps{ + Cookie_domain: cfg.CookieDomain, + External_url: cfg.ExternalURL, + Recaptcha_secret: cfg.RecaptchaSecret, + Metrics: metricsRegistry, + } + + router.GET("/getuids", userSyncDeps.GetUIDs) + router.GET("/setuid", userSyncDeps.SetUID) + router.POST("/optout", userSyncDeps.OptOut) + router.GET("/optout", userSyncDeps.OptOut) pbc.InitPrebidCache(cfg.CacheURL) diff --git a/pbs_light_test.go b/pbs_light_test.go index 7cacbd1c81b..2f2a2429b9c 100644 --- a/pbs_light_test.go +++ b/pbs_light_test.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "encoding/base64" "encoding/json" "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-server/config" @@ -78,13 +77,10 @@ func TestCookieSyncHasCookies(t *testing.T) { req, _ := http.NewRequest("POST", "/cookie_sync", csbuf) - pcs := pbs.ParseUIDCookie(req) - pcs.UIDs["adnxs"] = "1234" - pcs.UIDs["audienceNetwork"] = "2345" - j, _ := json.Marshal(pcs) - b64 := base64.URLEncoding.EncodeToString(j) - uid_cookie := http.Cookie{Name: "uids", Value: b64} - req.AddCookie(&uid_cookie) + pcs := pbs.ParseUserSyncMapFromRequest(req) + pcs.TrySync("adnxs", "1234") + pcs.TrySync("audienceNetwork", "2345") + req.AddCookie(pcs.ToHTTPCookie()) rr := httptest.NewRecorder() router.ServeHTTP(rr, req)