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  }