From 5db3bb377934f8e8569c0d0377825df01ff49bac Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 13 Dec 2020 16:31:12 +0100 Subject: [PATCH] Workaround for old Androids not connecting to new Let's Encrypt hosts (#2014) * Rename .java to .kt * convert OkHttpUtils to Kotlin * trust new letsencrypt root cert * cleanup OkHttpUtils * add link to lets encrypt cert to OkHttpUtils --- app/build.gradle | 1 + .../keylesspalace/tusky/di/NetworkModule.kt | 7 +- .../keylesspalace/tusky/util/OkHttpUtils.java | 87 ------------- .../keylesspalace/tusky/util/OkHttpUtils.kt | 115 ++++++++++++++++++ 4 files changed, 120 insertions(+), 90 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 21883df4b..018217a7c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -139,6 +139,7 @@ dependencies { implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" + implementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion" implementation "org.conscrypt:conscrypt-android:2.4.0" diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index b349d50d9..ca60bac8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -24,7 +24,7 @@ import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.OkHttpUtils +import com.keylesspalace.tusky.util.okhttpClient import dagger.Module import dagger.Provides import okhttp3.OkHttpClient @@ -32,6 +32,7 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.create import javax.inject.Singleton /** @@ -55,7 +56,7 @@ class NetworkModule { accountManager: AccountManager, context: Context ): OkHttpClient { - return OkHttpUtils.getCompatibleClientBuilder(context) + return okhttpClient(context) .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { @@ -81,5 +82,5 @@ class NetworkModule { @Provides @Singleton - fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create(MastodonApi::class.java) + fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create() } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java deleted file mode 100644 index 22e13e5ec..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * Lesser General Public License as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser - * General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with Tusky. If - * not, see . */ - -package com.keylesspalace.tusky.util; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.preference.PreferenceManager; - -import com.keylesspalace.tusky.BuildConfig; - -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.util.concurrent.TimeUnit; - -import okhttp3.Cache; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; - -public class OkHttpUtils { - - @NonNull - public static OkHttpClient.Builder getCompatibleClientBuilder(@NonNull Context context) { - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - - boolean httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false); - String httpServer = preferences.getString("httpProxyServer", ""); - int httpPort; - try { - httpPort = Integer.parseInt(preferences.getString("httpProxyPort", "-1")); - } catch (NumberFormatException e) { - // user has entered wrong port, fall back to no proxy - httpPort = -1; - } - - int cacheSize = 25*1024*1024; // 25 MiB - - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .addInterceptor(getUserAgentInterceptor()) - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .cache(new Cache(context.getCacheDir(), cacheSize)); - - if (httpProxyEnabled && !httpServer.isEmpty() && (httpPort > 0) && (httpPort < 65535)) { - InetSocketAddress address = InetSocketAddress.createUnresolved(httpServer, httpPort); - builder.proxy(new Proxy(Proxy.Type.HTTP, address)); - } - - return builder; - } - - /** - * Add a custom User-Agent that contains Tusky & Android Version to all requests - * Example: - * User-Agent: Tusky/1.1.2 Android/5.0.2 - */ - @NonNull - private static Interceptor getUserAgentInterceptor() { - return chain -> { - Request originalRequest = chain.request(); - Request requestWithUserAgent = originalRequest.newBuilder() - .header("User-Agent", "Tusky/"+ BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE) - .build(); - return chain.proceed(requestWithUserAgent); - }; - } - -} - - diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt new file mode 100644 index 000000000..3e1b89c6a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt @@ -0,0 +1,115 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Tusky. If + * not, see . */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.os.Build +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.BuildConfig +import okhttp3.Cache +import okhttp3.OkHttp +import okhttp3.OkHttpClient +import okhttp3.tls.HandshakeCertificates +import java.io.ByteArrayInputStream +import java.net.InetSocketAddress +import java.net.Proxy +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit + +fun okhttpClient(context: Context): OkHttpClient.Builder { + val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) + val httpServer = preferences.getNonNullString("httpProxyServer", "") + val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 + + val cacheSize = 25 * 1024 * 1024 // 25 MiB + val builder = OkHttpClient.Builder() + .addInterceptor { chain -> + /** + * Add a custom User-Agent that contains Tusky, Android and Okhttp Version to all requests + * Example: + * User-Agent: Tusky/1.1.2 Android/5.0.2 + * */ + val requestWithUserAgent = chain.request().newBuilder() + .header( + "User-Agent", + "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" + ) + .build() + chain.proceed(requestWithUserAgent) + } + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cache(Cache(context.cacheDir, cacheSize.toLong())) + + if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { + val address = InetSocketAddress.createUnresolved(httpServer, httpPort) + builder.proxy(Proxy(Proxy.Type.HTTP, address)) + } + + // trust the new Let's Encrypt root certificate that is not available on Android < 7.1.1 + // new cert https://letsencrypt.org/certs/isrgrootx1.pem + // see https://letsencrypt.org/2020/11/06/own-two-feet.html + // see https://stackoverflow.com/questions/64844311/certpathvalidatorexception-connecting-to-a-lets-encrypt-host-on-android-m-or-ea + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + val isgCert = """ + -----BEGIN CERTIFICATE----- + MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw + TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh + cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 + WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu + ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY + MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc + h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ + 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U + A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW + T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH + B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC + B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv + KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn + OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn + jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw + qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI + rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV + HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq + hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL + ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ + 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK + NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 + ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur + TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC + jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc + oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq + 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA + mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d + emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= + -----END CERTIFICATE----- + """.trimIndent() + val cf = CertificateFactory.getInstance("X.509") + val isgCertificate = cf.generateCertificate(ByteArrayInputStream(isgCert.toByteArray(charset("UTF-8")))) + val certificates = HandshakeCertificates.Builder() + .addTrustedCertificate(isgCertificate as X509Certificate) + .addPlatformTrustedCertificates() + .build() + builder.sslSocketFactory( + certificates.sslSocketFactory(), + certificates.trustManager + ) + } + return builder +}