1 /*
2  * Copyright (C) 2020 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.network.retriever
18 
19 import android.content.Intent
20 import android.net.Network
21 import com.android.statementservice.retriever.AbstractAsset
22 import com.android.statementservice.retriever.AndroidAppAsset
23 import com.android.statementservice.retriever.Statement
24 import com.android.statementservice.retriever.WebAsset
25 import com.android.statementservice.utils.StatementUtils.tryOrNull
26 import kotlinx.coroutines.Dispatchers
27 import kotlinx.coroutines.async
28 import kotlinx.coroutines.awaitAll
29 import kotlinx.coroutines.withContext
30 import java.net.URL
31 
32 /**
33  * Retrieves the JSON configured at a given domain that's compliant with the Digital Asset Links
34  * specification, returning the list of statements which serve as assertions by the web server as
35  * to what other assets it can be connected with.
36  *
37  * Relevant to this app, it allows the website to report which Android app package and signature
38  * digest has been approved by the website owner, which considers them as the same author and safe
39  * to automatically delegate web [Intent]s to.
40  *
41  * The relevant data classes are [WebAsset], [AndroidAppAsset], and [Statement].
42  */
43 class StatementRetriever {
44 
45     companion object {
46         private const val HTTP_CONNECTION_TIMEOUT_MILLIS = 5000
47         private const val HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = (1024 * 1024).toLong()
48         private const val MAX_INCLUDE_LEVEL = 1
49         private const val WELL_KNOWN_STATEMENT_PATH = "/.well-known/assetlinks.json"
50     }
51 
52     private val fetcher = UrlFetcher()
53 
54     data class Result(
55         val statements: List<Statement>,
56         val responseCode: Int?
57     ) {
58         companion object {
59             val EMPTY = Result(emptyList(), null)
60         }
61 
62         constructor(statements: List<Statement>, webResult: UrlFetcher.Response) : this(
63             statements,
64             webResult.responseCode
65         )
66     }
67 
68     suspend fun retrieve(source: AbstractAsset, network: Network? = null) = when (source) {
69         // TODO:(b/171219506): Does this have to be implemented?
70         is AndroidAppAsset -> null
71         is WebAsset -> retrieveFromWeb(source, network)
72         else -> null
73     }
74 
75     private suspend fun retrieveFromWeb(asset: WebAsset, network: Network? = null): Result? {
76         val url = computeAssociationJsonUrl(asset) ?: return null
77         return retrieve(url, MAX_INCLUDE_LEVEL, asset, network)
78     }
79 
80     private fun computeAssociationJsonUrl(asset: WebAsset) = tryOrNull {
81         URL(asset.scheme, asset.domain, asset.port, WELL_KNOWN_STATEMENT_PATH).toExternalForm()
82     }
83 
84     private suspend fun retrieve(
85         urlString: String,
86         maxIncludeLevel: Int,
87         source: AbstractAsset,
88         network: Network? = null
89     ): Result {
90         if (maxIncludeLevel < 0) {
91             return Result.EMPTY
92         }
93 
94         return withContext(Dispatchers.IO) {
95             val url = try {
96                 @Suppress("BlockingMethodInNonBlockingContext")
97                 URL(urlString)
98             } catch (ignored: Exception) {
99                 return@withContext Result.EMPTY
100             }
101 
102             val webResponse = fetcher.fetch(
103                 url = url,
104                 connectionTimeoutMillis = HTTP_CONNECTION_TIMEOUT_MILLIS,
105                 fileSizeLimit = HTTP_CONTENT_SIZE_LIMIT_IN_BYTES,
106                 network
107             ).successValueOrNull() ?: return@withContext Result.EMPTY
108 
109             val content = webResponse.content ?: return@withContext Result(emptyList(), webResponse)
110             val (statements, delegates) = StatementParser.parseStatementList(content, source)
111                 .successValueOrNull() ?: return@withContext Result(emptyList(), webResponse)
112 
113             val delegatedStatements = delegates
114                 .map { async { retrieve(it, maxIncludeLevel - 1, source).statements } }
115                 .awaitAll()
116                 .flatten()
117 
118             Result(statements + delegatedStatements, webResponse)
119         }
120     }
121 }
122