blob: 93d7ef1180df7e65b8574a361b6fc0c0c6ef75db [file] [log] [blame]
// Copyright 2022 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.android_webview.test;
import android.annotation.SuppressLint;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.chromium.android_webview.AwBrowserContext;
import org.chromium.android_webview.AwContents;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.net.test.util.WebServer.HTTPHeader;
import org.chromium.net.test.util.WebServer.HTTPRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Integration test for the X-Requested-With origin trial in WebView.
*
* <p>This test is temporary, and should be deleted once the WebViewXRequestedWithDeprecation trial
* ends.
*
* <p>Tests in this class start a server on specific ports since the port number is part of the
* origin, which is encoded in the static trial tokens. To reduce the likelihood of collisions when
* running the full test suite, the tests use different ports, but it is still possible that the
* server may not be able to start if the same test is run multiple times in quick succession.
*/
@Batch(Batch.PER_CLASS)
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@CommandLineFlags.Add({
"origin-trial-public-key=dRCs+TocuKkocNKa0AtZ4awrt9XKH2SQCI6o4FY6BNA=",
"enable-features=PersistentOriginTrials,WebViewXRequestedWithHeaderControl"
})
public class XRWOriginTrialTest extends AwParameterizedTest {
private static final String ORIGIN_TRIAL_HEADER = "Origin-Trial";
private static final String CRITICAL_ORIGIN_TRIAL_HEADER = "Critical-Origin-Trial";
private static final String XRW_HEADER = "X-Requested-With";
private static final String PERSISTENT_TRIAL_NAME = "WebViewXRequestedWithDeprecation";
private static final String REQUEST_PATH = "/";
@Rule public AwActivityTestRule mActivityTestRule;
private TestAwContentsClient mContentsClient;
private AwContents mAwContents;
public XRWOriginTrialTest(AwSettingsMutation param) {
this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
}
@Before
public void setUp() throws Exception {
mContentsClient = new TestAwContentsClient();
AwTestContainerView testContainerView =
mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient);
mAwContents = testContainerView.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
}
@SuppressLint("VisibleForTests")
@After
public void tearDown() throws Exception {
// Clean up the stored tokens after tests
mActivityTestRule.runOnUiThread(
() -> {
AwBrowserContext context = mActivityTestRule.getAwBrowserContext();
context.clearPersistentOriginTrialStorageForTesting();
});
mActivityTestRule.destroyAwContentsOnMainSync(mAwContents);
}
@Test
@SmallTest
public void testNoHeaderWithoutOriginTrial() throws Throwable {
// Port number unique to this test method. Other tests should use different ports.
try (TestWebServer server = TestWebServer.start(22433)) {
String requestUrl = setResponseHeadersForUrl(server, Collections.emptyMap());
loadUrlSync(requestUrl);
Assert.assertFalse(getLastRequestHeaders(server, REQUEST_PATH).containsKey(XRW_HEADER));
Assert.assertEquals(1, server.getRequestCount(REQUEST_PATH));
// Make a second request to verify the header stays off if no trial is set.
loadUrlSync(requestUrl);
Assert.assertFalse(getLastRequestHeaders(server, REQUEST_PATH).containsKey(XRW_HEADER));
Assert.assertEquals(2, server.getRequestCount(REQUEST_PATH));
}
}
@Test
@SmallTest
public void testHeaderOnSecondRequestWithTrial() throws Throwable {
/* Generated with
tools/origin_trials/generate_token.py http://localhost:22443 \
WebViewXRequestedWithDeprecation --expire-timestamp=2000000000
*/
final String trialToken =
"AyNFCtEW5OxS8NeOGQ5IN10l6pQiiDRWtvgLq7teZDxi7gl//fxZ/EBVYXDWqYs8LQ5IhCx/xya5ZHh1NT"
+ "FA1AwAAABpeyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDoyMjQ0MyIsICJmZWF0dXJlIjogIl"
+ "dlYlZpZXdYUmVxdWVzdGVkV2l0aERlcHJlY2F0aW9uIiwgImV4cGlyeSI6IDIwMDAwMDAwMDB9";
// Port number unique to this test method. Other tests should use different ports.
try (TestWebServer server = TestWebServer.start(22443)) {
var headers = Map.of(ORIGIN_TRIAL_HEADER, trialToken);
String requestUrl = setResponseHeadersForUrl(server, headers);
loadUrlSync(requestUrl);
Assert.assertFalse(getLastRequestHeaders(server, REQUEST_PATH).containsKey(XRW_HEADER));
Assert.assertEquals(1, server.getRequestCount(REQUEST_PATH));
// Make a second request to verify the header is enabled when the trial is active.
loadUrlSync(requestUrl);
Assert.assertTrue(getLastRequestHeaders(server, REQUEST_PATH).containsKey(XRW_HEADER));
Assert.assertEquals(2, server.getRequestCount(REQUEST_PATH));
}
}
@Test
@SmallTest
public void testCriticalEnablesHeader() throws Throwable {
/* Generated with
tools/origin_trials/generate_token.py http://localhost:22453 \
WebViewXRequestedWithDeprecation --expire-timestamp=2000000000
*/
final String trialToken =
"A/kHrOHw8h7WZ6L54iqkbhLpjf8m6dhrvKfZ1IQS3lF32ZFowFpx3E9LFYftApKPUZ5HBkSr5GI1UQra8c"
+ "nK3QQAAABpeyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDoyMjQ1MyIsICJmZWF0dXJlIjogIl"
+ "dlYlZpZXdYUmVxdWVzdGVkV2l0aERlcHJlY2F0aW9uIiwgImV4cGlyeSI6IDIwMDAwMDAwMDB9";
// Port number unique to this test method. Other tests should use different ports.
try (TestWebServer server = TestWebServer.start(22453)) {
var headers =
Map.of(
ORIGIN_TRIAL_HEADER,
trialToken,
CRITICAL_ORIGIN_TRIAL_HEADER,
PERSISTENT_TRIAL_NAME);
String requestUrl = setResponseHeadersForUrl(server, headers);
// When the trial is requested as critical, the WebView should automatically make two
// requests to enable the trial on the last one.
loadUrlSync(requestUrl);
Assert.assertTrue(getLastRequestHeaders(server, REQUEST_PATH).containsKey(XRW_HEADER));
Assert.assertEquals(2, server.getRequestCount(REQUEST_PATH));
}
}
@Test
@SmallTest
public void testThirdPartyTokensEnablesHeader() throws Throwable {
// Port number unique to this test method. Other tests should use different ports.
final int mainServerPort = 22463;
final int thirdPartyPort = 22473;
// Generated with tools/origin_trials/generate_token.py http://localhost:22473
// WebViewXRequestedWithDeprecation --expire-timestamp=2000000000 --is-third-party
final String trialToken =
"A6cHxkfwJmfXXuUv6PrTqnYqPcrnrDj50ZrzAJaIR394yEKISBDrhAiLecCfb1fSBA/8H4jAHQf0uUREEm"
+ "HLcwYAAAB/eyJvcmlnaW4iOiAiaHR0cDovL2xvY2FsaG9zdDoyMjQ3MyIsICJmZWF0dXJlIjogIl"
+ "dlYlZpZXdYUmVxdWVzdGVkV2l0aERlcHJlY2F0aW9uIiwgImV4cGlyeSI6IDIwMDAwMDAwMDAsIC"
+ "Jpc1RoaXJkUGFydHkiOiB0cnVlfQ==";
try (TestWebServer primaryServer = TestWebServer.start(mainServerPort);
TestWebServer thirdPartyServer = TestWebServer.startAdditional(thirdPartyPort)) {
// This is our target request, which should have the XRW header.
final String thirdPartyEnabledCheckPath = "/cross-origin";
String crossOriginUrl =
thirdPartyServer.setResponse(
thirdPartyEnabledCheckPath,
"window.test_done = 1",
List.of(new Pair<>("Content-Type", "application/javascript")));
// This script is loaded from the main origin but injecting a third-party token,
// which creates a deliberate mismatch between the injecting origin and the target
// origin.
String injectJs =
"window.test_done = 0;\n"
+ "const otMeta = document.createElement('meta');\n"
+ "otMeta.httpEquiv = 'origin-trial';\n"
+ "otMeta.content = '"
+ trialToken
+ "';\n"
+ "document.head.append(otMeta);\n"
+ "console.log(otMeta);\n"
// Inject an extra script tag to make a request with trial enabled.
+ "const triggerScriptLoad = function() {\n"
+ " const targetScript = document.createElement('script');\n"
+ " targetScript.src = '"
+ crossOriginUrl
+ "';\n"
+ " document.head.append(targetScript);\n"
+ " console.log(targetScript);\n"
+ "};" // triggerScriptLoad
+ "setTimeout(triggerScriptLoad, 500);";
// Load the inject script from the primary origin, to confirm we can inject a
// cross-origin token.
final String injectScriptPath = "/inject.js";
String injectUrl =
primaryServer.setResponse(
injectScriptPath,
injectJs,
List.of(new Pair<>("Content-Type", "application/javascript")));
// The main web site which simply loads the injectJs script.
String mainResponse =
"<!DOCTYPE html><head><script src=\""
+ injectUrl
+ "\"></script>\n<body>hello world";
String requestUrl =
primaryServer.setResponse(REQUEST_PATH, mainResponse, Collections.emptyList());
loadUrlSync(requestUrl);
waitForTestDone();
Assert.assertEquals(1, primaryServer.getRequestCount(injectScriptPath));
Assert.assertEquals(1, thirdPartyServer.getRequestCount(thirdPartyEnabledCheckPath));
Map<String, String> headerMap =
getLastRequestHeaders(thirdPartyServer, thirdPartyEnabledCheckPath);
Assert.assertTrue(headerMap.containsKey(XRW_HEADER));
}
}
/** Wait for {@code window.test_done == 1}. */
private void waitForTestDone() {
AwActivityTestRule.pollInstrumentationThread(
() -> {
try {
return Integer.parseInt(
mActivityTestRule.executeJavaScriptAndWaitForResult(
mAwContents, mContentsClient, "window.test_done"))
== 1;
} catch (Exception e) {
Assert.fail("Unable to get success");
}
return false;
});
}
private void loadUrlSync(String requestUrl) throws Exception {
OnPageFinishedHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper();
mActivityTestRule.loadUrlSync(mAwContents, onPageFinishedHelper, requestUrl);
Assert.assertEquals(requestUrl, onPageFinishedHelper.getUrl());
}
@NonNull
private Map<String, String> getLastRequestHeaders(TestWebServer server, String requestPath) {
HTTPRequest lastRequest = server.getLastRequest(requestPath);
HTTPHeader[] headers = lastRequest.getHeaders();
Map<String, String> headerMap = new HashMap<>();
for (var header : headers) {
headerMap.put(header.key, header.value);
}
return headerMap;
}
private String setResponseHeadersForUrl(TestWebServer server, Map<String, String> headers) {
List<Pair<String, String>> headerList = new ArrayList<>();
for (var entry : headers.entrySet()) {
headerList.add(new Pair<>(entry.getKey(), entry.getValue()));
}
return server.setResponse("/", "<!DOCTYPE html><html><body>Hello, World", headerList);
}
}