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.measurement.util.BaseUriExtractor.getBaseUri; 19 import static com.android.adservices.service.measurement.util.MathUtils.extractValidNumberInRange; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_INVALID; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; 22 23 import android.annotation.NonNull; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.util.Pair; 27 28 import com.android.adservices.LoggerFactory; 29 import com.android.adservices.data.enrollment.EnrollmentDao; 30 import com.android.adservices.data.measurement.DatastoreManager; 31 import com.android.adservices.data.measurement.DatastoreManagerFactory; 32 import com.android.adservices.errorlogging.ErrorLogUtil; 33 import com.android.adservices.service.Flags; 34 import com.android.adservices.service.FlagsFactory; 35 import com.android.adservices.service.common.AllowLists; 36 import com.android.adservices.service.common.WebAddresses; 37 import com.android.adservices.service.measurement.EventSurfaceType; 38 import com.android.adservices.service.measurement.MeasurementHttpClient; 39 import com.android.adservices.service.measurement.Source; 40 import com.android.adservices.service.measurement.TriggerSpec; 41 import com.android.adservices.service.measurement.TriggerSpecs; 42 import com.android.adservices.service.measurement.reporting.DebugReportApi; 43 import com.android.adservices.service.measurement.util.Enrollment; 44 import com.android.adservices.service.measurement.util.UnsignedLong; 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import org.json.JSONArray; 48 import org.json.JSONException; 49 import org.json.JSONObject; 50 51 import java.io.IOException; 52 import java.io.OutputStream; 53 import java.io.OutputStreamWriter; 54 import java.net.HttpURLConnection; 55 import java.net.MalformedURLException; 56 import java.net.URL; 57 import java.net.URLConnection; 58 import java.nio.charset.StandardCharsets; 59 import java.util.ArrayList; 60 import java.util.Collections; 61 import java.util.HashSet; 62 import java.util.List; 63 import java.util.Locale; 64 import java.util.Map; 65 import java.util.Optional; 66 import java.util.Set; 67 import java.util.concurrent.TimeUnit; 68 import java.util.stream.Collectors; 69 70 /** 71 * Download and decode Response Based Registration 72 * 73 * @hide 74 */ 75 public class AsyncSourceFetcher { 76 77 private static final long ONE_DAY_IN_SECONDS = TimeUnit.DAYS.toSeconds(1); 78 private static final String DEFAULT_ANDROID_APP_SCHEME = "android-app"; 79 private static final String DEFAULT_ANDROID_APP_URI_PREFIX = DEFAULT_ANDROID_APP_SCHEME + "://"; 80 private final MeasurementHttpClient mNetworkConnection; 81 private final EnrollmentDao mEnrollmentDao; 82 private final Flags mFlags; 83 private final Context mContext; 84 private final DatastoreManager mDatastoreManager; 85 private final DebugReportApi mDebugReportApi; 86 AsyncSourceFetcher(Context context)87 public AsyncSourceFetcher(Context context) { 88 this( 89 context, 90 EnrollmentDao.getInstance(), 91 FlagsFactory.getFlags(), 92 DatastoreManagerFactory.getDatastoreManager(context), 93 new DebugReportApi(context, FlagsFactory.getFlags())); 94 } 95 96 @VisibleForTesting AsyncSourceFetcher( Context context, EnrollmentDao enrollmentDao, Flags flags, DatastoreManager datastoreManager, DebugReportApi debugReportApi)97 public AsyncSourceFetcher( 98 Context context, 99 EnrollmentDao enrollmentDao, 100 Flags flags, 101 DatastoreManager datastoreManager, 102 DebugReportApi debugReportApi) { 103 mContext = context; 104 mEnrollmentDao = enrollmentDao; 105 mFlags = flags; 106 mNetworkConnection = new MeasurementHttpClient(context); 107 mDatastoreManager = datastoreManager; 108 mDebugReportApi = debugReportApi; 109 } 110 parseCommonSourceParams( JSONObject json, AsyncRegistration asyncRegistration, Source.Builder builder, String enrollmentId)111 private boolean parseCommonSourceParams( 112 JSONObject json, 113 AsyncRegistration asyncRegistration, 114 Source.Builder builder, 115 String enrollmentId) 116 throws JSONException { 117 if (json.isNull(SourceHeaderContract.DESTINATION) 118 && json.isNull(SourceHeaderContract.WEB_DESTINATION)) { 119 throw new JSONException("Expected a destination"); 120 } 121 long sourceEventTime = asyncRegistration.getRequestTime(); 122 UnsignedLong eventId = new UnsignedLong(0L); 123 if (!json.isNull(SourceHeaderContract.SOURCE_EVENT_ID)) { 124 Optional<UnsignedLong> maybeEventId = 125 FetcherUtil.extractUnsignedLong(json, SourceHeaderContract.SOURCE_EVENT_ID); 126 if (!maybeEventId.isPresent()) { 127 return false; 128 } 129 eventId = maybeEventId.get(); 130 } 131 builder.setEventId(eventId); 132 long expiry; 133 if (!json.isNull(SourceHeaderContract.EXPIRY)) { 134 UnsignedLong expiryUnsigned = 135 extractValidNumberInRange( 136 new UnsignedLong(json.getString(SourceHeaderContract.EXPIRY)), 137 new UnsignedLong(mFlags 138 .getMeasurementMinReportingRegisterSourceExpirationInSeconds()), 139 new UnsignedLong(mFlags 140 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds()) 141 ); 142 // Relies on expiryUnsigned not using the 64th bit. 143 expiry = expiryUnsigned.getValue(); 144 if (asyncRegistration.getSourceType() == Source.SourceType.EVENT) { 145 expiry = roundSecondsToWholeDays(expiry); 146 } 147 } else { 148 expiry = mFlags.getMeasurementMaxReportingRegisterSourceExpirationInSeconds(); 149 } 150 builder.setExpiryTime(sourceEventTime + TimeUnit.SECONDS.toMillis(expiry)); 151 long effectiveExpiry = expiry; 152 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOW)) { 153 long eventReportWindow; 154 UnsignedLong eventReportWindowUnsigned = 155 extractValidNumberInRange( 156 new UnsignedLong( 157 json.getString(SourceHeaderContract.EVENT_REPORT_WINDOW)), 158 new UnsignedLong(mFlags 159 .getMeasurementMinimumEventReportWindowInSeconds()), 160 new UnsignedLong(mFlags 161 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds()) 162 ); 163 // Relies on eventReportWindowUnsigned not using the 64th bit. 164 eventReportWindow = Math.min(expiry, eventReportWindowUnsigned.getValue()); 165 effectiveExpiry = eventReportWindow; 166 builder.setEventReportWindow(TimeUnit.SECONDS.toMillis(eventReportWindow)); 167 } 168 long aggregateReportWindow; 169 if (!json.isNull(SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW)) { 170 // Registration will be rejected if parsing unsigned long throws. 171 UnsignedLong aggregateReportWindowUnsigned = 172 extractValidNumberInRange( 173 new UnsignedLong( 174 json.getString( 175 SourceHeaderContract.AGGREGATABLE_REPORT_WINDOW)), 176 new UnsignedLong(mFlags 177 .getMeasurementMinimumAggregatableReportWindowInSeconds()), 178 new UnsignedLong(mFlags 179 .getMeasurementMaxReportingRegisterSourceExpirationInSeconds()) 180 ); 181 // Relies on aggregateReportWindowUnsigned not using the 64th bit. 182 aggregateReportWindow = Math.min(expiry, aggregateReportWindowUnsigned.getValue()); 183 } else { 184 aggregateReportWindow = expiry; 185 } 186 builder.setAggregatableReportWindow( 187 sourceEventTime + TimeUnit.SECONDS.toMillis(aggregateReportWindow)); 188 189 if (!json.isNull(SourceHeaderContract.PRIORITY)) { 190 Optional<Long> maybePriority = 191 FetcherUtil.extractLongString(json, SourceHeaderContract.PRIORITY); 192 if (!maybePriority.isPresent()) { 193 return false; 194 } 195 builder.setPriority(maybePriority.get()); 196 } 197 198 if (!json.isNull(SourceHeaderContract.DEBUG_REPORTING)) { 199 builder.setIsDebugReporting(json.optBoolean(SourceHeaderContract.DEBUG_REPORTING)); 200 } 201 if (!json.isNull(SourceHeaderContract.DEBUG_KEY)) { 202 Optional<UnsignedLong> maybeDebugKey = 203 FetcherUtil.extractUnsignedLong(json, SourceHeaderContract.DEBUG_KEY); 204 if (maybeDebugKey.isPresent()) { 205 builder.setDebugKey(maybeDebugKey.get()); 206 } 207 } 208 if (!json.isNull(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY)) { 209 long installAttributionWindow = 210 extractValidNumberInRange( 211 json.getLong(SourceHeaderContract.INSTALL_ATTRIBUTION_WINDOW_KEY), 212 mFlags.getMeasurementMinInstallAttributionWindow(), 213 mFlags.getMeasurementMaxInstallAttributionWindow()); 214 builder.setInstallAttributionWindow( 215 TimeUnit.SECONDS.toMillis(installAttributionWindow)); 216 } else { 217 builder.setInstallAttributionWindow( 218 TimeUnit.SECONDS.toMillis(mFlags.getMeasurementMaxInstallAttributionWindow())); 219 } 220 if (!json.isNull(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY)) { 221 long installCooldownWindow = 222 extractValidNumberInRange( 223 json.getLong(SourceHeaderContract.POST_INSTALL_EXCLUSIVITY_WINDOW_KEY), 224 mFlags.getMeasurementMinPostInstallExclusivityWindow(), 225 mFlags.getMeasurementMaxPostInstallExclusivityWindow()); 226 builder.setInstallCooldownWindow(TimeUnit.SECONDS.toMillis(installCooldownWindow)); 227 } else { 228 builder.setInstallCooldownWindow( 229 TimeUnit.SECONDS.toMillis( 230 mFlags.getMeasurementMinPostInstallExclusivityWindow())); 231 } 232 if (mFlags.getMeasurementEnableReinstallReattribution()) { 233 if (!json.isNull(SourceHeaderContract.REINSTALL_REATTRIBUTION_WINDOW_KEY)) { 234 long reinstallReattributionWindow = 235 extractValidNumberInRange( 236 json.getLong( 237 SourceHeaderContract.REINSTALL_REATTRIBUTION_WINDOW_KEY), 238 0L, 239 mFlags.getMeasurementMaxReinstallReattributionWindowSeconds()); 240 builder.setReinstallReattributionWindow(reinstallReattributionWindow); 241 } else { 242 builder.setReinstallReattributionWindow(0L); 243 } 244 } 245 // This "filter_data" field is used to generate reports. 246 if (!json.isNull(SourceHeaderContract.FILTER_DATA)) { 247 JSONObject maybeFilterData = json.optJSONObject(SourceHeaderContract.FILTER_DATA); 248 if (maybeFilterData != null && maybeFilterData.has("source_type")) { 249 LoggerFactory.getMeasurementLogger() 250 .d("Source filter-data includes 'source_type' key."); 251 return false; 252 } 253 if (!FetcherUtil.areValidAttributionFilters( 254 maybeFilterData, 255 mFlags, 256 /* canIncludeLookbackWindow= */ false, 257 /* shouldCheckFilterSize= */ true)) { 258 LoggerFactory.getMeasurementLogger().d("Source filter-data is invalid."); 259 return false; 260 } 261 builder.setFilterDataString(maybeFilterData.toString()); 262 } 263 264 Uri appUri = null; 265 if (!json.isNull(SourceHeaderContract.DESTINATION)) { 266 appUri = Uri.parse(json.getString(SourceHeaderContract.DESTINATION)); 267 if (appUri.getScheme() == null) { 268 LoggerFactory.getMeasurementLogger() 269 .d("App destination is missing app scheme, adding."); 270 appUri = Uri.parse(DEFAULT_ANDROID_APP_URI_PREFIX + appUri); 271 } 272 if (!DEFAULT_ANDROID_APP_SCHEME.equals(appUri.getScheme())) { 273 LoggerFactory.getMeasurementLogger() 274 .e( 275 "Invalid scheme for app destination: %s; dropping the source.", 276 appUri.getScheme()); 277 return false; 278 } 279 } 280 281 String enrollmentBlockList = 282 mFlags.getMeasurementPlatformDebugAdIdMatchingEnrollmentBlocklist(); 283 Set<String> blockedEnrollmentsString = 284 new HashSet<>(AllowLists.splitAllowList(enrollmentBlockList)); 285 if (!AllowLists.doesAllowListAllowAll(enrollmentBlockList) 286 && !blockedEnrollmentsString.contains(enrollmentId) 287 && !json.isNull(SourceHeaderContract.DEBUG_AD_ID)) { 288 builder.setDebugAdId(json.optString(SourceHeaderContract.DEBUG_AD_ID)); 289 } 290 291 Set<String> allowedEnrollmentsString = 292 new HashSet<>( 293 AllowLists.splitAllowList( 294 mFlags.getMeasurementDebugJoinKeyEnrollmentAllowlist())); 295 if (allowedEnrollmentsString.contains(enrollmentId) 296 && !json.isNull(SourceHeaderContract.DEBUG_JOIN_KEY)) { 297 builder.setDebugJoinKey(json.optString(SourceHeaderContract.DEBUG_JOIN_KEY)); 298 } 299 300 if (asyncRegistration.isWebRequest() 301 // Only validate when non-null in request 302 && asyncRegistration.getOsDestination() != null 303 && !asyncRegistration.getOsDestination().equals(appUri)) { 304 LoggerFactory.getMeasurementLogger() 305 .d("Expected destination to match with the supplied one!"); 306 return false; 307 } 308 309 if (appUri != null) { 310 builder.setAppDestinations(Collections.singletonList(getBaseUri(appUri))); 311 } 312 313 boolean shouldMatchAtLeastOneWebDestination = 314 asyncRegistration.isWebRequest() && asyncRegistration.getWebDestination() != null; 315 boolean matchedOneWebDestination = false; 316 317 if (!json.isNull(SourceHeaderContract.WEB_DESTINATION)) { 318 Set<Uri> destinationSet = new HashSet<>(); 319 JSONArray jsonDestinations; 320 Object obj = json.get(SourceHeaderContract.WEB_DESTINATION); 321 if (obj instanceof String) { 322 jsonDestinations = new JSONArray(); 323 jsonDestinations.put(json.getString(SourceHeaderContract.WEB_DESTINATION)); 324 } else { 325 jsonDestinations = json.getJSONArray(SourceHeaderContract.WEB_DESTINATION); 326 } 327 if (jsonDestinations.length() 328 > mFlags.getMeasurementMaxDistinctWebDestinationsInSourceRegistration()) { 329 LoggerFactory.getMeasurementLogger() 330 .d("Source registration exceeded the number of allowed destinations."); 331 return false; 332 } 333 if (jsonDestinations.length() == 0 && appUri == null) { 334 throw new JSONException("Expected a destination"); 335 } 336 for (int i = 0; i < jsonDestinations.length(); i++) { 337 Uri destination = Uri.parse(jsonDestinations.getString(i)); 338 if (shouldMatchAtLeastOneWebDestination 339 && asyncRegistration.getWebDestination().equals(destination)) { 340 matchedOneWebDestination = true; 341 } 342 Optional<Uri> topPrivateDomainAndScheme = 343 WebAddresses.topPrivateDomainAndScheme(destination); 344 if (topPrivateDomainAndScheme.isEmpty()) { 345 LoggerFactory.getMeasurementLogger() 346 .d( 347 "Unable to extract top private domain and scheme from web " 348 + "destination."); 349 return false; 350 } else { 351 destinationSet.add(topPrivateDomainAndScheme.get()); 352 } 353 } 354 List<Uri> destinationList = new ArrayList<>(destinationSet); 355 if (!destinationList.isEmpty()) { 356 builder.setWebDestinations(destinationList); 357 } 358 } 359 360 if (mFlags.getMeasurementEnableCoarseEventReportDestinations() 361 && !json.isNull(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)) { 362 builder.setCoarseEventReportDestinations( 363 json.getBoolean(SourceHeaderContract.COARSE_EVENT_REPORT_DESTINATIONS)); 364 } 365 366 if (shouldMatchAtLeastOneWebDestination && !matchedOneWebDestination) { 367 LoggerFactory.getMeasurementLogger() 368 .d("Expected at least one web_destination to match with the supplied one!"); 369 return false; 370 } 371 372 Source.TriggerDataMatching triggerDataMatching = Source.TriggerDataMatching.MODULUS; 373 374 if (mFlags.getMeasurementEnableTriggerDataMatching() 375 && !json.isNull(SourceHeaderContract.TRIGGER_DATA_MATCHING)) { 376 // If the token for trigger_data_matching is not in the predefined list, it will throw 377 // IllegalArgumentException that will be caught by the overall parser. 378 triggerDataMatching = 379 Source.TriggerDataMatching.valueOf( 380 json 381 .getString(SourceHeaderContract.TRIGGER_DATA_MATCHING) 382 .toUpperCase(Locale.ENGLISH)); 383 builder.setTriggerDataMatching(triggerDataMatching); 384 } 385 386 JSONObject eventReportWindows = null; 387 Integer maxEventLevelReports = null; 388 if (!json.isNull(SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS)) { 389 Object maxEventLevelReportsObj = json.get( 390 SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS); 391 maxEventLevelReports = 392 json.getInt(SourceHeaderContract.MAX_EVENT_LEVEL_REPORTS); 393 if (!FetcherUtil.is64BitInteger(maxEventLevelReportsObj) || maxEventLevelReports < 0 394 || maxEventLevelReports > mFlags.getMeasurementFlexApiMaxEventReports()) { 395 return false; 396 } 397 builder.setMaxEventLevelReports(maxEventLevelReports); 398 } 399 400 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOWS)) { 401 if (!json.isNull(SourceHeaderContract.EVENT_REPORT_WINDOW)) { 402 LoggerFactory.getMeasurementLogger() 403 .d( 404 "Only one of event_report_window and event_report_windows is" 405 + " expected"); 406 return false; 407 } 408 Optional<JSONObject> maybeEventReportWindows = 409 getValidEventReportWindows( 410 new JSONObject( 411 json.getString(SourceHeaderContract.EVENT_REPORT_WINDOWS)), 412 expiry); 413 if (!maybeEventReportWindows.isPresent()) { 414 LoggerFactory.getMeasurementLogger() 415 .d("Invalid value for event_report_windows"); 416 return false; 417 } 418 eventReportWindows = maybeEventReportWindows.get(); 419 builder.setEventReportWindows(eventReportWindows.toString()); 420 } 421 422 if (mFlags.getMeasurementFlexibleEventReportingApiEnabled() 423 && (!json.isNull(SourceHeaderContract.TRIGGER_SPECS) 424 || !json.isNull(SourceHeaderContract.TRIGGER_DATA))) { 425 String triggerSpecString; 426 if (!json.isNull(SourceHeaderContract.TRIGGER_DATA)) { 427 if (!json.isNull(SourceHeaderContract.TRIGGER_SPECS)) { 428 LoggerFactory.getMeasurementLogger().d( 429 "Only one of trigger_data or trigger_specs is expected"); 430 return false; 431 } 432 JSONArray triggerData = json.getJSONArray(SourceHeaderContract.TRIGGER_DATA); 433 // Empty top-level trigger data results in an empty trigger specs list. 434 if (triggerData.length() == 0) { 435 triggerSpecString = triggerData.toString(); 436 // Populated top-level trigger data results in one trigger spec object. 437 } else { 438 JSONArray triggerSpecsArray = new JSONArray(); 439 JSONObject triggerSpec = new JSONObject(); 440 triggerSpec.put(SourceHeaderContract.TRIGGER_DATA, triggerData); 441 triggerSpecsArray.put(triggerSpec); 442 triggerSpecString = triggerSpecsArray.toString(); 443 } 444 } else { 445 triggerSpecString = json.getString(SourceHeaderContract.TRIGGER_SPECS); 446 } 447 448 final int finalMaxEventLevelReports = 449 Source.getOrDefaultMaxEventLevelReports( 450 asyncRegistration.getSourceType(), 451 maxEventLevelReports, 452 mFlags); 453 454 Optional<TriggerSpec[]> maybeTriggerSpecArray = 455 getValidTriggerSpecs( 456 triggerSpecString, 457 eventReportWindows, 458 effectiveExpiry, 459 asyncRegistration.getSourceType(), 460 finalMaxEventLevelReports, 461 triggerDataMatching); 462 463 if (!maybeTriggerSpecArray.isPresent()) { 464 LoggerFactory.getMeasurementLogger().d("Invalid Trigger Spec format"); 465 return false; 466 } 467 468 builder.setTriggerSpecs( 469 new TriggerSpecs( 470 maybeTriggerSpecArray.get(), 471 finalMaxEventLevelReports, 472 null)); 473 } 474 475 if (mFlags.getMeasurementEnableSharedSourceDebugKey() 476 && !json.isNull(SourceHeaderContract.SHARED_DEBUG_KEY)) { 477 try { 478 builder.setSharedDebugKey( 479 new UnsignedLong(json.getString(SourceHeaderContract.SHARED_DEBUG_KEY))); 480 } catch (NumberFormatException e) { 481 LoggerFactory.getMeasurementLogger() 482 .e(e, "parseCommonSourceParams: parsing shared debug key failed"); 483 } 484 } 485 486 if (mFlags.getMeasurementEnableAttributionScope() 487 && !populateAttributionScopeFields(json, builder)) { 488 return false; 489 } 490 return true; 491 } 492 493 // Populates attribution scope fields if they are available. 494 // Returns false if the json fields are invalid. 495 // Note returning true doesn't indicate whether the fields are populated or not. populateAttributionScopeFields(JSONObject json, Source.Builder builder)496 private boolean populateAttributionScopeFields(JSONObject json, Source.Builder builder) 497 throws JSONException { 498 // Parses attribution scopes. 499 List<String> attributionScopes = new ArrayList<>(); 500 if (!json.isNull(SourceHeaderContract.ATTRIBUTION_SCOPES)) { 501 Optional<List<String>> maybeAttributionScopes = 502 FetcherUtil.extractStringArray( 503 json, 504 SourceHeaderContract.ATTRIBUTION_SCOPES, 505 mFlags.getMeasurementMaxAttributionScopesPerSource(), 506 mFlags.getMeasurementMaxAttributionScopeLength()); 507 if (maybeAttributionScopes.isEmpty()) { 508 return false; 509 } 510 attributionScopes = maybeAttributionScopes.get(); 511 builder.setAttributionScopes(attributionScopes); 512 } 513 514 if (json.isNull(SourceHeaderContract.ATTRIBUTION_SCOPE_LIMIT)) { 515 if (!attributionScopes.isEmpty()) { 516 LoggerFactory.getMeasurementLogger() 517 .e( 518 "Attribution scope limit should be set if attribution scopes are " 519 + "not empty."); 520 return false; 521 } 522 if (!json.isNull(SourceHeaderContract.MAX_EVENT_STATES)) { 523 LoggerFactory.getMeasurementLogger() 524 .e( 525 "Attribution scope limit should be set if max event states is " 526 + "set."); 527 return false; 528 } 529 return true; 530 } 531 // Parses attribution scope limit, can be optional. 532 long attributionScopeLimit = 533 Long.parseLong(json.optString(SourceHeaderContract.ATTRIBUTION_SCOPE_LIMIT)); 534 if (attributionScopeLimit <= 0 || attributionScopes.size() > attributionScopeLimit) { 535 LoggerFactory.getMeasurementLogger() 536 .e( 537 "Attribution scope limit should be positive and not be smaller " 538 + "than the number of attribution scopes."); 539 return false; 540 } 541 builder.setAttributionScopeLimit(attributionScopeLimit); 542 543 // Parsing max event states, can be optional. 544 if (!json.isNull(SourceHeaderContract.MAX_EVENT_STATES)) { 545 long maxEventStates = 546 Long.parseLong(json.optString(SourceHeaderContract.MAX_EVENT_STATES)); 547 if (maxEventStates <= 0 548 || maxEventStates 549 > mFlags.getMeasurementMaxReportStatesPerSourceRegistration()) { 550 LoggerFactory.getMeasurementLogger() 551 .e( 552 "Max event states should be a positive integer and smaller than max" 553 + " report states per source registration."); 554 return false; 555 } 556 builder.setMaxEventStates(maxEventStates); 557 } 558 return true; 559 } 560 getValidTriggerSpecs( String triggerSpecString, JSONObject eventReportWindows, long expiry, Source.SourceType sourceType, int maxEventLevelReports, Source.TriggerDataMatching triggerDataMatching)561 private Optional<TriggerSpec[]> getValidTriggerSpecs( 562 String triggerSpecString, 563 JSONObject eventReportWindows, 564 long expiry, 565 Source.SourceType sourceType, 566 int maxEventLevelReports, 567 Source.TriggerDataMatching triggerDataMatching) { 568 List<Pair<Long, Long>> parsedEventReportWindows = 569 Source.getOrDefaultEventReportWindowsForFlex( 570 eventReportWindows, sourceType, TimeUnit.SECONDS.toMillis(expiry), mFlags); 571 long defaultStart = parsedEventReportWindows.get(0).first; 572 List<Long> defaultEnds = 573 parsedEventReportWindows.stream().map((x) -> x.second).collect(Collectors.toList()); 574 try { 575 JSONArray triggerSpecArray = new JSONArray(triggerSpecString); 576 TriggerSpec[] validTriggerSpecs = new TriggerSpec[triggerSpecArray.length()]; 577 Set<UnsignedLong> triggerDataSet = new HashSet<>(); 578 for (int i = 0; i < triggerSpecArray.length(); i++) { 579 Optional<TriggerSpec> maybeTriggerSpec = getValidTriggerSpec( 580 triggerSpecArray.getJSONObject(i), 581 expiry, 582 defaultStart, 583 defaultEnds, 584 triggerDataSet, 585 maxEventLevelReports); 586 if (!maybeTriggerSpec.isPresent()) { 587 return Optional.empty(); 588 } 589 validTriggerSpecs[i] = maybeTriggerSpec.get(); 590 } 591 // Check cardinality of trigger_data across the whole trigger spec array 592 if (triggerDataSet.size() > mFlags.getMeasurementFlexApiMaxTriggerDataCardinality()) { 593 return Optional.empty(); 594 } 595 if (mFlags.getMeasurementEnableTriggerDataMatching() 596 && triggerDataMatching == Source.TriggerDataMatching.MODULUS 597 && !isContiguousStartingAtZero(triggerDataSet)) { 598 return Optional.empty(); 599 } 600 return Optional.of(validTriggerSpecs); 601 } catch (JSONException | IllegalArgumentException ex) { 602 LoggerFactory.getMeasurementLogger().d(ex, "Trigger Spec parsing failed"); 603 return Optional.empty(); 604 } 605 } 606 getValidTriggerSpec( JSONObject triggerSpecJson, long expiry, long defaultStart, List<Long> defaultEnds, Set<UnsignedLong> triggerDataSet, int maxEventLevelReports)607 private Optional<TriggerSpec> getValidTriggerSpec( 608 JSONObject triggerSpecJson, 609 long expiry, 610 long defaultStart, 611 List<Long> defaultEnds, 612 Set<UnsignedLong> triggerDataSet, 613 int maxEventLevelReports) throws JSONException { 614 Optional<JSONArray> maybeTriggerDataListJson = extractLongJsonArray( 615 triggerSpecJson, TriggerSpecs.FlexEventReportJsonKeys.TRIGGER_DATA); 616 if (maybeTriggerDataListJson.isEmpty()) { 617 return Optional.empty(); 618 } 619 List<UnsignedLong> triggerDataList = 620 TriggerSpec.getTriggerDataArrayFromJSON(maybeTriggerDataListJson.get()); 621 if (triggerDataList.isEmpty() 622 || triggerDataList.size() 623 > mFlags.getMeasurementFlexApiMaxTriggerDataCardinality()) { 624 return Optional.empty(); 625 } 626 // Check exclusivity of trigger_data across the whole trigger spec array, and validate 627 // trigger data magnitude. 628 for (UnsignedLong triggerData : triggerDataList) { 629 if (!triggerDataSet.add(triggerData) 630 || triggerData.compareTo(TriggerSpecs.MAX_TRIGGER_DATA_VALUE) > 0) { 631 return Optional.empty(); 632 } 633 } 634 635 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS)) { 636 Optional<JSONObject> maybeEventReportWindows = 637 getValidEventReportWindows( 638 triggerSpecJson.getJSONObject( 639 TriggerSpecs.FlexEventReportJsonKeys.EVENT_REPORT_WINDOWS), 640 expiry); 641 if (!maybeEventReportWindows.isPresent()) { 642 return Optional.empty(); 643 } 644 } 645 646 TriggerSpec.SummaryOperatorType summaryWindowOperator = 647 TriggerSpec.SummaryOperatorType.COUNT; 648 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_WINDOW_OPERATOR)) { 649 // If a summary window operator is not in the predefined list, it will throw 650 // IllegalArgumentException that will be caught by the overall parser. 651 summaryWindowOperator = 652 TriggerSpec.SummaryOperatorType.valueOf( 653 triggerSpecJson 654 .getString( 655 TriggerSpecs.FlexEventReportJsonKeys 656 .SUMMARY_WINDOW_OPERATOR) 657 .toUpperCase(Locale.ENGLISH)); 658 } 659 List<Long> summaryBuckets = null; 660 if (!triggerSpecJson.isNull(TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS)) { 661 Optional<JSONArray> maybeSummaryBucketsJson = extractLongJsonArray( 662 triggerSpecJson, TriggerSpecs.FlexEventReportJsonKeys.SUMMARY_BUCKETS); 663 664 if (maybeSummaryBucketsJson.isEmpty()) { 665 return Optional.empty(); 666 } 667 668 summaryBuckets = TriggerSpec.getLongListFromJSON(maybeSummaryBucketsJson.get()); 669 670 if (summaryBuckets.isEmpty() || summaryBuckets.size() > maxEventLevelReports 671 || !TriggerSpec.isStrictIncreasing(summaryBuckets)) { 672 return Optional.empty(); 673 } 674 675 for (Long bucket : summaryBuckets) { 676 if (bucket < 0L || bucket > TriggerSpecs.MAX_BUCKET_THRESHOLD) { 677 return Optional.empty(); 678 } 679 } 680 } 681 682 return Optional.of( 683 new TriggerSpec.Builder( 684 triggerSpecJson, 685 defaultStart, 686 defaultEnds, 687 maxEventLevelReports).build()); 688 } 689 getValidEventReportWindows(JSONObject jsonReportWindows, long expiry)690 private Optional<JSONObject> getValidEventReportWindows(JSONObject jsonReportWindows, 691 long expiry) throws JSONException { 692 // Start time in seconds 693 long startTime = 0; 694 if (!jsonReportWindows.isNull(TriggerSpecs.FlexEventReportJsonKeys.START_TIME)) { 695 if (!FetcherUtil.is64BitInteger(jsonReportWindows.get( 696 TriggerSpecs.FlexEventReportJsonKeys.START_TIME))) { 697 return Optional.empty(); 698 } 699 // We continue to use startTime in seconds for validation but convert it to milliseconds 700 // for the return JSONObject. 701 startTime = 702 jsonReportWindows.getLong(TriggerSpecs.FlexEventReportJsonKeys.START_TIME); 703 jsonReportWindows.put(TriggerSpecs.FlexEventReportJsonKeys.START_TIME, 704 TimeUnit.SECONDS.toMillis(startTime)); 705 } 706 if (startTime < 0 || startTime > expiry) { 707 return Optional.empty(); 708 } 709 710 Optional<JSONArray> maybeWindowEndsJson = extractLongJsonArray( 711 jsonReportWindows, TriggerSpecs.FlexEventReportJsonKeys.END_TIMES); 712 713 if (maybeWindowEndsJson.isEmpty()) { 714 return Optional.empty(); 715 } 716 717 List<Long> windowEnds = TriggerSpec.getLongListFromJSON(maybeWindowEndsJson.get()); 718 719 int windowEndsSize = windowEnds.size(); 720 if (windowEnds.isEmpty() 721 || windowEndsSize > mFlags.getMeasurementFlexApiMaxEventReportWindows()) { 722 return Optional.empty(); 723 } 724 725 // Clamp last window end to expiry and min event report window. 726 Long lastWindowsEnd = windowEnds.get(windowEndsSize - 1); 727 if (lastWindowsEnd < 0) { 728 return Optional.empty(); 729 } 730 windowEnds.set(windowEndsSize - 1, extractValidNumberInRange( 731 lastWindowsEnd, 732 mFlags.getMeasurementMinimumEventReportWindowInSeconds(), 733 expiry)); 734 735 if (windowEndsSize > 1) { 736 // Clamp first window end to min event report window 737 Long firstWindowsEnd = windowEnds.get(0); 738 if (firstWindowsEnd < 0) { 739 return Optional.empty(); 740 } 741 windowEnds.set(0, Math.max( 742 firstWindowsEnd, 743 mFlags.getMeasurementMinimumEventReportWindowInSeconds())); 744 } 745 746 if (startTime >= windowEnds.get(0) || !TriggerSpec.isStrictIncreasing(windowEnds)) { 747 return Optional.empty(); 748 } 749 750 jsonReportWindows.put( 751 TriggerSpecs.FlexEventReportJsonKeys.END_TIMES, 752 // Convert end times to milliseconds for internal implementation. 753 new JSONArray(windowEnds.stream().map((x) -> 754 TimeUnit.SECONDS.toMillis(x)).collect(Collectors.toList()))); 755 756 return Optional.of(jsonReportWindows); 757 } 758 759 /** Parse a {@code Source}, given response headers, adding the {@code Source} to a given list */ 760 @VisibleForTesting parseSource( AsyncRegistration asyncRegistration, String enrollmentId, Map<String, List<String>> headers, AsyncFetchStatus asyncFetchStatus)761 public Optional<Source> parseSource( 762 AsyncRegistration asyncRegistration, 763 String enrollmentId, 764 Map<String, List<String>> headers, 765 AsyncFetchStatus asyncFetchStatus) { 766 boolean arDebugPermission = asyncRegistration.getDebugKeyAllowed(); 767 LoggerFactory.getMeasurementLogger() 768 .d("Source ArDebug permission enabled %b", arDebugPermission); 769 Source.Builder builder = new Source.Builder(); 770 builder.setRegistrationId(asyncRegistration.getRegistrationId()); 771 builder.setPublisher(getBaseUri(asyncRegistration.getTopOrigin())); 772 builder.setEnrollmentId(enrollmentId); 773 builder.setRegistrant(asyncRegistration.getRegistrant()); 774 builder.setSourceType(asyncRegistration.getSourceType()); 775 builder.setAttributionMode(Source.AttributionMode.TRUTHFULLY); 776 builder.setEventTime(asyncRegistration.getRequestTime()); 777 builder.setAdIdPermission(asyncRegistration.hasAdIdPermission()); 778 builder.setArDebugPermission(arDebugPermission); 779 builder.setPublisherType( 780 asyncRegistration.isWebRequest() ? EventSurfaceType.WEB : EventSurfaceType.APP); 781 Optional<Uri> registrationUriOrigin = 782 WebAddresses.originAndScheme(asyncRegistration.getRegistrationUri()); 783 if (!registrationUriOrigin.isPresent()) { 784 LoggerFactory.getMeasurementLogger() 785 .d( 786 "AsyncSourceFetcher: " 787 + "Invalid or empty registration uri - " 788 + asyncRegistration.getRegistrationUri()); 789 return Optional.empty(); 790 } 791 builder.setRegistrationOrigin(registrationUriOrigin.get()); 792 793 builder.setPlatformAdId(asyncRegistration.getPlatformAdId()); 794 795 List<String> field = 796 headers.get(SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 797 if (field == null || field.size() != 1) { 798 LoggerFactory.getMeasurementLogger() 799 .d( 800 "AsyncSourceFetcher: " 801 + "Invalid Attribution-Reporting-Register-Source header."); 802 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_ERROR); 803 return Optional.empty(); 804 } 805 String registrationHeaderStr = field.get(0); 806 807 boolean isHeaderErrorDebugReportEnabled = 808 FetcherUtil.isHeaderErrorDebugReportEnabled( 809 headers.get(SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_INFO), 810 mFlags); 811 try { 812 JSONObject json = new JSONObject(registrationHeaderStr); 813 boolean isValid = 814 parseCommonSourceParams(json, asyncRegistration, builder, enrollmentId); 815 if (!isValid) { 816 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 817 return Optional.empty(); 818 } 819 if (!json.isNull(SourceHeaderContract.AGGREGATION_KEYS)) { 820 if (!areValidAggregationKeys( 821 json.getJSONObject(SourceHeaderContract.AGGREGATION_KEYS))) { 822 asyncFetchStatus.setEntityStatus( 823 AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 824 return Optional.empty(); 825 } 826 builder.setAggregateSource(json.getString(SourceHeaderContract.AGGREGATION_KEYS)); 827 } 828 if (mFlags.getMeasurementEnableXNA() 829 && !json.isNull(SourceHeaderContract.SHARED_AGGREGATION_KEYS)) { 830 // Parsed as JSONArray for validation 831 JSONArray sharedAggregationKeys = 832 json.getJSONArray(SourceHeaderContract.SHARED_AGGREGATION_KEYS); 833 builder.setSharedAggregationKeys(sharedAggregationKeys.toString()); 834 } 835 if (mFlags.getMeasurementEnableSharedFilterDataKeysXNA() 836 && !json.isNull(SourceHeaderContract.SHARED_FILTER_DATA_KEYS)) { 837 // Parsed as JSONArray for validation 838 JSONArray sharedFilterDataKeys = 839 json.getJSONArray(SourceHeaderContract.SHARED_FILTER_DATA_KEYS); 840 builder.setSharedFilterDataKeys(sharedFilterDataKeys.toString()); 841 } 842 if (mFlags.getMeasurementEnablePreinstallCheck() 843 && !json.isNull(SourceHeaderContract.DROP_SOURCE_IF_INSTALLED)) { 844 builder.setDropSourceIfInstalled( 845 json.getBoolean(SourceHeaderContract.DROP_SOURCE_IF_INSTALLED)); 846 } 847 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.SUCCESS); 848 return Optional.of(builder.build()); 849 } catch (JSONException e) { 850 String errMsg = "Source JSON parsing failed"; 851 LoggerFactory.getMeasurementLogger().d(e, errMsg); 852 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.PARSING_ERROR); 853 if (isHeaderErrorDebugReportEnabled) { 854 mDatastoreManager.runInTransaction( 855 (dao) -> { 856 mDebugReportApi.scheduleHeaderErrorReport( 857 asyncRegistration.getRegistrationUri(), 858 asyncRegistration.getRegistrant(), 859 SourceHeaderContract 860 .HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE, 861 enrollmentId, 862 errMsg, 863 registrationHeaderStr, 864 dao); 865 }); 866 } 867 return Optional.empty(); 868 } catch (IllegalArgumentException | ArithmeticException e) { 869 LoggerFactory.getMeasurementLogger().d(e, "AsyncSourceFetcher: IllegalArgumentException" 870 + " or ArithmeticException"); 871 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.VALIDATION_ERROR); 872 return Optional.empty(); 873 } 874 } 875 876 /** Provided a testing hook. */ 877 @NonNull 878 @VisibleForTesting openUrl(@onNull URL url)879 public URLConnection openUrl(@NonNull URL url) throws IOException { 880 return mNetworkConnection.setup(url); 881 } 882 883 /** 884 * Fetch a source type registration. 885 * 886 * @param asyncRegistration a {@link AsyncRegistration}, a request the record. 887 * @param asyncFetchStatus a {@link AsyncFetchStatus}, stores Ad Tech server status. 888 * @param asyncRedirects a {@link AsyncRedirects}, stores redirects. 889 */ fetchSource( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, AsyncRedirects asyncRedirects)890 public Optional<Source> fetchSource( 891 AsyncRegistration asyncRegistration, 892 AsyncFetchStatus asyncFetchStatus, 893 AsyncRedirects asyncRedirects) { 894 HttpURLConnection urlConnection = null; 895 Map<String, List<String>> headers; 896 if (!asyncRegistration.getRegistrationUri().getScheme().equalsIgnoreCase("https")) { 897 LoggerFactory.getMeasurementLogger().d("Invalid scheme for registrationUri."); 898 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.INVALID_URL); 899 return Optional.empty(); 900 } 901 // TODO(b/276825561): Fix code duplication between fetchSource & fetchTrigger request flow 902 try { 903 urlConnection = 904 (HttpURLConnection) 905 openUrl(new URL(asyncRegistration.getRegistrationUri().toString())); 906 urlConnection.setRequestMethod("POST"); 907 urlConnection.setRequestProperty( 908 SourceRequestContract.SOURCE_INFO, 909 asyncRegistration.getSourceType().getValue()); 910 urlConnection.setInstanceFollowRedirects(false); 911 String body = asyncRegistration.getPostBody(); 912 if (mFlags.getFledgeMeasurementReportAndRegisterEventApiEnabled() && body != null) { 913 asyncFetchStatus.setPARequestStatus(true); 914 urlConnection.setRequestProperty("Content-Type", "text/plain"); 915 urlConnection.setDoOutput(true); 916 OutputStream os = urlConnection.getOutputStream(); 917 OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8); 918 osw.write(body); 919 osw.flush(); 920 osw.close(); 921 } 922 923 headers = urlConnection.getHeaderFields(); 924 asyncFetchStatus.setResponseSize(FetcherUtil.calculateHeadersCharactersLength(headers)); 925 int responseCode = urlConnection.getResponseCode(); 926 LoggerFactory.getMeasurementLogger().d("Response code = " + responseCode); 927 if (!FetcherUtil.isRedirect(responseCode) && !FetcherUtil.isSuccess(responseCode)) { 928 asyncFetchStatus.setResponseStatus( 929 AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE); 930 return Optional.empty(); 931 } 932 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.SUCCESS); 933 } catch (MalformedURLException e) { 934 LoggerFactory.getMeasurementLogger().d(e, "Malformed registration target URL"); 935 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.INVALID_URL); 936 return Optional.empty(); 937 } catch (IOException e) { 938 LoggerFactory.getMeasurementLogger().e(e, "Failed to get registration response"); 939 asyncFetchStatus.setResponseStatus(AsyncFetchStatus.ResponseStatus.NETWORK_ERROR); 940 return Optional.empty(); 941 } finally { 942 if (urlConnection != null) { 943 urlConnection.disconnect(); 944 } 945 } 946 947 asyncRedirects.configure(headers, mFlags, asyncRegistration); 948 949 if (!isSourceHeaderPresent(headers)) { 950 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_MISSING); 951 asyncFetchStatus.setRedirectOnlyStatus(true); 952 return Optional.empty(); 953 } 954 955 Optional<String> enrollmentId = 956 mFlags.isDisableMeasurementEnrollmentCheck() 957 ? WebAddresses.topPrivateDomainAndScheme( 958 asyncRegistration.getRegistrationUri()) 959 .map(Uri::toString) 960 : Enrollment.getValidEnrollmentId( 961 asyncRegistration.getRegistrationUri(), 962 asyncRegistration.getRegistrant().getAuthority(), 963 mEnrollmentDao, 964 mContext, 965 mFlags); 966 if (enrollmentId.isEmpty()) { 967 LoggerFactory.getMeasurementLogger() 968 .d( 969 "fetchSource: Valid enrollment id not found. Registration URI: %s", 970 asyncRegistration.getRegistrationUri()); 971 asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.INVALID_ENROLLMENT); 972 ErrorLogUtil.e( 973 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_INVALID, 974 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 975 return Optional.empty(); 976 } 977 978 Optional<Source> parsedSource = 979 parseSource(asyncRegistration, enrollmentId.get(), headers, asyncFetchStatus); 980 return parsedSource; 981 } 982 isSourceHeaderPresent(Map<String, List<String>> headers)983 private boolean isSourceHeaderPresent(Map<String, List<String>> headers) { 984 return headers.containsKey( 985 SourceHeaderContract.HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE); 986 } 987 areValidAggregationKeys(JSONObject aggregationKeys)988 private boolean areValidAggregationKeys(JSONObject aggregationKeys) { 989 if (aggregationKeys.length() 990 > mFlags.getMeasurementMaxAggregateKeysPerSourceRegistration()) { 991 LoggerFactory.getMeasurementLogger() 992 .d( 993 "Aggregation-keys have more entries than permitted. %s", 994 aggregationKeys.length()); 995 return false; 996 } 997 for (String id : aggregationKeys.keySet()) { 998 if (!FetcherUtil.isValidAggregateKeyId(id)) { 999 LoggerFactory.getMeasurementLogger() 1000 .d("SourceFetcher: aggregation key ID is invalid. %s", id); 1001 return false; 1002 } 1003 String keyPiece = aggregationKeys.optString(id); 1004 if (!FetcherUtil.isValidAggregateKeyPiece(keyPiece, mFlags)) { 1005 LoggerFactory.getMeasurementLogger() 1006 .d("SourceFetcher: aggregation key-piece is invalid. %s", keyPiece); 1007 return false; 1008 } 1009 } 1010 return true; 1011 } 1012 isContiguousStartingAtZero(Set<UnsignedLong> unsignedLongs)1013 private static boolean isContiguousStartingAtZero(Set<UnsignedLong> unsignedLongs) { 1014 UnsignedLong upperBound = new UnsignedLong(((long) unsignedLongs.size()) - 1L); 1015 for (UnsignedLong unsignedLong : unsignedLongs) { 1016 if (unsignedLong.compareTo(upperBound) > 0) { 1017 return false; 1018 } 1019 } 1020 return true; 1021 } 1022 extractLongJsonArray(JSONObject json, String key)1023 private static Optional<JSONArray> extractLongJsonArray(JSONObject json, String key) 1024 throws JSONException { 1025 JSONArray jsonArray = json.getJSONArray(key); 1026 for (int i = 0; i < jsonArray.length(); i++) { 1027 if (!FetcherUtil.is64BitInteger(jsonArray.get(i))) { 1028 return Optional.empty(); 1029 } 1030 } 1031 return Optional.of(jsonArray); 1032 } 1033 roundSecondsToWholeDays(long seconds)1034 private static long roundSecondsToWholeDays(long seconds) { 1035 long remainder = seconds % ONE_DAY_IN_SECONDS; 1036 // Return value should be at least one whole day. 1037 boolean roundUp = (remainder >= ONE_DAY_IN_SECONDS / 2L) || (seconds == remainder); 1038 return seconds - remainder + (roundUp ? ONE_DAY_IN_SECONDS : 0); 1039 } 1040 1041 private interface SourceHeaderContract { 1042 String HEADER_ATTRIBUTION_REPORTING_REGISTER_SOURCE = 1043 "Attribution-Reporting-Register-Source"; 1044 // Header for enable header error verbose debug reports. 1045 String HEADER_ATTRIBUTION_REPORTING_INFO = "Attribution-Reporting-Info"; 1046 String SOURCE_EVENT_ID = "source_event_id"; 1047 String DEBUG_KEY = "debug_key"; 1048 String DESTINATION = "destination"; 1049 String EXPIRY = "expiry"; 1050 String EVENT_REPORT_WINDOW = "event_report_window"; 1051 String AGGREGATABLE_REPORT_WINDOW = "aggregatable_report_window"; 1052 String PRIORITY = "priority"; 1053 String INSTALL_ATTRIBUTION_WINDOW_KEY = "install_attribution_window"; 1054 String POST_INSTALL_EXCLUSIVITY_WINDOW_KEY = "post_install_exclusivity_window"; 1055 String REINSTALL_REATTRIBUTION_WINDOW_KEY = "reinstall_reattribution_window"; 1056 String FILTER_DATA = "filter_data"; 1057 String WEB_DESTINATION = "web_destination"; 1058 String AGGREGATION_KEYS = "aggregation_keys"; 1059 String SHARED_AGGREGATION_KEYS = "shared_aggregation_keys"; 1060 String DEBUG_REPORTING = "debug_reporting"; 1061 String DEBUG_JOIN_KEY = "debug_join_key"; 1062 String DEBUG_AD_ID = "debug_ad_id"; 1063 String COARSE_EVENT_REPORT_DESTINATIONS = "coarse_event_report_destinations"; 1064 String TRIGGER_SPECS = "trigger_specs"; 1065 String MAX_EVENT_LEVEL_REPORTS = "max_event_level_reports"; 1066 String EVENT_REPORT_WINDOWS = "event_report_windows"; 1067 String SHARED_DEBUG_KEY = "shared_debug_key"; 1068 String SHARED_FILTER_DATA_KEYS = "shared_filter_data_keys"; 1069 String DROP_SOURCE_IF_INSTALLED = "drop_source_if_installed"; 1070 String TRIGGER_DATA_MATCHING = "trigger_data_matching"; 1071 String TRIGGER_DATA = "trigger_data"; 1072 String ATTRIBUTION_SCOPES = "attribution_scopes"; 1073 String ATTRIBUTION_SCOPE_LIMIT = "attribution_scope_limit"; 1074 String MAX_EVENT_STATES = "max_event_states"; 1075 } 1076 1077 private interface SourceRequestContract { 1078 String SOURCE_INFO = "Attribution-Reporting-Source-Info"; 1079 } 1080 } 1081