• 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 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