1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.service.customaudience; 18 19 import android.adservices.common.AdSelectionSignals; 20 import android.adservices.common.AdTechIdentifier; 21 import android.net.Uri; 22 23 import androidx.annotation.NonNull; 24 import androidx.annotation.Nullable; 25 26 import com.android.adservices.LoggerFactory; 27 import com.android.adservices.data.common.DBAdData; 28 import com.android.adservices.data.customaudience.DBTrustedBiddingData; 29 import com.android.adservices.service.common.AdTechUriValidator; 30 import com.android.adservices.service.common.JsonUtils; 31 import com.android.adservices.service.common.ValidatorUtil; 32 33 import org.json.JSONArray; 34 import org.json.JSONException; 35 import org.json.JSONObject; 36 37 import java.util.ArrayList; 38 import java.util.List; 39 import java.util.Objects; 40 import java.util.Optional; 41 42 /** 43 * A parser and validator for a JSON response that is fetched during the Custom Audience background 44 * fetch process. 45 */ 46 public class CustomAudienceUpdatableDataReader { 47 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 48 public static final String USER_BIDDING_SIGNALS_KEY = "user_bidding_signals"; 49 public static final String TRUSTED_BIDDING_DATA_KEY = "trusted_bidding_data"; 50 public static final String TRUSTED_BIDDING_URI_KEY = "trusted_bidding_uri"; 51 public static final String TRUSTED_BIDDING_KEYS_KEY = "trusted_bidding_keys"; 52 public static final String ADS_KEY = "ads"; 53 public static final String RENDER_URI_KEY = "render_uri"; 54 public static final String METADATA_KEY = "metadata"; 55 public static final String AD_COUNTERS_KEY = "ad_counter_keys"; 56 public static final String AD_FILTERS_KEY = "ad_filters"; 57 public static final String STRING_ERROR_FORMAT = "Unexpected format parsing %s in %s"; 58 59 private static final String FIELD_FOUND_LOG_FORMAT = "%s Found %s in JSON response"; 60 private static final String VALIDATED_FIELD_LOG_FORMAT = 61 "%s Validated %s found in JSON response"; 62 private static final String FIELD_NOT_FOUND_LOG_FORMAT = "%s %s not found in JSON response"; 63 private static final String SKIP_INVALID_JSON_TYPE_LOG_FORMAT = 64 "%s Invalid JSON type while parsing a single item in the %s found in JSON response;" 65 + " ignoring and continuing. Error message: %s"; 66 67 private final JSONObject mResponseObject; 68 private final String mResponseHash; 69 private final AdTechIdentifier mBuyer; 70 private final int mMaxUserBiddingSignalsSizeB; 71 private final int mMaxTrustedBiddingDataSizeB; 72 private final int mMaxAdsSizeB; 73 private final int mMaxNumAds; 74 private final ReadFiltersFromJsonStrategy mGetFiltersFromJsonObjectStrategy; 75 76 /** 77 * Creates a {@link CustomAudienceUpdatableDataReader} that will read updatable data from a 78 * given {@link JSONObject} and log with the given identifying {@code responseHash}. 79 * 80 * @param responseObject a {@link JSONObject} that may contain user bidding signals, trusted 81 * bidding data, and/or a list of ads 82 * @param responseHash a String that uniquely identifies the response which is used in logging 83 * @param buyer the buyer ad tech's eTLD+1 84 * @param maxUserBiddingSignalsSizeB the configured maximum size in bytes allocated for user 85 * bidding signals 86 * @param maxTrustedBiddingDataSizeB the configured maximum size in bytes allocated for trusted 87 * bidding data 88 * @param maxAdsSizeB the configured maximum size in bytes allocated for ads 89 * @param maxNumAds the configured maximum number of ads allowed per update 90 * @param filteringEnabled whether or not ad selection filtering fields should be read 91 */ CustomAudienceUpdatableDataReader( @onNull JSONObject responseObject, @NonNull String responseHash, @NonNull AdTechIdentifier buyer, int maxUserBiddingSignalsSizeB, int maxTrustedBiddingDataSizeB, int maxAdsSizeB, int maxNumAds, boolean filteringEnabled)92 protected CustomAudienceUpdatableDataReader( 93 @NonNull JSONObject responseObject, 94 @NonNull String responseHash, 95 @NonNull AdTechIdentifier buyer, 96 int maxUserBiddingSignalsSizeB, 97 int maxTrustedBiddingDataSizeB, 98 int maxAdsSizeB, 99 int maxNumAds, 100 boolean filteringEnabled) { 101 Objects.requireNonNull(responseObject); 102 Objects.requireNonNull(responseHash); 103 Objects.requireNonNull(buyer); 104 105 mResponseObject = responseObject; 106 mResponseHash = responseHash; 107 mBuyer = buyer; 108 mMaxUserBiddingSignalsSizeB = maxUserBiddingSignalsSizeB; 109 mMaxTrustedBiddingDataSizeB = maxTrustedBiddingDataSizeB; 110 mMaxAdsSizeB = maxAdsSizeB; 111 mMaxNumAds = maxNumAds; 112 mGetFiltersFromJsonObjectStrategy = 113 ReadFiltersFromJsonStrategyFactory.getStrategy(filteringEnabled); 114 } 115 116 /** 117 * Returns the user bidding signals extracted from the input object, if found. 118 * 119 * @throws JSONException if the key is found but the schema is incorrect 120 * @throws NullPointerException if the key found by the field is null 121 * @throws IllegalArgumentException if the extracted signals fail data validation 122 */ 123 @Nullable getUserBiddingSignalsFromJsonObject()124 public AdSelectionSignals getUserBiddingSignalsFromJsonObject() 125 throws JSONException, NullPointerException, IllegalArgumentException { 126 if (mResponseObject.has(USER_BIDDING_SIGNALS_KEY)) { 127 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 128 129 // Note that because the user bidding signals are stored in the response as a full JSON 130 // object already, the signals do not need to be validated further; the JSON must have 131 // been valid to be extracted successfully 132 JSONObject signalsJsonObj = 133 Objects.requireNonNull(mResponseObject.getJSONObject(USER_BIDDING_SIGNALS_KEY)); 134 String signalsString = signalsJsonObj.toString(); 135 136 if (signalsString.length() > mMaxUserBiddingSignalsSizeB) { 137 throw new IllegalArgumentException(); 138 } 139 140 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 141 return AdSelectionSignals.fromString(signalsString); 142 } else { 143 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 144 return null; 145 } 146 } 147 148 /** 149 * Returns the trusted bidding data extracted from the input object, if found. 150 * 151 * @throws JSONException if the key is found but the schema is incorrect 152 * @throws NullPointerException if the key found by the field is null 153 * @throws IllegalArgumentException if the extracted data fails data validation 154 */ 155 @Nullable getTrustedBiddingDataFromJsonObject()156 public DBTrustedBiddingData getTrustedBiddingDataFromJsonObject() 157 throws JSONException, NullPointerException, IllegalArgumentException { 158 if (mResponseObject.has(TRUSTED_BIDDING_DATA_KEY)) { 159 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 160 161 JSONObject dataJsonObj = mResponseObject.getJSONObject(TRUSTED_BIDDING_DATA_KEY); 162 163 String uri = 164 JsonUtils.getStringFromJson( 165 dataJsonObj, 166 TRUSTED_BIDDING_URI_KEY, 167 String.format( 168 STRING_ERROR_FORMAT, 169 TRUSTED_BIDDING_URI_KEY, 170 TRUSTED_BIDDING_DATA_KEY)); 171 Uri parsedUri = Uri.parse(uri); 172 173 JSONArray keysJsonArray = dataJsonObj.getJSONArray(TRUSTED_BIDDING_KEYS_KEY); 174 int keysListLength = keysJsonArray.length(); 175 List<String> keysList = new ArrayList<>(keysListLength); 176 for (int i = 0; i < keysListLength; i++) { 177 try { 178 keysList.add( 179 JsonUtils.getStringFromJsonArrayAtIndex( 180 keysJsonArray, 181 i, 182 String.format( 183 STRING_ERROR_FORMAT, 184 TRUSTED_BIDDING_KEYS_KEY, 185 TRUSTED_BIDDING_DATA_KEY))); 186 } catch (JSONException | NullPointerException exception) { 187 // Skip any keys that are malformed and continue to the next in the list; note 188 // that if the entire given list of keys is junk, then any existing trusted 189 // bidding keys are cleared from the custom audience 190 sLogger.v( 191 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 192 mResponseHash, 193 TRUSTED_BIDDING_KEYS_KEY, 194 Optional.ofNullable(exception.getMessage()).orElse("<null>")); 195 } 196 } 197 198 AdTechUriValidator uriValidator = 199 new AdTechUriValidator( 200 ValidatorUtil.AD_TECH_ROLE_BUYER, 201 mBuyer.toString(), 202 this.getClass().getSimpleName(), 203 TrustedBiddingDataValidator.TRUSTED_BIDDING_URI_FIELD_NAME); 204 uriValidator.validate(parsedUri); 205 206 DBTrustedBiddingData trustedBiddingData = 207 new DBTrustedBiddingData.Builder().setUri(parsedUri).setKeys(keysList).build(); 208 209 if (trustedBiddingData.size() > mMaxTrustedBiddingDataSizeB) { 210 throw new IllegalArgumentException(); 211 } 212 213 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 214 return trustedBiddingData; 215 } else { 216 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 217 return null; 218 } 219 } 220 221 /** 222 * Returns the list of ads extracted from the input object, if found. 223 * 224 * @throws JSONException if the key is found but the schema is incorrect 225 * @throws NullPointerException if the key found by the field is null 226 * @throws IllegalArgumentException if the extracted ads fail data validation 227 */ 228 @Nullable getAdsFromJsonObject()229 public List<DBAdData> getAdsFromJsonObject() 230 throws JSONException, NullPointerException, IllegalArgumentException { 231 if (mResponseObject.has(ADS_KEY)) { 232 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, ADS_KEY); 233 234 JSONArray adsJsonArray = mResponseObject.getJSONArray(ADS_KEY); 235 int adsSize = 0; 236 int adsListLength = adsJsonArray.length(); 237 List<DBAdData> adsList = new ArrayList<>(); 238 for (int i = 0; i < adsListLength; i++) { 239 try { 240 JSONObject adDataJsonObj = adsJsonArray.getJSONObject(i); 241 242 // Note: getString() coerces values to be strings; use get() instead 243 Object uri = adDataJsonObj.get(RENDER_URI_KEY); 244 if (!(uri instanceof String)) { 245 throw new JSONException( 246 "Unexpected format parsing " + RENDER_URI_KEY + " in " + ADS_KEY); 247 } 248 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri)); 249 250 // By passing in an empty ad tech identifier string, ad tech identifier host 251 // matching is skipped 252 AdTechUriValidator uriValidator = 253 new AdTechUriValidator( 254 ValidatorUtil.AD_TECH_ROLE_BUYER, 255 "", 256 this.getClass().getSimpleName(), 257 RENDER_URI_KEY); 258 uriValidator.validate(parsedUri); 259 260 String metadata = 261 Objects.requireNonNull(adDataJsonObj.getJSONObject(METADATA_KEY)) 262 .toString(); 263 264 DBAdData.Builder adDataBuilder = 265 new DBAdData.Builder().setRenderUri(parsedUri).setMetadata(metadata); 266 267 mGetFiltersFromJsonObjectStrategy.readFilters(adDataBuilder, adDataJsonObj); 268 DBAdData adData = adDataBuilder.build(); 269 adsList.add(adData); 270 adsSize += adData.size(); 271 } catch (JSONException 272 | NullPointerException 273 | IllegalArgumentException exception) { 274 // Skip any ads that are malformed and continue to the next in the list; 275 // note 276 // that if the entire given list of ads is junk, then any existing ads are 277 // cleared from the custom audience 278 sLogger.v( 279 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 280 mResponseHash, 281 ADS_KEY, 282 Optional.ofNullable(exception.getMessage()).orElse("<null>")); 283 } 284 } 285 286 if (adsSize > mMaxAdsSizeB) { 287 throw new IllegalArgumentException(); 288 } 289 290 if (adsList.size() > mMaxNumAds) { 291 throw new IllegalArgumentException(); 292 } 293 294 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, ADS_KEY); 295 return adsList; 296 } else { 297 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, ADS_KEY); 298 return null; 299 } 300 } 301 302 303 } 304