blob: 57809acc84b104f10e6ec380e7d1c992fb92bd24 [file] [log] [blame]
Joseph Wen6a34bb22015-02-25 14:00:39 -05001/*
2 * Copyright (C) 2015 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
17package com.android.statementservice;
18
19import android.content.BroadcastReceiver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.os.Bundle;
25import android.os.Handler;
26import android.os.ResultReceiver;
27import android.util.Log;
28import android.util.Patterns;
29
30import com.android.statementservice.retriever.Utils;
31
32import java.net.MalformedURLException;
33import java.net.URL;
34import java.util.ArrayList;
35import java.util.Collections;
36import java.util.List;
37import java.util.regex.Pattern;
38
39/**
40 * Receives {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION} broadcast and calls
41 * {@link DirectStatementService} to verify the request. Calls
42 * {@link PackageManager#verifyIntentFilter} to notify {@link PackageManager} the result of the
43 * verification.
44 *
45 * This implementation of the API will send a HTTP request for each host specified in the query.
46 * To avoid overwhelming the network at app install time, {@code MAX_HOSTS_PER_REQUEST} limits
47 * the maximum number of hosts in a query. If a query contains more than
48 * {@code MAX_HOSTS_PER_REQUEST} hosts, it will fail immediately without making any HTTP request
49 * and call {@link PackageManager#verifyIntentFilter} with
50 * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}.
51 */
52public final class IntentFilterVerificationReceiver extends BroadcastReceiver {
53 private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName();
54
55 private static final Integer MAX_HOSTS_PER_REQUEST = 10;
56
57 private static final String HANDLE_ALL_URLS_RELATION
58 = "delegate_permission/common.handle_all_urls";
59
60 private static final String ANDROID_ASSET_FORMAT = "{\"namespace\": \"android_app\", "
61 + "\"package_name\": \"%s\", \"sha256_cert_fingerprints\": [\"%s\"]}";
62 private static final String WEB_ASSET_FORMAT = "{\"namespace\": \"web\", \"site\": \"%s\"}";
63 private static final Pattern ANDROID_PACKAGE_NAME_PATTERN =
64 Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$");
65 private static final String TOO_MANY_HOSTS_FORMAT =
66 "Request contains %d hosts which is more than the allowed %d.";
67
68 private static void sendErrorToPackageManager(PackageManager packageManager,
69 int verificationId) {
70 packageManager.verifyIntentFilter(verificationId,
71 PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
72 Collections.<String>emptyList());
73 }
74
75 @Override
76 public void onReceive(Context context, Intent intent) {
77 final String action = intent.getAction();
78 if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) {
79 Bundle inputExtras = intent.getExtras();
80 if (inputExtras != null) {
81 Intent serviceIntent = new Intent(context, DirectStatementService.class);
82 serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION);
83
84 int verificationId = inputExtras.getInt(
85 PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID);
86 String scheme = inputExtras.getString(
87 PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME);
88 String hosts = inputExtras.getString(
89 PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS);
90 String packageName = inputExtras.getString(
91 PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME);
92
93 Log.i(TAG, "Verify IntentFilter for " + hosts);
94
95 Bundle extras = new Bundle();
96 extras.putString(DirectStatementService.EXTRA_RELATION, HANDLE_ALL_URLS_RELATION);
97
98 String[] hostList = hosts.split(" ");
99 if (hostList.length > MAX_HOSTS_PER_REQUEST) {
100 Log.w(TAG, String.format(TOO_MANY_HOSTS_FORMAT,
101 hostList.length, MAX_HOSTS_PER_REQUEST));
102 sendErrorToPackageManager(context.getPackageManager(), verificationId);
103 return;
104 }
105
106 try {
107 ArrayList<String> sourceAssets = new ArrayList<String>();
108 for (String host : hostList) {
Christopher Tated268a222016-02-19 16:48:28 -0800109 // "*.example.tld" is validated via https://example.tld
110 if (host.startsWith("*.")) {
111 host = host.substring(2);
112 }
Joseph Wen6a34bb22015-02-25 14:00:39 -0500113 sourceAssets.add(createWebAssetString(scheme, host));
114 }
115 extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
116 sourceAssets);
117 } catch (MalformedURLException e) {
118 Log.w(TAG, "Error when processing input host: " + e.getMessage());
119 sendErrorToPackageManager(context.getPackageManager(), verificationId);
120 return;
121 }
122 try {
123 extras.putString(DirectStatementService.EXTRA_TARGET_ASSET_DESCRIPTOR,
124 createAndroidAssetString(context, packageName));
125 } catch (NameNotFoundException e) {
126 Log.w(TAG, "Error when processing input Android package: " + e.getMessage());
127 sendErrorToPackageManager(context.getPackageManager(), verificationId);
128 return;
129 }
130 extras.putParcelable(DirectStatementService.EXTRA_RESULT_RECEIVER,
131 new IsAssociatedResultReceiver(
132 new Handler(), context.getPackageManager(), verificationId));
133
134 serviceIntent.putExtras(extras);
135 context.startService(serviceIntent);
136 }
137 } else {
138 Log.w(TAG, "Intent action not supported: " + action);
139 }
140 }
141
142 private String createAndroidAssetString(Context context, String packageName)
143 throws NameNotFoundException {
144 if (!ANDROID_PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
145 throw new NameNotFoundException("Input package name is not valid.");
146 }
147
148 List<String> certFingerprints =
149 Utils.getCertFingerprintsFromPackageManager(packageName, context);
150
151 return String.format(ANDROID_ASSET_FORMAT, packageName,
152 Utils.joinStrings("\", \"", certFingerprints));
153 }
154
155 private String createWebAssetString(String scheme, String host) throws MalformedURLException {
156 if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
157 throw new MalformedURLException("Input host is not valid.");
158 }
159 if (!scheme.equals("http") && !scheme.equals("https")) {
160 throw new MalformedURLException("Input scheme is not valid.");
161 }
162
163 return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
164 }
165
166 /**
167 * Receives the result of {@code StatementService.CHECK_ACTION} from
168 * {@link DirectStatementService} and passes it back to {@link PackageManager}.
169 */
170 private static class IsAssociatedResultReceiver extends ResultReceiver {
171
172 private final int mVerificationId;
173 private final PackageManager mPackageManager;
174
175 public IsAssociatedResultReceiver(Handler handler, PackageManager packageManager,
176 int verificationId) {
177 super(handler);
178 mVerificationId = verificationId;
179 mPackageManager = packageManager;
180 }
181
182 @Override
183 protected void onReceiveResult(int resultCode, Bundle resultData) {
184 if (resultCode == DirectStatementService.RESULT_SUCCESS) {
185 if (resultData.getBoolean(DirectStatementService.IS_ASSOCIATED)) {
186 mPackageManager.verifyIntentFilter(mVerificationId,
187 PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS,
188 Collections.<String>emptyList());
189 } else {
190 mPackageManager.verifyIntentFilter(mVerificationId,
191 PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
192 resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES));
193 }
194 } else {
195 sendErrorToPackageManager(mPackageManager, mVerificationId);
196 }
197 }
198 }
199}