diff --git a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt index 314961463..57b31abfb 100644 --- a/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt +++ b/native/kotlin/api/kotlin/src/integrationTest/kotlin/ApiUrlDiscoveryTest.kt @@ -297,6 +297,31 @@ class ApiUrlDiscoveryTest { ) } + @Test + fun testAllowedHostnamesDoesNotBreakValidSites() = runTest { + val httpClient = WpHttpClient.DefaultHttpClient(emptyList()) + val executor = WpRequestExecutor(httpClient) + val loginClient = WpLoginClient(requestExecutor = executor) + + // First, configure an allowed hostname override for a specific cert/hostname pair + httpClient.addAllowedAlternativeNamesForHostname( + "vanilla.wpmt.co", + listOf("wordpress-1315525-4803651.cloudwaysapps.com") + ) + + // The override should work + assertEquals( + "https://vanilla.wpmt.co/wp-admin/authorize-application.php", + loginClient.apiDiscovery("https://wordpress-1315525-4803651.cloudwaysapps.com") + .assertSuccess().applicationPasswordsAuthenticationUrl.url() + ) + + // Other valid SSL sites should still work via fallback to default hostname verification. + // google.com uses wildcard/SAN certificates which require proper OkHttp verification. + val reason = loginClient.apiDiscovery("https://google.com").assertFailureFindApiRoot() + assertInstanceOf(FindApiRootFailure.ProbablyNotAWordPressSite::class.java, reason) + } + @Test fun testCustomOkHttpClient() = runTest { val executor = diff --git a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt index 27c350095..927d58cde 100644 --- a/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt +++ b/native/kotlin/api/kotlin/src/main/kotlin/rs/wordpress/api/kotlin/WpHttpClient.kt @@ -2,6 +2,7 @@ package rs.wordpress.api.kotlin import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.internal.tls.OkHostnameVerifier import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSession @@ -25,9 +26,7 @@ sealed class WpHttpClient { private fun buildClient(): OkHttpClient { return OkHttpClient.Builder().apply { this@DefaultHttpClient.interceptors.forEach { addInterceptor(it) } - if (allowedHostnames.isNotEmpty()) { - hostnameVerifier(WpRequestExecutorHostnameVerifier(allowedHostnames)) - } + hostnameVerifier(WpRequestExecutorHostnameVerifier(allowedHostnames)) }.build() } @@ -41,9 +40,12 @@ sealed class WpHttpClient { private class WpRequestExecutorHostnameVerifier(private val allowedHostnames: Map>) : HostnameVerifier { - override fun verify(hostname: String?, session: SSLSession?): Boolean = - session?.let { - val peerPrincipalName = it.peerPrincipal.name.replace("CN=", "") - peerPrincipalName == hostname || allowedHostnames[peerPrincipalName]?.contains(hostname) ?: false - } ?: false + override fun verify(hostname: String?, session: SSLSession?): Boolean { + if (hostname == null || session == null) return false + + // Check our custom allowlist first, then fall back to default OkHttp verification + val peerPrincipalName = session.peerPrincipal.name.replace("CN=", "") + val customMatch = allowedHostnames[peerPrincipalName]?.contains(hostname) ?: false + return customMatch || OkHostnameVerifier.verify(hostname, session) + } }