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 static android.adservices.customaudience.CustomAudience.FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS; 20 import static android.adservices.customaudience.CustomAudience.PRIORITY_DEFAULT; 21 22 import android.adservices.common.AdSelectionSignals; 23 import android.adservices.common.AdTechIdentifier; 24 import android.adservices.common.ComponentAdData; 25 import android.adservices.customaudience.CustomAudience; 26 import android.net.Uri; 27 28 import androidx.annotation.NonNull; 29 import androidx.annotation.Nullable; 30 31 import com.android.adservices.LoggerFactory; 32 import com.android.adservices.data.common.DBAdData; 33 import com.android.adservices.data.customaudience.DBTrustedBiddingData; 34 import com.android.adservices.service.common.AdRenderIdValidator; 35 import com.android.adservices.service.common.AdTechUriValidator; 36 import com.android.adservices.service.common.JsonUtils; 37 import com.android.adservices.service.common.ValidatorUtil; 38 39 import org.json.JSONArray; 40 import org.json.JSONException; 41 import org.json.JSONObject; 42 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Objects; 46 import java.util.Optional; 47 48 // TODO(b/283857101): Delete and use CustomAudienceBlob instead. 49 /** 50 * A parser and validator for a JSON response that is fetched during the Custom Audience background 51 * fetch process. 52 */ 53 public class CustomAudienceUpdatableDataReader { 54 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 55 public static final String USER_BIDDING_SIGNALS_KEY = "user_bidding_signals"; 56 public static final String TRUSTED_BIDDING_DATA_KEY = "trusted_bidding_data"; 57 public static final String TRUSTED_BIDDING_URI_KEY = "trusted_bidding_uri"; 58 public static final String TRUSTED_BIDDING_KEYS_KEY = "trusted_bidding_keys"; 59 public static final String ADS_KEY = "ads"; 60 public static final String RENDER_URI_KEY = "render_uri"; 61 public static final String METADATA_KEY = "metadata"; 62 public static final String AD_COUNTERS_KEY = "ad_counter_keys"; 63 public static final String AD_FILTERS_KEY = "ad_filters"; 64 public static final String AD_RENDER_ID_KEY = "ad_render_id"; 65 public static final String STRING_ERROR_FORMAT = "Unexpected format parsing %s in %s"; 66 public static final String AUCTION_SERVER_REQUEST_FLAGS_KEY = "auction_server_request_flags"; 67 public static final String PRIORITY_KEY = "priority"; 68 public static final String OMIT_ADS_VALUE = "omit_ads"; 69 public static final String COMPONENT_ADS_KEY = "component_ads"; 70 71 public static final String FIELD_FOUND_LOG_FORMAT = "%s Found %s in JSON response"; 72 public static final String VALIDATED_FIELD_LOG_FORMAT = 73 "%s Validated %s found in JSON response"; 74 public static final String FIELD_NOT_FOUND_LOG_FORMAT = "%s %s not found in JSON response"; 75 public static final String SKIP_INVALID_JSON_TYPE_LOG_FORMAT = 76 "%s Invalid JSON type while parsing a single item in the %s found in JSON response;" 77 + " ignoring and continuing. Error message: %s"; 78 public static final String EXIT_INVALID_JSON_TYPE_LOG_FORMAT = 79 "%s Invalid JSON type while parsing a single item in the %s found in JSON response;" 80 + " exiting the whole list. Error message: %s"; 81 public static final String COMPONENT_ADS_SIZE_EXCEEDS_MAX = 82 "Size of component ads list exceeds the maximum allowed."; 83 84 private final JSONObject mResponseObject; 85 private final String mResponseHash; 86 private final AdTechIdentifier mBuyer; 87 private final int mMaxUserBiddingSignalsSizeB; 88 private final int mMaxTrustedBiddingDataSizeB; 89 private final int mMaxAdsSizeB; 90 private final int mMaxNumAds; 91 private final ReadFiltersFromJsonStrategy mGetFiltersFromJsonObjectStrategy; 92 private final ReadAdRenderIdFromJsonStrategy mReadAdRenderIdFromJsonStrategy; 93 private final AdRenderIdValidator mComponentAdRenderIdValidator; 94 private final int mMaxNumComponentAds; 95 96 /** 97 * Creates a {@link CustomAudienceUpdatableDataReader} that will read updatable data from a 98 * given {@link JSONObject} and log with the given identifying {@code responseHash}. 99 * 100 * @param responseObject a {@link JSONObject} that may contain user bidding signals, trusted 101 * bidding data, and/or a list of ads 102 * @param responseHash a String that uniquely identifies the response which is used in logging 103 * @param buyer the buyer ad tech's eTLD+1 104 * @param maxUserBiddingSignalsSizeB the configured maximum size in bytes allocated for user 105 * bidding signals 106 * @param maxTrustedBiddingDataSizeB the configured maximum size in bytes allocated for trusted 107 * bidding data 108 * @param maxAdsSizeB the configured maximum size in bytes allocated for ads 109 * @param maxNumAds the configured maximum number of ads allowed per update 110 * @param frequencyCapFilteringEnabled whether or not frequency cap filtering fields should be 111 * read 112 * @param appInstallFilteringEnabled whether or not app install filtering fields should be read 113 * @param adRenderIdEnabled whether ad render id field should be read 114 * @param adRenderIdMaxLength the max length of the ad render id 115 * @param componentAdRenderIdMaxLength the max length of the component ad render id 116 * @param maxNumComponentAds the maximum number of component ads in a custom audience 117 */ CustomAudienceUpdatableDataReader( @onNull JSONObject responseObject, @NonNull String responseHash, @NonNull AdTechIdentifier buyer, int maxUserBiddingSignalsSizeB, int maxTrustedBiddingDataSizeB, int maxAdsSizeB, int maxNumAds, boolean frequencyCapFilteringEnabled, boolean appInstallFilteringEnabled, boolean adRenderIdEnabled, long adRenderIdMaxLength, int componentAdRenderIdMaxLength, int maxNumComponentAds)118 protected CustomAudienceUpdatableDataReader( 119 @NonNull JSONObject responseObject, 120 @NonNull String responseHash, 121 @NonNull AdTechIdentifier buyer, 122 int maxUserBiddingSignalsSizeB, 123 int maxTrustedBiddingDataSizeB, 124 int maxAdsSizeB, 125 int maxNumAds, 126 boolean frequencyCapFilteringEnabled, 127 boolean appInstallFilteringEnabled, 128 boolean adRenderIdEnabled, 129 long adRenderIdMaxLength, 130 int componentAdRenderIdMaxLength, 131 int maxNumComponentAds) { 132 Objects.requireNonNull(responseObject); 133 Objects.requireNonNull(responseHash); 134 Objects.requireNonNull(buyer); 135 136 mResponseObject = responseObject; 137 mResponseHash = responseHash; 138 mBuyer = buyer; 139 mMaxUserBiddingSignalsSizeB = maxUserBiddingSignalsSizeB; 140 mMaxTrustedBiddingDataSizeB = maxTrustedBiddingDataSizeB; 141 mMaxAdsSizeB = maxAdsSizeB; 142 mMaxNumAds = maxNumAds; 143 mGetFiltersFromJsonObjectStrategy = 144 ReadFiltersFromJsonStrategyFactory.getStrategy( 145 frequencyCapFilteringEnabled, appInstallFilteringEnabled); 146 mReadAdRenderIdFromJsonStrategy = 147 ReadAdRenderIdFromJsonStrategyFactory.getStrategy( 148 adRenderIdEnabled, adRenderIdMaxLength); 149 mComponentAdRenderIdValidator = 150 AdRenderIdValidator.createEnabledInstance(componentAdRenderIdMaxLength); 151 mMaxNumComponentAds = maxNumComponentAds; 152 } 153 154 /** 155 * Returns the user bidding signals extracted from the input object, if found. 156 * 157 * @throws JSONException if the key is found but the schema is incorrect 158 * @throws NullPointerException if the key found by the field is null 159 * @throws IllegalArgumentException if the extracted signals fail data validation 160 */ 161 @Nullable getUserBiddingSignalsFromJsonObject()162 public AdSelectionSignals getUserBiddingSignalsFromJsonObject() 163 throws JSONException, NullPointerException, IllegalArgumentException { 164 if (mResponseObject.has(USER_BIDDING_SIGNALS_KEY)) { 165 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 166 167 // Note that because the user bidding signals are stored in the response as a full JSON 168 // object already, the signals do not need to be validated further; the JSON must have 169 // been valid to be extracted successfully 170 JSONObject signalsJsonObj = 171 Objects.requireNonNull(mResponseObject.getJSONObject(USER_BIDDING_SIGNALS_KEY)); 172 String signalsString = signalsJsonObj.toString(); 173 174 if (signalsString.length() > mMaxUserBiddingSignalsSizeB) { 175 throw new IllegalArgumentException(); 176 } 177 178 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 179 return AdSelectionSignals.fromString(signalsString); 180 } else { 181 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, USER_BIDDING_SIGNALS_KEY); 182 return null; 183 } 184 } 185 186 /** 187 * Returns the trusted bidding data extracted from the input object, if found. 188 * 189 * @throws JSONException if the key is found but the schema is incorrect 190 * @throws NullPointerException if the key found by the field is null 191 * @throws IllegalArgumentException if the extracted data fails data validation 192 */ 193 @Nullable getTrustedBiddingDataFromJsonObject()194 public DBTrustedBiddingData getTrustedBiddingDataFromJsonObject() 195 throws JSONException, NullPointerException, IllegalArgumentException { 196 if (mResponseObject.has(TRUSTED_BIDDING_DATA_KEY)) { 197 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 198 199 JSONObject dataJsonObj = mResponseObject.getJSONObject(TRUSTED_BIDDING_DATA_KEY); 200 201 String uri = 202 JsonUtils.getStringFromJson( 203 dataJsonObj, 204 TRUSTED_BIDDING_URI_KEY, 205 String.format( 206 STRING_ERROR_FORMAT, 207 TRUSTED_BIDDING_URI_KEY, 208 TRUSTED_BIDDING_DATA_KEY)); 209 Uri parsedUri = Uri.parse(uri); 210 211 JSONArray keysJsonArray = dataJsonObj.getJSONArray(TRUSTED_BIDDING_KEYS_KEY); 212 int keysListLength = keysJsonArray.length(); 213 List<String> keysList = new ArrayList<>(keysListLength); 214 for (int i = 0; i < keysListLength; i++) { 215 try { 216 keysList.add( 217 JsonUtils.getStringFromJsonArrayAtIndex( 218 keysJsonArray, 219 i, 220 String.format( 221 STRING_ERROR_FORMAT, 222 TRUSTED_BIDDING_KEYS_KEY, 223 TRUSTED_BIDDING_DATA_KEY))); 224 } catch (JSONException | NullPointerException exception) { 225 // Skip any keys that are malformed and continue to the next in the list; note 226 // that if the entire given list of keys is junk, then any existing trusted 227 // bidding keys are cleared from the custom audience 228 sLogger.v( 229 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 230 mResponseHash, 231 TRUSTED_BIDDING_KEYS_KEY, 232 Optional.ofNullable(exception.getMessage()).orElse("<null>")); 233 } 234 } 235 236 AdTechUriValidator uriValidator = 237 new AdTechUriValidator( 238 ValidatorUtil.AD_TECH_ROLE_BUYER, 239 mBuyer.toString(), 240 this.getClass().getSimpleName(), 241 TrustedBiddingDataValidator.TRUSTED_BIDDING_URI_FIELD_NAME); 242 uriValidator.validate(parsedUri); 243 244 DBTrustedBiddingData trustedBiddingData = 245 new DBTrustedBiddingData.Builder().setUri(parsedUri).setKeys(keysList).build(); 246 247 if (trustedBiddingData.size() > mMaxTrustedBiddingDataSizeB) { 248 throw new IllegalArgumentException(); 249 } 250 251 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 252 return trustedBiddingData; 253 } else { 254 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, TRUSTED_BIDDING_DATA_KEY); 255 return null; 256 } 257 } 258 259 /** 260 * Returns the list of ads extracted from the input object, if found. 261 * 262 * @throws JSONException if the key is found but the schema is incorrect 263 * @throws NullPointerException if the key found by the field is null 264 * @throws IllegalArgumentException if the extracted ads fail data validation 265 */ 266 @Nullable getAdsFromJsonObject()267 public List<DBAdData> getAdsFromJsonObject() 268 throws JSONException, NullPointerException, IllegalArgumentException { 269 if (mResponseObject.has(ADS_KEY)) { 270 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, ADS_KEY); 271 272 JSONArray adsJsonArray = mResponseObject.getJSONArray(ADS_KEY); 273 int adsSize = 0; 274 int adsListLength = adsJsonArray.length(); 275 List<DBAdData> adsList = new ArrayList<>(); 276 for (int i = 0; i < adsListLength; i++) { 277 try { 278 JSONObject adDataJsonObj = adsJsonArray.getJSONObject(i); 279 280 // Note: getString() coerces values to be strings; use get() instead 281 Object uri = adDataJsonObj.get(RENDER_URI_KEY); 282 if (!(uri instanceof String)) { 283 throw new JSONException( 284 "Unexpected format parsing " + RENDER_URI_KEY + " in " + ADS_KEY); 285 } 286 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri)); 287 288 // By passing in an empty ad tech identifier string, ad tech identifier host 289 // matching is skipped 290 AdTechUriValidator uriValidator = 291 new AdTechUriValidator( 292 ValidatorUtil.AD_TECH_ROLE_BUYER, 293 "", 294 this.getClass().getSimpleName(), 295 RENDER_URI_KEY); 296 uriValidator.validate(parsedUri); 297 298 String metadata = 299 Objects.requireNonNull(adDataJsonObj.getJSONObject(METADATA_KEY)) 300 .toString(); 301 302 DBAdData.Builder adDataBuilder = 303 new DBAdData.Builder().setRenderUri(parsedUri).setMetadata(metadata); 304 305 mGetFiltersFromJsonObjectStrategy.readFilters(adDataBuilder, adDataJsonObj); 306 mReadAdRenderIdFromJsonStrategy.readId(adDataBuilder, adDataJsonObj); 307 DBAdData adData = adDataBuilder.build(); 308 adsList.add(adData); 309 adsSize += adData.size(); 310 } catch (JSONException 311 | NullPointerException 312 | IllegalArgumentException exception) { 313 // Skip any ads that are malformed and continue to the next in the list; 314 // note 315 // that if the entire given list of ads is junk, then any existing ads are 316 // cleared from the custom audience 317 sLogger.v( 318 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 319 mResponseHash, 320 ADS_KEY, 321 Optional.ofNullable(exception.getMessage()).orElse("<null>")); 322 } 323 } 324 325 if (adsSize > mMaxAdsSizeB) { 326 throw new IllegalArgumentException(); 327 } 328 329 if (adsList.size() > mMaxNumAds) { 330 throw new IllegalArgumentException(); 331 } 332 333 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, ADS_KEY); 334 return adsList; 335 } else { 336 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, ADS_KEY); 337 return null; 338 } 339 } 340 341 /** 342 * Returns the server auction request bitfield extracted from the response, if found. 343 * 344 * @throws JSONException if the value found at the key is not a {@link JSONArray} 345 */ 346 @CustomAudience.AuctionServerRequestFlag getAuctionServerRequestFlags()347 public int getAuctionServerRequestFlags() throws JSONException { 348 @CustomAudience.AuctionServerRequestFlag int result = 0; 349 if (mResponseObject.has(AUCTION_SERVER_REQUEST_FLAGS_KEY)) { 350 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, AUCTION_SERVER_REQUEST_FLAGS_KEY); 351 JSONArray array = mResponseObject.getJSONArray(AUCTION_SERVER_REQUEST_FLAGS_KEY); 352 for (int i = 0; i < array.length(); i++) { 353 if (OMIT_ADS_VALUE.equals(array.getString(i))) { 354 if ((result & FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS) == 0) { 355 // Only set the flag and print the log once 356 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, OMIT_ADS_VALUE); 357 result = result | FLAG_AUCTION_SERVER_REQUEST_OMIT_ADS; 358 } 359 } 360 } 361 } else { 362 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, AUCTION_SERVER_REQUEST_FLAGS_KEY); 363 } 364 return result; 365 } 366 367 /** 368 * Returns the priority value extracted from the response, if found. 369 * 370 * @throws JSONException if the value found at the key is not a {@link JSONObject} 371 */ getPriority()372 public double getPriority() throws JSONException { 373 double result = PRIORITY_DEFAULT; 374 if (mResponseObject.has(PRIORITY_KEY)) { 375 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, PRIORITY_KEY); 376 JSONObject priorityJsonObject = 377 Objects.requireNonNull(mResponseObject.getJSONObject(PRIORITY_KEY)); 378 double priorityDouble = priorityJsonObject.getDouble(PRIORITY_KEY); 379 380 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, PRIORITY_KEY); 381 result = priorityDouble; 382 } else { 383 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, PRIORITY_KEY); 384 } 385 return result; 386 } 387 388 /** 389 * Returns the list of component ads extracted from the input object, if found. 390 * 391 * @throws JSONException if the key is found but the schema is incorrect 392 * @throws NullPointerException if the key found by the field is null 393 * @throws IllegalArgumentException if the extracted component ads fail data validation 394 */ 395 @Nullable getComponentAdsFromJsonObject()396 public List<ComponentAdData> getComponentAdsFromJsonObject() 397 throws JSONException, NullPointerException, IllegalArgumentException { 398 if (mResponseObject.has(COMPONENT_ADS_KEY)) { 399 sLogger.v(FIELD_FOUND_LOG_FORMAT, mResponseHash, COMPONENT_ADS_KEY); 400 401 JSONArray componentAdsJsonArray = mResponseObject.getJSONArray(COMPONENT_ADS_KEY); 402 int componentAdsListLength = componentAdsJsonArray.length(); 403 404 // TODO(b/381392728):investigate whether we need an overall size on list of component 405 if (componentAdsListLength > mMaxNumComponentAds) { 406 sLogger.v(COMPONENT_ADS_SIZE_EXCEEDS_MAX); 407 throw new IllegalArgumentException(COMPONENT_ADS_SIZE_EXCEEDS_MAX); 408 } 409 410 List<ComponentAdData> componentAdsList = new ArrayList<>(); 411 for (int i = 0; i < componentAdsListLength; i++) { 412 try { 413 JSONObject componentAdDataJsonObj = componentAdsJsonArray.getJSONObject(i); 414 415 // Note: getString() coerces values to be strings; use get() instead 416 Object uri = componentAdDataJsonObj.get(RENDER_URI_KEY); 417 if (!(uri instanceof String)) { 418 throw new JSONException( 419 "Unexpected format parsing " 420 + RENDER_URI_KEY 421 + " in " 422 + COMPONENT_ADS_KEY); 423 } 424 Uri parsedUri = Uri.parse(Objects.requireNonNull((String) uri)); 425 426 AdTechUriValidator uriValidator = 427 new AdTechUriValidator( 428 ValidatorUtil.AD_TECH_ROLE_BUYER, 429 mBuyer.toString(), 430 this.getClass().getSimpleName(), 431 RENDER_URI_KEY); 432 uriValidator.validate(parsedUri); 433 434 Object adRenderId = componentAdDataJsonObj.get(AD_RENDER_ID_KEY); 435 if (!(adRenderId instanceof String adRenderIdString)) { 436 throw new JSONException( 437 "Unexpected format parsing " 438 + AD_RENDER_ID_KEY 439 + " in " 440 + COMPONENT_ADS_KEY); 441 } 442 mComponentAdRenderIdValidator.validate(adRenderIdString); 443 444 ComponentAdData componentAdData = 445 new ComponentAdData(parsedUri, adRenderIdString); 446 componentAdsList.add(componentAdData); 447 } catch (JSONException 448 | NullPointerException 449 | IllegalArgumentException exception) { 450 // Skip this component ad if it has issues 451 sLogger.v( 452 SKIP_INVALID_JSON_TYPE_LOG_FORMAT, 453 mResponseHash, 454 COMPONENT_ADS_KEY, 455 Optional.ofNullable(exception.getMessage()).orElse("<null>")); 456 } 457 } 458 sLogger.v(VALIDATED_FIELD_LOG_FORMAT, mResponseHash, COMPONENT_ADS_KEY); 459 return componentAdsList; 460 } else { 461 sLogger.v(FIELD_NOT_FOUND_LOG_FORMAT, mResponseHash, COMPONENT_ADS_KEY); 462 return null; 463 } 464 } 465 } 466