diff options
author | 2024-10-18 17:54:01 -0700 | |
---|---|---|
committer | 2024-10-31 14:48:18 -0700 | |
commit | 9a55bf3590c138313102d932691bbb78690c70e8 (patch) | |
tree | 8511f42666c0d3a328d8c714e2e743a3945c7828 | |
parent | 88f97ae557999411b16661db8380e217a5bc2c36 (diff) |
Update statementservice assetlink.json parsing
This updates the statement service json parser to parse the new
relation_extensions introduced to the digital assetlinks protocol for
dynamic app links.
The parser is also changed to take the last value in a json object if
multiple values for the same key is found. This is done to make it
consistent with the json parser used in the DAL service used by GMS Core
devices.
Bug: 307557449
Test: manual
Flag: EXEMPT external library
Change-Id: Id550a53f2aa932959f27ecdb9556b6dde0c5fb52
6 files changed, 257 insertions, 12 deletions
diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp index ff1a756479b6..90e1808b7d44 100644 --- a/packages/StatementService/Android.bp +++ b/packages/StatementService/Android.bp @@ -35,6 +35,7 @@ android_app { privileged: true, certificate: "platform", static_libs: [ + "StatementServiceParser", "androidx.appcompat_appcompat", "androidx.collection_collection-ktx", "androidx.work_work-runtime", diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt index 455e8085af50..ad137400fa86 100644 --- a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt +++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt @@ -28,6 +28,8 @@ import java.io.StringReader import java.util.ArrayList import com.android.statementservice.retriever.WebAsset import com.android.statementservice.retriever.AndroidAppAsset +import com.android.statementservice.retriever.DynamicAppLinkComponent +import org.json.JSONObject /** * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset], @@ -97,13 +99,45 @@ object StatementParser { FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION) ) val target = AssetFactory.create(targetObject) + val dynamicAppLinkComponents = parseDynamicAppLinkComponents( + statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS) + ) val statements = (0 until relations.length()) .map { relations.getString(it) } .map(Relation::create) - .map { Statement.create(source, target, it) } + .map { Statement.create(source, target, it, dynamicAppLinkComponents) } return Result.Success(ParsedStatement(statements, listOfNotNull(delegate))) } + private fun parseDynamicAppLinkComponents( + statement: JSONObject? + ): List<DynamicAppLinkComponent> { + val relationExtensions = statement?.optJSONObject( + StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS + ) ?: return emptyList() + val handleAllUrlsRelationExtension = relationExtensions.optJSONObject( + StatementUtils.RELATION.toString() + ) ?: return emptyList() + val components = handleAllUrlsRelationExtension.optJSONArray( + StatementUtils.RELATION_EXTENSION_FIELD_DAL_COMPONENTS + ) ?: return emptyList() + + return (0 until components.length()) + .map { components.getJSONObject(it) } + .map { parseComponent(it) } + } + + private fun parseComponent(component: JSONObject): DynamicAppLinkComponent { + val query = component.optJSONObject("?") + return DynamicAppLinkComponent.create( + component.optBoolean("exclude", false), + component.optString("#"), + component.optString("/"), + query?.keys()?.asSequence()?.associateWith { query.getString(it) }, + component.optString("comments") + ) + } + data class ParsedStatement(val statements: List<Statement>, val delegates: List<String>) } diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java new file mode 100644 index 000000000000..dc27e125e204 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 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.annotation.Nullable; + +import java.util.Map; + +/** + * A immutable value type representing a dynamic app link component + */ +public final class DynamicAppLinkComponent { + private final boolean mExclude; + private final String mFragment; + private final String mPath; + private final Map<String, String> mQuery; + private final String mComments; + + private DynamicAppLinkComponent(boolean exclude, String fragment, String path, + Map<String, String> query, String comments) { + mExclude = exclude; + mFragment = fragment; + mPath = path; + mQuery = query; + mComments = comments; + } + + /** + * Returns true or false indicating whether this rule should be a exclusion rule. + */ + public boolean getExclude() { + return mExclude; + } + + /** + * Returns a optional pattern string for matching URL fragments. + */ + @Nullable + public String getFragment() { + return mFragment; + } + + /** + * Returns a optional pattern string for matching URL paths. + */ + @Nullable + public String getPath() { + return mPath; + } + + /** + * Returns a optional pattern string for matching a single key-value pair in the URL query + * params. + */ + @Nullable + public Map<String, String> getQuery() { + return mQuery; + } + + /** + * Returns a optional comment string for this component. + */ + @Nullable + public String getComments() { + return mComments; + } + + /** + * Creates a new DynamicAppLinkComponent object. + */ + public static DynamicAppLinkComponent create(boolean exclude, String fragment, String path, + Map<String, String> query, String comments) { + return new DynamicAppLinkComponent(exclude, fragment, path, query, comments); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DynamicAppLinkComponent rule = (DynamicAppLinkComponent) o; + + if (mExclude != rule.mExclude) { + return false; + } + if (!mFragment.equals(rule.mFragment)) { + return false; + } + if (!mPath.equals(rule.mPath)) { + return false; + } + if (!mQuery.equals(rule.mQuery)) { + return false; + } + if (!mComments.equals(rule.mComments)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = Boolean.hashCode(mExclude); + result = 31 * result + mFragment.hashCode(); + result = 31 * result + mPath.hashCode(); + result = 31 * result + mQuery.hashCode(); + result = 31 * result + mComments.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder statement = new StringBuilder(); + statement.append("HandleAllUriRule: "); + statement.append(mExclude); + statement.append(", "); + statement.append(mFragment); + statement.append(", "); + statement.append(mPath); + statement.append(", "); + statement.append(mQuery); + statement.append(", "); + statement.append(mComments); + return statement.toString(); + } +} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java index ce063ea5c143..7635e8234dc0 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java @@ -46,12 +46,6 @@ public final class JsonParser { while (reader.hasNext()) { String fieldName = reader.nextName(); - if (output.has(fieldName)) { - errorMsg = "Duplicate field name."; - reader.skipValue(); - continue; - } - JsonToken token = reader.peek(); if (token.equals(JsonToken.BEGIN_ARRAY)) { output.put(fieldName, new JSONArray(parseArray(reader))); diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java index f8bab3ef170f..b5e204651307 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java @@ -23,6 +23,10 @@ import com.android.statementservice.network.retriever.StatementRetriever; import kotlin.coroutines.Continuation; +import java.util.Collections; +import java.util.List; + + /** * An immutable value type representing a statement, consisting of a source, target, and relation. * This reflects an assertion that the relation holds for the source, target pair. For example, if a @@ -32,7 +36,21 @@ import kotlin.coroutines.Continuation; * { * "relation": ["delegate_permission/common.handle_all_urls"], * "target" : {"namespace": "android_app", "package_name": "com.example.app", - * "sha256_cert_fingerprints": ["00:11:22:33"] } + * "sha256_cert_fingerprints": ["00:11:22:33"] }, + * "relation_extensions": { + * "delegate_permission/common_handle_all_urls": { + * "dynamic_app_link_components": [ + * { + * "/": "/foo*", + * "exclude": true, + * "comments": "App should not handle paths that start with foo" + * }, + * { + * "/": "*", + * "comments": "Catch all other paths" + * } + * ] + * } * } * </pre> * @@ -40,7 +58,7 @@ import kotlin.coroutines.Continuation; * return a {@link Statement} with {@link #getSource} equal to the input parameter, * {@link #getRelation} equal to * - * <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre> + * <pre>Relation.create("delegate_permission", "common.handle_all_urls");</pre> * * and with {@link #getTarget} equal to * @@ -48,17 +66,23 @@ import kotlin.coroutines.Continuation; * + "\"package_name\": \"com.example.app\"}" * + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}"); * </pre> + * + * If extensions exist for the handle_all_urls relation then {@link #getDynamicAppLinkComponents} + * will return a list of parsed {@link DynamicAppLinkComponent}s. */ public final class Statement { private final AbstractAsset mTarget; private final Relation mRelation; private final AbstractAsset mSource; + private final List<DynamicAppLinkComponent> mDynamicAppLinkComponents; - private Statement(AbstractAsset source, AbstractAsset target, Relation relation) { + private Statement(AbstractAsset source, AbstractAsset target, Relation relation, + List<DynamicAppLinkComponent> components) { mSource = source; mTarget = target; mRelation = relation; + mDynamicAppLinkComponents = Collections.unmodifiableList(components); } /** @@ -86,6 +110,14 @@ public final class Statement { } /** + * Returns the relation matching rules of the statement. + */ + @NonNull + public List<DynamicAppLinkComponent> getDynamicAppLinkComponents() { + return mDynamicAppLinkComponents; + } + + /** * Creates a new Statement object for the specified target asset and relation. For example: * <pre> * Asset asset = Asset.Factory.create( @@ -95,8 +127,9 @@ public final class Statement { * </pre> */ public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target, - @NonNull Relation relation) { - return new Statement(source, target, relation); + @NonNull Relation relation, + @NonNull List<DynamicAppLinkComponent> components) { + return new Statement(source, target, relation, components); } @Override @@ -119,6 +152,9 @@ public final class Statement { if (!mSource.equals(statement.mSource)) { return false; } + if (!mDynamicAppLinkComponents.equals(statement.mDynamicAppLinkComponents)) { + return false; + } return true; } @@ -128,6 +164,7 @@ public final class Statement { int result = mTarget.hashCode(); result = 31 * result + mRelation.hashCode(); result = 31 * result + mSource.hashCode(); + result = 31 * result + mDynamicAppLinkComponents.hashCode(); return result; } @@ -140,6 +177,10 @@ public final class Statement { statement.append(mTarget); statement.append(", "); statement.append(mRelation); + if (!mDynamicAppLinkComponents.isEmpty()) { + statement.append(", "); + statement.append(mDynamicAppLinkComponents); + } return statement.toString(); } } diff --git a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt index 4837aad3a025..47c69b4b3a44 100644 --- a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt +++ b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt @@ -17,8 +17,17 @@ package com.android.statementservice.utils import android.content.Context +import android.content.UriRelativeFilter +import android.content.UriRelativeFilter.FRAGMENT +import android.content.UriRelativeFilter.PATH +import android.content.UriRelativeFilter.QUERY +import android.content.UriRelativeFilterGroup +import android.content.UriRelativeFilterGroup.ACTION_ALLOW +import android.content.UriRelativeFilterGroup.ACTION_BLOCK import android.content.pm.PackageManager import android.util.Patterns +import com.android.statementservice.parser.parseMatchingExpression +import com.android.statementservice.retriever.DynamicAppLinkComponent import com.android.statementservice.retriever.Relation import java.net.URL import java.security.MessageDigest @@ -52,7 +61,9 @@ internal object StatementUtils { */ const val ASSET_DESCRIPTOR_FIELD_RELATION = "relation" const val ASSET_DESCRIPTOR_FIELD_TARGET = "target" + const val ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS = "relation_extensions" const val DELEGATE_FIELD_DELEGATE = "include" + const val RELATION_EXTENSION_FIELD_DAL_COMPONENTS = "dynamic_app_link_components" val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') @@ -160,4 +171,23 @@ internal object StatementUtils { // Hosts with *. for wildcard subdomain support are verified against their root domain fun createWebAssetString(host: String) = WEB_ASSET_FORMAT.format(URL("https", host.removePrefix("*."), "").toString()) + + fun createUriRelativeFilterGroup(component: DynamicAppLinkComponent): UriRelativeFilterGroup { + val group = UriRelativeFilterGroup(if (component.exclude) ACTION_BLOCK else ACTION_ALLOW) + component.fragment?.let { + val (type, filter) = parseMatchingExpression(it) + group.addUriRelativeFilter(UriRelativeFilter(FRAGMENT, type, filter)) + } + component.path?.let { + val (type, filter) = parseMatchingExpression(it) + group.addUriRelativeFilter(UriRelativeFilter(PATH, type, filter)) + } + component.query?.let { + for ((k, v) in it) { + val (type, filter) = parseMatchingExpression(k + "=" + v) + group.addUriRelativeFilter(UriRelativeFilter(QUERY, type, filter)) + } + } + return group + } } |