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
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 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