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