From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../org/mozilla/geckoview/test/WebExecutorTest.kt | 545 +++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt (limited to 'mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt') diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt new file mode 100644 index 0000000000..3f4af40a0b --- /dev/null +++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt @@ -0,0 +1,545 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.geckoview.test + +import android.os.Build +import android.os.SystemClock +import androidx.test.filters.MediumTest +import androidx.test.filters.SdkSuppress +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* // ktlint-disable no-wildcard-imports +import org.json.JSONObject +import org.junit.After +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mozilla.gecko.util.ThreadUtils +import org.mozilla.geckoview.* // ktlint-disable no-wildcard-imports +import org.mozilla.geckoview.test.util.RuntimeCreator +import org.mozilla.geckoview.test.util.TestServer +import java.io.IOException +import java.lang.IllegalStateException +import java.math.BigInteger +import java.net.UnknownHostException +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.security.MessageDigest +import java.util.* // ktlint-disable no-wildcard-imports + +@MediumTest +@RunWith(Parameterized::class) +class WebExecutorTest { + companion object { + const val TEST_PORT: Int = 4242 + const val TEST_ENDPOINT: String = "http://localhost:$TEST_PORT" + + @get:Parameterized.Parameters(name = "{0}") + @JvmStatic + val parameters: List> = listOf( + arrayOf("#conservative"), + arrayOf("#normal"), + ) + } + + @field:Parameterized.Parameter(0) + @JvmField + var id: String = "" + + lateinit var executor: GeckoWebExecutor + lateinit var server: TestServer + + @Before + fun setup() { + // Using @UiThreadTest here does not seem to block + // the tests which are not using @UiThreadTest, so we do that + // ourselves here as GeckoRuntime needs to be initialized + // on the UI thread. + runBlocking(Dispatchers.Main) { + executor = GeckoWebExecutor(RuntimeCreator.getRuntime()) + } + + server = TestServer(InstrumentationRegistry.getInstrumentation().targetContext) + server.start(TEST_PORT) + } + + @After + fun cleanup() { + server.stop() + } + + private fun fetch(request: WebRequest): WebResponse { + return fetch(request, GeckoWebExecutor.FETCH_FLAGS_NONE) + } + + private fun fetch(request: WebRequest, flags: Int): WebResponse { + return executor.fetch(request, flags).pollDefault()!! + } + + fun WebResponse.getBodyBytes(): ByteBuffer { + body!!.use { + return ByteBuffer.wrap(it.readBytes()) + } + } + + fun WebResponse.getJSONBody(): JSONObject { + val bytes = this.getBodyBytes() + val bodyString = Charset.forName("UTF-8").decode(bytes).toString() + return JSONObject(bodyString) + } + + private fun randomString(count: Int): String { + val chars = "01234567890abcdefghijklmnopqrstuvwxyz[],./?;'" + val builder = StringBuilder(count) + val rand = Random(System.currentTimeMillis()) + + for (i in 0 until count) { + builder.append(chars[rand.nextInt(chars.length)]) + } + + return builder.toString() + } + + fun webRequestBuilder(uri: String): WebRequest.Builder { + val beConservative = when (id) { + "#conservative" -> true + else -> false + } + return WebRequest.Builder(uri).beConservative(beConservative) + } + + fun webRequest(uri: String): WebRequest { + return webRequestBuilder(uri).build() + } + + @Test + fun smoke() { + val uri = "$TEST_ENDPOINT/anything" + val bodyString = randomString(8192) + val referrer = "http://foo/bar" + + val request = webRequestBuilder(uri) + .method("POST") + .header("Header1", "Clobbered") + .header("Header1", "Value") + .addHeader("Header2", "Value1") + .addHeader("Header2", "Value2") + .referrer(referrer) + .header("Content-Type", "text/plain") + .body(bodyString) + .build() + + val response = fetch(request) + + assertThat("URI should match", response.uri, equalTo(uri)) + assertThat("Status could should match", response.statusCode, equalTo(200)) + assertThat("Content type should match", response.headers["Content-Type"], equalTo("application/json; charset=utf-8")) + assertThat("Redirected should match", response.redirected, equalTo(false)) + assertThat("isSecure should match", response.isSecure, equalTo(false)) + + val body = response.getJSONBody() + assertThat("Method should match", body.getString("method"), equalTo("POST")) + assertThat("Headers should match", body.getJSONObject("headers").getString("Header1"), equalTo("Value")) + assertThat("Headers should match", body.getJSONObject("headers").getString("Header2"), equalTo("Value1, Value2")) + assertThat("Headers should match", body.getJSONObject("headers").getString("Content-Type"), equalTo("text/plain")) + assertThat("Referrer should match", body.getJSONObject("headers").getString("Referer"), equalTo("http://foo/")) + assertThat("Data should match", body.getString("data"), equalTo(bodyString)) + } + + @Test + fun testFetchAsset() { + val response = fetch(webRequest("$TEST_ENDPOINT/assets/www/hello.html")) + assertThat("Status should match", response.statusCode, equalTo(200)) + assertThat("Body should have bytes", response.getBodyBytes().remaining(), greaterThan(0)) + } + + @Test + fun testStatus() { + val response = fetch(webRequest("$TEST_ENDPOINT/status/500")) + assertThat("Status code should match", response.statusCode, equalTo(500)) + } + + @Test + fun testRedirect() { + val response = fetch(webRequest("$TEST_ENDPOINT/redirect-to?url=/status/200")) + + assertThat("URI should match", response.uri, equalTo(TEST_ENDPOINT + "/status/200")) + assertThat("Redirected should match", response.redirected, equalTo(true)) + assertThat("Status code should match", response.statusCode, equalTo(200)) + } + + @Test + fun testDisallowRedirect() { + val response = fetch(webRequest("$TEST_ENDPOINT/redirect-to?url=/status/200"), GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS) + + assertThat("URI should match", response.uri, equalTo("$TEST_ENDPOINT/redirect-to?url=/status/200")) + assertThat("Redirected should match", response.redirected, equalTo(false)) + assertThat("Status code should match", response.statusCode, equalTo(302)) + } + + @Test + fun testRedirectLoop() { + val thrown = assertThrows(WebRequestError::class.java) { + fetch(webRequest("$TEST_ENDPOINT/redirect/100")) + } + assertThat(thrown, equalTo(WebRequestError(WebRequestError.ERROR_REDIRECT_LOOP, WebRequestError.ERROR_CATEGORY_NETWORK))) + } + + @Test + fun testAuth() { + // We don't support authentication yet, but want to make sure it doesn't do anything + // silly like try to prompt the user. + val response = fetch(webRequest("$TEST_ENDPOINT/basic-auth/foo/bar")) + assertThat("Status code should match", response.statusCode, equalTo(401)) + } + + @Test + fun testSslError() { + val uri = if (env.isAutomation) { + "https://expired.example.com/" + } else { + "https://expired.badssl.com/" + } + + try { + fetch(webRequest(uri)) + throw IllegalStateException("fetch() should have thrown") + } catch (e: WebRequestError) { + assertThat("Category should match", e.category, equalTo(WebRequestError.ERROR_CATEGORY_SECURITY)) + assertThat("Code should match", e.code, equalTo(WebRequestError.ERROR_SECURITY_BAD_CERT)) + assertThat("Certificate should be present", e.certificate, notNullValue()) + assertThat("Certificate issuer should be present", e.certificate?.issuerX500Principal?.name, not(isEmptyOrNullString())) + } + } + + @Test + fun testSecure() { + val response = fetch(webRequest("https://example.com")) + assertThat("Status should match", response.statusCode, equalTo(200)) + assertThat("isSecure should match", response.isSecure, equalTo(true)) + + val expectedSubject = if (env.isAutomation) { + "CN=example.com" + } else { + "CN=www.example.org,OU=Technology,O=Internet Corporation for Assigned Names and Numbers,L=Los Angeles,ST=California,C=US" + } + + val expectedIssuer = if (env.isAutomation) { + "OU=Profile Guided Optimization,O=Mozilla Testing,CN=Temporary Certificate Authority" + } else { + "CN=DigiCert SHA2 Secure Server CA,O=DigiCert Inc,C=US" + } + + assertThat( + "Subject should match", + response.certificate?.subjectX500Principal?.name, + equalTo(expectedSubject), + ) + assertThat( + "Issuer should match", + response.certificate?.issuerX500Principal?.name, + equalTo(expectedIssuer), + ) + } + + @Test + fun testCookies() { + val uptimeMillis = SystemClock.uptimeMillis() + val response = fetch(webRequest("$TEST_ENDPOINT/cookies/set/uptimeMillis/$uptimeMillis")) + + // We get redirected to /cookies which returns the cookies that were sent in the request + assertThat("URI should match", response.uri, equalTo("$TEST_ENDPOINT/cookies")) + assertThat("Status code should match", response.statusCode, equalTo(200)) + + val body = response.getJSONBody() + assertThat( + "Body should match", + body.getJSONObject("cookies").getString("uptimeMillis"), + equalTo(uptimeMillis.toString()), + ) + + val anotherBody = fetch(webRequest("$TEST_ENDPOINT/cookies")).getJSONBody() + assertThat( + "Body should match", + anotherBody.getJSONObject("cookies").getString("uptimeMillis"), + equalTo(uptimeMillis.toString()), + ) + } + + @Test + fun testAnonymousSendCookies() { + val uptimeMillis = SystemClock.uptimeMillis() + val response = fetch(webRequest("$TEST_ENDPOINT/cookies/set/uptimeMillis/$uptimeMillis"), GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS) + + // We get redirected to /cookies which returns the cookies that were sent in the request + assertThat("URI should match", response.uri, equalTo("$TEST_ENDPOINT/cookies")) + assertThat("Status code should match", response.statusCode, equalTo(200)) + + val body = response.getJSONBody() + assertThat( + "Cookies should not be set for the test server", + body.getJSONObject("cookies").length(), + equalTo(0), + ) + } + + @Test + fun testAnonymousGetCookies() { + // Ensure a cookie is set for the test server + testCookies() + + val response = fetch( + webRequest("$TEST_ENDPOINT/cookies"), + GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS, + ) + + assertThat("Status code should match", response.statusCode, equalTo(200)) + val cookies = response.getJSONBody().getJSONObject("cookies") + assertThat("Cookies should be empty", cookies.length(), equalTo(0)) + } + + @Test + fun testPrivateCookies() { + val clearData = GeckoResult() + ThreadUtils.runOnUiThread { + clearData.completeFrom( + RuntimeCreator.getRuntime() + .storageController + .clearData(StorageController.ClearFlags.ALL), + ) + } + + clearData.pollDefault() + + val uptimeMillis = SystemClock.uptimeMillis() + val response = fetch(webRequest("$TEST_ENDPOINT/cookies/set/uptimeMillis/$uptimeMillis"), GeckoWebExecutor.FETCH_FLAGS_PRIVATE) + + // We get redirected to /cookies which returns the cookies that were sent in the request + assertThat("URI should match", response.uri, equalTo("$TEST_ENDPOINT/cookies")) + assertThat("Status code should match", response.statusCode, equalTo(200)) + + val body = response.getJSONBody() + assertThat( + "Cookies should be set for the test server", + body.getJSONObject("cookies").getString("uptimeMillis"), + equalTo(uptimeMillis.toString()), + ) + + val anotherBody = fetch(webRequest("$TEST_ENDPOINT/cookies"), GeckoWebExecutor.FETCH_FLAGS_PRIVATE).getJSONBody() + assertThat( + "Body should match", + anotherBody.getJSONObject("cookies").getString("uptimeMillis"), + equalTo(uptimeMillis.toString()), + ) + + val yetAnotherBody = fetch(webRequest("$TEST_ENDPOINT/cookies")).getJSONBody() + assertThat( + "Cookies set in private session are not supposed to be seen in normal download", + yetAnotherBody.getJSONObject("cookies").length(), + equalTo(0), + ) + } + + @Test + fun testSpeculativeConnect() { + // We don't have a way to know if it succeeds or not, but at least we can ensure + // it doesn't explode. + executor.speculativeConnect("http://localhost") + + // This is just a fence to ensure the above actually ran. + fetch(webRequest("$TEST_ENDPOINT/cookies")) + } + + @Test + fun testResolveV4() { + val addresses = executor.resolve("localhost").pollDefault()!! + assertThat( + "Addresses should not be null", + addresses, + notNullValue(), + ) + assertThat( + "First address should be loopback", + addresses.first().isLoopbackAddress, + equalTo(true), + ) + assertThat( + "First address size should be 4", + addresses.first().address.size, + equalTo(4), + ) + } + + @Test + @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) + fun testResolveV6() { + val addresses = executor.resolve("ip6-localhost").pollDefault()!! + assertThat( + "Addresses should not be null", + addresses, + notNullValue(), + ) + assertThat( + "First address should be loopback", + addresses.first().isLoopbackAddress, + equalTo(true), + ) + assertThat( + "First address size should be 16", + addresses.first().address.size, + equalTo(16), + ) + } + + @Test + fun testFetchUnknownHost() { + val thrown = assertThrows(WebRequestError::class.java) { + fetch(webRequest("https://this.should.not.resolve")) + } + assertThat(thrown, equalTo(WebRequestError(WebRequestError.ERROR_UNKNOWN_HOST, WebRequestError.ERROR_CATEGORY_URI))) + } + + @Test(expected = UnknownHostException::class) + fun testResolveError() { + executor.resolve("this.should.not.resolve").pollDefault() + } + + @Test + fun testFetchStream() { + val expectedCount = 1 * 1024 * 1024 // 1MB + val response = executor.fetch(webRequest("$TEST_ENDPOINT/bytes/$expectedCount")).pollDefault()!! + + assertThat("Status code should match", response.statusCode, equalTo(200)) + assertThat("Content-Length should match", response.headers["Content-Length"]!!.toInt(), equalTo(expectedCount)) + + val stream = response.body!! + val bytes = stream.readBytes() + stream.close() + + assertThat("Byte counts should match", bytes.size, equalTo(expectedCount)) + + val digest = MessageDigest.getInstance("SHA-256").digest(bytes) + assertThat( + "Hashes should match", + response.headers["X-SHA-256"], + equalTo(String.format("%064x", BigInteger(1, digest))), + ) + } + + @Test(expected = IOException::class) + fun testFetchStreamError() { + val expectedCount = 1 * 1024 * 1024 // 1MB + val response = executor.fetch( + webRequest("$TEST_ENDPOINT/bytes/$expectedCount"), + GeckoWebExecutor.FETCH_FLAGS_STREAM_FAILURE_TEST, + ).pollDefault()!! + + assertThat("Status code should match", response.statusCode, equalTo(200)) + assertThat("Content-Length should match", response.headers["Content-Length"]!!.toInt(), equalTo(expectedCount)) + + val stream = response.body!! + val bytes = ByteArray(1) + stream.read(bytes) + } + + @Test(expected = IOException::class) + fun readClosedStream() { + val response = executor.fetch(webRequest("$TEST_ENDPOINT/bytes/1024")).pollDefault()!! + + assertThat("Status code should match", response.statusCode, equalTo(200)) + + val stream = response.body!! + stream.close() + stream.readBytes() + } + + @Test(expected = IOException::class) + fun readTimeout() { + val expectedCount = 10 + val response = executor.fetch(webRequest("$TEST_ENDPOINT/trickle/$expectedCount")).pollDefault()!! + + assertThat("Status code should match", response.statusCode, equalTo(200)) + assertThat("Content-Length should match", response.headers["Content-Length"]!!.toInt(), equalTo(expectedCount)) + + // Only allow 1ms of blocking. This should reliably timeout with 1MB of data. + response.setReadTimeoutMillis(1) + + val stream = response.body!! + stream.readBytes() + } + + @Test + fun testFetchStreamCancel() { + val expectedCount = 1 * 1024 * 1024 // 1MB + val response = executor.fetch(webRequest("$TEST_ENDPOINT/bytes/$expectedCount")).pollDefault()!! + + assertThat("Status code should match", response.statusCode, equalTo(200)) + assertThat("Content-Length should match", response.headers["Content-Length"]!!.toInt(), equalTo(expectedCount)) + + val stream = response.body!! + + assertThat("Stream should have 0 bytes available", stream.available(), equalTo(0)) + + // Wait a second. Not perfect, but should be enough time for at least one buffer + // to be appended if things are not going as they should. + SystemClock.sleep(1000) + + assertThat("Stream should still have 0 bytes available", stream.available(), equalTo(0)) + + stream.close() + } + + @Test + fun unsupportedUriScheme() { + val illegal = mapOf( + "" to "", + "a" to "a", + "ab" to "ab", + "abc" to "abc", + "htt" to "htt", + "123456789" to "123456789", + "1234567890" to "1234567890", + "12345678901" to "1234567890", + "file://test" to "file://tes", + "moz-extension://what" to "moz-extens", + ) + + for ((uri, truncated) in illegal) { + try { + fetch(webRequest(uri)) + throw IllegalStateException("fetch() should have thrown") + } catch (e: IllegalArgumentException) { + assertThat( + "Message should match", + e.message, + equalTo("Unsupported URI scheme: $truncated"), + ) + } + } + + val legal = listOf( + "http://$TEST_ENDPOINT\n", + "http://$TEST_ENDPOINT/🥲", + "http://$TEST_ENDPOINT/abc", + ) + + for (uri in legal) { + try { + fetch(webRequest(uri)) + throw IllegalStateException("fetch() should have thrown") + } catch (e: WebRequestError) { + assertThat( + "Request should pass initial validation.", + true, + equalTo(true), + ) + } + } + } +} -- cgit v1.2.3