github.com/prebid/prebid-server/v2@v2.18.0/amp/parse.go (about)

     1  package amp
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  
    10  	tcf2 "github.com/prebid/go-gdpr/vendorconsent/tcf2"
    11  
    12  	"github.com/prebid/openrtb/v20/openrtb2"
    13  	"github.com/prebid/prebid-server/v2/errortypes"
    14  	"github.com/prebid/prebid-server/v2/privacy"
    15  	"github.com/prebid/prebid-server/v2/privacy/ccpa"
    16  	"github.com/prebid/prebid-server/v2/privacy/gdpr"
    17  )
    18  
    19  // Params defines the parameters of an AMP request.
    20  type Params struct {
    21  	Account           string
    22  	AdditionalConsent string
    23  	CanonicalURL      string
    24  	Consent           string
    25  	ConsentType       int64
    26  	Debug             bool
    27  	GdprApplies       *bool
    28  	Origin            string
    29  	Size              Size
    30  	Slot              string
    31  	StoredRequestID   string
    32  	Targeting         string
    33  	Timeout           *uint64
    34  	Trace             string
    35  }
    36  
    37  // Size defines size information of an AMP request.
    38  type Size struct {
    39  	Height         int64
    40  	Multisize      []openrtb2.Format
    41  	OverrideHeight int64
    42  	OverrideWidth  int64
    43  	Width          int64
    44  }
    45  
    46  // Policy consent types
    47  const (
    48  	ConsentNone      = 0
    49  	ConsentTCF1      = 1
    50  	ConsentTCF2      = 2
    51  	ConsentUSPrivacy = 3
    52  )
    53  
    54  // ReadPolicy returns a privacy writer in accordance to the query values consent, consent_type and gdpr_applies.
    55  // Returned policy writer could either be GDPR, CCPA or NilPolicy. The second return value is a warning.
    56  func ReadPolicy(ampParams Params, pbsConfigGDPREnabled bool) (privacy.PolicyWriter, error) {
    57  	if len(ampParams.Consent) == 0 {
    58  		return privacy.NilPolicyWriter{}, nil
    59  	}
    60  
    61  	var rv privacy.PolicyWriter = privacy.NilPolicyWriter{}
    62  	var warning error
    63  	var warningMsg string
    64  
    65  	// If consent_type was set to CCPA or GDPR TCF2, we return a valid writer even if the consent string is invalid
    66  	switch ampParams.ConsentType {
    67  	case ConsentTCF1:
    68  		warningMsg = "TCF1 consent is deprecated and no longer supported."
    69  	case ConsentTCF2:
    70  		if pbsConfigGDPREnabled {
    71  			rv = buildGdprTCF2ConsentWriter(ampParams)
    72  			// Log warning if GDPR consent string is invalid
    73  			warningMsg = validateTCf2ConsentString(ampParams.Consent)
    74  		}
    75  	case ConsentUSPrivacy:
    76  		rv = ccpa.ConsentWriter{Consent: ampParams.Consent}
    77  		if ccpa.ValidateConsent(ampParams.Consent) {
    78  			if parseGdprApplies(ampParams.GdprApplies) == 1 {
    79  				// Log warning because AMP request comes with both a valid CCPA string and gdpr_applies set to true
    80  				warningMsg = "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string"
    81  			}
    82  		} else {
    83  			// Log warning if CCPA string is invalid
    84  			warningMsg = fmt.Sprintf("Consent string '%s' is not a valid CCPA consent string.", ampParams.Consent)
    85  		}
    86  	default:
    87  		if ccpa.ValidateConsent(ampParams.Consent) {
    88  			rv = ccpa.ConsentWriter{Consent: ampParams.Consent}
    89  			if parseGdprApplies(ampParams.GdprApplies) == 1 {
    90  				warningMsg = "AMP request gdpr_applies value was ignored because provided consent string is a CCPA consent string"
    91  			}
    92  		} else if pbsConfigGDPREnabled && len(validateTCf2ConsentString(ampParams.Consent)) == 0 {
    93  			rv = buildGdprTCF2ConsentWriter(ampParams)
    94  		} else {
    95  			warningMsg = fmt.Sprintf("Consent string '%s' is not recognized as one of the supported formats CCPA or TCF2.", ampParams.Consent)
    96  		}
    97  	}
    98  
    99  	if len(warningMsg) > 0 {
   100  		warning = &errortypes.Warning{
   101  			Message:     warningMsg,
   102  			WarningCode: errortypes.InvalidPrivacyConsentWarningCode,
   103  		}
   104  	}
   105  	return rv, warning
   106  }
   107  
   108  // buildGdprTCF2ConsentWriter returns a gdpr.ConsentWriter that will set regs.ext.gdpr to the value
   109  // of 1 if gdpr_applies wasn't defined. The reason for this is that this function gets called when
   110  // GDPR applies, even if field gdpr_applies wasn't set in the AMP endpoint query.
   111  func buildGdprTCF2ConsentWriter(ampParams Params) gdpr.ConsentWriter {
   112  	writer := gdpr.ConsentWriter{Consent: ampParams.Consent}
   113  
   114  	// If gdpr_applies was not set, regs.ext.gdpr must equal 1
   115  	var gdprValue int8 = 1
   116  	if ampParams.GdprApplies != nil {
   117  		// set regs.ext.gdpr if non-nil gdpr_applies was set to true
   118  		gdprValue = parseGdprApplies(ampParams.GdprApplies)
   119  	}
   120  	writer.RegExtGDPR = &gdprValue
   121  
   122  	return writer
   123  }
   124  
   125  // parseGdprApplies returns a 0 if gdprApplies was not set or if false, and a 1 if
   126  // gdprApplies was set to true
   127  func parseGdprApplies(gdprApplies *bool) int8 {
   128  	gdpr := int8(0)
   129  
   130  	if gdprApplies != nil && *gdprApplies {
   131  		gdpr = int8(1)
   132  	}
   133  
   134  	return gdpr
   135  }
   136  
   137  // ParseParams parses the AMP parameters from a HTTP request.
   138  func validateTCf2ConsentString(consent string) string {
   139  	if tcf2.IsConsentV2(consent) {
   140  		if _, err := tcf2.ParseString(consent); err != nil {
   141  			return err.Error()
   142  		}
   143  	} else {
   144  		return fmt.Sprintf("Consent string '%s' is not a valid TCF2 consent string.", consent)
   145  	}
   146  	return ""
   147  }
   148  
   149  // ParseParams parses the AMP parameters from a HTTP request.
   150  func ParseParams(httpRequest *http.Request) (Params, error) {
   151  	query := httpRequest.URL.Query()
   152  
   153  	tagID := query.Get("tag_id")
   154  	if len(tagID) == 0 {
   155  		return Params{}, errors.New("AMP requests require an AMP tag_id")
   156  	}
   157  
   158  	params := Params{
   159  		Account:           query.Get("account"),
   160  		AdditionalConsent: query.Get("addtl_consent"),
   161  		CanonicalURL:      query.Get("curl"),
   162  		Consent:           chooseConsent(query.Get("consent_string"), query.Get("gdpr_consent")),
   163  		ConsentType:       parseInt(query.Get("consent_type")),
   164  		Debug:             query.Get("debug") == "1",
   165  		Origin:            query.Get("__amp_source_origin"),
   166  		Size: Size{
   167  			Height:         parseInt(query.Get("h")),
   168  			Multisize:      parseMultisize(query.Get("ms")),
   169  			OverrideHeight: parseInt(query.Get("oh")),
   170  			OverrideWidth:  parseInt(query.Get("ow")),
   171  			Width:          parseInt(query.Get("w")),
   172  		},
   173  		Slot:            query.Get("slot"),
   174  		StoredRequestID: tagID,
   175  		Targeting:       query.Get("targeting"),
   176  		Trace:           query.Get("trace"),
   177  	}
   178  	var err error
   179  	urlQueryGdprApplies := query.Get("gdpr_applies")
   180  	if len(urlQueryGdprApplies) > 0 {
   181  		if params.GdprApplies, err = parseBoolPtr(urlQueryGdprApplies); err != nil {
   182  			return params, err
   183  		}
   184  	}
   185  
   186  	urlQueryTimeout := query.Get("timeout")
   187  	if len(urlQueryTimeout) > 0 {
   188  		if params.Timeout, err = parseIntPtr(urlQueryTimeout); err != nil {
   189  			return params, err
   190  		}
   191  	}
   192  
   193  	return params, nil
   194  }
   195  
   196  func parseIntPtr(value string) (*uint64, error) {
   197  	var rv uint64
   198  	var err error
   199  
   200  	if rv, err = strconv.ParseUint(value, 10, 64); err != nil {
   201  		return nil, err
   202  	}
   203  	return &rv, nil
   204  }
   205  
   206  func parseInt(value string) int64 {
   207  	if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
   208  		return parsed
   209  	}
   210  	return 0
   211  }
   212  
   213  func parseBoolPtr(value string) (*bool, error) {
   214  	var rv bool
   215  	var err error
   216  
   217  	if rv, err = strconv.ParseBool(value); err != nil {
   218  		return nil, err
   219  	}
   220  	return &rv, nil
   221  }
   222  
   223  func parseMultisize(multisize string) []openrtb2.Format {
   224  	if multisize == "" {
   225  		return nil
   226  	}
   227  
   228  	sizeStrings := strings.Split(multisize, ",")
   229  	sizes := make([]openrtb2.Format, 0, len(sizeStrings))
   230  	for _, sizeString := range sizeStrings {
   231  		wh := strings.Split(sizeString, "x")
   232  		if len(wh) != 2 {
   233  			return nil
   234  		}
   235  		f := openrtb2.Format{
   236  			W: parseInt(wh[0]),
   237  			H: parseInt(wh[1]),
   238  		}
   239  		if f.W == 0 && f.H == 0 {
   240  			return nil
   241  		}
   242  
   243  		sizes = append(sizes, f)
   244  	}
   245  	return sizes
   246  }
   247  
   248  func chooseConsent(consent, gdprConsent string) string {
   249  	if len(consent) > 0 {
   250  		return consent
   251  	}
   252  
   253  	// Fallback to 'gdpr_consent' for compatibility until it's no longer used. This was our original
   254  	// implementation before the same AMP macro was reused for CCPA.
   255  	return gdprConsent
   256  }