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
diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp
index ff1a756..90e1808 100644
--- a/packages/StatementService/Android.bp
+++ b/packages/StatementService/Android.bp
@@ -35,6 +35,7 @@
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 455e8085..ad137400 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.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 @@
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 0000000..dc27e12
--- /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 ce063ea..7635e82 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 @@
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 f8bab3e..b5e2046 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 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 @@
* {
* "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 @@
* 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 @@
* + "\"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 @@
}
/**
+ * 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 @@
* </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 @@
if (!mSource.equals(statement.mSource)) {
return false;
}
+ if (!mDynamicAppLinkComponents.equals(statement.mDynamicAppLinkComponents)) {
+ return false;
+ }
return true;
}
@@ -128,6 +164,7 @@
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 @@
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 4837aad..47c69b4 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 @@
*/
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 @@
// 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
+ }
}