chromium/chrome/android/javatests/src/org/chromium/chrome/browser/ssl/CaptivePortalTest.java

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.ssl;

import android.util.Base64;

import androidx.annotation.IntDef;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.test.params.ParameterizedCommandLineFlags;
import org.chromium.base.test.params.ParameterizedCommandLineFlags.Switches;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.browser.TabTitleObserver;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.security_interstitials.CaptivePortalHelper;
import org.chromium.net.X509Util;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.ServerCertificate;
import org.chromium.net.test.util.CertTestUtil;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** Tests for the Captive portal interstitial. */
@RunWith(ChromeJUnit4ClassRunner.class)
@MediumTest
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@ParameterizedCommandLineFlags({
    @Switches(),
    @Switches("enable-features=" + ChromeFeatureList.CAPTIVE_PORTAL_CERTIFICATE_LIST),
})
public class CaptivePortalTest {
    private static final String CAPTIVE_PORTAL_INTERSTITIAL_TITLE_PREFIX = "Connect to";
    private static final String SSL_INTERSTITIAL_TITLE = "Privacy error";
    private static final int INTERSTITIAL_TITLE_UPDATE_TIMEOUT_SECONDS = 5;

    // UMA events copied from ssl_error_handler.h.
    @IntDef({
        UMAEvent.HANDLE_ALL,
        UMAEvent.SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE,
        UMAEvent.SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE,
        UMAEvent.SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE,
        UMAEvent.SHOW_SSL_INTERSTITIAL_OVERRIDABLE,
        UMAEvent.WWW_MISMATCH_FOUND,
        UMAEvent.WWW_MISMATCH_URL_AVAILABLE,
        UMAEvent.WWW_MISMATCH_URL_NOT_AVAILABLE,
        UMAEvent.SHOW_BAD_CLOCK,
        UMAEvent.CAPTIVE_PORTAL_CERT_FOUND,
        UMAEvent.WWW_MISMATCH_FOUND_IN_SAN,
        UMAEvent.SHOW_MITM_SOFTWARE_INTERSTITIAL,
        UMAEvent.OS_REPORTS_CAPTIVE_PORTAL,
        UMAEvent.SHOW_BLOCKED_INTERCEPTION_INTERSTITIAL,
        UMAEvent.SHOW_LEGACY_TLS_INTERSTITIAL
    })
    @Retention(RetentionPolicy.SOURCE)
    private @interface UMAEvent {
        int HANDLE_ALL = 0;
        int SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE = 1;
        int SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE = 2;
        int SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE = 3;
        int SHOW_SSL_INTERSTITIAL_OVERRIDABLE = 4;
        int WWW_MISMATCH_FOUND = 5; // Deprecated in M59 by WWW_MISMATCH_FOUND_IN_SAN.
        int WWW_MISMATCH_URL_AVAILABLE = 6;
        int WWW_MISMATCH_URL_NOT_AVAILABLE = 7;
        int SHOW_BAD_CLOCK = 8;
        int CAPTIVE_PORTAL_CERT_FOUND = 9;
        int WWW_MISMATCH_FOUND_IN_SAN = 10;
        int SHOW_MITM_SOFTWARE_INTERSTITIAL = 11;
        int OS_REPORTS_CAPTIVE_PORTAL = 12;
        int SHOW_BLOCKED_INTERCEPTION_INTERSTITIAL = 13;
        int SHOW_LEGACY_TLS_INTERSTITIAL = 14; // Deprecated in M98.
    }

    @Rule
    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();

    private EmbeddedTestServer mServer;

    @Before
    public void setUp() {
        mActivityTestRule.startMainActivityWithURL(UrlConstants.NTP_URL);
        mServer =
                EmbeddedTestServer.createAndStartHTTPSServer(
                        ApplicationProvider.getApplicationContext(),
                        ServerCertificate.CERT_MISMATCHED_NAME);

        CaptivePortalHelper.setOSReportsCaptivePortalForTesting(false);
        CaptivePortalHelper.setCaptivePortalCertificateForTesting("sha256/test");
    }

    /**
     * Navigate the tab to an interstitial with a name mismatch error and check if this /* results
     * in a captive portal interstitial.
     */
    private void navigateAndCheckCaptivePortalInterstitial() throws Exception {
        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        ChromeTabUtils.loadUrlOnUiThread(
                tab, mServer.getURL("/chrome/test/data/android/navigate/simple.html"));

        new TabTitleObserver(tab, CAPTIVE_PORTAL_INTERSTITIAL_TITLE_PREFIX) {
            @Override
            protected boolean doesTitleMatch(String expectedTitle, String actualTitle) {
                return actualTitle.indexOf(expectedTitle) == 0;
            }
        }.waitForTitleUpdate(INTERSTITIAL_TITLE_UPDATE_TIMEOUT_SECONDS);
        Assert.assertEquals(
                0,
                ChromeTabUtils.getTitleOnUiThread(tab)
                        .indexOf(CAPTIVE_PORTAL_INTERSTITIAL_TITLE_PREFIX));
    }

    @Test
    public void testCaptivePortalCertificateListFeature() throws Exception {
        // Add the SPKI of the root cert to captive portal certificate list.
        byte[] rootCertSPKI =
                CertTestUtil.getPublicKeySha256(
                        X509Util.createCertificateFromBytes(
                                CertTestUtil.pemToDer(mServer.getRootCertPemPath())));
        Assert.assertTrue(rootCertSPKI != null);
        CaptivePortalHelper.setCaptivePortalCertificateForTesting(
                "sha256/" + Base64.encodeToString(rootCertSPKI, Base64.NO_WRAP));

        navigateAndCheckCaptivePortalInterstitial();

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.HANDLE_ALL));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler",
                        UMAEvent.SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.CAPTIVE_PORTAL_CERT_FOUND));
        Assert.assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.OS_REPORTS_CAPTIVE_PORTAL));
    }

    @Test
    public void testOSReportsCaptivePortal() throws Exception {
        CaptivePortalHelper.setOSReportsCaptivePortalForTesting(true);
        navigateAndCheckCaptivePortalInterstitial();

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.HANDLE_ALL));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler",
                        UMAEvent.SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE));
        Assert.assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.CAPTIVE_PORTAL_CERT_FOUND));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.OS_REPORTS_CAPTIVE_PORTAL));
    }

    /**
     * When CaptivePortalInterstitial feature is disabled, the result of OS captive portal APIs
     * should be ignored, and a generic SSL interstitial should be displayed.
     */
    @Test
    @CommandLineFlags.Add({"disable-features=CaptivePortalInterstitial"})
    public void testOSReportsCaptivePortal_FeatureDisabled() throws Exception {
        CaptivePortalHelper.setOSReportsCaptivePortalForTesting(true);

        Tab tab = mActivityTestRule.getActivity().getActivityTab();
        ChromeTabUtils.loadUrlOnUiThread(
                tab, mServer.getURL("/chrome/test/data/android/navigate/simple.html"));

        new TabTitleObserver(tab, SSL_INTERSTITIAL_TITLE)
                .waitForTitleUpdate(INTERSTITIAL_TITLE_UPDATE_TIMEOUT_SECONDS);

        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.HANDLE_ALL));
        Assert.assertEquals(
                1,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler",
                        UMAEvent.SHOW_SSL_INTERSTITIAL_OVERRIDABLE));
        Assert.assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.CAPTIVE_PORTAL_CERT_FOUND));
        Assert.assertEquals(
                0,
                RecordHistogram.getHistogramValueCountForTesting(
                        "interstitial.ssl_error_handler", UMAEvent.OS_REPORTS_CAPTIVE_PORTAL));
    }
}