diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 420d94959687..a01766b1fdd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -299,11 +299,19 @@ jobs: strategy: fail-fast: false matrix: - api-level: - - 21 - - 23 - - 29 - - 34 + include: + - api-level: 21 + arch: x86 + target: default + - api-level: 23 + arch: x86 + target: default + - api-level: 29 + arch: x86 + target: default + - api-level: 34 + arch: x86_64 + target: default steps: - name: Checkout @@ -338,7 +346,7 @@ jobs: uses: actions/cache@v5 id: avd-cache with: - key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} + key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }} path: | ~/.android/avd/* ~/.android/adb* @@ -351,7 +359,9 @@ jobs: with: api-level: ${{ matrix.api-level }} force-avd-creation: false - arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + emulator-boot-timeout: 1200 # No window, no audio, and use swiftshader for headless environments emulator-options: > -no-window @@ -367,7 +377,18 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - arch: ${{ matrix.api-level == '34' && 'x86_64' || 'x86' }} + target: ${{ matrix.target }} + arch: ${{ matrix.arch }} + emulator-boot-timeout: 1200 + # Match the snapshot creation options. The action default includes -no-snapshot, + # which forces a slow cold boot. + emulator-options: > + -no-window + -gpu swiftshader_indirect + -noaudio + -no-boot-anim + -camera-back none + -memory 2048 script: ./gradlew -PandroidBuild=true connectedCheck env: API_LEVEL: ${{ matrix.api-level }} @@ -375,6 +396,92 @@ jobs: - name: Build Release App run: ./gradlew android-test-app:lint android-test-app:assembleRelease + android37: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + + - name: Configure JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 + with: + distribution: 'temurin' + java-version: 21 + + - name: Enable KVM group perms + # https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Verify KVM + run: | + sudo apt-get install -y cpu-checker + kvm-ok || echo "KVM is not accelerated" + kvm-ok || exit 1 + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c + + - name: Gradle cache + run: ./gradlew :android-test:test + + - name: Run Android 37 Tests + run: | + SDKMANAGER="$(find "$ANDROID_HOME/cmdline-tools" -path '*/bin/sdkmanager' | sort | tail -n 1)" + AVDMANAGER="$(find "$ANDROID_HOME/cmdline-tools" -path '*/bin/avdmanager' | sort | tail -n 1)" + ADB="$ANDROID_HOME/platform-tools/adb" + EMULATOR="$ANDROID_HOME/emulator/emulator" + export ANDROID_AVD_HOME="$HOME/.android/avd" + mkdir -p "$ANDROID_AVD_HOME" + + yes | "$SDKMANAGER" --licenses > /dev/null + "$SDKMANAGER" --install \ + 'build-tools;36.0.0' \ + platform-tools \ + 'platforms;android-37.0' \ + emulator \ + 'system-images;android-37.0;google_apis_playstore_ps16k;x86_64' \ + --channel=0 > /dev/null + + printf 'no\n' | "$AVDMANAGER" create avd \ + --force \ + --name test \ + --package 'system-images;android-37.0;google_apis_playstore_ps16k;x86_64' + + "$AVDMANAGER" list avd + + "$EMULATOR" \ + -port 5554 \ + -avd test \ + -no-window \ + -gpu swiftshader_indirect \ + -no-snapshot \ + -noaudio \ + -no-boot-anim \ + -camera-back none \ + -memory 4096 & + + "$ADB" -s emulator-5554 wait-for-device + timeout 1200 bash -c 'until [[ "$("$1" -s emulator-5554 shell getprop sys.boot_completed | tr -d "\r")" == "1" ]]; do sleep 2; done' -- "$ADB" + timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check input | grep -q "found"; do sleep 2; done' -- "$ADB" + timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check package | grep -q "found"; do sleep 2; done' -- "$ADB" + timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check activity | grep -q "found"; do sleep 2; done' -- "$ADB" + "$ADB" -s emulator-5554 shell getprop ro.build.version.sdk + "$ADB" -s emulator-5554 shell getprop ro.build.version.release + + ./gradlew -PandroidBuild=true connectedCheck + env: + API_LEVEL: 37.0 + + - name: Stop Android 37 Emulator + if: always() + run: | + "$ANDROID_HOME/platform-tools/adb" -s emulator-5554 emu kill || true + loom: runs-on: ubuntu-latest @@ -440,4 +547,3 @@ jobs: - name: Run with Jlink run: ./gradlew module-tests:imageRun -PokhttpModuleTests=true - diff --git a/android-test-app/build.gradle.kts b/android-test-app/build.gradle.kts index 345ea057f26f..e56326e65c54 100644 --- a/android-test-app/build.gradle.kts +++ b/android-test-app/build.gradle.kts @@ -1,15 +1,14 @@ @file:Suppress("UnstableApiUsage") -import okhttp3.buildsupport.testJavaVersion - - plugins { id("okhttp.base-conventions") id("com.android.application") } android { - compileSdk = 36 + compileSdk { + version = release(37) + } namespace = "okhttp.android.testapp" @@ -18,7 +17,7 @@ android { defaultConfig { minSdk = 21 - targetSdk = 36 + targetSdk = 37 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android-test-app/src/androidTest/kotlin/okhttp/android/testapp/PublicSuffixDatabaseTest.kt b/android-test-app/src/androidTest/kotlin/okhttp/android/testapp/PublicSuffixDatabaseTest.kt index 5580f5dd332a..ec5783928413 100644 --- a/android-test-app/src/androidTest/kotlin/okhttp/android/testapp/PublicSuffixDatabaseTest.kt +++ b/android-test-app/src/androidTest/kotlin/okhttp/android/testapp/PublicSuffixDatabaseTest.kt @@ -15,15 +15,23 @@ */ package okhttp3.android +import androidx.test.platform.app.InstrumentationRegistry import assertk.assertThat import assertk.assertions.isEqualTo import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttp +import org.junit.Before import org.junit.Test /** * Run with "./gradlew :android-test-app:connectedCheck -PandroidBuild=true" and make sure ANDROID_SDK_ROOT is set. */ class PublicSuffixDatabaseTest { + @Before + fun setUp() { + OkHttp.initialize(InstrumentationRegistry.getInstrumentation().targetContext) + } + @Test fun testTopLevelDomain() { assertThat("https://www.google.com/robots.txt".toHttpUrl().topPrivateDomain()).isEqualTo("google.com") diff --git a/android-test/build.gradle.kts b/android-test/build.gradle.kts index 10fe81bd7262..72255d953cb6 100644 --- a/android-test/build.gradle.kts +++ b/android-test/build.gradle.kts @@ -8,7 +8,9 @@ plugins { } android { - compileSdk = 36 + compileSdk { + version = release(37) + } namespace = "okhttp.android.test" @@ -24,26 +26,16 @@ android { ) } - if (androidBuild) { - sourceSets["androidTest"].java.srcDirs( - "../okhttp-brotli/src/test/java", - "../okhttp-dnsoverhttps/src/test/java", - "../okhttp-logging-interceptor/src/test/java", - "../okhttp-sse/src/test/java" - ) - } - compileOptions { targetCompatibility(JavaVersion.VERSION_11) sourceCompatibility(JavaVersion.VERSION_11) } testOptions { - targetSdk = 34 + targetSdk = 37 unitTests.isIncludeAndroidResources = true } - // issue merging due to conflict with httpclient and something else packagingOptions.resources.excludes += setOf( "META-INF/DEPENDENCIES", @@ -55,11 +47,25 @@ android { ) } +if (androidBuild) { + androidComponents { + onVariants(selector().all()) { variant -> + variant.androidTest?.sources?.java?.apply { + addStaticSourceDirectory("../okhttp-brotli/src/test/java") + addStaticSourceDirectory("../okhttp-dnsoverhttps/src/test/java") + addStaticSourceDirectory("../okhttp-logging-interceptor/src/test/java") + addStaticSourceDirectory("../okhttp-sse/src/test/java") + } + } + } +} + dependencies { implementation(libs.kotlin.reflect) implementation(libs.playservices.safetynet) "friendsImplementation"(projects.okhttp) "friendsImplementation"(projects.okhttpDnsoverhttps) + implementation(libs.androidx.activity) testImplementation(projects.okhttp) testImplementation(libs.junit) @@ -114,3 +120,8 @@ junitPlatform { excludeTags("Remote") } } + +tasks.withType { + // Fix for robolectric https://github.com/robolectric/robolectric/pull/10996 + jvmArgs("--add-opens", "java.base/jdk.internal.access=ALL-UNNAMED") +} diff --git a/android-test/src/androidTest/java/okhttp/android/test/EchTest.kt b/android-test/src/androidTest/java/okhttp/android/test/EchTest.kt new file mode 100644 index 000000000000..5b914a277664 --- /dev/null +++ b/android-test/src/androidTest/java/okhttp/android/test/EchTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp.android.test + +import assertk.assertThat +import assertk.assertions.isNotNull +import assertk.assertions.matchesPredicate +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test + +@Tag("Remote") +class EchTest { + + @Test + fun testHttpsRequest() { + val client: OkHttpClient = + OkHttpClient + .Builder() + .build() + + val cloudflareEchBody = + client.sendRequest(Request.Builder().url("https://cloudflare-ech.com/").build()) { + it.body.string() + } + assertThat(cloudflareEchBody).matchesPredicate { it.contains("ECH enabled") } + + val cloudflareBody = client.sendRequest( + Request.Builder().url("https://crypto.cloudflare.com/cdn-cgi/trace").build() + ) { + it.body.string() + } + assertThat(cloudflareBody).matchesPredicate { it.contains("ECH enabled") } + + val tlsEchBody = client.sendRequest(Request.Builder().url("https://tls-ech.dev/").build()) { + it.body.string() + } + assertThat(tlsEchBody).matchesPredicate { it.contains("ECH enabled") } + } + + private fun OkHttpClient.sendRequest(request: Request, fn: (Response) -> T): T { + val response = newCall(request).execute() + + return response.use { + fn(it) + } + } +} diff --git a/android-test/src/main/AndroidManifest.xml b/android-test/src/main/AndroidManifest.xml index 9a74ac7f8e7d..b0732f7778d6 100644 --- a/android-test/src/main/AndroidManifest.xml +++ b/android-test/src/main/AndroidManifest.xml @@ -4,6 +4,6 @@ - + diff --git a/android-test/src/main/res/xml/network_security_config.xml b/android-test/src/main/res/xml/network_security_config.xml index 786dddecc784..700ee2437021 100644 --- a/android-test/src/main/res/xml/network_security_config.xml +++ b/android-test/src/main/res/xml/network_security_config.xml @@ -2,4 +2,13 @@ - \ No newline at end of file + + localhost + + + cloudflare-ech.com + crypto.cloudflare.com + tls-ech.dev + + + diff --git a/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt b/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt index 59fc723ab598..4416db6b5540 100644 --- a/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt +++ b/android-test/src/test/kotlin/okhttp/android/test/AndroidSocketAdapterTest.kt @@ -58,7 +58,12 @@ class AndroidSocketAdapterTest( val sslSocket = socketFactory.createSocket() as SSLSocket assertTrue(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions( + call = null, + sslSocket = sslSocket, + hostname = null, + protocols = listOf(HTTP_2, HTTP_1_1) + ) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } @@ -89,7 +94,12 @@ class AndroidSocketAdapterTest( object : DelegatingSSLSocket(context.socketFactory.createSocket() as SSLSocket) {} assertFalse(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions( + call = null, + sslSocket = sslSocket, + hostname = null, + protocols = listOf(HTTP_2, HTTP_1_1) + ) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } diff --git a/android-test/src/test/resources/robolectric.properties b/android-test/src/test/resources/robolectric.properties new file mode 100644 index 000000000000..8a093a9991d3 --- /dev/null +++ b/android-test/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=36 diff --git a/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts b/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts index 6138b8f833fd..35fbd39ea8a2 100644 --- a/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/okhttp.jvm-conventions.gradle.kts @@ -41,7 +41,6 @@ tasks.withType { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) freeCompilerArgs.addAll( - "-Xjvm-default=all", "-Xexpect-actual-classes", ) } diff --git a/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts b/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts index 08dc0e1759d4..562894854cb6 100644 --- a/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts @@ -24,6 +24,10 @@ dependencies { tasks.withType { useJUnitPlatform() jvmArgs("-Dokhttp.platform=$platform") + if (testJavaVersion >= 9) { + // Fix for robolectric https://github.com/robolectric/robolectric/pull/10996 + jvmArgs("--add-opens", "java.base/jdk.internal.access=ALL-UNNAMED") + } if (platform == "loom") { jvmArgs("-Djdk.tracePinnedThreads=short") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2767a99b634..d6d76df1d0ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.1.1" +agp = "9.2.0" amazon-corretto = "2.5.0" android-junit5 = "2.0.1" androidx-activity = "1.11.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c61a118f7ddb..1a704683a002 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/mockwebserver-junit5/build.gradle.kts b/mockwebserver-junit5/build.gradle.kts index a759a0c5c36b..0638f0d49d7e 100644 --- a/mockwebserver-junit5/build.gradle.kts +++ b/mockwebserver-junit5/build.gradle.kts @@ -15,7 +15,6 @@ dependencies { compileOnly(libs.animalsniffer.annotations) testRuntimeOnly(libs.junit.jupiter.engine) - testImplementation(libs.kotlin.junit5) testImplementation(projects.okhttpTestingSupport) testImplementation(libs.assertk) } diff --git a/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt b/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt index 60b0d28034e1..73589ec0a1b6 100644 --- a/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt +++ b/mockwebserver/src/main/kotlin/mockwebserver3/MockWebServer.kt @@ -473,7 +473,12 @@ public class MockWebServer : Closeable { openClientSockets.add(sslSocket) if (protocolNegotiationEnabled) { - Platform.get().configureTlsExtensions(sslSocket, null, protocols) + Platform.get().configureTlsExtensions( + call = null, + sslSocket = sslSocket, + hostname = null, + protocols = protocols, + ) } sslSocket.startHandshake() diff --git a/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt b/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt index bc9a3a12b4cd..e2f50723ec71 100644 --- a/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt +++ b/mockwebserver/src/test/java/mockwebserver3/internal/http2/Http2Server.kt @@ -82,7 +82,12 @@ class Http2Server( true, ) as SSLSocket sslSocket.useClientMode = false - Platform.get().configureTlsExtensions(sslSocket, null, listOf(Protocol.HTTP_2)) + Platform.get().configureTlsExtensions( + call = null, + sslSocket = sslSocket, + hostname = null, + protocols = listOf(Protocol.HTTP_2), + ) sslSocket.startHandshake() return sslSocket } diff --git a/native-image-tests/build.gradle.kts b/native-image-tests/build.gradle.kts index d930d2b2580e..a89aace43d54 100644 --- a/native-image-tests/build.gradle.kts +++ b/native-image-tests/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { testImplementation(projects.mockwebserver3Junit5) testImplementation(libs.assertk) testRuntimeOnly(libs.junit.jupiter.engine) - testImplementation(libs.kotlin.junit5) testImplementation(libs.junit.jupiter.params) } diff --git a/okhttp-osgi-tests/build.gradle.kts b/okhttp-osgi-tests/build.gradle.kts index 0ef1794d5abe..fbaeef80e208 100644 --- a/okhttp-osgi-tests/build.gradle.kts +++ b/okhttp-osgi-tests/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { testImplementation(projects.okhttpTestingSupport) testImplementation(libs.junit) testImplementation(libs.kotlin.test.common) - testImplementation(libs.kotlin.test.junit) testImplementation(libs.assertk) testImplementation(libs.aqute.resolve) diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 5700f275c045..35024db87b61 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -1283,12 +1283,16 @@ public abstract interface class okhttp3/TrailersSource { public static final field Companion Lokhttp3/TrailersSource$Companion; public static final field EMPTY Lokhttp3/TrailersSource; public abstract fun get ()Lokhttp3/Headers; - public fun peek ()Lokhttp3/Headers; + public abstract fun peek ()Lokhttp3/Headers; } public final class okhttp3/TrailersSource$Companion { } +public final class okhttp3/TrailersSource$DefaultImpls { + public static fun peek (Lokhttp3/TrailersSource;)Lokhttp3/Headers; +} + public abstract interface class okhttp3/WebSocket { public abstract fun cancel ()V public abstract fun close (ILjava/lang/String;)Z diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index d065655fb030..a37c2deac8ab 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -1282,12 +1282,16 @@ public abstract interface class okhttp3/TrailersSource { public static final field Companion Lokhttp3/TrailersSource$Companion; public static final field EMPTY Lokhttp3/TrailersSource; public abstract fun get ()Lokhttp3/Headers; - public fun peek ()Lokhttp3/Headers; + public abstract fun peek ()Lokhttp3/Headers; } public final class okhttp3/TrailersSource$Companion { } +public final class okhttp3/TrailersSource$DefaultImpls { + public static fun peek (Lokhttp3/TrailersSource;)Lokhttp3/Headers; +} + public abstract interface class okhttp3/WebSocket { public abstract fun cancel ()V public abstract fun close (ILjava/lang/String;)Z diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 5e15372ee64d..0c5effab190d 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -58,7 +58,7 @@ kotlin { android { namespace = "okhttp.okhttp3" compileSdk { - version = release(36) + version = release(37) } minSdk = 21 @@ -108,7 +108,6 @@ kotlin { implementation(libs.assertk) implementation(libs.kotlin.test.annotations) implementation(libs.kotlin.test.common) - implementation(libs.kotlin.test.junit) implementation(libs.junit) implementation(libs.junit.jupiter.api) implementation(libs.junit.jupiter.params) @@ -192,6 +191,7 @@ kotlin { implementation(libs.junit.vintage.engine) implementation(libs.kotlin.test.annotations) implementation(libs.kotlin.test.common) + implementation(libs.kotlin.test.junit) implementation(libs.robolectric) } } diff --git a/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/AndroidDnsResolverDnsTest.kt b/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/AndroidDnsResolverDnsTest.kt new file mode 100644 index 000000000000..5de1047a8726 --- /dev/null +++ b/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/AndroidDnsResolverDnsTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isNull +import java.net.InetAddress +import java.net.UnknownHostException +import kotlin.test.assertFailsWith +import okhttp3.ech.EchConfig +import okio.ByteString +import org.junit.Test + +class AndroidDnsResolverDnsTest { + private val address = InetAddress.getByName("192.0.2.1") + + @Test + fun lookupReturnsAddressesAndCachesEchConfig() { + val echConfig = FakeEchConfig + val dns = + AndroidDnsResolverDns( + FakeDnsLookup( + "example.com" to AndroidDnsResult(listOf(address), echConfig), + ), + ) + + assertThat(dns.lookup("example.com")).isEqualTo(listOf(address)) + assertThat(dns.getEchConfig("example.com")).isEqualTo(echConfig) + assertThat(dns.getEchConfig("other.example")).isNull() + } + + @Test + fun lookupWithoutEchConfigClearsStaleEchConfig() { + val echConfig = FakeEchConfig + val lookup = + FakeDnsLookup( + "example.com" to AndroidDnsResult(listOf(address), echConfig), + ) + val dns = AndroidDnsResolverDns(lookup) + + dns.lookup("example.com") + assertThat(dns.getEchConfig("example.com")).isEqualTo(echConfig) + + lookup["example.com"] = AndroidDnsResult(listOf(address), null) + dns.lookup("example.com") + assertThat(dns.getEchConfig("example.com")).isNull() + } + + @Test + fun lookupPropagatesUnknownHostException() { + val dns = AndroidDnsResolverDns(FakeDnsLookup()) + + assertFailsWith { + dns.lookup("missing.example") + } + } + + private class FakeDnsLookup( + vararg responses: Pair, + ) : AndroidDnsLookup { + private val responses = responses.toMap().toMutableMap() + + operator fun set( + hostname: String, + result: AndroidDnsResult, + ) { + responses[hostname] = result + } + + override fun lookup(hostname: String): AndroidDnsResult = responses[hostname] ?: throw UnknownHostException(hostname) + } + + private object FakeEchConfig : EchConfig { + override val config: ByteString = ByteString.EMPTY + } +} diff --git a/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfigurationTest.kt b/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfigurationTest.kt new file mode 100644 index 000000000000..47fdfbb1a7f3 --- /dev/null +++ b/okhttp/src/androidHostTest/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfigurationTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform.android + +import android.security.NetworkSecurityPolicy +import assertk.assertThat +import assertk.assertions.isEqualTo +import okhttp3.ech.EchMode +import org.junit.Test + +class AndroidEchModeConfigurationTest { + @Test + fun mapsNetworkSecurityPolicyModes() { + assertThat( + EchMode.fromNetworkSecurityPolicy(NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC), + ).isEqualTo(EchMode.Opportunistic) + assertThat( + EchMode.fromNetworkSecurityPolicy(NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_ENABLED), + ).isEqualTo(EchMode.Strict) + assertThat( + EchMode.fromNetworkSecurityPolicy(NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_DISABLED), + ).isEqualTo(EchMode.Disabled) + assertThat(EchMode.fromNetworkSecurityPolicy(-1)).isEqualTo(EchMode.Unspecified) + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt index 671427de4e67..109502dbb59e 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt @@ -26,6 +26,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.AndroidPlatform.Companion.Tag @@ -40,7 +41,7 @@ import okhttp3.internal.tls.TrustRootIndex /** Android 10+ (API 29+). */ @SuppressSignatureCheck -class Android10Platform : +open class Android10Platform : Platform(), ContextAwarePlatform { override var applicationContext: Context? = null @@ -72,6 +73,7 @@ class Android10Platform : } override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, @@ -79,7 +81,12 @@ class Android10Platform : // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } - ?.configureTlsExtensions(sslSocket, hostname, protocols) + ?.configureTlsExtensions( + call = call, + sslSocket = sslSocket, + hostname = hostname, + protocols = protocols, + ) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android17Platform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android17Platform.kt new file mode 100644 index 000000000000..b17a7e83e33f --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android17Platform.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform + +import android.annotation.SuppressLint +import android.content.Context +import android.net.DnsResolver +import android.os.Build +import android.os.StrictMode +import android.security.NetworkSecurityPolicy +import android.util.CloseGuard +import android.util.Log +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.RequiresApi +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager +import okhttp3.Call +import okhttp3.Dns +import okhttp3.Protocol +import okhttp3.ech.EchModeConfiguration +import okhttp3.internal.SuppressSignatureCheck +import okhttp3.internal.platform.AndroidPlatform.Companion.Tag +import okhttp3.internal.platform.android.Android17SocketAdapter +import okhttp3.internal.platform.android.AndroidCertificateChainCleaner +import okhttp3.internal.platform.android.AndroidEchModeConfiguration +import okhttp3.internal.tls.CertificateChainCleaner +import okhttp3.internal.tls.TrustRootIndex + +/** + * Android 17+ (API 37+). + * + * This platform uses the post-API 36 Android TLS and DNS APIs directly, including domain + * encryption policy, HTTPS/SVCB DNS records from [DnsResolver], and Encrypted Client Hello (ECH) + * configuration on TLS sockets. + */ +@SuppressSignatureCheck +class Android17Platform + @RequiresApi(37) + internal constructor() : + Platform(), + ContextAwarePlatform { + override var applicationContext: Context? = null + + private val socketAdapter by lazy { + Android17SocketAdapter.buildIfSupported()!! + } + + override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = socketAdapter.trustManager(sslSocketFactory) + + override fun newSSLContext(): SSLContext { + StrictMode.noteSlowCall("newSSLContext") + + return super.newSSLContext() + } + + override fun buildTrustRootIndex(trustManager: X509TrustManager): TrustRootIndex { + StrictMode.noteSlowCall("buildTrustRootIndex") + + return super.buildTrustRootIndex(trustManager) + } + + override fun configureTlsExtensions( + call: Call?, + sslSocket: SSLSocket, + hostname: String?, + protocols: List, + ) { + socketAdapter.configureTlsExtensions( + call = call, + sslSocket = sslSocket, + hostname = hostname, + protocols = protocols, + ) + } + + override fun getSelectedProtocol(sslSocket: SSLSocket): String? = socketAdapter.getSelectedProtocol(sslSocket) + + @RequiresApi(36) + override fun getStackTraceForCloseable(closer: String): Any = CloseGuard().apply { open(closer) } + + @RequiresApi(36) + override fun logCloseableLeak( + message: String, + stackTrace: Any?, + ) { + (stackTrace as CloseGuard).warnIfOpen() + } + + @SuppressLint("NewApi") + override fun isCleartextTrafficPermitted(hostname: String): Boolean = + NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(hostname) + + @SuppressLint("NewApi") + internal override val echModeConfiguration: EchModeConfiguration = AndroidEchModeConfiguration() + + override fun buildCertificateChainCleaner(trustManager: X509TrustManager): CertificateChainCleaner = + AndroidCertificateChainCleaner.buildIfSupported(trustManager)!! + + override fun log( + message: String, + level: Int, + t: Throwable?, + ) { + if (level == WARN) { + Log.w(Tag, message, t) + } else { + Log.i(Tag, message, t) + } + } + + @SuppressLint("NewApi") + override fun platformDns(): Dns = AndroidDnsResolverDns() + + companion object { + val isSupported: Boolean = (isAndroid && Build.VERSION.SDK_INT >= 37) + + @ChecksSdkIntAtLeast(37) + fun buildIfSupported(): Platform? = if (isSupported) Android17Platform() else null + } + } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidDnsResolverDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidDnsResolverDns.kt new file mode 100644 index 000000000000..bc526159204c --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidDnsResolverDns.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform + +import android.annotation.SuppressLint +import android.net.DnsResolver +import android.net.DnsResolver.Callback +import android.net.dns.HttpsEndpoint +import android.net.ssl.EchConfigList +import android.os.HandlerThread +import androidx.annotation.RequiresApi +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.TimeoutException +import okhttp3.Dns +import okhttp3.EchAware +import okhttp3.ech.EchConfig +import okio.ByteString +import okio.ByteString.Companion.toByteString + +@Suppress("NewApi") +@RequiresApi(36) +internal class AndroidDnsResolverDns internal constructor( + private val dnsResolver: AndroidDnsLookup = AndroidDnsResolver(), +) : Dns, + EchAware { + private val echConfigs = ConcurrentHashMap() + + override fun lookup(hostname: String): List { + val result = dnsResolver.lookup(hostname) + result.echConfig?.let { + echConfigs[hostname] = it + } ?: echConfigs.remove(hostname) + return result.addresses + } + + override fun getEchConfig(host: String): EchConfig? = echConfigs[host] +} + +internal data class AndroidDnsResult( + val addresses: List, + val echConfig: EchConfig?, +) + +internal data class AndroidEchConfig( + val echConfigList: EchConfigList, +) : EchConfig { + @get:SuppressLint("NewApi") + override val config: ByteString + get() = echConfigList.toBytes().toByteString() +} + +internal fun interface AndroidDnsLookup { + @Throws(UnknownHostException::class) + fun lookup(hostname: String): AndroidDnsResult +} + +@Suppress("NewApi") +@RequiresApi(36) +internal class AndroidDnsResolver( + private val dnsResolver: DnsResolver = + HandlerThread("OkHttp DnsResolver").let { handlerThread -> + handlerThread.start() + DnsResolver(PlatformRegistry.applicationContext!!, handlerThread.looper) + }, + private val executor: Executor = Executor { it.run() }, + private val timeoutSeconds: Long = 5L, +) : AndroidDnsLookup { + override fun lookup(hostname: String): AndroidDnsResult { + val endpoint = queryHttps(hostname) + return AndroidDnsResult( + addresses = endpoint.ipAddresses, + echConfig = endpoint.echConfigOrNull(), + ) + } + + private fun queryHttps(hostname: String): HttpsEndpoint = + execute(hostname) { callback -> + @Suppress("WrongConstant") + dnsResolver.query( + null, + hostname, + DnsResolver.FLAG_EMPTY, + executor, + SECONDS.toMillis(1L).toInt(), + null, + callback, + ) + } + + private fun execute( + hostname: String, + query: (Callback) -> Unit, + ): T { + val result = CompletableFuture() + + query( + object : Callback { + override fun onAnswer( + answer: T, + rcode: Int, + ) { + result.complete(answer) + } + + override fun onError(e: DnsResolver.DnsException) { + result.completeExceptionally(e) + } + }, + ) + + return try { + result.get(timeoutSeconds, SECONDS) + } catch (e: ExecutionException) { + throw (e.cause as? DnsResolver.DnsException)?.toUnknownHostException(hostname) + ?: UnknownHostException("Broken system behaviour for dns lookup of $hostname").apply { + initCause(e.cause) + } + } catch (e: TimeoutException) { + throw UnknownHostException("DNS lookup timed out for $hostname").apply { + initCause(e) + } + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + throw UnknownHostException("Interrupted DNS lookup for $hostname").apply { + initCause(e) + } + } + } +} + +@SuppressLint("NewApi") +private fun HttpsEndpoint.echConfigOrNull(): AndroidEchConfig? { + val httpsRecord = httpsRecords.firstOrNull() ?: return null + return try { + httpsRecord.echConfigList?.let(::AndroidEchConfig) + } catch (e: IllegalArgumentException) { + // TODO: remove this guard when Android handles malformed or absent ECH parameters. + // https://issuetracker.google.com/issues/319957694 + null + } +} + +@SuppressLint("NewApi") +private fun DnsResolver.DnsException.toUnknownHostException(hostname: String): UnknownHostException = + UnknownHostException("DNS lookup failed for $hostname").apply { + initCause(this@toUnknownHostException) + } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt index 4f94d192b034..47bee577e84d 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/AndroidPlatform.kt @@ -31,6 +31,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.android.AndroidCertificateChainCleaner @@ -90,6 +91,7 @@ class AndroidPlatform : ?.trustManager(sslSocketFactory) override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, @@ -97,7 +99,12 @@ class AndroidPlatform : // No TLS extensions if the socket class is custom. socketAdapters .find { it.matchesSocket(sslSocket) } - ?.configureTlsExtensions(sslSocket, hostname, protocols) + ?.configureTlsExtensions( + call = call, + sslSocket = sslSocket, + hostname = hostname, + protocols = protocols, + ) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt index 4c912f5e61c1..4a62c4ecc89a 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/PlatformRegistry.kt @@ -25,7 +25,8 @@ actual object PlatformRegistry { AndroidLog.enable() val androidPlatform = - Android10Platform.buildIfSupported() + Android17Platform.buildIfSupported() + ?: Android10Platform.buildIfSupported() ?: AndroidPlatform.buildIfSupported() if (androidPlatform != null) return androidPlatform diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt index 83a3f4f41cbb..0d0e479f4b1d 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android10SocketAdapter.kt @@ -21,6 +21,7 @@ import android.os.Build import java.io.IOException import java.lang.IllegalArgumentException import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.Platform @@ -54,6 +55,7 @@ class Android10SocketAdapter : SocketAdapter { @SuppressLint("NewApi") override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android17SocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android17SocketAdapter.kt new file mode 100644 index 000000000000..093fa8c0de7a --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/Android17SocketAdapter.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform.android + +import android.annotation.SuppressLint +import android.net.ssl.SSLSockets +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.RequiresApi +import javax.net.ssl.SSLSocket +import okhttp3.Call +import okhttp3.Protocol +import okhttp3.ech.EchConfig +import okhttp3.ech.EchMode +import okhttp3.internal.SuppressSignatureCheck +import okhttp3.internal.connection.RealCall +import okhttp3.internal.platform.Android17Platform +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.Platform.Companion.isAndroid + +/** + * Socket adapter for Android 17+ platform TLS APIs. + * + * Unlike the older Android socket adapters, this calls public platform APIs directly instead of + * using reflection or Conscrypt-specific hooks. It configures session tickets, ALPN, and ECH on + * Android's `SSLSocket` implementation. + * + * These API assumptions make it unsuitable for earlier Android versions; use + * [Android17Platform] to select this adapter only when the runtime SDK supports it. + */ +@SuppressLint("NewApi") +@SuppressSignatureCheck +class Android17SocketAdapter + @RequiresApi(36) + internal constructor() : SocketAdapter { + init { + println("AndroidCanarySocketAdapter") + } + + override fun matchesSocket(sslSocket: SSLSocket): Boolean = SSLSockets.isSupportedSocket(sslSocket) + + override fun isSupported(): Boolean = Companion.isSupported() + + override fun getSelectedProtocol(sslSocket: SSLSocket): String? = + // SSLSocket.getApplicationProtocol returns "" if application protocols values will not + // be used. Observed if you didn't specify SSLParameters.setApplicationProtocols + when (val protocol = sslSocket.applicationProtocol) { + null, "" -> null + else -> protocol + } + + override fun configureTlsExtensions( + call: Call?, + sslSocket: SSLSocket, + hostname: String?, + protocols: List, + ) { + SSLSockets.setUseSessionTickets(sslSocket, true) + + val sslParameters = sslSocket.sslParameters + + // Enable ALPN. + sslParameters.applicationProtocols = Platform.alpnProtocolNames(protocols).toTypedArray() + + sslSocket.sslParameters = sslParameters + + if (hostname != null) { + val client = (call as? RealCall)?.client ?: return + + val echModeConfiguration = client.echModeConfiguration + + val echMode = + call.tag(EchMode::class) { + echModeConfiguration.echMode(hostname) + } + + if (echMode.attempt) { + echModeConfiguration + .applyEch(sslSocket, echMode, hostname, client.dns) + ?.let { echConfig -> + call.tag(EchConfig::class) { echConfig } + } + } + } + } + + @SuppressSignatureCheck + companion object { + fun buildIfSupported(): SocketAdapter? = if (isSupported()) Android17SocketAdapter() else null + + @ChecksSdkIntAtLeast(api = 36) + fun isSupported() = isAndroid && Build.VERSION.SDK_INT >= 36 + } + } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfiguration.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfiguration.kt new file mode 100644 index 000000000000..3c7bbdd97992 --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidEchModeConfiguration.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.internal.platform.android + +import android.annotation.SuppressLint +import android.net.ssl.EchConfigMismatchException +import android.net.ssl.SSLSockets +import android.security.NetworkSecurityPolicy +import androidx.annotation.RequiresApi +import javax.net.ssl.SSLException +import javax.net.ssl.SSLSocket +import okhttp3.Dns +import okhttp3.EchAware +import okhttp3.ech.EchConfig +import okhttp3.ech.EchMode +import okhttp3.ech.EchModeConfiguration +import okhttp3.internal.platform.AndroidEchConfig +import okio.IOException + +/** + * Android implementation of [EchModeConfiguration] for API 37+. + * + * This bridges OkHttp's platform-neutral ECH policy to Android's native ECH APIs: + * [NetworkSecurityPolicy] supplies the per-host domain encryption policy, [Dns] may provide an + * HTTPS/SVCB ECH configuration list, and [SSLSockets] applies that configuration to the TLS socket. + */ +@RequiresApi(37) +internal class AndroidEchModeConfiguration : EchModeConfiguration { + @Suppress("NewApi") + override fun echMode(host: String): EchMode { + val domainEncryptionMode = NetworkSecurityPolicy.getInstance().getDomainEncryptionMode(host) + return EchMode.fromNetworkSecurityPolicy(domainEncryptionMode) + } + + @SuppressLint("NewApi") + override fun isEchConfigError(e: SSLException): Boolean = e is EchConfigMismatchException + + @Suppress("NewApi") + override fun applyEch( + sslSocket: SSLSocket, + echMode: EchMode, + host: String, + dns: Dns, + ): EchConfig? { + // The Android DNS implementation returns AndroidEchConfig instances. Other Dns + // implementations are valid; they simply won't be able to configure Android ECH sockets. + val echConfig = (dns as? EchAware)?.getEchConfig(host) as? AndroidEchConfig + + if (echConfig != null) { + SSLSockets.setEchConfigList( + sslSocket, + echConfig.echConfigList, + ) + return echConfig + } else if (echMode.require) { + throw IOException("Unable to apply required ECH config for $host") + } + return null + } +} + +internal fun EchMode.Companion.fromNetworkSecurityPolicy(domainEncryptionMode: Int): EchMode = + when (domainEncryptionMode) { + NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC -> EchMode.Opportunistic + NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_ENABLED -> EchMode.Strict + NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_DISABLED -> EchMode.Disabled + else -> EchMode.Unspecified + } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt index 9adf56dba2b2..baf09963cd57 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/AndroidSocketAdapter.kt @@ -19,6 +19,7 @@ import android.os.Build import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.platform.AndroidPlatform import okhttp3.internal.platform.Platform @@ -45,6 +46,7 @@ open class AndroidSocketAdapter( override fun matchesSocket(sslSocket: SSLSocket): Boolean = sslSocketClass.isInstance(sslSocket) override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt index 6d2b8beb0e62..77d78bd05d17 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/BouncyCastleSocketAdapter.kt @@ -16,6 +16,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.platform.Platform import org.bouncycastle.jsse.BCSSLSocket @@ -38,6 +39,7 @@ class BouncyCastleSocketAdapter : SocketAdapter { } override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt index 006e593b7bcf..b699a05bbf2c 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt @@ -16,6 +16,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.platform.Platform import org.conscrypt.Conscrypt @@ -36,6 +37,7 @@ class ConscryptSocketAdapter : SocketAdapter { } override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt index 10b619dec831..62cce3742759 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/DeferredSocketAdapter.kt @@ -16,6 +16,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol /** @@ -36,11 +37,12 @@ class DeferredSocketAdapter( override fun matchesSocket(sslSocket: SSLSocket): Boolean = socketAdapterFactory.matchesSocket(sslSocket) override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, ) { - getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, hostname, protocols) + getDelegate(sslSocket)?.configureTlsExtensions(call, sslSocket, hostname, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? = getDelegate(sslSocket)?.getSelectedProtocol(sslSocket) diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt index 40776f6bfb17..df9ce411d347 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/android/SocketAdapter.kt @@ -18,6 +18,7 @@ package okhttp3.internal.platform.android import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol interface SocketAdapter { @@ -30,6 +31,7 @@ interface SocketAdapter { fun matchesSocketFactory(sslSocketFactory: SSLSocketFactory): Boolean = false fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt index d7fdd38d564c..cad80e246d17 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt @@ -18,6 +18,7 @@ package okhttp3 import java.net.InetAddress import java.net.UnknownHostException import okhttp3.Dns.Companion.SYSTEM +import okhttp3.ech.EchConfig /** * A domain name service that resolves IP addresses for host names. Most applications will use the @@ -57,3 +58,16 @@ fun interface Dns { } } } + +/** + * A [Dns] implementation that can also return HTTPS or SVCB ECH configuration for a host. + */ +internal interface EchAware { + /** + * Returns ECH configuration for [host], or null if no configuration is available. + * + * The returned [EchConfig] type is platform-specific. On Android this wraps an `EchConfigList` + * suitable for configuring the TLS socket. + */ + fun getEchConfig(host: String): EchConfig? +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Handshake.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Handshake.kt index 4c61fc4b7bf6..1d1e13ff9dae 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Handshake.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Handshake.kt @@ -21,6 +21,7 @@ import java.security.cert.Certificate import java.security.cert.X509Certificate import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSession +import okhttp3.ech.EchConfig import okhttp3.internal.toImmutableList /** @@ -40,6 +41,7 @@ class Handshake internal constructor( @get:JvmName("cipherSuite") val cipherSuite: CipherSuite, /** Returns a possibly-empty list of certificates that identify this peer. */ @get:JvmName("localCertificates") val localCertificates: List, + internal val echConfig: EchConfig? = null, // Delayed provider of peerCertificates, to allow lazy cleaning. peerCertificatesFn: () -> List, ) { @@ -194,7 +196,11 @@ class Handshake internal constructor( localCertificates: List, ): Handshake { val peerCertificatesCopy = peerCertificates.toImmutableList() - return Handshake(tlsVersion, cipherSuite, localCertificates.toImmutableList()) { + return Handshake( + tlsVersion = tlsVersion, + cipherSuite = cipherSuite, + localCertificates = localCertificates.toImmutableList(), + ) { peerCertificatesCopy } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt index bea47f62e899..3ad2c93d8766 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt @@ -30,6 +30,7 @@ import javax.net.ssl.X509TrustManager import kotlin.time.Duration as KotlinDuration import okhttp3.Protocol.HTTP_1_1 import okhttp3.Protocol.HTTP_2 +import okhttp3.ech.EchModeConfiguration import okhttp3.internal.asFactory import okhttp3.internal.checkDuration import okhttp3.internal.concurrent.TaskRunner @@ -273,6 +274,8 @@ open class OkHttpClient internal constructor( builder.connectionPool = it } + internal val echModeConfiguration: EchModeConfiguration = builder.echModeConfiguration + constructor() : this(Builder()) init { @@ -597,7 +600,7 @@ open class OkHttpClient internal constructor( internal var followSslRedirects = true internal var cookieJar: CookieJar = CookieJar.NO_COOKIES internal var cache: Cache? = null - internal var dns: Dns = Dns.SYSTEM + internal var dns: Dns = Platform.get().platformDns() internal var proxy: Proxy? = null internal var proxySelector: ProxySelector? = null internal var proxyAuthenticator: Authenticator = Authenticator.NONE @@ -618,6 +621,7 @@ open class OkHttpClient internal constructor( internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE internal var routeDatabase: RouteDatabase? = null internal var taskRunner: TaskRunner? = null + internal var echModeConfiguration: EchModeConfiguration = Platform.get().echModeConfiguration internal constructor(okHttpClient: OkHttpClient) : this() { this.dispatcher = okHttpClient.dispatcher @@ -653,6 +657,7 @@ open class OkHttpClient internal constructor( this.minWebSocketMessageToCompress = okHttpClient.minWebSocketMessageToCompress this.routeDatabase = okHttpClient.routeDatabase this.taskRunner = okHttpClient.taskRunner + this.echModeConfiguration = okHttpClient.echModeConfiguration } /** diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchConfig.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchConfig.kt new file mode 100644 index 000000000000..2186eaeaa993 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchConfig.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.ech + +import okio.ByteString + +/** + * Configuration for Encrypted Client Hello (ECH). + * + * This contains the parameters required for a client to encrypt its ClientHello message, + * protecting sensitive fields such as the Server Name Indication (SNI) from passive observers. + * These parameters are typically retrieved from DNS via HTTPS or SVCB records, and platform + * implementations may carry additional native objects needed to configure TLS sockets. + */ +internal interface EchConfig { + /** The serialized ECH configuration list from DNS. */ + val config: ByteString +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchMode.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchMode.kt new file mode 100644 index 000000000000..f1de08904ea5 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchMode.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.ech + +/** + * Configures the behavior of Encrypted Client Hello (ECH) for TLS connections. + */ +internal enum class EchMode( + /** True if OkHttp should attempt to configure ECH for the TLS connection. */ + val attempt: Boolean, + /** True if the connection must fail when ECH cannot be configured or negotiated. */ + val require: Boolean, + /** True if OkHttp should retry without ECH when the server rejects the ECH configuration. */ + val fallback: Boolean = false, +) { + /** + * The ECH mode is not specified. ECH will not be attempted or required. + */ + Unspecified(attempt = false, require = false), + + /** ECH is disabled. */ + Disabled( + attempt = false, + require = false, + ), + + /** + * Attempt ECH if configuration is available, but fall back to standard TLS if it fails. + */ + Opportunistic( + attempt = true, + require = false, + fallback = true, + ), + + /** + * Attempt ECH if the configuration is available. + */ + Strict( + attempt = true, + require = false, + ), + + /** + * Attempt ECH and fail the connection if it cannot be established. + */ + FailClosed(attempt = true, require = true), + + /** + * Retry with ECH disabled. + */ + Fallback(attempt = false, require = false), + ; + + /** Companion for extension functions and Java interop. */ + companion object +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchModeConfiguration.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchModeConfiguration.kt new file mode 100644 index 000000000000..66a88dab7f4a --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ech/EchModeConfiguration.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * 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 okhttp3.ech + +import javax.net.ssl.SSLException +import javax.net.ssl.SSLSocket +import okhttp3.Dns + +/** + * Configuration and management for Encrypted Client Hello (ECH). + * + * This interface provides the mechanism to determine the ECH strategy for a given host, + * apply ECH parameters to an [SSLSocket], and identify ECH-specific connection failures. + */ +internal interface EchModeConfiguration { + /** + * Determines the [EchMode] strategy to be used for the specified [host]. + * + * @param host the hostname for which the ECH strategy is requested. + * @return the [EchMode] to be applied during the connection process. + */ + fun echMode(host: String): EchMode + + /** + * Configures [sslSocket] with Encrypted Client Hello (ECH) parameters for [host]. + * + * Implementations may use [dns] to retrieve ECH configuration records. If [echMode] requires + * ECH and no configuration can be applied, this should throw an [java.io.IOException]. Returns + * the configuration that was applied, or null when no ECH configuration was used. + */ + fun applyEch( + sslSocket: SSLSocket, + echMode: EchMode, + host: String, + dns: Dns, + ): EchConfig? + + /** + * Returns true if [e] indicates a failure due to an invalid or expired ECH configuration. + * + * This typically occurs when the server's ECH public key has rotated. When this returns + * true, the client may use the server-provided "retry_config" to update its configuration + * and attempt the connection again. + * + * @param e the exception thrown during the SSL handshake. + */ + fun isEchConfigError(e: SSLException): Boolean = false + + /** Built-in [EchModeConfiguration] instances. */ + companion object { + /** + * A default implementation of [EchModeConfiguration] that performs no ECH-related actions + * and always returns [EchMode.Unspecified]. + */ + val Unspecified = + object : EchModeConfiguration { + override fun echMode(host: String): EchMode = EchMode.Unspecified + + override fun applyEch( + sslSocket: SSLSocket, + echMode: EchMode, + host: String, + dns: Dns, + ): EchConfig? { + check(!echMode.attempt) + return null + } + } + } +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt index e90421ccdc44..3a2751e8e250 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/ConnectPlan.kt @@ -33,6 +33,7 @@ import okhttp3.Handshake.Companion.handshake import okhttp3.Protocol import okhttp3.Request import okhttp3.Route +import okhttp3.ech.EchConfig import okhttp3.internal.closeQuietly import okhttp3.internal.concurrent.TaskRunner import okhttp3.internal.concurrent.withLock @@ -345,7 +346,12 @@ class ConnectPlan internal constructor( var success = false try { if (connectionSpec.supportsTlsExtensions) { - Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols) + Platform.get().configureTlsExtensions( + call = call, + sslSocket = sslSocket, + hostname = address.url.host, + protocols = address.protocols, + ) } // Force handshake. This can throw! @@ -378,9 +384,10 @@ class ConnectPlan internal constructor( val handshake = Handshake( - unverifiedHandshake.tlsVersion, - unverifiedHandshake.cipherSuite, - unverifiedHandshake.localCertificates, + tlsVersion = unverifiedHandshake.tlsVersion, + cipherSuite = unverifiedHandshake.cipherSuite, + localCertificates = unverifiedHandshake.localCertificates, + echConfig = call.tag(EchConfig::class), ) { certificatePinner.certificateChainCleaner!!.clean( unverifiedHandshake.peerCertificates, @@ -406,6 +413,8 @@ class ConnectPlan internal constructor( protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1 success = true } finally { + // ECH rejection is surfaced as an SSLException by the platform. Let it propagate so + // RetryAndFollowUpInterceptor can classify it with EchModeConfiguration.isEchConfigError(). Platform.get().afterHandshake(sslSocket) if (!success) { sslSocket.closeQuietly() diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt index 458c3eaad396..02abd3d71124 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt @@ -30,17 +30,20 @@ import java.net.ProtocolException import java.net.Proxy import java.net.SocketTimeoutException import java.security.cert.CertificateException +import javax.net.ssl.SSLException import javax.net.ssl.SSLHandshakeException import javax.net.ssl.SSLPeerUnverifiedException import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okhttp3.ech.EchMode import okhttp3.internal.canReuseConnectionFor import okhttp3.internal.closeQuietly import okhttp3.internal.connection.Exchange import okhttp3.internal.connection.RealCall import okhttp3.internal.http2.ConnectionShutdownException +import okhttp3.internal.platform.Platform import okhttp3.internal.stripBody import okhttp3.internal.withSuppressed @@ -138,6 +141,21 @@ class RetryAndFollowUpInterceptor : Interceptor { ): Boolean { val requestSendStarted = e !is ConnectionShutdownException + if (e is SSLException) { + val echModeConfiguration = call.client.echModeConfiguration + val echMode = echModeConfiguration.echMode(call.request().url.host) + if ( + call.tag(EchMode::class) != EchMode.Fallback && + echMode.fallback && + echModeConfiguration.isEchConfigError(e) + ) { + // Mark this call so the next connection attempt skips ECH. Without this guard a fallback + // connection that also fails with an ECH-classified SSLException could retry indefinitely. + Platform.get().log("Should retry here with ECH disabled") + call.tag(EchMode::class) { EchMode.Fallback } + } + } + // The application layer has forbidden retries. if (!chain.retryOnConnectionFailure) return false diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt index 6247d485331f..f3d362cd37ad 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Jdk9Platform.kt @@ -20,6 +20,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol import okhttp3.internal.SuppressSignatureCheck @@ -32,6 +33,7 @@ import okhttp3.internal.SuppressSignatureCheck open class Jdk9Platform : Platform() { @SuppressSignatureCheck override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt index f33d97972b94..ba82cd9ae691 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt @@ -31,8 +31,11 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call +import okhttp3.Dns import okhttp3.OkHttpClient import okhttp3.Protocol +import okhttp3.ech.EchModeConfiguration import okhttp3.internal.publicsuffix.PublicSuffixDatabase import okhttp3.internal.readFieldOrNull import okhttp3.internal.tls.BasicCertificateChainCleaner @@ -113,6 +116,7 @@ open class Platform { * Configure TLS extensions on `sslSocket` for `route`. */ open fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, @@ -160,6 +164,9 @@ open class Platform { open fun isCleartextTrafficPermitted(hostname: String): Boolean = true + internal open val echModeConfiguration: EchModeConfiguration + get() = EchModeConfiguration.Unspecified + /** * Returns an object that holds a stack trace created at the moment this method is executed. This * should be used specifically for [java.io.Closeable] objects and in conjunction with @@ -179,7 +186,8 @@ open class Platform { ) { var logMessage = message if (stackTrace == null) { - logMessage += " To see where this was allocated, set the OkHttpClient logger level to " + + logMessage += + " To see where this was allocated, set the OkHttpClient logger level to " + "FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);" } log(logMessage, WARN, stackTrace as Throwable?) @@ -201,10 +209,13 @@ open class Platform { } } + open fun platformDns(): Dns = Dns.SYSTEM + override fun toString(): String = javaClass.simpleName companion object { - @Volatile private var platform = findPlatform() + @Volatile + private var platform = findPlatform() const val INFO = 4 const val WARN = 5 diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt index 83e2e57ee61b..780d17d55d26 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/BouncyCastlePlatform.kt @@ -22,6 +22,7 @@ import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol import org.bouncycastle.jsse.BCSSLSocket import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider @@ -56,6 +57,7 @@ class BouncyCastlePlatform private constructor() : Platform() { ) override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, @@ -69,7 +71,7 @@ class BouncyCastlePlatform private constructor() : Platform() { sslSocket.parameters = sslParameters } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(call, sslSocket, hostname, protocols) } } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt index 51ef4ca4b3c2..3bfab099ccb8 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/ConscryptPlatform.kt @@ -25,6 +25,7 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol import org.conscrypt.Conscrypt import org.conscrypt.ConscryptHostnameVerifier @@ -75,6 +76,7 @@ class ConscryptPlatform private constructor() : Platform() { override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? = null override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, @@ -87,7 +89,7 @@ class ConscryptPlatform private constructor() : Platform() { val names = alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(call, sslSocket, hostname, protocols) } } diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt index 5ae0567cd212..c536cdefab3c 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt @@ -20,6 +20,7 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.Proxy import javax.net.ssl.SSLSocket +import okhttp3.Call import okhttp3.Protocol /** OpenJDK 8 with `org.mortbay.jetty.alpn:alpn-boot` in the boot class path. */ @@ -31,6 +32,7 @@ class Jdk8WithJettyBootPlatform( private val serverProviderClass: Class<*>, ) : Platform() { override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List, diff --git a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt index e4d50a391144..1ea8ad0d13cf 100644 --- a/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt +++ b/okhttp/src/jvmMain/kotlin/okhttp3/internal/platform/OpenJSSEPlatform.kt @@ -22,6 +22,7 @@ import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import okhttp3.Call import okhttp3.Protocol /** @@ -60,6 +61,7 @@ class OpenJSSEPlatform private constructor() : Platform() { ) override fun configureTlsExtensions( + call: Call?, sslSocket: SSLSocket, hostname: String?, protocols: List<@JvmSuppressWildcards Protocol>, @@ -75,7 +77,7 @@ class OpenJSSEPlatform private constructor() : Platform() { sslSocket.sslParameters = sslParameters } } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(call, sslSocket, hostname, protocols) } }