Allow to use self-signed certificates.

By default OkHttpClient will not allow self-signed certificates, but
some of app users use them.

This is disabled by default, should be enabled explicitly.

It also allows any CN in self-signed certificate.

Signed-off-by: Yahor Berdnikau <egorr.berd@gmail.com>
This commit is contained in:
Yahor Berdnikau 2017-12-25 12:25:54 +01:00
parent 4f44977e55
commit 1333534988
6 changed files with 214 additions and 4 deletions

View File

@ -1,6 +1,7 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.Okio
import org.amshove.kluent.`should be`
import org.amshove.kluent.`should contain`
@ -8,6 +9,7 @@ import org.amshove.kluent.`should not be`
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
import retrofit2.Response
import java.io.InputStream
import java.nio.charset.Charset
import java.text.SimpleDateFormat
import java.util.Calendar
@ -24,16 +26,25 @@ val dateFormat by lazy(LazyThreadSafetyMode.NONE, {
})
fun MockWebServerRule.enqueueResponse(resourceName: String) {
this.mockWebServer.enqueue(MockResponse()
mockWebServer.enqueueResponse(resourceName)
}
fun MockWebServer.enqueueResponse(resourceName: String) {
enqueue(MockResponse()
.setBody(loadJsonResponse(resourceName))
.setHeader("Content-Type", "application/json;charset=UTF-8"))
}
fun MockWebServerRule.loadJsonResponse(name: String): String {
fun Any.loadJsonResponse(name: String): String {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.readString(Charset.forName("UTF-8"))
}
fun Any.loadResourceStream(name: String): InputStream {
val source = Okio.buffer(Okio.source(javaClass.classLoader.getResourceAsStream(name)))
return source.inputStream()
}
fun <T> assertResponseSuccessful(response: Response<T>) {
response.isSuccessful `should be` true
response.body() `should not be` null

View File

@ -0,0 +1,94 @@
package org.moire.ultrasonic.api.subsonic
import okhttp3.mockwebserver.MockWebServer
import org.amshove.kluent.`should throw`
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.InputStream
import java.net.InetAddress
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.TrustManagerFactory
private const val PORT = 8443
private const val HOST = "localhost"
/**
* Integration test to check [SubsonicAPIClient] interaction with different SSL scenarios.
*/
class SubsonicApiSSLTest {
private val mockWebServer = MockWebServer()
@Before
fun setUp() {
val sslContext = createSSLContext(loadResourceStream("self-signed.pem"),
loadResourceStream("self-signed.p12"), "")
mockWebServer.useHttps(sslContext.socketFactory, false)
mockWebServer.start(InetAddress.getByName(HOST), PORT)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
private fun createSSLContext(certificatePemStream: InputStream,
certificatePkcs12Stream: InputStream,
password: String): SSLContext {
var cert: X509Certificate? = null
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType())
trustStore.load(null)
certificatePemStream.use {
cert = (CertificateFactory.getInstance("X.509")
.generateCertificate(certificatePemStream)) as X509Certificate
}
val alias = cert?.subjectX500Principal?.name ?:
throw IllegalStateException("Failed to load certificate")
trustStore.setCertificateEntry(alias, cert)
val tmf = TrustManagerFactory.getInstance("X509")
tmf.init(trustStore)
val trustManagers = tmf.trustManagers
val sslContext = SSLContext.getInstance("TLS")
val ks = KeyStore.getInstance("PKCS12")
ks.load(certificatePkcs12Stream, password.toCharArray())
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(ks, password.toCharArray())
sslContext.init(kmf.keyManagers, trustManagers, null)
return sslContext
}
@Test
fun `Should fail request if self-signed certificate support is disabled`() {
val client = createSubsonicClient(false)
mockWebServer.enqueueResponse("ping_ok.json")
val fail = {
client.api.ping().execute()
}
fail `should throw` SSLHandshakeException::class
}
@Test
fun `Should pass request if self-signed certificate support is enabled`() {
val client = createSubsonicClient(true)
mockWebServer.enqueueResponse("ping_ok.json")
val response = client.api.ping().execute()
assertResponseSuccessful(response)
}
private fun createSubsonicClient(allowSelfSignedCertificate: Boolean) = SubsonicAPIClient(
"https://$HOST:$PORT/", USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID,
allowSelfSignedCertificate = allowSelfSignedCertificate)
}

View File

@ -12,7 +12,7 @@ class MockWebServerRule : TestRule {
val mockWebServer = MockWebServer()
override fun apply(base: Statement?, description: Description?): Statement {
val ruleStatement = object : Statement() {
return object : Statement() {
override fun evaluate() {
try {
mockWebServer.start()
@ -22,6 +22,5 @@ class MockWebServerRule : TestRule {
}
}
}
return ruleStatement
}
}

View File

@ -0,0 +1,84 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -17,7 +17,11 @@ import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
private const val READ_TIMEOUT = 60_000L
@ -36,6 +40,7 @@ class SubsonicAPIClient(baseUrl: String,
password: String,
minimalProtocolVersion: SubsonicAPIVersions,
clientID: String,
allowSelfSignedCertificate: Boolean = false,
debug: Boolean = false) {
private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) {
protocolVersion = it
@ -56,6 +61,7 @@ class SubsonicAPIClient(baseUrl: String,
private val okHttpClient = OkHttpClient.Builder()
.readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (allowSelfSignedCertificate) allowSelfSignedCertificates() }
.addInterceptor { chain ->
// Adds default request params
val originalRequest = chain.request()
@ -165,4 +171,20 @@ class SubsonicAPIClient(baseUrl: String,
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
this.addInterceptor(loggingInterceptor)
}
@SuppressWarnings("TrustAllX509TrustManager")
private fun OkHttpClient.Builder.allowSelfSignedCertificates() {
val trustManager = object : X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf(trustManager), SecureRandom())
sslSocketFactory(sslContext.socketFactory, trustManager)
hostnameVerifier { _, _ -> true }
}
}