• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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