github.com/prebid/prebid-server/v2@v2.18.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/v2/config" 13 "github.com/prebid/prebid-server/v2/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 // DefaultResponseFormat is the default SyncType for this syncer. 30 DefaultResponseFormat() 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 formatOverride string 54 } 55 56 // NewSyncer creates a new Syncer from the provided configuration, or return an error if macro substition 57 // fails or an endpoint url is invalid. 58 func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer, bidder string) (Syncer, error) { 59 if syncerConfig.Key == "" { 60 return nil, ErrSyncerKeyRequired 61 } 62 63 if syncerConfig.IFrame == nil && syncerConfig.Redirect == nil { 64 return nil, ErrSyncerEndpointRequired 65 } 66 67 syncer := standardSyncer{ 68 key: syncerConfig.Key, 69 defaultSyncType: resolveDefaultSyncType(syncerConfig), 70 supportCORS: syncerConfig.SupportCORS != nil && *syncerConfig.SupportCORS, 71 formatOverride: syncerConfig.FormatOverride, 72 } 73 74 if syncerConfig.IFrame != nil { 75 var err error 76 syncer.iframe, err = buildTemplate(bidder, config.SyncResponseFormatIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame, syncerConfig.FormatOverride) 77 if err != nil { 78 return nil, fmt.Errorf("iframe %v", err) 79 } 80 if err := validateTemplate(syncer.iframe); err != nil { 81 return nil, fmt.Errorf("iframe %v", err) 82 } 83 } 84 85 if syncerConfig.Redirect != nil { 86 var err error 87 syncer.redirect, err = buildTemplate(bidder, config.SyncResponseFormatRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect, syncerConfig.FormatOverride) 88 if err != nil { 89 return nil, fmt.Errorf("redirect %v", err) 90 } 91 if err := validateTemplate(syncer.redirect); err != nil { 92 return nil, fmt.Errorf("redirect %v", err) 93 } 94 } 95 96 return syncer, nil 97 } 98 99 func resolveDefaultSyncType(syncerConfig config.Syncer) SyncType { 100 if syncerConfig.IFrame != nil { 101 return SyncTypeIFrame 102 } 103 return SyncTypeRedirect 104 } 105 106 // macro substitution regex 107 var ( 108 macroRegexExternalHost = regexp.MustCompile(`{{\s*\.ExternalURL\s*}}`) 109 macroRegexSyncerKey = regexp.MustCompile(`{{\s*\.SyncerKey\s*}}`) 110 macroRegexBidderName = regexp.MustCompile(`{{\s*\.BidderName\s*}}`) 111 macroRegexSyncType = regexp.MustCompile(`{{\s*\.SyncType\s*}}`) 112 macroRegexUserMacro = regexp.MustCompile(`{{\s*\.UserMacro\s*}}`) 113 macroRegexRedirect = regexp.MustCompile(`{{\s*\.RedirectURL\s*}}`) 114 macroRegex = regexp.MustCompile(`{{\s*\..*?\s*}}`) 115 ) 116 117 func buildTemplate(bidderName, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint, formatOverride string) (*template.Template, error) { 118 redirectTemplate := syncerEndpoint.RedirectURL 119 if redirectTemplate == "" { 120 redirectTemplate = hostConfig.RedirectURL 121 } 122 123 if formatOverride != "" { 124 syncTypeValue = formatOverride 125 } 126 127 externalURL := chooseExternalURL(syncerEndpoint.ExternalURL, syncerExternalURL, hostConfig.ExternalURL) 128 129 redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, bidderName) 130 redirectURL = macroRegexBidderName.ReplaceAllLiteralString(redirectURL, bidderName) 131 redirectURL = macroRegexSyncType.ReplaceAllLiteralString(redirectURL, syncTypeValue) 132 redirectURL = macroRegexUserMacro.ReplaceAllLiteralString(redirectURL, syncerEndpoint.UserMacro) 133 redirectURL = macroRegexExternalHost.ReplaceAllLiteralString(redirectURL, externalURL) 134 redirectURL = escapeTemplate(redirectURL) 135 136 url := macroRegexRedirect.ReplaceAllString(syncerEndpoint.URL, redirectURL) 137 138 templateName := strings.ToLower(bidderName) + "_usersync_url" 139 return template.New(templateName).Parse(url) 140 } 141 142 // chooseExternalURL selects the external url to use for the template, where the most specific config wins. 143 func chooseExternalURL(syncerEndpointURL, syncerURL, hostConfigURL string) string { 144 if syncerEndpointURL != "" { 145 return syncerEndpointURL 146 } 147 148 if syncerURL != "" { 149 return syncerURL 150 } 151 152 return hostConfigURL 153 } 154 155 // escapeTemplate url encodes a string template leaving the macro tags unaffected. 156 func escapeTemplate(x string) string { 157 escaped := strings.Builder{} 158 159 i := 0 160 for _, m := range macroRegex.FindAllStringIndex(x, -1) { 161 escaped.WriteString(url.QueryEscape(x[i:m[0]])) 162 escaped.WriteString(x[m[0]:m[1]]) 163 i = m[1] 164 } 165 escaped.WriteString(url.QueryEscape(x[i:])) 166 167 return escaped.String() 168 } 169 170 var templateTestValues = macros.UserSyncPrivacy{ 171 GDPR: "anyGDPR", 172 GDPRConsent: "anyGDPRConsent", 173 USPrivacy: "anyCCPAConsent", 174 } 175 176 func validateTemplate(template *template.Template) error { 177 url, err := macros.ResolveMacros(template, templateTestValues) 178 if err != nil { 179 return err 180 } 181 182 if !validator.IsURL(url) || !validator.IsRequestURL(url) { 183 return fmt.Errorf(`composed url: "%s" is invalid`, url) 184 } 185 186 return nil 187 } 188 189 func (s standardSyncer) Key() string { 190 return s.key 191 } 192 193 func (s standardSyncer) DefaultResponseFormat() SyncType { 194 switch s.formatOverride { 195 case config.SyncResponseFormatIFrame: 196 return SyncTypeIFrame 197 case config.SyncResponseFormatRedirect: 198 return SyncTypeRedirect 199 default: 200 return s.defaultSyncType 201 } 202 } 203 204 func (s standardSyncer) SupportsType(syncTypes []SyncType) bool { 205 supported := s.filterSupportedSyncTypes(syncTypes) 206 return len(supported) > 0 207 } 208 209 func (s standardSyncer) filterSupportedSyncTypes(syncTypes []SyncType) []SyncType { 210 supported := make([]SyncType, 0, len(syncTypes)) 211 for _, syncType := range syncTypes { 212 switch syncType { 213 case SyncTypeIFrame: 214 if s.iframe != nil { 215 supported = append(supported, SyncTypeIFrame) 216 } 217 case SyncTypeRedirect: 218 if s.redirect != nil { 219 supported = append(supported, SyncTypeRedirect) 220 } 221 } 222 } 223 return supported 224 } 225 226 func (s standardSyncer) GetSync(syncTypes []SyncType, userSyncMacros macros.UserSyncPrivacy) (Sync, error) { 227 syncType, err := s.chooseSyncType(syncTypes) 228 if err != nil { 229 return Sync{}, err 230 } 231 232 syncTemplate := s.chooseTemplate(syncType) 233 234 url, err := macros.ResolveMacros(syncTemplate, userSyncMacros) 235 if err != nil { 236 return Sync{}, err 237 } 238 239 sync := Sync{ 240 URL: url, 241 Type: syncType, 242 SupportCORS: s.supportCORS, 243 } 244 return sync, nil 245 } 246 247 func (s standardSyncer) chooseSyncType(syncTypes []SyncType) (SyncType, error) { 248 if len(syncTypes) == 0 { 249 return SyncTypeUnknown, errNoSyncTypesProvided 250 } 251 252 supported := s.filterSupportedSyncTypes(syncTypes) 253 if len(supported) == 0 { 254 return SyncTypeUnknown, errNoSyncTypesSupported 255 } 256 257 // prefer default type 258 for _, syncType := range supported { 259 if syncType == s.defaultSyncType { 260 return syncType, nil 261 } 262 } 263 264 return syncTypes[0], nil 265 } 266 267 func (s standardSyncer) chooseTemplate(syncType SyncType) *template.Template { 268 switch syncType { 269 case SyncTypeIFrame: 270 return s.iframe 271 case SyncTypeRedirect: 272 return s.redirect 273 default: 274 return nil 275 } 276 }