Force OkHttp3 client to keep a TLS connection open
Asked Answered
A

0

8

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()
}
Antepenult answered 31/7, 2018 at 13:42 Comment(2)
Double check the connection times on your website too. I had a load balancer on mine that was set to timeout after 30seconds. I ran long-term websockets and they kept getting closed, but changed it to 3600seconds to match my intentions... took a long time to figure outDeodand
Mine don't necessarily need to be long term. It seems to manifest itself only when changing the type of the request. Also, it is a single-threaded server running on a very small IoT device, not a web-server, so we don't want to keep the connection open much longer than 5 seconds.Antepenult

© 2022 - 2024 — McMap. All rights reserved.