diff options
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 + } } |