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<Uri>) 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