github.com/prebid/prebid-server@v0.275.0/usersync/syncer.go (about) 1 package usersync 2 3 import ( 4 "errors" 5 "fmt" 6 "net/url" 7 "regexp" 8 "strings" 9 "text/template" 10 11 validator "github.com/asaskevich/govalidator" 12 "github.com/prebid/prebid-server/config" 13 "github.com/prebid/prebid-server/macros" 14 ) 15 16 var ( 17 ErrSyncerEndpointRequired = errors.New("at least one endpoint (iframe and/or redirect) is required") 18 ErrSyncerKeyRequired = errors.New("key is required") 19 errNoSyncTypesProvided = errors.New("no sync types provided") 20 errNoSyncTypesSupported = errors.New("no sync types supported") 21 ) 22 23 // Syncer represents the user sync configuration for a bidder or a shared set of bidders. 24 type Syncer interface { 25 // Key is the name of the syncer as stored in the user's cookie. This is often, but not 26 // necessarily, a one-to-one mapping with a bidder. 27 Key() string 28 29 // DefaultSyncType is the default SyncType for this syncer. 30 DefaultSyncType() SyncType 31 32 // SupportsType returns true if the syncer supports at least one of the specified sync types. 33 SupportsType(syncTypes []SyncType) bool 34 35 // GetSync returns a user sync for the user's device to perform, or an error if the none of the 36 // sync types are supported or if macro substitution fails. 37 GetSync(syncTypes []SyncType, userSyncMacros macros.UserSyncPrivacy) (Sync, error) 38 } 39 40 // Sync represents a user sync to be performed by the user's device. 41 type Sync struct { 42 URL string 43 Type SyncType 44 SupportCORS bool 45 } 46 47 type standardSyncer struct { 48 key string 49 defaultSyncType SyncType 50 iframe *template.Template 51 redirect *template.Template 52 supportCORS bool 53 } 54 55 const ( 56 setuidSyncTypeIFrame = "b" // b = blank HTML response 57 setuidSyncTypeRedirect = "i" // i = image response 58 ) 59 60 // NewSyncer creates a new Syncer from the provided configuration, or return an error if macro substition 61 // fails or an endpoint url is invalid. 62 func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder string) (Syncer, error) { 63 if syncerConfig.Key == "" { 64 return nil, ErrSyncerKeyRequired 65 } 66 67 if syncerConfig.IFrame == nil && syncerConfig.Redirect == nil { 68 return nil, ErrSyncerEndpointRequired 69 } 70 71 syncer := standardSyncer{ 72 key: syncerConfig.Key, 73 defaultSyncType: resolveDefaultSyncType(syncerConfig), 74 supportCORS: syncerConfig.SupportCORS != nil && *syncerConfig.SupportCORS, 75 } 76 77 if syncerConfig.IFrame != nil { 78 var err error 79 syncer.iframe, err = buildTemplate(bidder, setuidSyncTypeIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame) 80 if err != nil { 81 return nil, fmt.Errorf("iframe %v", err) 82 } 83 if err := validateTemplate(syncer.iframe); err != nil { 84 return nil, fmt.Errorf("iframe %v", err) 85 } 86 } 87 88 if syncerConfig.Redirect != nil { 89 var err error 90 syncer.redirect, err = buildTemplate(bidder, setuidSyncTypeRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect) 91 if err != nil { 92 return nil, fmt.Errorf("redirect %v", err) 93 } 94 if err := validateTemplate(syncer.redirect); err != nil { 95 return nil, fmt.Errorf("redirect %v", err) 96 } 97 } 98 99 return syncer, nil 100 } 101 102 func resolveDefaultSyncType(syncerConfig config.Syncer) SyncType { 103 if syncerConfig.IFrame != nil { 104 return SyncTypeIFrame 105 } 106 return SyncTypeRedirect 107 } 108 109 // macro substitution regex 110 var ( 111 macroRegexExternalHost = regexp.MustCompile(`{{\s*\.ExternalURL\s*}}`) 112 macroRegexSyncerKey = regexp.MustCompile(`{{\s*\.SyncerKey\s*}}`) 113 macroRegexBidderName = regexp.MustCompile(`{{\s*\.BidderName\s*}}`) 114 macroRegexSyncType = regexp.MustCompile(`{{\s*\.SyncType\s*}}`) 115 macroRegexUserMacro = regexp.MustCompile(`{{\s*\.UserMacro\s*}}`) 116 macroRegexRedirect = regexp.MustCompile(`{{\s*\.RedirectURL\s*}}`) 117 macroRegex = regexp.MustCompile(`{{\s*\..*?\s*}}`) 118 ) 119 120 func buildTemplate(bidderName, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) { 121 redirectTemplate := syncerEndpoint.RedirectURL 122 if redirectTemplate == "" { 123 redirectTemplate = hostConfig.RedirectURL 124 } 125 126 externalURL := chooseExternalURL(syncerEndpoint.ExternalURL, syncerExternalURL, hostConfig.ExternalURL) 127 128 redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, bidderName) 129 redirectURL = macroRegexBidderName.ReplaceAllLiteralString(redirectURL, bidderName) 130 redirectURL = macroRegexSyncType.ReplaceAllLiteralString(redirectURL, syncTypeValue) 131 redirectURL = macroRegexUserMacro.ReplaceAllLiteralString(redirectURL, syncerEndpoint.UserMacro) 132 redirectURL = macroRegexExternalHost.ReplaceAllLiteralString(redirectURL, externalURL) 133 redirectURL = escapeTemplate(redirectURL) 134 135 url := macroRegexRedirect.ReplaceAllString(syncerEndpoint.URL, redirectURL) 136 137 templateName := strings.ToLower(bidderName) + "_usersync_url" 138 return template.New(templateName).Parse(url) 139 } 140 141 // chooseExternalURL selects the external url to use for the template, where the most specific config wins. 142 func chooseExternalURL(syncerEndpointURL, syncerURL, hostConfigURL string) string { 143 if syncerEndpointURL != "" { 144 return syncerEndpointURL 145 } 146 147 if syncerURL != "" { 148 return syncerURL 149 } 150 151 return hostConfigURL 152 } 153 154 // escapeTemplate url encodes a string template leaving the macro tags unaffected. 155 func escapeTemplate(x string) string { 156 escaped := strings.Builder{} 157 158 i := 0 159 for _, m := range macroRegex.FindAllStringIndex(x, -1) { 160 escaped.WriteString(url.QueryEscape(x[i:m[0]])) 161 escaped.WriteString(x[m[0]:m[1]]) 162 i = m[1] 163 } 164 escaped.WriteString(url.QueryEscape(x[i:])) 165 166 return escaped.String() 167 } 168 169 var templateTestValues = macros.UserSyncPrivacy{ 170 GDPR: "anyGDPR", 171 GDPRConsent: "anyGDPRConsent", 172 USPrivacy: "anyCCPAConsent", 173 } 174 175 func validateTemplate(template *template.Template) error { 176 url, err := macros.ResolveMacros(template, templateTestValues) 177 if err != nil { 178 return err 179 } 180 181 if !validator.IsURL(url) || !validator.IsRequestURL(url) { 182 return fmt.Errorf(`composed url: "%s" is invalid`, url) 183 } 184 185 return nil 186 } 187 188 func (s standardSyncer) Key() string { 189 return s.key 190 } 191 192 func (s standardSyncer) DefaultSyncType() SyncType { 193 return s.defaultSyncType 194 } 195 196 func (s standardSyncer) SupportsType(syncTypes []SyncType) bool { 197 supported := s.filterSupportedSyncTypes(syncTypes) 198 return len(supported) > 0 199 } 200 201 func (s standardSyncer) filterSupportedSyncTypes(syncTypes []SyncType) []SyncType { 202 supported := make([]SyncType, 0, len(syncTypes)) 203 for _, syncType := range syncTypes { 204 switch syncType { 205 case SyncTypeIFrame: 206 if s.iframe != nil { 207 supported = append(supported, SyncTypeIFrame) 208 } 209 case SyncTypeRedirect: 210 if s.redirect != nil { 211 supported = append(supported, SyncTypeRedirect) 212 } 213 } 214 } 215 return supported 216 } 217 218 func (s standardSyncer) GetSync(syncTypes []SyncType, userSyncMacros macros.UserSyncPrivacy) (Sync, error) { 219 syncType, err := s.chooseSyncType(syncTypes) 220 if err != nil { 221 return Sync{}, err 222 } 223 224 syncTemplate := s.chooseTemplate(syncType) 225 226 url, err := macros.ResolveMacros(syncTemplate, userSyncMacros) 227 if err != nil { 228 return Sync{}, err 229 } 230 231 sync := Sync{ 232 URL: url, 233 Type: syncType, 234 SupportCORS: s.supportCORS, 235 } 236 return sync, nil 237 } 238 239 func (s standardSyncer) chooseSyncType(syncTypes []SyncType) (SyncType, error) { 240 if len(syncTypes) == 0 { 241 return SyncTypeUnknown, errNoSyncTypesProvided 242 } 243 244 supported := s.filterSupportedSyncTypes(syncTypes) 245 if len(supported) == 0 { 246 return SyncTypeUnknown, errNoSyncTypesSupported 247 } 248 249 // prefer default type 250 for _, syncType := range supported { 251 if syncType == s.defaultSyncType { 252 return syncType, nil 253 } 254 } 255 256 return syncTypes[0], nil 257 } 258 259 func (s standardSyncer) chooseTemplate(syncType SyncType) *template.Template { 260 switch syncType { 261 case SyncTypeIFrame: 262 return s.iframe 263 case SyncTypeRedirect: 264 return s.redirect 265 default: 266 return nil 267 } 268 }