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