diff --git a/.circleci/config.yml b/.circleci/config.yml index afe75ce9d5d1..5ebdc8927f5e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,9 @@ commands: parameters: platform: type: string + testjdk: + type: string + default: "" steps: - restore_cache: keys: @@ -17,7 +20,7 @@ commands: name: Run tests command: ./gradlew --build-cache --parallel --continue test environment: - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dokhttp.platform=<< parameters.platform >> -Dorg.gradle.workers.max=3 -Xmx1G + GRADLE_OPTS: -Dorg.gradle.daemon=false -Dokhttp.platform=<< parameters.platform >> -Dorg.gradle.workers.max=3 -Xmx1G << parameters.testjdk >> - save_cache: paths: @@ -79,9 +82,9 @@ jobs: # Under normal usage, saves compiled results from master at least once a day key: v4-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}-{{ epoch }} - checkjdk8: + checkjdk11: docker: - - image: circleci/openjdk:8u222-jdk-stretch + - image: circleci/openjdk:11.0.4-jdk-stretch environment: JVM_OPTS: -Xmx1g @@ -114,7 +117,7 @@ jobs: testjdk8: docker: - - image: circleci/openjdk:8u222-jdk-stretch + - image: circleci/openjdk:11.0.4-jdk-stretch environment: JVM_OPTS: -Xmx1g @@ -125,10 +128,11 @@ jobs: - runtests: platform: jdk8 + testjdk: -Dtest.java.home=/usr/lib/jvm/java-8-openjdk-amd64 testjdk8alpn: docker: - - image: circleci/openjdk:8u222-jdk-stretch + - image: circleci/openjdk:11.0.4-jdk-stretch environment: JVM_OPTS: -Xmx1g @@ -139,6 +143,7 @@ jobs: - runtests: platform: jdk8alpn + testjdk: -Dtest.java.home=/usr/lib/jvm/java-8-openjdk-amd64 -Dalpn.boot.version=8.1.13.v20181017 testopenjsse: docker: @@ -153,6 +158,7 @@ jobs: - runtests: platform: openjsse + testjdk: -Dtest.java.home=/usr/lib/jvm/java-8-openjdk-amd64 testjdk11: docker: @@ -205,7 +211,7 @@ workflows: filters: branches: only: master - - checkjdk8: + - checkjdk11: filters: branches: ignore: @@ -245,7 +251,7 @@ workflows: only: master jobs: - compile - - checkjdk8: + - checkjdk11: requires: - compile - testjdk8: diff --git a/android-test/src/androidTest/java/okhttp/android/test/OkHttpTest.kt b/android-test/src/androidTest/java/okhttp/android/test/OkHttpTest.kt index 30461171423d..9f4b8bd140d2 100644 --- a/android-test/src/androidTest/java/okhttp/android/test/OkHttpTest.kt +++ b/android-test/src/androidTest/java/okhttp/android/test/OkHttpTest.kt @@ -48,6 +48,9 @@ import java.net.InetAddress import java.net.UnknownHostException import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket +import okhttp3.internal.platform.Platform +import okhttp3.internal.platform.AndroidPlatform +import okhttp3.internal.platform.AndroidQPlatform /** * Run with "./gradlew :android-test:connectedCheck" and make sure ANDROID_SDK_ROOT is set. @@ -67,8 +70,7 @@ class OkHttpTest { @Before fun createClient() { - client = OkHttpClient.Builder() - .build() + client = OkHttpClient.Builder().build() } @After @@ -76,6 +78,15 @@ class OkHttpTest { client.dispatcher.executorService.shutdownNow() } + @Test + fun testPlatform() { + if (Build.VERSION.SDK_INT >= 29) { + assertTrue(Platform.get() is AndroidQPlatform) + } else { + assertTrue(Platform.get() is AndroidPlatform) + } + } + @Test fun testRequest() { assumeNetwork() diff --git a/build.gradle b/build.gradle index b71f328cc706..80227a3154a8 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ import net.ltgt.gradle.errorprone.CheckSeverity buildscript { ext.versions = [ - 'android': '4.1.1.4', 'animalSniffer': '1.17', 'assertj': '3.11.0', 'bouncycastle': '1.62', @@ -25,7 +24,7 @@ buildscript { ext.deps = [ 'picocli': "info.picocli:picocli:${versions.picocli}", - 'android': "com.google.android:android:${versions.android}", + 'android': "org.robolectric:android-all:9-robolectric-4913185-2", 'animalSniffer': "org.codehaus.mojo:animal-sniffer-annotations:${versions.animalSniffer}", 'assertj': "org.assertj:assertj-core:${versions.assertj}", 'bouncycastle': "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}", @@ -132,11 +131,16 @@ subprojects { project -> } def platform = System.getProperty("okhttp.platform", "jdk8") + def platformJavaHome = System.getProperty('test.java.home') test { jvmArgs += "-Dlistener=okhttp3.testing.InstallUncaughtExceptionHandlerListener" jvmArgs += "-Dokhttp.platform=$platform" + if (platformJavaHome != null) { + executable = "$platformJavaHome/bin/java" + } + maxParallelForks Runtime.runtime.availableProcessors() * 2 testLogging { exceptionFormat = 'full' @@ -211,6 +215,12 @@ tasks.wrapper { * https://github.com/xjdr/xio/blob/master/alpn-boot.gradle */ def alpnBootVersion() { + def version = System.getProperty('alpn.boot.version') + + if (version != null) { + return version + } + def javaVersion = System.getProperty("java.version") def patchVersionMatcher = (javaVersion =~ /1\.8\.0_(\d+)(-.*)?/) if (!patchVersionMatcher.find()) return null diff --git a/mockwebserver/src/main/java/okhttp3/mockwebserver/MockWebServer.kt b/mockwebserver/src/main/java/okhttp3/mockwebserver/MockWebServer.kt index aded20dcf0ca..0385c2fc75e8 100644 --- a/mockwebserver/src/main/java/okhttp3/mockwebserver/MockWebServer.kt +++ b/mockwebserver/src/main/java/okhttp3/mockwebserver/MockWebServer.kt @@ -507,7 +507,7 @@ class MockWebServer : ExternalResource(), Closeable { openClientSockets.add(socket) if (protocolNegotiationEnabled) { - Platform.get().configureTlsExtensions(sslSocket, null, protocols) + Platform.get().configureTlsExtensions(sslSocket, protocols) } sslSocket.startHandshake() diff --git a/mockwebserver/src/test/java/okhttp3/mockwebserver/internal/http2/Http2Server.java b/mockwebserver/src/test/java/okhttp3/mockwebserver/internal/http2/Http2Server.java index 160159d98e97..1f3039daac3a 100644 --- a/mockwebserver/src/test/java/okhttp3/mockwebserver/internal/http2/Http2Server.java +++ b/mockwebserver/src/test/java/okhttp3/mockwebserver/internal/http2/Http2Server.java @@ -92,7 +92,7 @@ private SSLSocket doSsl(Socket socket) throws IOException { SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket( socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); sslSocket.setUseClientMode(false); - Platform.get().configureTlsExtensions(sslSocket, null, + Platform.get().configureTlsExtensions(sslSocket, Collections.singletonList(Protocol.HTTP_2)); sslSocket.startHandshake(); return sslSocket; diff --git a/okhttp/build.gradle b/okhttp/build.gradle index 087ca5b6f807..8c88317eb9f6 100644 --- a/okhttp/build.gradle +++ b/okhttp/build.gradle @@ -25,9 +25,9 @@ task copyJavaTemplates(type: Copy) { dependencies { api deps.okio api deps.kotlinStdlib + compileOnly deps.android compileOnly deps.conscrypt compileOnly deps.openjsse - compileOnly deps.android compileOnly deps.jsr305 compileOnly deps.animalSniffer @@ -50,6 +50,10 @@ afterEvaluate { project -> } } +animalsniffer { + ignore 'javax.net.ssl.SSLParameters', 'javax.net.ssl.SSLSocket' +} + task japicmp(type: me.champeau.gradle.japicmp.JapicmpTask, dependsOn: 'jar') { oldClasspath = files(baselineJar(project, baselineVersion)) newClasspath = files(jar.archivePath) diff --git a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt index 6b299e997ae1..a68eaf25e01c 100644 --- a/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt +++ b/okhttp/src/main/java/okhttp3/internal/connection/RealConnection.kt @@ -344,7 +344,7 @@ class RealConnection( // Configure the socket's ciphers, TLS versions, and extensions. val connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket) if (connectionSpec.supportsTlsExtensions) { - Platform.get().configureTlsExtensions(sslSocket, address.url.host, address.protocols) + Platform.get().configureTlsExtensions(sslSocket, address.protocols) } // Force handshake. This can throw! diff --git a/okhttp/src/main/java/okhttp3/internal/platform/AndroidPlatform.kt b/okhttp/src/main/java/okhttp3/internal/platform/AndroidPlatform.kt index 331850b07c8c..80567048d0e4 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/AndroidPlatform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/AndroidPlatform.kt @@ -17,6 +17,7 @@ package okhttp3.internal.platform import android.os.Build import okhttp3.Protocol +import okhttp3.internal.platform.android.AndroidCertificateChainCleaner import okhttp3.internal.platform.android.CloseGuard import okhttp3.internal.platform.android.ConscryptSocketAdapter import okhttp3.internal.platform.android.DeferredSocketAdapter @@ -30,10 +31,8 @@ import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.net.InetSocketAddress import java.net.Socket -import java.security.cert.Certificate import java.security.cert.TrustAnchor import java.security.cert.X509Certificate -import javax.net.ssl.SSLPeerUnverifiedException import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -73,12 +72,11 @@ class AndroidPlatform : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, - protocols: List + protocols: List<@JvmSuppressWildcards Protocol> ) { // No TLS extensions if the socket class is custom. socketAdapters.find { it.matchesSocket(sslSocket) } - ?.configureTlsExtensions(sslSocket, hostname, protocols) + ?.configureTlsExtensions(sslSocket, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket) = @@ -168,37 +166,6 @@ class AndroidPlatform : Platform() { super.buildTrustRootIndex(trustManager) } - /** - * X509TrustManagerExtensions was added to Android in API 17 (Android 4.2, released in late 2012). - * This is the best way to get a clean chain on Android because it uses the same code as the TLS - * handshake. - */ - internal class AndroidCertificateChainCleaner( - private val x509TrustManagerExtensions: Any, - private val checkServerTrusted: Method - ) : CertificateChainCleaner() { - - @Suppress("UNCHECKED_CAST") - @Throws(SSLPeerUnverifiedException::class) - override // Reflection on List. - fun clean(chain: List, hostname: String): List = try { - val certificates = (chain as List).toTypedArray() - checkServerTrusted.invoke( - x509TrustManagerExtensions, certificates, "RSA", hostname) as List - } catch (e: InvocationTargetException) { - val exception = SSLPeerUnverifiedException(e.message) - exception.initCause(e) - throw exception - } catch (e: IllegalAccessException) { - throw AssertionError(e) - } - - override fun equals(other: Any?): Boolean = - other is AndroidCertificateChainCleaner // All instances are equivalent. - - override fun hashCode(): Int = 0 - } - /** * A trust manager for Android applications that customize the trust manager. * @@ -224,12 +191,21 @@ class AndroidPlatform : Platform() { } companion object { + val isAndroid: Boolean = try { + // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. + Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl") + true + } catch (_: ClassNotFoundException) { + false + } + val isSupported: Boolean = try { // Trigger an early exception over a fatal error, prefer a RuntimeException over Error. Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl") // Fail Fast - check(Build.VERSION.SDK_INT >= 21) { "Expected Android API level 21+ but was ${Build.VERSION.SDK_INT}" } + check( + Build.VERSION.SDK_INT >= 21) { "Expected Android API level 21+ but was ${Build.VERSION.SDK_INT}" } true } catch (_: ClassNotFoundException) { diff --git a/okhttp/src/main/java/okhttp3/internal/platform/AndroidQPlatform.kt b/okhttp/src/main/java/okhttp3/internal/platform/AndroidQPlatform.kt new file mode 100644 index 000000000000..bd6ec4257d4e --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/platform/AndroidQPlatform.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * 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.os.Build +import android.security.NetworkSecurityPolicy +import okhttp3.internal.platform.AndroidPlatform.Companion.isAndroid +import okhttp3.internal.platform.android.AndroidQCertificateChainCleaner +import okhttp3.internal.tls.CertificateChainCleaner +import java.io.IOException +import java.net.InetSocketAddress +import java.net.Socket +import javax.net.ssl.X509TrustManager + +/** Android 29+. */ +class AndroidQPlatform : Jdk9Platform() { + @Throws(IOException::class) + override fun connectSocket( + socket: Socket, + address: InetSocketAddress, + connectTimeout: Int + ) { + socket.connect(address, connectTimeout) + } + + override fun isCleartextTrafficPermitted(hostname: String): Boolean = + NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(hostname) + + override fun buildCertificateChainCleaner(trustManager: X509TrustManager): CertificateChainCleaner = + AndroidQCertificateChainCleaner(trustManager) + + companion object { + val isSupported: Boolean = isAndroid && Build.VERSION.SDK_INT >= 29 + + fun buildIfSupported(): Platform? = if (isSupported) AndroidQPlatform() else null + } +} diff --git a/okhttp/src/main/java/okhttp3/internal/platform/ConscryptPlatform.kt b/okhttp/src/main/java/okhttp3/internal/platform/ConscryptPlatform.kt index a5f9911c215c..3ab0859988b8 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/ConscryptPlatform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/ConscryptPlatform.kt @@ -64,21 +64,17 @@ class ConscryptPlatform private constructor() : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, - protocols: List + protocols: List<@JvmSuppressWildcards Protocol> ) { if (Conscrypt.isConscrypt(sslSocket)) { - // Enable SNI and session tickets. - if (hostname != null) { - Conscrypt.setUseSessionTickets(sslSocket, true) - Conscrypt.setHostname(sslSocket, hostname) - } + // Enable session tickets. + Conscrypt.setUseSessionTickets(sslSocket, true) // Enable ALPN. val names = alpnProtocolNames(protocols) Conscrypt.setApplicationProtocols(sslSocket, names.toTypedArray()) } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(sslSocket, protocols) } } diff --git a/okhttp/src/main/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt b/okhttp/src/main/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt index 92e8d1c96e0b..602b736c5b02 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/Jdk8WithJettyBootPlatform.kt @@ -32,7 +32,6 @@ class Jdk8WithJettyBootPlatform( ) : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List ) { val names = alpnProtocolNames(protocols) @@ -58,9 +57,9 @@ class Jdk8WithJettyBootPlatform( } } - override fun getSelectedProtocol(socket: SSLSocket): String? { + override fun getSelectedProtocol(sslSocket: SSLSocket): String? { try { - val provider = Proxy.getInvocationHandler(getMethod.invoke(null, socket)) as AlpnProvider + val provider = Proxy.getInvocationHandler(getMethod.invoke(null, sslSocket)) as AlpnProvider if (!provider.unsupported && provider.selected == null) { Platform.get().log(INFO, "ALPN callback dropped: HTTP/2 is disabled. " + "Is alpn-boot on the boot class path?", diff --git a/okhttp/src/main/java/okhttp3/internal/platform/Jdk9Platform.kt b/okhttp/src/main/java/okhttp3/internal/platform/Jdk9Platform.kt index 6d6e1d1fcc35..71fdf72ac76f 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/Jdk9Platform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/Jdk9Platform.kt @@ -15,50 +15,33 @@ */ package okhttp3.internal.platform -import java.lang.reflect.InvocationTargetException -import java.lang.reflect.Method -import javax.net.ssl.SSLParameters +import okhttp3.Protocol import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager -import okhttp3.Protocol /** OpenJDK 9+. */ -class Jdk9Platform( - @JvmField val setProtocolMethod: Method, - @JvmField val getProtocolMethod: Method -) : Platform() { +open class Jdk9Platform() : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, - protocols: List + protocols: List<@JvmSuppressWildcards Protocol> ) { - try { - val sslParameters = sslSocket.sslParameters + val sslParameters = sslSocket.sslParameters - val names = alpnProtocolNames(protocols) + val names = alpnProtocolNames(protocols) - setProtocolMethod.invoke(sslParameters, names.toTypedArray()) + sslParameters.applicationProtocols = names.toTypedArray() - sslSocket.sslParameters = sslParameters - } catch (e: IllegalAccessException) { - throw AssertionError("failed to set SSL parameters", e) - } catch (e: InvocationTargetException) { - throw AssertionError("failed to set SSL parameters", e) - } + sslSocket.sslParameters = sslParameters } - override fun getSelectedProtocol(socket: SSLSocket): String? = try { + override fun getSelectedProtocol(socket: SSLSocket): String? { // SSLSocket.getApplicationProtocol returns "" if application protocols values will not // be used. Observed if you didn't specify SSLParameters.setApplicationProtocols - when (val protocol = getProtocolMethod.invoke(socket) as String?) { + return when (val protocol = socket.applicationProtocol) { null, "" -> null else -> protocol } - } catch (e: IllegalAccessException) { - throw AssertionError("failed to get ALPN selected protocol", e) - } catch (e: InvocationTargetException) { - throw AssertionError("failed to get ALPN selected protocol", e) } public override fun trustManager(sslSocketFactory: SSLSocketFactory): X509TrustManager? { @@ -71,17 +54,13 @@ class Jdk9Platform( } companion object { - fun buildIfSupported(): Jdk9Platform? = - try { - // Find JDK 9 methods - val setProtocolMethod = SSLParameters::class.java.getMethod("setApplicationProtocols", - Array::class.java) - val getProtocolMethod = SSLSocket::class.java.getMethod("getApplicationProtocol") + val isAvailable: Boolean + + init { + val majorVersion: Int = Integer.getInteger("java.specification.version") ?: 8 + isAvailable = majorVersion >= 9 + } - Jdk9Platform(setProtocolMethod, getProtocolMethod) - } catch (_: NoSuchMethodException) { - // pre JDK 9 - null - } + fun buildIfSupported(): Jdk9Platform? = if (isAvailable) Jdk9Platform() else null } } diff --git a/okhttp/src/main/java/okhttp3/internal/platform/OpenJSSEPlatform.kt b/okhttp/src/main/java/okhttp3/internal/platform/OpenJSSEPlatform.kt index bf80f2212049..7e5f62810b76 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/OpenJSSEPlatform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/OpenJSSEPlatform.kt @@ -56,8 +56,7 @@ class OpenJSSEPlatform private constructor() : Platform() { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, - protocols: List + protocols: List<@JvmSuppressWildcards Protocol> ) { if (sslSocket is org.openjsse.javax.net.ssl.SSLSocket) { val sslParameters = sslSocket.sslParameters @@ -70,7 +69,7 @@ class OpenJSSEPlatform private constructor() : Platform() { sslSocket.sslParameters = sslParameters } } else { - super.configureTlsExtensions(sslSocket, hostname, protocols) + super.configureTlsExtensions(sslSocket, protocols) } } diff --git a/okhttp/src/main/java/okhttp3/internal/platform/Platform.kt b/okhttp/src/main/java/okhttp3/internal/platform/Platform.kt index 99607dfa35e0..df05aa92f6a6 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/Platform.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/Platform.kt @@ -110,7 +110,6 @@ open class Platform { */ open fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List<@JvmSuppressWildcards Protocol> ) { } @@ -208,6 +207,12 @@ open class Platform { /** Attempt to match the host runtime to a capable Platform implementation. */ private fun findPlatform(): Platform { + val androidQ = AndroidQPlatform.buildIfSupported() + + if (androidQ != null) { + return androidQ + } + val android = AndroidPlatform.buildIfSupported() if (android != null) { diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidCertificateChainCleaner.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidCertificateChainCleaner.kt new file mode 100644 index 000000000000..996c772ea84e --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidCertificateChainCleaner.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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 okhttp3.internal.tls.CertificateChainCleaner +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import javax.net.ssl.SSLPeerUnverifiedException + +/** + * Legacy Android implementation of CertificateChainCleaner relying on reflection. + * + * X509TrustManagerExtensions was added to Android in API 17 (Android 4.2, released in late 2012). + * This is the best way to get a clean chain on Android because it uses the same code as the TLS + * handshake. + */ +internal class AndroidCertificateChainCleaner( + private val x509TrustManagerExtensions: Any, + private val checkServerTrusted: Method +) : CertificateChainCleaner() { + + @Suppress("UNCHECKED_CAST") // Reflection on List + @Throws(SSLPeerUnverifiedException::class) + override + fun clean(chain: List, hostname: String): List = try { + val certificates = (chain as List).toTypedArray() + checkServerTrusted.invoke( + x509TrustManagerExtensions, certificates, "RSA", hostname) as List + } catch (e: InvocationTargetException) { + throw SSLPeerUnverifiedException(e.message).apply { initCause(e) } + } catch (e: IllegalAccessException) { + throw AssertionError(e) + } + + override fun equals(other: Any?): Boolean = + other is AndroidCertificateChainCleaner // All instances are equivalent. + + override fun hashCode(): Int = 0 +} \ No newline at end of file diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidQCertificateChainCleaner.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidQCertificateChainCleaner.kt new file mode 100644 index 000000000000..5462c44c260e --- /dev/null +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidQCertificateChainCleaner.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * 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.net.http.X509TrustManagerExtensions +import okhttp3.internal.tls.CertificateChainCleaner +import java.security.cert.Certificate +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import javax.net.ssl.SSLPeerUnverifiedException +import javax.net.ssl.X509TrustManager + +/** + * Android Q+ implementation of CertificateChainCleaner using direct Android API calls. + * + * X509TrustManagerExtensions was added to Android in API 17 (Android 4.2, released in late 2012). + * This is the best way to get a clean chain on Android because it uses the same code as the TLS + * handshake. + */ +internal class AndroidQCertificateChainCleaner( + trustManager: X509TrustManager +) : CertificateChainCleaner() { + val extensions = X509TrustManagerExtensions(trustManager) + + @Suppress("UNCHECKED_CAST") + @Throws(SSLPeerUnverifiedException::class) + override + fun clean(chain: List, hostname: String): List { + val certificates = (chain as List).toTypedArray() + try { + return extensions.checkServerTrusted(certificates, "RSA", hostname) + } catch (ce: CertificateException) { + throw SSLPeerUnverifiedException(ce.message).apply { initCause(ce) } + } + } + + override fun equals(other: Any?): Boolean = + other is AndroidQCertificateChainCleaner // All instances are equivalent. + + override fun hashCode(): Int = 1 +} \ No newline at end of file diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidSocketAdapter.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidSocketAdapter.kt index 4ac9e4c0cf02..e58cf78daeb6 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidSocketAdapter.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/AndroidSocketAdapter.kt @@ -32,7 +32,6 @@ open class AndroidSocketAdapter(private val sslSocketClass: Class) SocketAdapter { private val setUseSessionTickets: Method = sslSocketClass.getDeclaredMethod("setUseSessionTickets", Boolean::class.javaPrimitiveType) - private val setHostname = sslSocketClass.getMethod("setHostname", String::class.java) private val getAlpnSelectedProtocol = sslSocketClass.getMethod("getAlpnSelectedProtocol") private val setAlpnProtocols = sslSocketClass.getMethod("setAlpnProtocols", ByteArray::class.java) @@ -47,18 +46,13 @@ open class AndroidSocketAdapter(private val sslSocketClass: Class) override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { try { - // Enable SNI and session tickets. - if (hostname != null) { - setUseSessionTickets.invoke(sslSocket, true) - // This is SSLParameters.setServerNames() in API 24+. - setHostname.invoke(sslSocket, hostname) - } + // Enable session tickets. + setUseSessionTickets.invoke(sslSocket, true) // Enable ALPN. setAlpnProtocols.invoke(sslSocket, diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt index 11f574b54445..4f23c881aeff 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/ConscryptSocketAdapter.kt @@ -43,16 +43,12 @@ object ConscryptSocketAdapter : SocketAdapter { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List ) { // No TLS extensions if the socket class is custom. if (matchesSocket(sslSocket)) { - // Enable SNI and session tickets. - if (hostname != null) { - Conscrypt.setUseSessionTickets(sslSocket, true) - Conscrypt.setHostname(sslSocket, hostname) - } + // Enable session tickets. + Conscrypt.setUseSessionTickets(sslSocket, true) // Enable ALPN. val names = Platform.alpnProtocolNames(protocols) diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/DeferredSocketAdapter.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/DeferredSocketAdapter.kt index 87d1277f2e4a..c3cef3dbeac3 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/android/DeferredSocketAdapter.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/DeferredSocketAdapter.kt @@ -39,10 +39,9 @@ class DeferredSocketAdapter(private val socketPackage: String) : SocketAdapter { override fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List ) { - getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, hostname, protocols) + getDelegate(sslSocket)?.configureTlsExtensions(sslSocket, protocols) } override fun getSelectedProtocol(sslSocket: SSLSocket): String? { diff --git a/okhttp/src/main/java/okhttp3/internal/platform/android/SocketAdapter.kt b/okhttp/src/main/java/okhttp3/internal/platform/android/SocketAdapter.kt index 5555f1c695f9..66fb0611c68e 100644 --- a/okhttp/src/main/java/okhttp3/internal/platform/android/SocketAdapter.kt +++ b/okhttp/src/main/java/okhttp3/internal/platform/android/SocketAdapter.kt @@ -28,7 +28,6 @@ interface SocketAdapter { open fun configureTlsExtensions( sslSocket: SSLSocket, - hostname: String?, protocols: List ) diff --git a/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java b/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java index 12e085e18cb6..484c1e0ded88 100644 --- a/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java +++ b/okhttp/src/test/java/okhttp3/internal/platform/Jdk9PlatformTest.java @@ -15,7 +15,6 @@ */ package okhttp3.internal.platform; -import java.lang.reflect.Method; import okhttp3.testing.PlatformRule; import org.junit.Rule; import org.junit.Test; @@ -30,17 +29,8 @@ public void buildsWhenJdk9() { assertThat(Jdk9Platform.Companion.buildIfSupported()).isNotNull(); } - @Test - public void findsAlpnMethods() { - Jdk9Platform platform = Jdk9Platform.Companion.buildIfSupported(); - - assertThat(platform.getProtocolMethod.getName()).isEqualTo("getApplicationProtocol"); - assertThat(platform.setProtocolMethod.getName()).isEqualTo("setApplicationProtocols"); - } - @Test public void testToStringIsClassname() throws NoSuchMethodException { - Method method = this.getClass().getMethod("toString"); - assertThat(new Jdk9Platform(method, method).toString()).isEqualTo("Jdk9Platform"); + assertThat(new Jdk9Platform().toString()).isEqualTo("Jdk9Platform"); } } diff --git a/okhttp/src/test/java/okhttp3/internal/platform/android/AndroidSocketAdapterTest.kt b/okhttp/src/test/java/okhttp3/internal/platform/android/AndroidSocketAdapterTest.kt index d9d601733696..f79644a12c11 100644 --- a/okhttp/src/test/java/okhttp3/internal/platform/android/AndroidSocketAdapterTest.kt +++ b/okhttp/src/test/java/okhttp3/internal/platform/android/AndroidSocketAdapterTest.kt @@ -19,6 +19,7 @@ import okhttp3.DelegatingSSLSocket import okhttp3.DelegatingSSLSocketFactory import okhttp3.Protocol.HTTP_1_1 import okhttp3.Protocol.HTTP_2 +import okhttp3.testing.PlatformRule import org.conscrypt.Conscrypt import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -26,6 +27,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assume.assumeFalse import org.junit.Assume.assumeTrue +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -35,11 +37,17 @@ import javax.net.ssl.SSLSocket @RunWith(Parameterized::class) class AndroidSocketAdapterTest(private val adapter: SocketAdapter) { - private val provider: Provider = Conscrypt.newProviderBuilder().provideTrustManager(true).build() - val context: SSLContext = SSLContext.getInstance("TLS", provider) + @Suppress("RedundantVisibilityModifier") + @JvmField + @Rule + public val platform = PlatformRule.conscrypt() - init { - context.init(null, null, null) + val context by lazy { + val provider: Provider = Conscrypt.newProviderBuilder().provideTrustManager(true).build() + + SSLContext.getInstance("TLS", provider).apply { + init(null, null, null) + } } @Test @@ -49,7 +57,7 @@ class AndroidSocketAdapterTest(private val adapter: SocketAdapter) { val sslSocket = socketFactory.createSocket() as SSLSocket assertTrue(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, "example.com", listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions(sslSocket, listOf(HTTP_2, HTTP_1_1)) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) } @@ -80,7 +88,7 @@ class AndroidSocketAdapterTest(private val adapter: SocketAdapter) { object : DelegatingSSLSocket(context.socketFactory.createSocket() as SSLSocket) {} assertFalse(adapter.matchesSocket(sslSocket)) - adapter.configureTlsExtensions(sslSocket, "example.com", listOf(HTTP_2, HTTP_1_1)) + adapter.configureTlsExtensions(sslSocket, listOf(HTTP_2, HTTP_1_1)) // not connected assertNull(adapter.getSelectedProtocol(sslSocket)) }