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 57 public List<Statement> getStatements() { 58 return mStatements; 59 } 60 61 @Override 62 public long getExpireMillis() { 63 return mExpireMillis; 64 } 65 66 private Result(List<Statement> statements, Long expireMillis) { 67 mStatements = statements; 68 mExpireMillis = expireMillis; 69 } 70 71 public static Result create(List<Statement> statements, Long expireMillis) { 72 return new Result(statements, expireMillis); 73 } 74 75 @Override 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 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 107 public int hashCode() { 108 int result = mStatements.hashCode(); 109 result = 31 * result + mExpireMillis.hashCode(); 110 return result; 111 } 112 } 113 114 public DirectStatementRetriever(URLFetcher urlFetcher, 115 AndroidPackageInfoFetcher androidFetcher) { 116 this.mUrlFetcher = urlFetcher; 117 this.mAndroidFetcher = androidFetcher; 118 } 119 120 @Override 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 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 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 178 private Result retrieveFromWeb(WebAsset asset) 179 throws AssociationServiceException { 180 return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset); 181 } 182 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