Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9056557d authored by Winson's avatar Winson
Browse files

Add DomainVerificationCollector

Gathers the domains declared in a package's manifest under <activity>
entries for use with domain verification auto verification and user
selection.

The behavior is split between v1 and v2, for apps that target pre-S,
as the set of domains that are verified must be maintained until the
app author is able to update to the stricter S schema.

The S schema is defined as part of the JavaDoc on the @ChangeId, but
will also be included in the developer docs once these changes are
merged.

Exempt-From-Owner-Approval: Already approved by owners on main branch

Bug: 163565712

Test: atest DomainVerificationCollectorTest

Change-Id: I58c9d2b3001f48b9904aa4617d546a8cee0d926e
parent 5a10db6a
Loading
Loading
Loading
Loading
+216 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.server.pm.domain.verify;

import android.annotation.NonNull;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.parsing.component.ParsedActivity;
import android.content.pm.parsing.component.ParsedIntentInfo;
import android.os.Binder;
import android.os.Build;
import android.util.ArraySet;
import android.util.Patterns;

import com.android.server.SystemConfig;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.parsing.pkg.AndroidPackage;

import java.util.List;
import java.util.Set;

public class DomainVerificationCollector {

    @NonNull
    private final PlatformCompat mPlatformCompat;

    @NonNull
    private final SystemConfig mSystemConfig;

    public DomainVerificationCollector(@NonNull PlatformCompat platformCompat,
            @NonNull SystemConfig systemConfig) {
        mPlatformCompat = platformCompat;
        mSystemConfig = systemConfig;
    }

    /**
     * With the updated form of the app links verification APIs, an app will be required to declare
     * domains inside an intent filter which includes all of the following:
     * <ul>
     *     <li>- android:autoVerify="true"</li>
     *     <li>- Intent.ACTION_VIEW</li>
     *     <li>- Intent.CATEGORY_BROWSABLE</li>
     *     <li>- Intent.CATEGORY_DEFAULT</li>
     *     <li>- Only IntentFilter.SCHEME_HTTP and/or IntentFilter.SCHEME_HTTPS,
     *           with no other schemes</li>
     * </ul>
     *
     * On prior versions of Android, Intent.CATEGORY_BROWSABLE was not a requirement, other
     * schemes were allowed, and setting autoVerify to true in any intent filter would implicitly
     * pretend that all intent filters were set to autoVerify="true".
     */
    @ChangeId
    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
    public static final long RESTRICT_DOMAINS = 175408749L;

    @NonNull
    public ArraySet<String> collectAllWebDomains(@NonNull AndroidPackage pkg) {
        return collectDomains(pkg, false);
    }

    /**
     * Effectively {@link #collectAllWebDomains(AndroidPackage)}, but requires
     * {@link IntentFilter#getAutoVerify()} == true.
     */
    @NonNull
    public ArraySet<String> collectAutoVerifyDomains(@NonNull AndroidPackage pkg) {
        return collectDomains(pkg, true);
    }

    @NonNull
    private ArraySet<String> collectDomains(@NonNull AndroidPackage pkg,
            boolean checkAutoVerify) {
        @SuppressWarnings("ConstantConditions")
        boolean restrictDomains = Binder.withCleanCallingIdentity(
                () -> mPlatformCompat.isChangeEnabled(RESTRICT_DOMAINS, buildMockAppInfo(pkg)));

        ArraySet<String> domains = new ArraySet<>();

        if (restrictDomains) {
            collectDomains(domains, pkg, checkAutoVerify);
        } else {
            collectDomainsLegacy(domains, pkg, checkAutoVerify);
        }

        return domains;
    }

