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