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  }