    /** @see #RESTRICT_DOMAINS */
    private void collectDomainsLegacy(@NonNull Set<String> domains,
            @NonNull AndroidPackage pkg, boolean checkAutoVerify) {
        if (!checkAutoVerify) {
            // Per-domain user selection state doesn't have a V1 equivalent on S, so just use V2
            collectDomains(domains, pkg, false);
            return;
        }

        List<ParsedActivity> activities = pkg.getActivities();
        int activitiesSize = activities.size();

        // Due to a bug in the platform, for backwards compatibility, assume that all linked apps
        // require auto verification, even if they forget to mark their manifest as such.
        boolean needsAutoVerify = mSystemConfig.getLinkedApps().contains(pkg.getPackageName());
        if (!needsAutoVerify) {
            for (int activityIndex = 0; activityIndex < activitiesSize && !needsAutoVerify;
                    activityIndex++) {
                ParsedActivity activity = activities.get(activityIndex);
                List<ParsedIntentInfo> intents = activity.getIntents();
                int intentsSize = intents.size();
                for (int intentIndex = 0; intentIndex < intentsSize && !needsAutoVerify;
                        intentIndex++) {
                    ParsedIntentInfo intent = intents.get(intentIndex);
                    needsAutoVerify = intent.needsVerification();
                }
            }

            if (!needsAutoVerify) {
                return;
            }
        }

        for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) {
            ParsedActivity activity = activities.get(activityIndex);
            List<ParsedIntentInfo> intents = activity.getIntents();
            int intentsSize = intents.size();
            for (int intentIndex = 0; intentIndex < intentsSize; intentIndex++) {
                ParsedIntentInfo intent = intents.get(intentIndex);
                if (intent.handlesWebUris(false)) {
                    int authorityCount = intent.countDataAuthorities();
                    for (int index = 0; index < authorityCount; index++) {
                        domains.add(intent.getDataAuthority(index).getHost());
                    }
                }
            }
        }
    }

    /** @see #RESTRICT_DOMAINS */
    private void collectDomains(@NonNull Set<String> domains,
            @NonNull AndroidPackage pkg, boolean checkAutoVerify) {
        List<ParsedActivity> activities = pkg.getActivities();
        int activitiesSize = activities.size();
        for (int activityIndex = 0; activityIndex < activitiesSize; activityIndex++) {
            ParsedActivity activity = activities.get(activityIndex);
            List<ParsedIntentInfo> intents = activity.getIntents();
            int intentsSize = intents.size();
            for (int intentIndex = 0; intentIndex < intentsSize; intentIndex++) {
                ParsedIntentInfo intent = intents.get(intentIndex);
                if (checkAutoVerify && !intent.getAutoVerify()) {
                    continue;
                }

                if (!intent.hasCategory(Intent.CATEGORY_DEFAULT)
                        || !intent.handlesWebUris(checkAutoVerify)) {
                    continue;
                }

                // TODO(b/159952358): There seems to be no way to associate the exact host
                //  with its scheme, meaning all hosts have to be verified as if they were
                //  web schemes. This means that given the following:
                //  <intent-filter android:autoVerify="true">
                //      ...
                //      <data android:scheme="https" android:host="one.example.com"/>
                //      <data android:scheme="https" android:host="two.example.com"/>
                //      <data android:host="three.example.com"/>
                //      <data android:scheme="nonWeb" android:host="four.example.com"/>
                //  </intent-filter>
                //  The verification agent will be asked to verify four.example.com, which the
                //  app will probably fail. This can be re-configured to work properly by the
                //  app developer by declaring a separate intent-filter. This may not be worth
                //  fixing.
                int authorityCount = intent.countDataAuthorities();
                for (int index = 0; index < authorityCount; index++) {
                    String host = intent.getDataAuthority(index).getHost();
                    // It's easy to misconfigure autoVerify intent filters, so to avoid
                    // adding unintended hosts, check if the host is an HTTP domain.
                    if (Patterns.DOMAIN_NAME.matcher(host).matches()) {
                        domains.add(host);
                    }
                }
            }
        }
    }

    /**
     * Passed to {@link PlatformCompat} because this can be invoked mid-install process, and
     * {@link PlatformCompat} will not be able to query the pending {@link ApplicationInfo} from
     * {@link PackageManager}.
     *
     * TODO(b/177613575): Can a different API be used?
     */
    @NonNull
    private ApplicationInfo buildMockAppInfo(@NonNull AndroidPackage pkg) {
        ApplicationInfo appInfo = new ApplicationInfo();
        appInfo.packageName = pkg.getPackageName();
        appInfo.targetSdkVersion = pkg.getTargetSdkVersion();
        return appInfo;
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ android_test {
        "androidx.test.runner",
        "junit",
        "services.core",
        "servicestests-utils",
        "truth-prebuilt",
    ],
    platform_apis: true,
+304 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.server.pm.test.domain.verify

import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.parsing.component.ParsedActivity
import android.content.pm.parsing.component.ParsedIntentInfo
import android.os.Build
import android.os.PatternMatcher
import android.util.ArraySet
import com.android.server.SystemConfig
import com.android.server.compat.PlatformCompat
import com.android.server.pm.domain.verify.DomainVerificationCollector
import com.android.server.pm.parsing.pkg.AndroidPackage
import com.android.server.testutils.mockThrowOnUnmocked
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.mockito.Mockito.any
import org.mockito.Mockito.eq

class DomainVerificationCollectorTest {

    companion object {
        private const val TEST_PKG_NAME = "com.test.pkg"
    }

    private val platformCompat: PlatformCompat = mockThrowOnUnmocked {
        whenever(isChangeEnabled(eq(DomainVerificationCollector.RESTRICT_DOMAINS), any())) {
            (arguments[1] as ApplicationInfo).targetSdkVersion >= Build.VERSION_CODES.S
        }
    }

    @Test
    fun verifyV1() {
        val pkg = mockPkg(useV2 = false, autoVerify = true)
        val collector = mockCollector()
        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com", "example4.com")
    }

    @Test
    fun verifyV1NoAutoVerify() {
        val pkg = mockPkg(useV2 = false, autoVerify = false)
        val collector = mockCollector()
        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg)).isEmpty()
    }

    @Test
    fun verifyV1ForceAutoVerify() {
        val pkg = mockPkg(useV2 = false, autoVerify = false)
        val collector = mockCollector(linkedApps = setOf(TEST_PKG_NAME))
        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com", "example4.com")
    }

    @Test
    fun verifyV1NoValidIntentFilter() {
        val pkg = mockThrowOnUnmocked<AndroidPackage> {
            whenever(packageName) { TEST_PKG_NAME }
            whenever(targetSdkVersion) { Build.VERSION_CODES.R }

            val activityList = listOf(
                    ParsedActivity().apply {
                        addIntent(
                                ParsedIntentInfo().apply {
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("http")
                                    addDataScheme("https")
                                    addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example1.com", null)
                                }
                        )
                    },
                    ParsedActivity().apply {
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(true)
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("http")
                                    addDataScheme("https")

                                    // The presence of a non-web-scheme as the only autoVerify
                                    // intent-filter, when non-forced, means that v1 will not pick
                                    // up the package for verification.
                                    addDataScheme("nonWebScheme")
                                    addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example2.com", null)
                                }
                        )
                    },
            )

            whenever(activities) { activityList }
        }

        val collector = mockCollector()
        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com")
        assertThat(collector.collectAutoVerifyDomains(pkg)).isEmpty()
    }

    @Test
    fun verifyV2() {
        val pkg = mockPkg(useV2 = true, autoVerify = true)
        val collector = mockCollector()

        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg))
                .containsExactly("example1.com", "example3.com")
    }

    @Test
    fun verifyV2NoAutoVerify() {
        val pkg = mockPkg(useV2 = true, autoVerify = false)
        val collector = mockCollector()

        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg)).isEmpty()
    }

    @Test
    fun verifyV2ForceAutoVerifyIgnored() {
        val pkg = mockPkg(useV2 = true, autoVerify = false)
        val collector = mockCollector(linkedApps = setOf(TEST_PKG_NAME))

        assertThat(collector.collectAllWebDomains(pkg))
                .containsExactly("example1.com", "example2.com", "example3.com")
        assertThat(collector.collectAutoVerifyDomains(pkg)).isEmpty()
    }

    private fun mockCollector(linkedApps: Set<String> = emptySet()): DomainVerificationCollector {
        val systemConfig = mockThrowOnUnmocked<SystemConfig> {
            whenever(this.linkedApps) { ArraySet(linkedApps) }
        }

        return DomainVerificationCollector(platformCompat, systemConfig)
    }

    private fun mockPkg(useV2: Boolean, autoVerify: Boolean): AndroidPackage {
        // Translate equivalent of the following manifest declaration. This string isn't actually
        // parsed, but it's a far easier to read representation of the test data.
        // language=XML
        """
            <xml>
                <intent-filter android:autoVerify="$autoVerify">
                    <action android:name="android.intent.action.VIEW"/>
                    <category android:name="android.intent.category.BROWSABLE"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                    <data android:scheme="http"/>
                    <data android:scheme="https"/>
                    <data android:path="/sub"/>
                    <data android:host="example1.com"/>
                </intent-filter>
                <intent-filter>
                    <action android:name="android.intent.action.VIEW"/>
                    <category android:name="android.intent.category.BROWSABLE"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                    <data android:scheme="http"/>
                    <data android:path="/sub2"/>
                    <data android:host="example2.com"/>
                </intent-filter>
                <intent-filter android:autoVerify="$autoVerify">
                    <action android:name="android.intent.action.VIEW"/>
                    <category android:name="android.intent.category.BROWSABLE"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                    <data android:scheme="https"/>
                    <data android:path="/sub3"/>
                    <data android:host="example3.com"/>
                </intent-filter>
                <intent-filter android:autoVerify="$autoVerify">
                    <action android:name="android.intent.action.VIEW"/>
                    <category android:name="android.intent.category.BROWSABLE"/>
                    <data android:scheme="https"/>
                    <data android:path="/sub4"/>
                    <data android:host="example4.com"/>
                </intent-filter>
                <intent-filter android:autoVerify="$autoVerify">
                    <action android:name="android.intent.action.VIEW"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                    <data android:scheme="https"/>
                    <data android:path="/sub5"/>
                    <data android:host="example5.com"/>
                </intent-filter>
                <intent-filter android:autoVerify="$autoVerify">
                    <category android:name="android.intent.category.BROWSABLE"/>
                    <category android:name="android.intent.category.DEFAULT"/>
                    <data android:scheme="https"/>
                    <data android:path="/sub5"/>
                    <data android:host="example5.com"/>
                </intent-filter>
            </xml>
        """.trimIndent()

        return mockThrowOnUnmocked<AndroidPackage> {
            whenever(packageName) { TEST_PKG_NAME }
            whenever(targetSdkVersion) {
                if (useV2) Build.VERSION_CODES.S else Build.VERSION_CODES.R
            }

            // The intents are split into separate Activities to test that multiple are collected
            val activityList = listOf(
                    ParsedActivity().apply {
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(autoVerify)
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("http")
                                    addDataScheme("https")
                                    addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example1.com", null)
                                }
                        )
                        addIntent(
                                ParsedIntentInfo().apply {
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("http")
                                    addDataPath("/sub2", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example2.com", null)
                                }
                        )
                    },
                    ParsedActivity().apply {
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(autoVerify)
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("https")
                                    addDataPath("/sub3", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example3.com", null)
                                }
                        )
                    },
                    ParsedActivity().apply {
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(autoVerify)
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addDataScheme("https")
                                    addDataPath("/sub4", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example4.com", null)
                                }
                        )
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(autoVerify)
                                    addAction(Intent.ACTION_VIEW)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("https")
                                    addDataPath("/sub5", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example5.com", null)
                                }
                        )
                        addIntent(
                                ParsedIntentInfo().apply {
                                    setAutoVerify(autoVerify)
                                    addCategory(Intent.CATEGORY_BROWSABLE)
                                    addCategory(Intent.CATEGORY_DEFAULT)
                                    addDataScheme("https")
                                    addDataPath("/sub6", PatternMatcher.PATTERN_LITERAL)
                                    addDataAuthority("example6.com", null)
                                }
                        )
                    },
            )

            whenever(activities) { activityList }
        }
    }
}
+22 −13

File changed.

Preview size limit exceeded, changes collapsed.