Home | History | Annotate | Download | only in retriever
      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