I am developing an application where I must communicate via HTTPS with an embedded IoT product that has a self-signed certificate. I successfully set up OkHttp to work with the self-signed certificate and am making network calls via Retrofit2 with the RxCallAdapters.
The embedded product can only handle a single connection at a time, so I have my underlying OkHttp instance configured to only allow one connection (to the best of my knowledge, maybe there is a better way).
If I am only making GET requests the handshake completes successfully and the connection is left open for the entire sequence of requests. The embedded product closes the connection after 5 seconds of inactivity, so it is expected for me to have to redo the handshake every once in awhile.
The issues start to come when doing PUT and POST requests. It seems that OkHttp will not keep the existing connection open when a stream of requests changes from one request type to another, or really anytime the request is a PUT or POST. For example:
HANDSHAKE - GET - GET - GET - HANDSHAKE - PUT - HANDSHAKE - GET ... etc.
How do I force OkHttp to keep the connection open between different types of requests? I know it shouldn't matter but seems to be related to the response code. The REST API on the embedded device gives 200 for GET, 201 for POST, and 204 for PUT.
Here is the relevant code that I use to configure OkHttp and my retrofit instance:
@Provides
@Singleton
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
val httpLoggingInterceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").d(message) }
setLogLevel(httpLoggingInterceptor)
return httpLoggingInterceptor
}
private fun setLogLevel(httpLoggingInterceptor: HttpLoggingInterceptor) {
if (BuildConfig.DEBUG) {
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
} else {
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.NONE
}
}
@Provides
@Singleton
fun provideContentTypeHeaderInterceptor(): Interceptor {
return Interceptor { chain ->
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
requestBuilder.header("Content-Type", "application/json")
chain.proceed(requestBuilder.build())
}
}
@Provides
@Singleton
@LocalOkHttpConnectionPool
fun provideLocalConnectionPool() = ConnectionPool(1, 5, TimeUnit.SECONDS)
@Provides
@Singleton
@LocalOkHttpClient
fun provideLocalOkHttpClient(headerInterceptor: Interceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
@LocalOkHttpConnectionPool connectionPool: ConnectionPool,
context: Context): OkHttpClient {
val cf = CertificateFactory.getInstance("X.509")
val cert = context.getResources().openRawResource(R.raw.ca) // Place your 'my_cert.crt' file in `res/raw`
val ca = cf.generateCertificate(cert)
cert.close()
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType)
keyStore.load(null, null)
keyStore.setCertificateEntry("ca", ca)
val tmfs = CompositeX509TrustManager.getTrustManagers(keyStore)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, tmfs, null)
val hostNameVerifier = HostnameVerifier { hostname, session ->
return@HostnameVerifier true
}
val connectionSpec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites()
.allEnabledTlsVersions()
.supportsTlsExtensions(true)
.build()
val connectionSpecs = mutableListOf(connectionSpec)
val okHttpClient = OkHttpClient.Builder()
.connectionSpecs(connectionSpecs)
.hostnameVerifier(hostNameVerifier)
.addInterceptor(headerInterceptor)
.addInterceptor(httpLoggingInterceptor)
.connectionPool(connectionPool)
.sslSocketFactory(sslContext.socketFactory, tmfs.first() as X509TrustManager)
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
return okHttpClient
}
@Provides
@Singleton
@LocalRetrofit
fun provideLocalRetrofit(@LocalOkHttpClient okHttpClient: OkHttpClient,
gson: Gson): Retrofit {
return Retrofit.Builder().baseUrl(NetworkUtils.BASE_AUTH_URL)
.addConverterFactory(NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build()
}