blob: 3ad71c44b5281b4f46c2f3664899ecc2247e8ae6 [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.retriever;
18
19import android.content.pm.PackageManager.NameNotFoundException;
20import android.util.Log;
21
22import org.json.JSONException;
23
24import java.io.IOException;
25import java.net.MalformedURLException;
26import java.net.URL;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.List;
30
31/**
32 * An implementation of {@link AbstractStatementRetriever} that directly retrieves statements from
33 * the asset.
34 */
35/* package private */ final class DirectStatementRetriever extends AbstractStatementRetriever {
36
37 private static final long DO_NOT_CACHE_RESULT = 0L;
38 private static final int HTTP_CONNECTION_TIMEOUT_MILLIS = 5000;
39 private static final long HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
40 private static final int MAX_INCLUDE_LEVEL = 1;
41 private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/associations.json";
42
43 private final URLFetcher mUrlFetcher;
44 private final AndroidPackageInfoFetcher mAndroidFetcher;
45
46 /**
47 * An immutable value type representing the retrieved statements and the expiration date.
48 */
49 public static class Result implements AbstractStatementRetriever.Result {
50
51 private final List<Statement> mStatements;
52 private final Long mExpireMillis;
53
54 @Override
55 public List<Statement> getStatements() {
56 return mStatements;
57 }
58
59 @Override
60 public long getExpireMillis() {
61 return mExpireMillis;
62 }
63
64 private Result(List<Statement> statements, Long expireMillis) {
65 mStatements = statements;
66 mExpireMillis = expireMillis;
67 }
68
69 public static Result create(List<Statement> statements, Long expireMillis) {
70 return new Result(statements, expireMillis);
71 }
72
73 @Override
74 public String toString() {
75 StringBuilder result = new StringBuilder();
76 result.append("Result: ");
77 result.append(mStatements.toString());
78 result.append(", mExpireMillis=");
79 result.append(mExpireMillis);
80 return result.toString();
81 }
82
83 @Override
84 public boolean equals(Object o) {
85 if (this == o) {
86 return true;
87 }
88 if (o == null || getClass() != o.getClass()) {
89 return false;
90 }
91
92 Result result = (Result) o;
93
94 if (!mExpireMillis.equals(result.mExpireMillis)) {
95 return false;
96 }
97 if (!mStatements.equals(result.mStatements)) {
98 return false;
99 }
100
101 return true;
102 }
103
104 @Override
105 public int hashCode() {
106 int result = mStatements.hashCode();
107 result = 31 * result + mExpireMillis.hashCode();
108 return result;
109 }
110 }
111
112 public DirectStatementRetriever(URLFetcher urlFetcher,
113 AndroidPackageInfoFetcher androidFetcher) {
114 this.mUrlFetcher = urlFetcher;
115 this.mAndroidFetcher = androidFetcher;
116 }
117
118 @Override
119 public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
120 if (source instanceof AndroidAppAsset) {
121 return retrieveFromAndroid((AndroidAppAsset) source);
122 } else if (source instanceof WebAsset) {
123 return retrieveFromWeb((WebAsset) source);
124 } else {
125 throw new AssociationServiceException("Namespace is not supported.");
126 }
127 }
128
129 private String computeAssociationJsonUrl(WebAsset asset) {
130 try {
131 return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
132 WELL_KNOWN_STATEMENT_PATH)
133 .toExternalForm();
134 } catch (MalformedURLException e) {
135 throw new AssertionError("Invalid domain name in database.");
136 }
137 }
138
139 private Result retrieveStatementFromUrl(String url, int maxIncludeLevel, AbstractAsset source)
140 throws AssociationServiceException {
141 List<Statement> statements = new ArrayList<Statement>();
142 if (maxIncludeLevel < 0) {
143 return Result.create(statements, DO_NOT_CACHE_RESULT);
144 }
145
146 WebContent webContent;
147 try {
148 webContent = mUrlFetcher.getWebContentFromUrl(new URL(url),
149 HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS);
150 } catch (IOException e) {
151 return Result.create(statements, DO_NOT_CACHE_RESULT);
152 }
153
154 try {
155 ParsedStatement result = StatementParser
156 .parseStatementList(webContent.getContent(), source);
157 statements.addAll(result.getStatements());
158 for (String delegate : result.getDelegates()) {
159 statements.addAll(
160 retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
161 .getStatements());
162 }
163 return Result.create(statements, webContent.getExpireTimeMillis());
164 } catch (JSONException e) {
165 return Result.create(statements, DO_NOT_CACHE_RESULT);
166 }
167 }
168
169 private Result retrieveFromWeb(WebAsset asset)
170 throws AssociationServiceException {
171 return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
172 }
173
174 private Result retrieveFromAndroid(AndroidAppAsset asset) throws AssociationServiceException {
175 try {
176 List<String> delegates = new ArrayList<String>();
177 List<Statement> statements = new ArrayList<Statement>();
178
179 List<String> certFps = mAndroidFetcher.getCertFingerprints(asset.getPackageName());
180 if (!Utils.hasCommonString(certFps, asset.getCertFingerprints())) {
181 throw new AssociationServiceException(
182 "Specified certs don't match the installed app.");
183 }
184
185 AndroidAppAsset actualSource = AndroidAppAsset.create(asset.getPackageName(), certFps);
186 for (String statementJson : mAndroidFetcher.getStatements(asset.getPackageName())) {
187 ParsedStatement result =
188 StatementParser.parseStatement(statementJson, actualSource);
189 statements.addAll(result.getStatements());
190 delegates.addAll(result.getDelegates());
191 }
192
193 for (String delegate : delegates) {
194 statements.addAll(retrieveStatementFromUrl(delegate, MAX_INCLUDE_LEVEL,
195 actualSource).getStatements());
196 }
197
198 return Result.create(statements, DO_NOT_CACHE_RESULT);
199 } catch (JSONException | NameNotFoundException e) {
200 Log.w(DirectStatementRetriever.class.getSimpleName(), e);
201 return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
202 }
203 }
204}