summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author William Loh <wloh@google.com> 2024-10-18 17:54:01 -0700
committer William Loh <wloh@google.com> 2024-10-31 14:48:18 -0700
commit9a55bf3590c138313102d932691bbb78690c70e8 (patch)
tree8511f42666c0d3a328d8c714e2e743a3945c7828
parent88f97ae557999411b16661db8380e217a5bc2c36 (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
-rw-r--r--packages/StatementService/Android.bp1
-rw-r--r--packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt36
-rw-r--r--packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java145
-rw-r--r--packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java6
-rw-r--r--packages/StatementService/src/com/android/statementservice/retriever/Statement.java51
-rw-r--r--packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt30
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
+ }
}