blob: d31b222d704d98d5a556359c251c02753b06fd1c [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.adservices.service.measurement.registration;
import android.adservices.measurement.RegistrationRequest;
import android.annotation.NonNull;
import android.net.Uri;
import com.android.adservices.LogUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Download and decode Trigger registration.
*
* @hide
*/
public class TriggerFetcher {
/**
* Provided a testing hook.
*/
public @NonNull URLConnection openUrl(@NonNull URL url) throws IOException {
return url.openConnection();
}
private static void parseEventTrigger(
@NonNull String text,
TriggerRegistration.Builder result) throws JSONException {
final JSONArray array = new JSONArray(text);
if (array.length() != 1) {
throw new JSONException("Expected list with 1 item");
}
final JSONObject inside = array.getJSONObject(0);
if (inside.has(EventTriggerContract.TRIGGER_DATA)
&& !inside.isNull(EventTriggerContract.TRIGGER_DATA)) {
result.setTriggerData(inside.getLong(EventTriggerContract.TRIGGER_DATA));
}
if (inside.has(EventTriggerContract.PRIORITY)
&& !inside.isNull(EventTriggerContract.PRIORITY)) {
result.setTriggerPriority(inside.getLong(EventTriggerContract.PRIORITY));
}
if (inside.has(EventTriggerContract.DEDUPLICATION_KEY)
&& !inside.isNull(EventTriggerContract.DEDUPLICATION_KEY)) {
result.setDeduplicationKey(inside.getLong(EventTriggerContract.DEDUPLICATION_KEY));
}
}
private static boolean parseTrigger(
@NonNull Uri topOrigin,
@NonNull Uri reportingOrigin,
@NonNull Map<String, List<String>> headers,
@NonNull List<TriggerRegistration> addToResults) {
boolean additionalResult = false;
TriggerRegistration.Builder result = new TriggerRegistration.Builder();
result.setTopOrigin(topOrigin);
result.setReportingOrigin(reportingOrigin);
List<String> field;
field = headers.get("Attribution-Reporting-Register-Event-Trigger");
if (field != null) {
if (field.size() != 1) {
LogUtil.d("Expected one event trigger!");
return false;
}
try {
parseEventTrigger(field.get(0), result);
} catch (JSONException e) {
LogUtil.d("Invalid JSON %s", e);
return false;
}
additionalResult = true;
}
field = headers.get("Attribution-Reporting-Register-Aggregatable-Trigger-Data");
if (field != null) {
if (field.size() != 1) {
LogUtil.d("Expected one aggregate trigger data!");
return false;
}
// Parses in aggregate trigger data. additionalResult will be false until then.
result.setAggregateTriggerData(field.get(0));
additionalResult = true;
}
field = headers.get("Attribution-Reporting-Register-Aggregatable-Values");
if (field != null) {
if (field.size() != 1) {
LogUtil.d("Expected one aggregatable values!");
return false;
}
// Parses in aggregate values. additionalResult will be false until then.
result.setAggregateValues(field.get(0));
additionalResult = true;
}
if (additionalResult) {
addToResults.add(result.build());
return true;
}
return false;
}
private void fetchTrigger(
@NonNull Uri topOrigin,
@NonNull Uri target,
boolean initialFetch,
@NonNull List<TriggerRegistration> registrationsOut) {
// Require https.
if (!target.getScheme().equals("https")) {
return;
}
URL url;
try {
url = new URL(target.toString());
} catch (MalformedURLException e) {
LogUtil.d("Malformed registration target URL %s", e);
return;
}
HttpURLConnection urlConnection;
try {
urlConnection = (HttpURLConnection) openUrl(url);
} catch (IOException e) {
LogUtil.d("Failed to open registration target URL %s", e);
return;
}
try {
urlConnection.setRequestMethod("POST");
urlConnection.setInstanceFollowRedirects(false);
Map<String, List<String>> headers = urlConnection.getHeaderFields();
int responseCode = urlConnection.getResponseCode();
LogUtil.d("Response code = " + responseCode);
if (!ResponseBasedFetcher.isRedirect(responseCode)
&& !ResponseBasedFetcher.isSuccess(responseCode)) {
return;
}
final boolean parsed = parseTrigger(topOrigin, target, headers, registrationsOut);
if (!parsed && initialFetch) {
LogUtil.d("Failed to parse initial fetch");
return;
}
ArrayList<Uri> redirects = new ArrayList();
ResponseBasedFetcher.parseRedirects(initialFetch, headers, redirects);
for (Uri redirect : redirects) {
fetchTrigger(topOrigin, redirect, false, registrationsOut);
}
} catch (IOException e) {
LogUtil.d("Failed to get registration response %s", e);
return;
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
/**
* Fetch a trigger type registration.
*/
public boolean fetchTrigger(@NonNull RegistrationRequest request,
@NonNull List<TriggerRegistration> out) {
if (request.getRegistrationType()
!= RegistrationRequest.REGISTER_TRIGGER) {
throw new IllegalArgumentException("Expected trigger registration");
}
fetchTrigger(
request.getTopOriginUri(),
request.getRegistrationUri(),
true, out);
return !out.isEmpty();
}
private interface EventTriggerContract {
String TRIGGER_DATA = "trigger_data";
String PRIORITY = "priority";
String DEDUPLICATION_KEY = "deduplication_key";
}
}