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
+}