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.SystemHealthParams.MAX_ATTRIBUTION_FILTERS; 19 import static com.android.adservices.service.measurement.SystemHealthParams.MAX_BYTES_PER_ATTRIBUTION_AGGREGATE_KEY_ID; 20 import static com.android.adservices.service.measurement.SystemHealthParams.MAX_BYTES_PER_ATTRIBUTION_FILTER_STRING; 21 import static com.android.adservices.service.measurement.SystemHealthParams.MAX_FILTER_MAPS_PER_FILTER_SET; 22 import static com.android.adservices.service.measurement.SystemHealthParams.MAX_VALUES_PER_ATTRIBUTION_FILTER; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_REGISTRATIONS; 24 25 import android.annotation.NonNull; 26 import android.net.Uri; 27 28 import com.android.adservices.LogUtil; 29 import com.android.adservices.service.Flags; 30 import com.android.adservices.service.FlagsFactory; 31 import com.android.adservices.service.measurement.Source; 32 import com.android.adservices.service.measurement.util.AdIdEncryption; 33 import com.android.adservices.service.measurement.util.Web; 34 import com.android.adservices.service.stats.AdServicesLogger; 35 import com.android.adservices.service.stats.MeasurementRegistrationResponseStats; 36 37 import org.json.JSONArray; 38 import org.json.JSONObject; 39 40 import java.util.ArrayList; 41 import java.util.HashMap; 42 import java.util.Iterator; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * Common handling for Response Based Registration 48 * 49 * @hide 50 */ 51 class FetcherUtil { 52 static final String REDIRECT_LIST_HEADER_KEY = "Attribution-Reporting-Redirect"; 53 static final String REDIRECT_LOCATION_HEADER_KEY = "Location"; 54 55 /** 56 * Determine all redirects. 57 * 58 * <p>Generates a list of: (url, allows_regular_redirects) tuples. Returns true if all steps 59 * succeed. Returns false if there are any failures. 60 */ parseRedirects( @onNull Map<String, List<String>> headers)61 static Map<AsyncRegistration.RedirectType, List<Uri>> parseRedirects( 62 @NonNull Map<String, List<String>> headers) { 63 Map<AsyncRegistration.RedirectType, List<Uri>> uriMap = new HashMap<>(); 64 uriMap.put(AsyncRegistration.RedirectType.LOCATION, parseLocationRedirects(headers)); 65 uriMap.put(AsyncRegistration.RedirectType.LIST, parseListRedirects(headers)); 66 return uriMap; 67 } 68 69 /** 70 * Check HTTP response codes that indicate a redirect. 71 */ isRedirect(int responseCode)72 static boolean isRedirect(int responseCode) { 73 return (responseCode / 100) == 3; 74 } 75 76 /** 77 * Check HTTP response code for success. 78 */ isSuccess(int responseCode)79 static boolean isSuccess(int responseCode) { 80 return (responseCode / 100) == 2; 81 } 82 83 /** 84 * Validate aggregate key ID. 85 */ isValidAggregateKeyId(String id)86 static boolean isValidAggregateKeyId(String id) { 87 return id != null && id.getBytes().length <= MAX_BYTES_PER_ATTRIBUTION_AGGREGATE_KEY_ID; 88 } 89 90 /** Validate aggregate deduplication key. */ isValidAggregateDeduplicationKey(String deduplicationKey)91 static boolean isValidAggregateDeduplicationKey(String deduplicationKey) { 92 if (deduplicationKey == null) { 93 return false; 94 } 95 try { 96 Long.parseUnsignedLong(deduplicationKey); 97 } catch (NumberFormatException exception) { 98 return false; 99 } 100 return true; 101 } 102 103 /** 104 * Validate aggregate key-piece. 105 */ isValidAggregateKeyPiece(String keyPiece)106 static boolean isValidAggregateKeyPiece(String keyPiece) { 107 if (keyPiece == null) { 108 return false; 109 } 110 int length = keyPiece.getBytes().length; 111 // Key-piece is restricted to a maximum of 128 bits and the hex strings therefore have at 112 // most 32 digits. 113 return (keyPiece.startsWith("0x") || keyPiece.startsWith("0X")) 114 && 2 < length && length < 35; 115 } 116 117 /** 118 * Validate attribution filters JSONArray. 119 */ areValidAttributionFilters(@onNull JSONArray filterSet)120 static boolean areValidAttributionFilters(@NonNull JSONArray filterSet) { 121 if (filterSet.length() > MAX_FILTER_MAPS_PER_FILTER_SET) { 122 return false; 123 } 124 for (int i = 0; i < filterSet.length(); i++) { 125 if (!areValidAttributionFilters(filterSet.optJSONObject(i))) { 126 return false; 127 } 128 } 129 return true; 130 } 131 132 /** 133 * Validate attribution filters JSONObject. 134 */ areValidAttributionFilters(JSONObject filtersObj)135 static boolean areValidAttributionFilters(JSONObject filtersObj) { 136 if (filtersObj == null || filtersObj.length() > MAX_ATTRIBUTION_FILTERS) { 137 return false; 138 } 139 Iterator<String> keys = filtersObj.keys(); 140 while (keys.hasNext()) { 141 String key = keys.next(); 142 if (key.getBytes().length > MAX_BYTES_PER_ATTRIBUTION_FILTER_STRING) { 143 return false; 144 } 145 JSONArray values = filtersObj.optJSONArray(key); 146 if (values == null || values.length() > MAX_VALUES_PER_ATTRIBUTION_FILTER) { 147 return false; 148 } 149 for (int i = 0; i < values.length(); i++) { 150 String value = values.optString(i); 151 if (value == null 152 || value.getBytes().length > MAX_BYTES_PER_ATTRIBUTION_FILTER_STRING) { 153 return false; 154 } 155 } 156 } 157 return true; 158 } 159 emitHeaderMetrics( Flags flags, AdServicesLogger logger, AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus)160 static void emitHeaderMetrics( 161 Flags flags, 162 AdServicesLogger logger, 163 AsyncRegistration asyncRegistration, 164 AsyncFetchStatus asyncFetchStatus) { 165 long headerSize = 0; 166 if (asyncFetchStatus.getResponseSize().isPresent()) { 167 headerSize = asyncFetchStatus.getResponseSize().get(); 168 } 169 long maxSize = flags.getMaxResponseBasedRegistrationPayloadSizeBytes(); 170 String adTechDomain = null; 171 172 if (headerSize > maxSize) { 173 adTechDomain = 174 Web.topPrivateDomainAndScheme(asyncRegistration.getRegistrationUri()) 175 .map(Uri::toString) 176 .orElse(null); 177 } 178 179 logger.logMeasurementRegistrationsResponseSize( 180 new MeasurementRegistrationResponseStats.Builder( 181 AD_SERVICES_MEASUREMENT_REGISTRATIONS, 182 getRegistrationType(asyncRegistration), 183 headerSize, 184 getSourceType(asyncRegistration), 185 getSurfaceType(asyncRegistration), 186 getStatus(asyncFetchStatus), 187 getFailureType(asyncFetchStatus), 188 asyncFetchStatus.getRegistrationDelay().get()) 189 .setAdTechDomain(adTechDomain) 190 .build()); 191 } 192 parseListRedirects(Map<String, List<String>> headers)193 private static List<Uri> parseListRedirects(Map<String, List<String>> headers) { 194 List<Uri> redirects = new ArrayList<>(); 195 List<String> field = headers.get(REDIRECT_LIST_HEADER_KEY); 196 int maxRedirects = FlagsFactory.getFlags().getMeasurementMaxRegistrationRedirects(); 197 if (field != null) { 198 for (int i = 0; i < Math.min(field.size(), maxRedirects); i++) { 199 redirects.add(Uri.parse(field.get(i))); 200 } 201 } 202 return redirects; 203 } 204 parseLocationRedirects(Map<String, List<String>> headers)205 private static List<Uri> parseLocationRedirects(Map<String, List<String>> headers) { 206 List<Uri> redirects = new ArrayList<>(); 207 List<String> field = headers.get(REDIRECT_LOCATION_HEADER_KEY); 208 if (field != null && !field.isEmpty()) { 209 redirects.add(Uri.parse(field.get(0))); 210 if (field.size() > 1) { 211 LogUtil.d("Expected one Location redirect only, others ignored!"); 212 } 213 } 214 return redirects; 215 } 216 calculateHeadersCharactersLength(Map<String, List<String>> headers)217 public static long calculateHeadersCharactersLength(Map<String, List<String>> headers) { 218 long size = 0; 219 for (String headerKey : headers.keySet()) { 220 if (headerKey != null) { 221 size = size + headerKey.length(); 222 List<String> headerValues = headers.get(headerKey); 223 if (headerValues != null) { 224 for (String headerValue : headerValues) { 225 size = size + headerValue.length(); 226 } 227 } 228 } 229 } 230 231 return size; 232 } 233 getEncryptedPlatformAdIdIfPresent( AsyncRegistration asyncRegistration, String enrollmentId)234 public static String getEncryptedPlatformAdIdIfPresent( 235 AsyncRegistration asyncRegistration, String enrollmentId) { 236 if (asyncRegistration.isAppRequest() 237 && asyncRegistration.hasAdIdPermission() 238 && asyncRegistration.getPlatformAdId() != null) { 239 return AdIdEncryption.encryptAdIdAndEnrollmentSha256( 240 asyncRegistration.getPlatformAdId(), enrollmentId); 241 } else { 242 return null; 243 } 244 } 245 getRegistrationType(AsyncRegistration asyncRegistration)246 private static int getRegistrationType(AsyncRegistration asyncRegistration) { 247 if (asyncRegistration.getType() == AsyncRegistration.RegistrationType.APP_SOURCE 248 || asyncRegistration.getType() == AsyncRegistration.RegistrationType.WEB_SOURCE) { 249 return RegistrationEnumsValues.TYPE_SOURCE; 250 } else if (asyncRegistration.getType() == AsyncRegistration.RegistrationType.APP_TRIGGER 251 || asyncRegistration.getType() == AsyncRegistration.RegistrationType.WEB_TRIGGER) { 252 return RegistrationEnumsValues.TYPE_TRIGGER; 253 } else { 254 return RegistrationEnumsValues.TYPE_UNKNOWN; 255 } 256 } 257 getSourceType(AsyncRegistration asyncRegistration)258 private static int getSourceType(AsyncRegistration asyncRegistration) { 259 if (asyncRegistration.getSourceType() == Source.SourceType.EVENT) { 260 return RegistrationEnumsValues.SOURCE_TYPE_EVENT; 261 } else if (asyncRegistration.getSourceType() == Source.SourceType.NAVIGATION) { 262 return RegistrationEnumsValues.SOURCE_TYPE_NAVIGATION; 263 } else { 264 return RegistrationEnumsValues.SOURCE_TYPE_UNKNOWN; 265 } 266 } 267 getSurfaceType(AsyncRegistration asyncRegistration)268 private static int getSurfaceType(AsyncRegistration asyncRegistration) { 269 if (asyncRegistration.getType() == AsyncRegistration.RegistrationType.APP_SOURCE 270 || asyncRegistration.getType() == AsyncRegistration.RegistrationType.APP_TRIGGER) { 271 return RegistrationEnumsValues.SURFACE_TYPE_APP; 272 } else if (asyncRegistration.getType() == AsyncRegistration.RegistrationType.WEB_SOURCE 273 || asyncRegistration.getType() == AsyncRegistration.RegistrationType.WEB_TRIGGER) { 274 return RegistrationEnumsValues.SURFACE_TYPE_WEB; 275 } else { 276 return RegistrationEnumsValues.SURFACE_TYPE_UNKNOWN; 277 } 278 } 279 getStatus(AsyncFetchStatus asyncFetchStatus)280 private static int getStatus(AsyncFetchStatus asyncFetchStatus) { 281 if (asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.SUCCESS 282 || (asyncFetchStatus.getResponseStatus() == AsyncFetchStatus.ResponseStatus.SUCCESS 283 && (asyncFetchStatus.getEntityStatus() 284 == AsyncFetchStatus.EntityStatus.UNKNOWN 285 || asyncFetchStatus.getEntityStatus() 286 == AsyncFetchStatus.EntityStatus.HEADER_MISSING))) { 287 // successful source/trigger fetching/parsing and successful redirects (with no header) 288 return RegistrationEnumsValues.STATUS_SUCCESS; 289 } else if (asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.UNKNOWN 290 && asyncFetchStatus.getResponseStatus() 291 == AsyncFetchStatus.ResponseStatus.UNKNOWN) { 292 return RegistrationEnumsValues.STATUS_UNKNOWN; 293 } else { 294 return RegistrationEnumsValues.STATUS_FAILURE; 295 } 296 } 297 getFailureType(AsyncFetchStatus asyncFetchStatus)298 private static int getFailureType(AsyncFetchStatus asyncFetchStatus) { 299 if (asyncFetchStatus.getResponseStatus() 300 == AsyncFetchStatus.ResponseStatus.SERVER_UNAVAILABLE 301 || asyncFetchStatus.getResponseStatus() 302 == AsyncFetchStatus.ResponseStatus.NETWORK_ERROR 303 || asyncFetchStatus.getResponseStatus() 304 == AsyncFetchStatus.ResponseStatus.INVALID_URL) { 305 return RegistrationEnumsValues.FAILURE_TYPE_NETWORK; 306 } else if (asyncFetchStatus.getEntityStatus() 307 == AsyncFetchStatus.EntityStatus.INVALID_ENROLLMENT) { 308 return RegistrationEnumsValues.FAILURE_TYPE_ENROLLMENT; 309 } else if (asyncFetchStatus.getEntityStatus() 310 == AsyncFetchStatus.EntityStatus.VALIDATION_ERROR 311 || asyncFetchStatus.getEntityStatus() == AsyncFetchStatus.EntityStatus.PARSING_ERROR 312 || asyncFetchStatus.getEntityStatus() 313 == AsyncFetchStatus.EntityStatus.HEADER_MISSING 314 || asyncFetchStatus.getEntityStatus() 315 == AsyncFetchStatus.EntityStatus.HEADER_ERROR) { 316 return RegistrationEnumsValues.FAILURE_TYPE_PARSING; 317 } else if (asyncFetchStatus.getEntityStatus() 318 == AsyncFetchStatus.EntityStatus.STORAGE_ERROR) { 319 return RegistrationEnumsValues.FAILURE_TYPE_STORAGE; 320 } else if (asyncFetchStatus.isRedirectError()) { 321 return RegistrationEnumsValues.FAILURE_TYPE_REDIRECT; 322 } else { 323 return RegistrationEnumsValues.FAILURE_TYPE_UNKNOWN; 324 } 325 } 326 /** AdservicesMeasurementRegistrations atom enum values. */ 327 public interface RegistrationEnumsValues { 328 int TYPE_UNKNOWN = 0; 329 int TYPE_SOURCE = 1; 330 int TYPE_TRIGGER = 2; 331 int SOURCE_TYPE_UNKNOWN = 0; 332 int SOURCE_TYPE_EVENT = 1; 333 int SOURCE_TYPE_NAVIGATION = 2; 334 int SURFACE_TYPE_UNKNOWN = 0; 335 int SURFACE_TYPE_WEB = 1; 336 int SURFACE_TYPE_APP = 2; 337 int STATUS_UNKNOWN = 0; 338 int STATUS_SUCCESS = 1; 339 int STATUS_FAILURE = 2; 340 int FAILURE_TYPE_UNKNOWN = 0; 341 int FAILURE_TYPE_PARSING = 1; 342 int FAILURE_TYPE_NETWORK = 2; 343 int FAILURE_TYPE_ENROLLMENT = 3; 344 int FAILURE_TYPE_REDIRECT = 4; 345 int FAILURE_TYPE_STORAGE = 5; 346 } 347 } 348