/*
* Copyright (C) 2015 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.statementservice.retriever;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
import org.json.JSONException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An implementation of {@link AbstractStatementRetriever} that directly retrieves statements from
* the asset.
*/
/* package private */ final class DirectStatementRetriever extends AbstractStatementRetriever {
private static final long DO_NOT_CACHE_RESULT = 0L;
private static final int HTTP_CONNECTION_TIMEOUT_MILLIS = 5000;
private static final int HTTP_CONNECTION_BACKOFF_MILLIS = 3000;
private static final int HTTP_CONNECTION_RETRY = 3;
private static final long HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
private static final int MAX_INCLUDE_LEVEL = 1;
private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json";
private final URLFetcher mUrlFetcher;
private final AndroidPackageInfoFetcher mAndroidFetcher;
/**
* An immutable value type representing the retrieved statements and the expiration date.
*/
public static class Result implements AbstractStatementRetriever.Result {
private final List<Statement> mStatements;
private final Long mExpireMillis;
@Override
public List<Statement> getStatements() {
return mStatements;
}
@Override
public long getExpireMillis() {
return mExpireMillis;
}
private Result(List<Statement> statements, Long expireMillis) {
mStatements = statements;
mExpireMillis = expireMillis;
}
public static Result create(List<Statement> statements, Long expireMillis) {
return new Result(statements, expireMillis);
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append("Result: ");
result.append(mStatements.toString());
result.append(", mExpireMillis=");
result.append(mExpireMillis);
return result.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Result result = (Result) o;
if (!mExpireMillis.equals(result.mExpireMillis)) {
return false;
}
if (!mStatements.equals(result.mStatements)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = mStatements.hashCode();
result = 31 * result + mExpireMillis.hashCode();
return result;
}
}
public DirectStatementRetriever(URLFetcher urlFetcher,
AndroidPackageInfoFetcher androidFetcher) {
this.mUrlFetcher = urlFetcher;
this.mAndroidFetcher = androidFetcher;
}
@Override
public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
if (source instanceof AndroidAppAsset) {
return retrieveFromAndroid((AndroidAppAsset) source);
} else if (source instanceof WebAsset) {
return retrieveFromWeb((WebAsset) source);
} else {
throw new AssociationServiceException("Namespace is not supported.");
}
}
private String computeAssociationJsonUrl(WebAsset asset) {
try {
return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
WELL_KNOWN_STATEMENT_PATH)
.toExternalForm();
} catch (MalformedURLException e) {
throw new AssertionError("Invalid domain name in database.");
}
}
private Result retrieveStatementFromUrl(String urlString, int maxIncludeLevel,
AbstractAsset source)
throws AssociationServiceException {
List<Statement> statements = new ArrayList<Statement>();
if (maxIncludeLevel < 0) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
WebContent webContent;
try {
URL url = new URL(urlString);
if (!source.followInsecureInclude()
&& !url.getProtocol().toLowerCase().equals("https")) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
webContent = mUrlFetcher.getWebContentFromUrlWithRetry(url,
HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS,
HTTP_CONNECTION_BACKOFF_MILLIS, HTTP_CONNECTION_RETRY);
} catch (IOException | InterruptedException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
try {
ParsedStatement result = StatementParser
.parseStatementList(webContent.getContent(), source);
statements.addAll(result.getStatements());
for (String delegate : result.getDelegates()) {
statements.addAll(
retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
.getStatements());
}
return Result.create(statements, webContent.getExpireTimeMillis());
} catch (JSONException | IOException e) {
return Result.create(statements, DO_NOT_CACHE_RESULT);
}
}
private Result retrieveFromWeb(WebAsset asset)
throws AssociationServiceException {
return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
}
private Result retrieveFromAndroid(AndroidAppAsset asset) throws AssociationServiceException {
try {
List<String> delegates = new ArrayList<String>();
List<Statement> statements = new ArrayList<Statement>();
List<String> certFps = mAndroidFetcher.getCertFingerprints(asset.getPackageName());
if (!Utils.hasCommonString(certFps, asset.getCertFingerprints())) {
throw new AssociationServiceException(
"Specified certs don't match the installed app.");
}
AndroidAppAsset actualSource = AndroidAppAsset.create(asset.getPackageName(), certFps);
for (String statementJson : mAndroidFetcher.getStatements(asset.getPackageName())) {
ParsedStatement result =
StatementParser.parseStatement(statementJson, actualSource);
statements.addAll(result.getStatements());
delegates.addAll(result.getDelegates());
}
for (String delegate : delegates) {
statements.addAll(retrieveStatementFromUrl(delegate, MAX_INCLUDE_LEVEL,
actualSource).getStatements());
}
return Result.create(statements, DO_NOT_CACHE_RESULT);
} catch (JSONException | IOException | NameNotFoundException e) {
Log.w(DirectStatementRetriever.class.getSimpleName(), e);
return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
}
}
}