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 }