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