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