• 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 package com.android.adservices.service.measurement.registration;
17 
18 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS;
19 
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.net.Uri;
23 
24 import com.android.adservices.LoggerFactory;
25 import com.android.adservices.data.measurement.DatastoreManager;
26 import com.android.adservices.service.Flags;
27 import com.android.adservices.service.FlagsFactory;
28 import com.android.adservices.service.common.AllowLists;
29 import com.android.adservices.service.common.WebAddresses;
30 import com.android.adservices.service.measurement.FilterMap;
31 import com.android.adservices.service.measurement.Source;
32 import com.android.adservices.service.measurement.aggregation.AggregateDebugReportData.AggregateDebugReportDataHeaderContract;
33 import com.android.adservices.service.measurement.aggregation.AggregateDebugReporting.AggregateDebugReportingHeaderContract;
34 import com.android.adservices.service.measurement.reporting.DebugReportApi;
35 import com.android.adservices.service.measurement.util.UnsignedLong;
36 import com.android.adservices.service.stats.AdServicesLogger;
37 import com.android.adservices.service.stats.MeasurementRegistrationResponseStats;
38 
39 import org.json.JSONArray;
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 
43 import java.math.BigDecimal;
44 import java.math.BigInteger;
45 import java.nio.charset.StandardCharsets;
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Optional;
53 import java.util.Set;
54 import java.util.regex.Pattern;
55 
56 /**
57  * Common handling for Response Based Registration
58  *
59  * @hide
60  */
61 public class FetcherUtil {
62     static final Pattern HEX_PATTERN = Pattern.compile("\\p{XDigit}+");
63     static final String DEFAULT_HEX_STRING = "0x0";
64     public static final BigInteger BIG_INTEGER_LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);
65     public static final BigDecimal BIG_DECIMAL_INT_MAX_VALUE =
66             BigDecimal.valueOf(Integer.MAX_VALUE);
67     public static final BigDecimal BIG_DECIMAL_INT_MIN_VALUE =
68             BigDecimal.valueOf(Integer.MIN_VALUE);
69 
70     /**
71      * Determine all redirects.
72      *
73      * <p>Generates a map of: (redirectType, List&lt;Uri&gt;)
74      */
parseRedirects( @onNull Map<String, List<String>> headers)75     static Map<AsyncRegistration.RedirectType, List<Uri>> parseRedirects(
76             @NonNull Map<String, List<String>> headers) {
77         Map<AsyncRegistration.RedirectType, List<Uri>> uriMap = new HashMap<>();
78         uriMap.put(AsyncRegistration.RedirectType.LOCATION, parseLocationRedirects(headers));
79         uriMap.put(AsyncRegistration.RedirectType.LIST, parseListRedirects(headers));
80         return uriMap;
81     }
82 
83     /**
84      * Check HTTP response codes that indicate a redirect.
85      */
isRedirect(int responseCode)86     static boolean isRedirect(int responseCode) {
87         return (responseCode / 100) == 3;
88     }
89 
90     /**
91      * Check HTTP response code for success.
92      */
isSuccess(int responseCode)93     static boolean isSuccess(int responseCode) {
94         return (responseCode / 100) == 2;
95     }
96 
97     /** Validates both string type and unsigned long parsing */
extractUnsignedLong(JSONObject obj, String key)98     public static Optional<UnsignedLong> extractUnsignedLong(JSONObject obj, String key) {
99         try {
100             Object maybeValue = obj.get(key);
101             if (!(maybeValue instanceof String)) {
102                 return Optional.empty();
103             }
104             return Optional.of(new UnsignedLong((String) maybeValue));
105         } catch (JSONException | NumberFormatException e) {
106             LoggerFactory.getMeasurementLogger()
107                     .e(e, "extractUnsignedLong: caught exception. Key: %s", key);
108             return Optional.empty();
109         }
110     }
111 
112     /** Validates both string type and long parsing */
extractLongString(JSONObject obj, String key)113     public static Optional<Long> extractLongString(JSONObject obj, String key) {
114         try {
115             Object maybeValue = obj.get(key);
116             if (!(maybeValue instanceof String)) {
117                 return Optional.empty();
118             }
119             return Optional.of(Long.parseLong((String) maybeValue));
120         } catch (JSONException | NumberFormatException e) {
121             LoggerFactory.getMeasurementLogger()
122                     .e(e, "extractLongString: caught exception. Key: %s", key);
123             return Optional.empty();
124         }
125     }
126 
127     /** Validates an integral number */
is64BitInteger(Object obj)128     public static boolean is64BitInteger(Object obj) {
129         return (obj instanceof Integer) || (obj instanceof Long);
130     }
131 
132     /** Validates both number type and long parsing */
extractLong(JSONObject obj, String key)133     public static Optional<Long> extractLong(JSONObject obj, String key) {
134         try {
135             Object maybeValue = obj.get(key);
136             if (!is64BitInteger(maybeValue)) {
137                 return Optional.empty();
138             }
139             return Optional.of(Long.parseLong(String.valueOf(maybeValue)));
140         } catch (JSONException | NumberFormatException e) {
141             LoggerFactory.getMeasurementLogger()
142                     .e(e, "extractLong: caught exception. Key: %s", key);
143             return Optional.empty();
144         }
145     }
146 
isIntegral(BigDecimal value)147     private static boolean isIntegral(BigDecimal value) {
148         // Simplified check using scale only
149         return value.stripTrailingZeros().scale() <= 0;
150     }
151 
152     /** Extract value of a numeric integral from JSONObject. */
extractIntegralValue(JSONObject obj, String key)153     public static Optional<BigDecimal> extractIntegralValue(JSONObject obj, String key) {
154         try {
155             Object maybeObject = obj.get(key);
156             Optional<BigDecimal> maybeIntegralValue = extractIntegralValue(maybeObject);
157             if (maybeIntegralValue.isPresent()) {
158                 return maybeIntegralValue;
159             }
160         } catch (JSONException | NumberFormatException e) {
161             LoggerFactory.getMeasurementLogger()
162                     .e(e, "extractIntegralValue: caught exception. Key: %s", key);
163             return Optional.empty();
164         }
165         return Optional.empty();
166     }
167 
168     /** Extract value of a numeric integral Object. */
extractIntegralValue(Object maybeIntegralValue)169     public static Optional<BigDecimal> extractIntegralValue(Object maybeIntegralValue) {
170             if (!(maybeIntegralValue instanceof Number)) {
171                 LoggerFactory.getMeasurementLogger()
172                         .e(
173                                 "extractIntegralValue: non numeric object given: %s",
174                                 String.valueOf(maybeIntegralValue));
175                 return Optional.empty();
176             }
177 
178             BigDecimal bd = new BigDecimal(maybeIntegralValue.toString());
179             if (!isIntegral(bd)) {
180                 LoggerFactory.getMeasurementLogger()
181                         .e(
182                                 "extractIntegralValue: non integral value found: %s",
183                                 String.valueOf(maybeIntegralValue));
184                 return Optional.empty();
185             }
186 
187             return Optional.of(bd);
188     }
189 
190     /** Extract value of an int from a map. */
extractIntegralInt(JSONObject map, String id)191     public static Optional<Integer> extractIntegralInt(JSONObject map, String id) {
192         Optional<BigDecimal> maybeBigDecimal = FetcherUtil.extractIntegralValue(map, id);
193         if (maybeBigDecimal.isEmpty()) {
194             LoggerFactory.getMeasurementLogger()
195                     .d("extractIntegralInt: value for" + " bucket %s is not an integer.", id);
196             return Optional.empty();
197         }
198         BigDecimal integralValue = maybeBigDecimal.get();
199         if (integralValue.compareTo(BIG_DECIMAL_INT_MAX_VALUE) > 0
200                 || integralValue.compareTo(BIG_DECIMAL_INT_MIN_VALUE) < 0) {
201             LoggerFactory.getMeasurementLogger()
202                     .d("extractIntegralInt: value is larger than int. %s", integralValue);
203             return Optional.empty();
204         }
205 
206         return Optional.of(integralValue.intValue());
207     }
208 
isValidLookbackWindow(JSONObject obj)209     private static boolean isValidLookbackWindow(JSONObject obj) {
210         Optional<BigDecimal> bd = extractIntegralValue(obj, FilterMap.LOOKBACK_WINDOW);
211         if (bd.isEmpty()) {
212             return false;
213         }
214 
215         BigDecimal lookbackWindowValue = bd.get();
216         if (lookbackWindowValue.compareTo(BigDecimal.ZERO) <= 0) {
217             LoggerFactory.getMeasurementLogger()
218                     .e(
219                             "isValidLookbackWindow: non positive lookback window found: %s",
220                             lookbackWindowValue.toString());
221             return false;
222         }
223 
224         return true;
225     }
226 
227     /** Extract string from an obj with max length. */
extractString(Object obj, int maxLength)228     public static Optional<String> extractString(Object obj, int maxLength) {
229         if (!(obj instanceof String)) {
230             LoggerFactory.getMeasurementLogger().e("obj should be a string.");
231             return Optional.empty();
232         }
233         String stringValue = (String) obj;
234         if (stringValue.length() > maxLength) {
235             LoggerFactory.getMeasurementLogger()
236                     .e("Length of string value should be non-empty and smaller than " + maxLength);
237             return Optional.empty();
238         }
239         return Optional.of(stringValue);
240     }
241 
242     /** Extract list of strings from an obj with max array size and max string length. */
extractStringArray( JSONObject json, String key, int maxArraySize, int maxStringLength)243     public static Optional<List<String>> extractStringArray(
244             JSONObject json, String key, int maxArraySize, int maxStringLength)
245             throws JSONException {
246         JSONArray jsonArray = json.getJSONArray(key);
247         if (jsonArray.length() > maxArraySize) {
248             LoggerFactory.getMeasurementLogger()
249                     .e("Json array size should not be greater " + "than " + maxArraySize);
250             return Optional.empty();
251         }
252         List<String> strings = new ArrayList<>();
253         for (int i = 0; i < jsonArray.length(); ++i) {
254             Optional<String> string = FetcherUtil.extractString(jsonArray.get(i), maxStringLength);
255             if (string.isEmpty()) {
256                 return Optional.empty();
257             }
258             strings.add(string.get());
259         }
260         return Optional.of(strings);
261     }
262 
263     /**
264      * Validate aggregate key ID.
265      */
isValidAggregateKeyId(String id)266     static boolean isValidAggregateKeyId(String id) {
267         return id != null
268                 && !id.isEmpty()
269                 && id.getBytes(StandardCharsets.UTF_8).length
270                         <= FlagsFactory.getFlags()
271                                 .getMeasurementMaxBytesPerAttributionAggregateKeyId();
272     }
273 
274     /** Validate aggregate deduplication key. */
isValidAggregateDeduplicationKey(String deduplicationKey)275     static boolean isValidAggregateDeduplicationKey(String deduplicationKey) {
276         if (deduplicationKey == null || deduplicationKey.isEmpty()) {
277             return false;
278         }
279         try {
280             Long.parseUnsignedLong(deduplicationKey);
281         } catch (NumberFormatException exception) {
282             return false;
283         }
284         return true;
285     }
286 
287     /**
288      * Validate aggregate key-piece.
289      */
isValidAggregateKeyPiece(String keyPiece, Flags flags)290     static boolean isValidAggregateKeyPiece(String keyPiece, Flags flags) {
291         if (keyPiece == null || keyPiece.isEmpty()) {
292             return false;
293         }
294         int length = keyPiece.getBytes(StandardCharsets.UTF_8).length;
295         if (!(keyPiece.startsWith("0x") || keyPiece.startsWith("0X"))) {
296             return false;
297         }
298         // Key-piece is restricted to a maximum of 128 bits and the hex strings therefore have
299         // at most 32 digits.
300         if (length < 3 || length > 34) {
301             return false;
302         }
303         if (!HEX_PATTERN.matcher(keyPiece.substring(2)).matches()) {
304             return false;
305         }
306         return true;
307     }
308 
309     /** Validate attribution filters JSONArray. */
areValidAttributionFilters( @onNull JSONArray filterSet, Flags flags, boolean canIncludeLookbackWindow, boolean shouldCheckFilterSize)310     static boolean areValidAttributionFilters(
311             @NonNull JSONArray filterSet,
312             Flags flags,
313             boolean canIncludeLookbackWindow,
314             boolean shouldCheckFilterSize) throws JSONException {
315         if (shouldCheckFilterSize
316                 && filterSet.length()
317                         > FlagsFactory.getFlags().getMeasurementMaxFilterMapsPerFilterSet()) {
318             return false;
319         }
320         for (int i = 0; i < filterSet.length(); i++) {
321             if (!areValidAttributionFilters(
322                     filterSet.optJSONObject(i),
323                     flags,
324                     canIncludeLookbackWindow,
325                     shouldCheckFilterSize)) {
326                 return false;
327             }
328         }
329         return true;
330     }
331 
332     /**
333      * Parses header error debug report opt-in info from "Attribution-Reporting-Info" header. The
334      * header is a structured header and only supports dictionary format. Check HTTP [RFC8941]
335      * Section3.2 for details.
336      *
337      * <p>Examples of this type of header:
338      *
339      * <ul>
340      *   <li>"Attribution-Reporting-Info":“report-header-errors=?0"
341      *   <li>"Attribution-Reporting-Info": “report-header-errors,chrome-param=value"
342      *   <li>"Attribution-Reporting-Info": "report-header-errors=?1;chrome-param=value,
343      *       report-header-errors=?0"
344      * </ul>
345      *
346      * <p>The header may contain information that is only used in Chrome. Android will ignore it and
347      * be less strict in parsing in the current version. When "report-header-errors" value can't be
348      * extracted, Android will skip sending the debug report instead of dropping the whole
349      * registration.
350      */
isHeaderErrorDebugReportEnabled( @ullable List<String> attributionInfoHeaders, Flags flags)351     public static boolean isHeaderErrorDebugReportEnabled(
352             @Nullable List<String> attributionInfoHeaders, Flags flags) {
353         if (attributionInfoHeaders == null || attributionInfoHeaders.size() == 0) {
354             return false;
355         }
356         if (!flags.getMeasurementEnableDebugReport()
357                 || !flags.getMeasurementEnableHeaderErrorDebugReport()) {
358             LoggerFactory.getMeasurementLogger().d("Debug report is disabled for header errors.");
359             return false;
360         }
361 
362         // When there are multiple headers or the same key appears multiple times, find the last
363         // appearance and get the value.
364         for (int i = attributionInfoHeaders.size() - 1; i >= 0; i--) {
365             String[] parsed = attributionInfoHeaders.get(i).split("[,;]+");
366             for (int j = parsed.length - 1; j >= 0; j--) {
367                 String parsedStr = parsed[j].trim();
368                 if (parsedStr.equals("report-header-errors")
369                         || parsedStr.equals("report-header-errors=?1")) {
370                     return true;
371                 } else if (parsedStr.equals("report-header-errors=?0")) {
372                     return false;
373                 }
374             }
375         }
376         // Skip sending the debug report when the key is not found.
377         return false;
378     }
379 
380     /** Validate attribution filters JSONObject. */
areValidAttributionFilters( JSONObject filtersObj, Flags flags, boolean canIncludeLookbackWindow, boolean shouldCheckFilterSize)381     static boolean areValidAttributionFilters(
382             JSONObject filtersObj,
383             Flags flags,
384             boolean canIncludeLookbackWindow,
385             boolean shouldCheckFilterSize) throws JSONException {
386         if (filtersObj == null) {
387             return false;
388         }
389         if (shouldCheckFilterSize
390                 && filtersObj.length()
391                         > FlagsFactory.getFlags().getMeasurementMaxAttributionFilters()) {
392             return false;
393         }
394 
395         Iterator<String> keys = filtersObj.keys();
396         while (keys.hasNext()) {
397             String key = keys.next();
398             if (shouldCheckFilterSize
399                     && key.getBytes(StandardCharsets.UTF_8).length
400                             > FlagsFactory.getFlags()
401                                     .getMeasurementMaxBytesPerAttributionFilterString()) {
402                 return false;
403             }
404             // Process known reserved keys that start with underscore first, then invalidate on
405             // catch-all.
406             if (flags.getMeasurementEnableLookbackWindowFilter()
407                     && FilterMap.LOOKBACK_WINDOW.equals(key)) {
408                 if (!canIncludeLookbackWindow || !isValidLookbackWindow(filtersObj)) {
409                     return false;
410                 }
411                 continue;
412             }
413             // Invalidate catch-all reserved prefix.
414             if (key.startsWith(FilterMap.RESERVED_PREFIX)) {
415                 return false;
416             }
417             JSONArray values = filtersObj.optJSONArray(key);
418             if (values == null) {
419                 return false;
420             }
421             if (shouldCheckFilterSize
422                     && values.length()
423                             > FlagsFactory.getFlags()
424                                     .getMeasurementMaxValuesPerAttributionFilter()) {
425                 return false;
426             }
427             for (int i = 0; i < values.length(); i++) {
428                 Object value = values.get(i);
429                 if (!(value instanceof String)) {
430                     return false;
431                 }
432                 if (shouldCheckFilterSize
433                         && ((String) value).getBytes(StandardCharsets.UTF_8).length
434                                 > FlagsFactory.getFlags()
435                                         .getMeasurementMaxBytesPerAttributionFilterString()) {
436                     return false;
437                 }
438             }
439         }
440         return true;
441     }
442 
getValidAggregateDebugReportingWithBudget( JSONObject aggregateDebugReporting, Flags flags)443     static Optional<String> getValidAggregateDebugReportingWithBudget(
444             JSONObject aggregateDebugReporting, Flags flags) throws JSONException {
445         try {
446             if (aggregateDebugReporting.isNull(AggregateDebugReportingHeaderContract.BUDGET)) {
447                 LoggerFactory.getMeasurementLogger()
448                         .d("Aggregate debug reporting budget is not present.");
449                 return Optional.empty();
450             }
451             Optional<Integer> optionalBudget =
452                     extractIntegralInt(
453                             aggregateDebugReporting, AggregateDebugReportingHeaderContract.BUDGET);
454             if (optionalBudget.isEmpty()) {
455                 LoggerFactory.getMeasurementLogger()
456                         .d("Aggregate debug reporting budget is invalid.");
457                 return Optional.empty();
458             }
459             int budget = optionalBudget.get();
460             if (budget <= 0 || budget > flags.getMeasurementMaxSumOfAggregateValuesPerSource()) {
461                 LoggerFactory.getMeasurementLogger()
462                         .d("Aggregate debug reporting budget value is out of bounds.");
463                 return Optional.empty();
464             }
465             Optional<JSONObject> validAggregateDebugReporting =
466                     getValidAggregateDebugReportingWithoutBudget(
467                             aggregateDebugReporting, flags, budget);
468             if (validAggregateDebugReporting.isPresent()) {
469                 validAggregateDebugReporting
470                         .get()
471                         .put(AggregateDebugReportingHeaderContract.BUDGET, budget);
472             }
473             return validAggregateDebugReporting.map(JSONObject::toString);
474         } catch (JSONException | NumberFormatException e) {
475             LoggerFactory.getMeasurementLogger()
476                     .d("getValidAggregateDebugReportingWithBudget threw an exception.");
477             return Optional.empty();
478         }
479     }
480 
getValidAggregateDebugReportingWithoutBudget( JSONObject aggregateDebugReporting, Flags flags)481     static Optional<String> getValidAggregateDebugReportingWithoutBudget(
482             JSONObject aggregateDebugReporting, Flags flags) throws JSONException {
483         try {
484             return getValidAggregateDebugReportingWithoutBudget(
485                             aggregateDebugReporting,
486                             flags,
487                             flags.getMeasurementMaxSumOfAggregateValuesPerSource())
488                     .map(JSONObject::toString);
489         } catch (JSONException | NumberFormatException e) {
490             LoggerFactory.getMeasurementLogger()
491                     .d("getValidAggregateDebugReportingWithoutBudget threw an exception.");
492             return Optional.empty();
493         }
494     }
495 
getValidAggregateDebugReportingWithoutBudget( JSONObject aggregateDebugReporting, Flags flags, int maxAggregateDebugDataValue)496     private static Optional<JSONObject> getValidAggregateDebugReportingWithoutBudget(
497             JSONObject aggregateDebugReporting, Flags flags, int maxAggregateDebugDataValue)
498             throws JSONException {
499         JSONObject validAggregateDebugReporting = new JSONObject();
500         String keyPiece =
501                 aggregateDebugReporting.optString(AggregateDebugReportingHeaderContract.KEY_PIECE);
502         if (!FetcherUtil.isValidAggregateKeyPiece(keyPiece, flags)) {
503             LoggerFactory.getMeasurementLogger()
504                     .d("Aggregate debug reporting key-piece is invalid.");
505             return Optional.empty();
506         }
507         validAggregateDebugReporting.put(AggregateDebugReportingHeaderContract.KEY_PIECE, keyPiece);
508 
509         if (!aggregateDebugReporting.isNull(
510                 AggregateDebugReportingHeaderContract.AGGREGATION_COORDINATOR_ORIGIN)) {
511             String origin =
512                     aggregateDebugReporting.getString(
513                             AggregateDebugReportingHeaderContract.AGGREGATION_COORDINATOR_ORIGIN);
514             String allowlist = flags.getMeasurementAggregationCoordinatorOriginList();
515             if (origin.isEmpty() || !isAllowlisted(allowlist, origin)) {
516                 LoggerFactory.getMeasurementLogger()
517                         .d("Aggregate debug reporting aggregation coordinator origin is invalid.");
518                 return Optional.empty();
519             }
520             validAggregateDebugReporting.put(
521                     AggregateDebugReportingHeaderContract.AGGREGATION_COORDINATOR_ORIGIN,
522                     Uri.parse(origin));
523         }
524         if (!aggregateDebugReporting.isNull(AggregateDebugReportingHeaderContract.DEBUG_DATA)) {
525             Set<String> existingReportTypes = new HashSet<>();
526             Optional<JSONArray> maybeValidDebugDataArr =
527                     getValidAggregateDebugReportingData(
528                             aggregateDebugReporting.getJSONArray(
529                                     AggregateDebugReportingHeaderContract.DEBUG_DATA),
530                             existingReportTypes,
531                             flags,
532                             maxAggregateDebugDataValue);
533             if (maybeValidDebugDataArr.isEmpty()) {
534                 return Optional.empty();
535             }
536             validAggregateDebugReporting.put(
537                     AggregateDebugReportingHeaderContract.DEBUG_DATA, maybeValidDebugDataArr.get());
538         }
539         return Optional.of(validAggregateDebugReporting);
540     }
541 
getValidAggregateDebugReportingData( JSONArray debugDataArr, Set<String> existingReportTypes, Flags flags, int maxAggregateDebugDataValue)542     private static Optional<JSONArray> getValidAggregateDebugReportingData(
543             JSONArray debugDataArr,
544             Set<String> existingReportTypes,
545             Flags flags,
546             int maxAggregateDebugDataValue)
547             throws JSONException {
548         JSONArray validDebugDataArr = new JSONArray();
549         for (int i = 0; i < debugDataArr.length(); i++) {
550             JSONObject debugDataObj = debugDataArr.getJSONObject(i);
551             JSONObject validDebugDataObj = new JSONObject();
552             if (debugDataObj.isNull(AggregateDebugReportDataHeaderContract.KEY_PIECE)
553                     || debugDataObj.isNull(AggregateDebugReportDataHeaderContract.VALUE)
554                     || debugDataObj.isNull(AggregateDebugReportDataHeaderContract.TYPES)) {
555                 LoggerFactory.getMeasurementLogger()
556                         .d("Aggregate debug reporting data is missing required keys.");
557                 return Optional.empty();
558             }
559 
560             String debugDatakeyPiece =
561                     debugDataObj.optString(AggregateDebugReportDataHeaderContract.KEY_PIECE);
562             if (!FetcherUtil.isValidAggregateKeyPiece(debugDatakeyPiece, flags)) {
563                 LoggerFactory.getMeasurementLogger()
564                         .d("Aggregate debug reporting data key-piece is invalid.");
565                 return Optional.empty();
566             }
567             validDebugDataObj.put(
568                     AggregateDebugReportDataHeaderContract.KEY_PIECE, debugDatakeyPiece);
569 
570             Optional<BigDecimal> optionalValue =
571                     extractIntegralValue(
572                             debugDataObj, AggregateDebugReportDataHeaderContract.VALUE);
573             if (optionalValue.isEmpty()) {
574                 LoggerFactory.getMeasurementLogger().d("Aggregate debug data value is invalid.");
575                 return Optional.empty();
576             }
577             BigDecimal value = optionalValue.get();
578             if (value.compareTo(BigDecimal.ZERO) <= 0
579                     || value.compareTo(new BigDecimal(maxAggregateDebugDataValue)) > 0) {
580                 LoggerFactory.getMeasurementLogger()
581                         .d("Aggregate debug reporting data value is invalid.");
582                 return Optional.empty();
583             }
584             validDebugDataObj.put(AggregateDebugReportDataHeaderContract.VALUE, value.intValue());
585 
586             Optional<List<String>> maybeDebugDataTypes =
587                     FetcherUtil.extractStringArray(
588                             debugDataObj,
589                             AggregateDebugReportDataHeaderContract.TYPES,
590                             Integer.MAX_VALUE,
591                             Integer.MAX_VALUE);
592             if (maybeDebugDataTypes.isEmpty() || maybeDebugDataTypes.get().isEmpty()) {
593                 LoggerFactory.getMeasurementLogger().d("Aggregate debug data type is invalid.");
594                 return Optional.empty();
595             }
596             List<String> validDebugDataTypes = new ArrayList<>();
597             for (String debugDataType : maybeDebugDataTypes.get()) {
598                 if (existingReportTypes.contains(debugDataType)) {
599                     LoggerFactory.getMeasurementLogger()
600                             .d(
601                                     "duplicate aggregate debug reporting data types within the"
602                                             + " same object or across multiple objects are not"
603                                             + " allowed.");
604                     return Optional.empty();
605                 }
606                 // Exclude report type if not recognized
607                 Optional<DebugReportApi.Type> maybeType =
608                         DebugReportApi.Type.findByValue(debugDataType);
609                 if (maybeType.isPresent()) {
610                     validDebugDataTypes.add(maybeType.get().getValue());
611                 }
612                 existingReportTypes.add(debugDataType);
613             }
614             validDebugDataObj.put(
615                     AggregateDebugReportDataHeaderContract.TYPES,
616                     new JSONArray(validDebugDataTypes));
617 
618             validDebugDataArr.put(validDebugDataObj);
619         }
620         return Optional.of(validDebugDataArr);
621     }
622 
isAllowlisted(String allowlist, String origin)623     private static boolean isAllowlisted(String allowlist, String origin) {
624         if (AllowLists.doesAllowListAllowAll(allowlist)) {
625             return true;
626         }
627         Set<String> elements = new HashSet<>(AllowLists.splitAllowList(allowlist));
628         return elements.contains(origin);
629     }
630 
getSourceRegistrantToLog(AsyncRegistration asyncRegistration)631     static String getSourceRegistrantToLog(AsyncRegistration asyncRegistration) {
632         if (asyncRegistration.isSourceRequest()) {
633             return asyncRegistration.getRegistrant().toString();
634         }
635 
636         return "";
637     }
638 
emitHeaderMetrics( long headerSizeLimitBytes, AdServicesLogger logger, AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, @Nullable String enrollmentId)639     static void emitHeaderMetrics(
640             long headerSizeLimitBytes,
641             AdServicesLogger logger,
642             AsyncRegistration asyncRegistration,
643             AsyncFetchStatus asyncFetchStatus,
644             @Nullable String enrollmentId) {
645         long headerSize = asyncFetchStatus.getResponseSize();
646         String adTechDomain = null;
647 
648         if (headerSize > headerSizeLimitBytes) {
649             adTechDomain =
650                     WebAddresses.topPrivateDomainAndScheme(asyncRegistration.getRegistrationUri())
651                             .map(Uri::toString)
652                             .orElse(null);
653         }
654 
655         logger.logMeasurementRegistrationsResponseSize(
656                 new MeasurementRegistrationResponseStats.Builder(
657                                 AD_SERVICES_MEASUREMENT_REGISTRATIONS,
658                                 getRegistrationType(asyncRegistration),
659                                 headerSize,
660                                 getSourceType(asyncRegistration),
661                                 getSurfaceType(asyncRegistration),
662                                 getStatus(asyncFetchStatus),
663                                 getFailureType(asyncFetchStatus),
664                                 asyncFetchStatus.getRegistrationDelay(),
665                                 getSourceRegistrantToLog(asyncRegistration),
666                                 asyncFetchStatus.getRetryCount(),
667                                 asyncFetchStatus.isRedirectOnly(),
668                                 asyncFetchStatus.isPARequest(),
669                                 asyncFetchStatus.getNumDeletedEntities(),
670                                 asyncFetchStatus.isEventLevelEpsilonConfigured(),
671                                 asyncFetchStatus.isTriggerAggregatableValueFiltersConfigured(),
672                                 asyncFetchStatus.isTriggerFilteringIdConfigured(),
673                                 asyncFetchStatus.isTriggerContextIdConfigured())
674                         .setAdTechDomain(adTechDomain)
675                         .build(),
676                 enrollmentId);
677     }
678 
parseListRedirects(Map<String, List<String>> headers)679     private static List<Uri> parseListRedirects(Map<String, List<String>> headers) {
680         List<Uri> redirects = new ArrayList<>();
681         List<String> field = headers.get(AsyncRedirects.REDIRECT_LIST_HEADER_KEY);
682         int maxRedirects = FlagsFactory.getFlags().getMeasurementMaxRegistrationRedirects();
683         if (field != null) {
684             for (int i = 0; i < Math.min(field.size(), maxRedirects); i++) {
685                 redirects.add(Uri.parse(field.get(i)));
686             }
687         }
688         return redirects;
689     }
690 
parseLocationRedirects(Map<String, List<String>> headers)691     private static List<Uri> parseLocationRedirects(Map<String, List<String>> headers) {
692         List<Uri> redirects = new ArrayList<>();
693         List<String> field = headers.get(AsyncRedirects.REDIRECT_LOCATION_HEADER_KEY);
694         if (field != null && !field.isEmpty()) {
695             redirects.add(Uri.parse(field.get(0)));
696             if (field.size() > 1) {
697                 LoggerFactory.getMeasurementLogger()
698                         .e("Expected one Location redirect only, others ignored!");
699             }
700         }
701         return redirects;
702     }
703 
calculateHeadersCharactersLength(Map<String, List<String>> headers)704     public static long calculateHeadersCharactersLength(Map<String, List<String>> headers) {
705         long size = 0;
706         for (String headerKey : headers.keySet()) {
707             if (headerKey != null) {
708                 size = size + headerKey.length();
709                 List<String> headerValues = headers.get(headerKey);
710                 if (headerValues != null) {
711                     for (String headerValue : headerValues) {
712                         size = size + headerValue.length();
713                     }
714                 }
715             }
716         }
717 
718         return size;
719     }
720 
getRegistrationType(AsyncRegistration asyncRegistration)721     private static int getRegistrationType(AsyncRegistration asyncRegistration) {
722         if (asyncRegistration.isSourceRequest()) {
723             return RegistrationEnumsValues.TYPE_SOURCE;
724         } else if (asyncRegistration.isTriggerRequest()) {
725             return RegistrationEnumsValues.TYPE_TRIGGER;
726         } else {
727             return RegistrationEnumsValues.TYPE_UNKNOWN;
728         }
729     }
730 
getSourceType(AsyncRegistration asyncRegistration)731     private static int getSourceType(AsyncRegistration asyncRegistration) {
732         if (asyncRegistration.getSourceType() == Source.SourceType.EVENT) {
733             return RegistrationEnumsValues.SOURCE_TYPE_EVENT;
734         } else if (asyncRegistration.getSourceType() == Source.SourceType.NAVIGATION) {
735             return RegistrationEnumsValues.SOURCE_TYPE_NAVIGATION;
736         } else {
737             return RegistrationEnumsValues.SOURCE_TYPE_UNKNOWN;
738         }
739     }
740 
getSurfaceType(AsyncRegistration asyncRegistration)741     private static int getSurfaceType(AsyncRegistration asyncRegistration) {
742         if (asyncRegistration.isAppRequest()) {
743             return RegistrationEnumsValues.SURFACE_TYPE_APP;
744         } else if (asyncRegistration.isWebRequest()) {
745             return RegistrationEnumsValues.SURFACE_TYPE_WEB;
746         } else {
747             return RegistrationEnumsValues.SURFACE_TYPE_UNKNOWN;
748         }
749     }
750 
getStatus(AsyncFetchStatus asyncFetchStatus)751     private static int getStatus(AsyncFetchStatus asyncFetchStatus) {
752         if (asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.SUCCESS
753                 || (asyncFetchStatus.getResponseStatus() == AsyncFetchStatus.ResponseStatus.SUCCESS
754                         && (asyncFetchStatus.getEntityStatus()
755                                         == AsyncFetchStatus.EntityStatus.UNKNOWN
756                                 || asyncFetchStatus.getEntityStatus()
757                                         == AsyncFetchStatus.EntityStatus.HEADER_MISSING))) {
758             // successful source/trigger fetching/parsing and successful redirects (with no header)
759             return RegistrationEnumsValues.STATUS_SUCCESS;
760         } else if (asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.UNKNOWN
761                 && asyncFetchStatus.getResponseStatus()
762                         == AsyncFetchStatus.ResponseStatus.UNKNOWN) {
763             return RegistrationEnumsValues.STATUS_UNKNOWN;
764         } else {
765             return RegistrationEnumsValues.STATUS_FAILURE;
766         }
767     }
768 
getFailureType(AsyncFetchStatus asyncFetchStatus)769     private static int getFailureType(AsyncFetchStatus asyncFetchStatus) {
770         if (asyncFetchStatus.getResponseStatus() == AsyncFetchStatus.ResponseStatus.NETWORK_ERROR) {
771             return RegistrationEnumsValues.FAILURE_TYPE_NETWORK;
772         } else if (asyncFetchStatus.getResponseStatus()
773                 == AsyncFetchStatus.ResponseStatus.INVALID_URL) {
774             return RegistrationEnumsValues.FAILURE_TYPE_INVALID_URL;
775         } else if (asyncFetchStatus.getResponseStatus()
776                 == AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE) {
777             return RegistrationEnumsValues.FAILURE_TYPE_SERVER_UNAVAILABLE;
778         } else if (asyncFetchStatus.getResponseStatus()
779                 == AsyncFetchStatus.ResponseStatus.HEADER_SIZE_LIMIT_EXCEEDED) {
780             return RegistrationEnumsValues.FAILURE_TYPE_HEADER_SIZE_LIMIT_EXCEEDED;
781         } else if (asyncFetchStatus.getEntityStatus()
782                 == AsyncFetchStatus.EntityStatus.INVALID_ENROLLMENT) {
783             return RegistrationEnumsValues.FAILURE_TYPE_ENROLLMENT;
784         } else if (asyncFetchStatus.getEntityStatus()
785                         == AsyncFetchStatus.EntityStatus.VALIDATION_ERROR
786                 || asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.PARSING_ERROR
787                 || asyncFetchStatus.getEntityStatus()
788                         == AsyncFetchStatus.EntityStatus.HEADER_ERROR) {
789             return RegistrationEnumsValues.FAILURE_TYPE_PARSING;
790         } else if (asyncFetchStatus.getEntityStatus()
791                 == AsyncFetchStatus.EntityStatus.STORAGE_ERROR) {
792             return RegistrationEnumsValues.FAILURE_TYPE_STORAGE;
793         } else if (asyncFetchStatus.isRedirectError()) {
794             return RegistrationEnumsValues.FAILURE_TYPE_REDIRECT;
795         } else {
796             return RegistrationEnumsValues.FAILURE_TYPE_UNKNOWN;
797         }
798     }
799 
800     /** AdservicesMeasurementRegistrations atom enum values. */
801     public interface RegistrationEnumsValues {
802         int TYPE_UNKNOWN = 0;
803         int TYPE_SOURCE = 1;
804         int TYPE_TRIGGER = 2;
805         int SOURCE_TYPE_UNKNOWN = 0;
806         int SOURCE_TYPE_EVENT = 1;
807         int SOURCE_TYPE_NAVIGATION = 2;
808         int SURFACE_TYPE_UNKNOWN = 0;
809         int SURFACE_TYPE_WEB = 1;
810         int SURFACE_TYPE_APP = 2;
811         int STATUS_UNKNOWN = 0;
812         int STATUS_SUCCESS = 1;
813         int STATUS_FAILURE = 2;
814         int FAILURE_TYPE_UNKNOWN = 0;
815         int FAILURE_TYPE_PARSING = 1;
816         int FAILURE_TYPE_NETWORK = 2;
817         int FAILURE_TYPE_ENROLLMENT = 3;
818         int FAILURE_TYPE_REDIRECT = 4;
819         int FAILURE_TYPE_STORAGE = 5;
820         int FAILURE_TYPE_HEADER_SIZE_LIMIT_EXCEEDED = 7;
821         int FAILURE_TYPE_SERVER_UNAVAILABLE = 8;
822         int FAILURE_TYPE_INVALID_URL = 9;
823     }
824 
825     /** Schedules an header error verbose debug report. */
sendHeaderErrorDebugReport( boolean isEnabled, DebugReportApi debugReportApi, DatastoreManager datastoreManager, Uri topOrigin, Uri registrationOrigin, Uri registrant, String headerName, String enrollmentId, @Nullable String originalHeaderString)826     public static void sendHeaderErrorDebugReport(
827             boolean isEnabled,
828             DebugReportApi debugReportApi,
829             DatastoreManager datastoreManager,
830             Uri topOrigin,
831             Uri registrationOrigin,
832             Uri registrant,
833             String headerName,
834             String enrollmentId,
835             @Nullable String originalHeaderString) {
836         if (isEnabled) {
837             datastoreManager.runInTransaction(
838                     (dao) -> {
839                         debugReportApi.scheduleHeaderErrorReport(
840                                 topOrigin,
841                                 registrationOrigin,
842                                 registrant,
843                                 headerName,
844                                 enrollmentId,
845                                 originalHeaderString,
846                                 dao);
847                     });
848         }
849     }
850 }
851