Merge branch 'handshake_test'
This commit is contained in:
commit
b7eb5b3271
@ -114,7 +114,11 @@ public class BaseActivity extends AppCompatActivity {
|
|||||||
protected void createMastodonAPI() {
|
protected void createMastodonAPI() {
|
||||||
mastodonApiDispatcher = new Dispatcher();
|
mastodonApiDispatcher = new Dispatcher();
|
||||||
|
|
||||||
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
Gson gson = new GsonBuilder()
|
||||||
|
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
||||||
|
.create();
|
||||||
|
|
||||||
|
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
|
||||||
.addInterceptor(new Interceptor() {
|
.addInterceptor(new Interceptor() {
|
||||||
@Override
|
@Override
|
||||||
public Response intercept(Chain chain) throws IOException {
|
public Response intercept(Chain chain) throws IOException {
|
||||||
@ -123,7 +127,8 @@ public class BaseActivity extends AppCompatActivity {
|
|||||||
Request.Builder builder = originalRequest.newBuilder();
|
Request.Builder builder = originalRequest.newBuilder();
|
||||||
String accessToken = getAccessToken();
|
String accessToken = getAccessToken();
|
||||||
if (accessToken != null) {
|
if (accessToken != null) {
|
||||||
builder.header("Authorization", String.format("Bearer %s", accessToken));
|
builder.header("Authorization", String.format("Bearer %s",
|
||||||
|
accessToken));
|
||||||
}
|
}
|
||||||
Request newRequest = builder.build();
|
Request newRequest = builder.build();
|
||||||
|
|
||||||
@ -133,10 +138,6 @@ public class BaseActivity extends AppCompatActivity {
|
|||||||
.dispatcher(mastodonApiDispatcher)
|
.dispatcher(mastodonApiDispatcher)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Gson gson = new GsonBuilder()
|
|
||||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
|
||||||
.create();
|
|
||||||
|
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
.baseUrl(getBaseUrl())
|
.baseUrl(getBaseUrl())
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
@ -149,6 +150,7 @@ public class BaseActivity extends AppCompatActivity {
|
|||||||
protected void createTuskyAPI() {
|
protected void createTuskyAPI() {
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
.baseUrl(getString(R.string.tusky_api_url))
|
.baseUrl(getString(R.string.tusky_api_url))
|
||||||
|
.client(OkHttpUtils.getCompatibleClient())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
tuskyAPI = retrofit.create(TuskyAPI.class);
|
tuskyAPI = retrofit.create(TuskyAPI.class);
|
||||||
|
@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -59,123 +60,6 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
@BindView(R.id.button_login) Button button;
|
@BindView(R.id.button_login) Button button;
|
||||||
@BindView(R.id.whats_an_instance) TextView whatsAnInstance;
|
@BindView(R.id.whats_an_instance) TextView whatsAnInstance;
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
|
||||||
* as the content of an HTTP request.
|
|
||||||
*/
|
|
||||||
private static String toQueryString(Map<String, String> parameters) {
|
|
||||||
StringBuilder s = new StringBuilder();
|
|
||||||
String between = "";
|
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
|
||||||
s.append(between);
|
|
||||||
s.append(Uri.encode(entry.getKey()));
|
|
||||||
s.append("=");
|
|
||||||
s.append(Uri.encode(entry.getValue()));
|
|
||||||
between = "&";
|
|
||||||
}
|
|
||||||
return s.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
|
||||||
private static String validateDomain(String s) {
|
|
||||||
// Strip any schemes out.
|
|
||||||
s = s.replaceFirst("http://", "");
|
|
||||||
s = s.replaceFirst("https://", "");
|
|
||||||
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
|
||||||
int at = s.indexOf('@');
|
|
||||||
if (at != -1) {
|
|
||||||
s = s.substring(at + 1);
|
|
||||||
}
|
|
||||||
return s.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOauthRedirectUri() {
|
|
||||||
String scheme = getString(R.string.oauth_scheme);
|
|
||||||
String host = getString(R.string.oauth_redirect_host);
|
|
||||||
return scheme + "://" + host + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void redirectUserToAuthorizeAndLogin(EditText editText) {
|
|
||||||
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
|
||||||
* activity_login there, and the server will redirect back to the app with its response. */
|
|
||||||
String endpoint = MastodonAPI.ENDPOINT_AUTHORIZE;
|
|
||||||
String redirectUri = getOauthRedirectUri();
|
|
||||||
Map<String, String> parameters = new HashMap<>();
|
|
||||||
parameters.put("client_id", clientId);
|
|
||||||
parameters.put("redirect_uri", redirectUri);
|
|
||||||
parameters.put("response_type", "code");
|
|
||||||
parameters.put("scope", OAUTH_SCOPES);
|
|
||||||
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
|
|
||||||
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
|
||||||
if (viewIntent.resolveActivity(getPackageManager()) != null) {
|
|
||||||
startActivity(viewIntent);
|
|
||||||
} else {
|
|
||||||
editText.setError(getString(R.string.error_no_web_browser_found));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private MastodonAPI getApiFor(String domain) {
|
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
|
||||||
.baseUrl("https://" + domain)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return retrofit.create(MastodonAPI.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
|
||||||
* app is run on a given server instance. So, after the first authentication, they are
|
|
||||||
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
|
||||||
*/
|
|
||||||
private void onButtonClick(final EditText editText) {
|
|
||||||
domain = validateDomain(editText.getText().toString());
|
|
||||||
/* Attempt to get client credentials from SharedPreferences, and if not present
|
|
||||||
* (such as in the case that the domain has never been accessed before)
|
|
||||||
* authenticate with the server and store the received credentials to use next
|
|
||||||
* time. */
|
|
||||||
String prefClientId = preferences.getString(domain + "/client_id", null);
|
|
||||||
String prefClientSecret = preferences.getString(domain + "/client_secret", null);
|
|
||||||
|
|
||||||
if (prefClientId != null && prefClientSecret != null) {
|
|
||||||
clientId = prefClientId;
|
|
||||||
clientSecret = prefClientSecret;
|
|
||||||
redirectUserToAuthorizeAndLogin(editText);
|
|
||||||
} else {
|
|
||||||
Callback<AppCredentials> callback = new Callback<AppCredentials>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(Call<AppCredentials> call, Response<AppCredentials> response) {
|
|
||||||
if (!response.isSuccessful()) {
|
|
||||||
editText.setError(getString(R.string.error_failed_app_registration));
|
|
||||||
Log.e(TAG, "App authentication failed. " + response.message());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
AppCredentials credentials = response.body();
|
|
||||||
clientId = credentials.clientId;
|
|
||||||
clientSecret = credentials.clientSecret;
|
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
|
||||||
editor.putString(domain + "/client_id", clientId);
|
|
||||||
editor.putString(domain + "/client_secret", clientSecret);
|
|
||||||
editor.apply();
|
|
||||||
redirectUserToAuthorizeAndLogin(editText);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Call<AppCredentials> call, Throwable t) {
|
|
||||||
editText.setError(getString(R.string.error_failed_app_registration));
|
|
||||||
t.printStackTrace();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES,
|
|
||||||
getString(R.string.app_website)).enqueue(callback);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
editText.setError(getString(R.string.error_invalid_domain));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@ -234,7 +118,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
} catch (PackageManager.NameNotFoundException e) {
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
Log.e(TAG, "The app version was not found. " + e.getMessage());
|
Log.e(TAG, "The app version was not found. " + e.getMessage());
|
||||||
}
|
}
|
||||||
if (preferences.getInt("lastUpdate", 0) != versionCode) {
|
if (preferences.getInt("lastUpdateVersion", 0) != versionCode) {
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
if (versionCode == 14) {
|
if (versionCode == 14) {
|
||||||
/* This version switches the order of scheme and host in the OAuth redirect URI.
|
/* This version switches the order of scheme and host in the OAuth redirect URI.
|
||||||
@ -243,7 +127,7 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
|
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
|
||||||
editor.clear();
|
editor.clear();
|
||||||
}
|
}
|
||||||
editor.putInt("lastUpdate", versionCode);
|
editor.putInt("lastUpdateVersion", versionCode);
|
||||||
editor.apply();
|
editor.apply();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -256,14 +140,128 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoginSuccess(String accessToken) {
|
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
@NonNull
|
||||||
editor.putString("domain", domain);
|
private static String validateDomain(String s) {
|
||||||
editor.putString("accessToken", accessToken);
|
// Strip any schemes out.
|
||||||
editor.commit();
|
s = s.replaceFirst("http://", "");
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
s = s.replaceFirst("https://", "");
|
||||||
startActivity(intent);
|
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
||||||
finish();
|
int at = s.indexOf('@');
|
||||||
|
if (at != -1) {
|
||||||
|
s = s.substring(at + 1);
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getOauthRedirectUri() {
|
||||||
|
String scheme = getString(R.string.oauth_scheme);
|
||||||
|
String host = getString(R.string.oauth_redirect_host);
|
||||||
|
return scheme + "://" + host + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private MastodonAPI getApiFor(String domain) {
|
||||||
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
|
.baseUrl("https://" + domain)
|
||||||
|
.client(OkHttpUtils.getCompatibleClient())
|
||||||
|
.addConverterFactory(GsonConverterFactory.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return retrofit.create(MastodonAPI.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain the oauth client credentials for this app. This is only necessary the first time the
|
||||||
|
* app is run on a given server instance. So, after the first authentication, they are
|
||||||
|
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
|
||||||
|
*/
|
||||||
|
private void onButtonClick(final EditText editText) {
|
||||||
|
domain = validateDomain(editText.getText().toString());
|
||||||
|
/* Attempt to get client credentials from SharedPreferences, and if not present
|
||||||
|
* (such as in the case that the domain has never been accessed before)
|
||||||
|
* authenticate with the server and store the received credentials to use next
|
||||||
|
* time. */
|
||||||
|
String prefClientId = preferences.getString(domain + "/client_id", null);
|
||||||
|
String prefClientSecret = preferences.getString(domain + "/client_secret", null);
|
||||||
|
|
||||||
|
if (prefClientId != null && prefClientSecret != null) {
|
||||||
|
clientId = prefClientId;
|
||||||
|
clientSecret = prefClientSecret;
|
||||||
|
redirectUserToAuthorizeAndLogin(editText);
|
||||||
|
} else {
|
||||||
|
Callback<AppCredentials> callback = new Callback<AppCredentials>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<AppCredentials> call,
|
||||||
|
Response<AppCredentials> response) {
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
editText.setError(getString(R.string.error_failed_app_registration));
|
||||||
|
Log.e(TAG, "App authentication failed. " + response.message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AppCredentials credentials = response.body();
|
||||||
|
clientId = credentials.clientId;
|
||||||
|
clientSecret = credentials.clientSecret;
|
||||||
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
editor.putString(domain + "/client_id", clientId);
|
||||||
|
editor.putString(domain + "/client_secret", clientSecret);
|
||||||
|
editor.apply();
|
||||||
|
redirectUserToAuthorizeAndLogin(editText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<AppCredentials> call, Throwable t) {
|
||||||
|
editText.setError(getString(R.string.error_failed_app_registration));
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
getApiFor(domain)
|
||||||
|
.authenticateApp(getString(R.string.app_name), getOauthRedirectUri(),
|
||||||
|
OAUTH_SCOPES, getString(R.string.app_website))
|
||||||
|
.enqueue(callback);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
editText.setError(getString(R.string.error_invalid_domain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
||||||
|
* as the content of an HTTP request.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private static String toQueryString(Map<String, String> parameters) {
|
||||||
|
StringBuilder s = new StringBuilder();
|
||||||
|
String between = "";
|
||||||
|
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||||
|
s.append(between);
|
||||||
|
s.append(Uri.encode(entry.getKey()));
|
||||||
|
s.append("=");
|
||||||
|
s.append(Uri.encode(entry.getValue()));
|
||||||
|
between = "&";
|
||||||
|
}
|
||||||
|
return s.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redirectUserToAuthorizeAndLogin(EditText editText) {
|
||||||
|
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
||||||
|
* activity_login there, and the server will redirect back to the app with its response. */
|
||||||
|
String endpoint = MastodonAPI.ENDPOINT_AUTHORIZE;
|
||||||
|
String redirectUri = getOauthRedirectUri();
|
||||||
|
Map<String, String> parameters = new HashMap<>();
|
||||||
|
parameters.put("client_id", clientId);
|
||||||
|
parameters.put("redirect_uri", redirectUri);
|
||||||
|
parameters.put("response_type", "code");
|
||||||
|
parameters.put("scope", OAUTH_SCOPES);
|
||||||
|
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
|
||||||
|
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
if (viewIntent.resolveActivity(getPackageManager()) != null) {
|
||||||
|
startActivity(viewIntent);
|
||||||
|
} else {
|
||||||
|
editText.setError(getString(R.string.error_no_web_browser_found));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -347,4 +345,14 @@ public class LoginActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onLoginSuccess(String accessToken) {
|
||||||
|
SharedPreferences.Editor editor = preferences.edit();
|
||||||
|
editor.putString("domain", domain);
|
||||||
|
editor.putString("accessToken", accessToken);
|
||||||
|
editor.commit();
|
||||||
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
|
|||||||
protected void createTuskyAPI() {
|
protected void createTuskyAPI() {
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
.baseUrl(getString(R.string.tusky_api_url))
|
.baseUrl(getString(R.string.tusky_api_url))
|
||||||
|
.client(OkHttpUtils.getCompatibleClient())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
tuskyAPI = retrofit.create(TuskyAPI.class);
|
tuskyAPI = retrofit.create(TuskyAPI.class);
|
||||||
|
@ -104,7 +104,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
|
|||||||
final String domain = preferences.getString("domain", null);
|
final String domain = preferences.getString("domain", null);
|
||||||
final String accessToken = preferences.getString("accessToken", null);
|
final String accessToken = preferences.getString("accessToken", null);
|
||||||
|
|
||||||
OkHttpClient okHttpClient = new OkHttpClient.Builder()
|
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
|
||||||
.addInterceptor(new Interceptor() {
|
.addInterceptor(new Interceptor() {
|
||||||
@Override
|
@Override
|
||||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||||
|
171
app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java
Normal file
171
app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
/* 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 <http://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import javax.net.ssl.SSLSocketFactory;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import okhttp3.ConnectionSpec;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
|
||||||
|
class OkHttpUtils {
|
||||||
|
static final String TAG = "OkHttpUtils"; // logging tag
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a Builder with the maximum range of TLS versions and cipher suites enabled.
|
||||||
|
*
|
||||||
|
* It first tries the "approved" list of cipher suites given in OkHttp (the default in
|
||||||
|
* ConnectionSpec.MODERN_TLS) and if that doesn't work falls back to the set of ALL enabled,
|
||||||
|
* then falls back to plain http.
|
||||||
|
*
|
||||||
|
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
static OkHttpClient.Builder getCompatibleClientBuilder() {
|
||||||
|
ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||||
|
.allEnabledCipherSuites()
|
||||||
|
.supportsTlsExtensions(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<ConnectionSpec> specList = new ArrayList<>();
|
||||||
|
specList.add(ConnectionSpec.MODERN_TLS);
|
||||||
|
specList.add(fallback);
|
||||||
|
specList.add(ConnectionSpec.CLEARTEXT);
|
||||||
|
|
||||||
|
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||||
|
.connectionSpecs(specList);
|
||||||
|
|
||||||
|
return enableHigherTlsOnPreLollipop(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
static OkHttpClient getCompatibleClient() {
|
||||||
|
return getCompatibleClientBuilder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OkHttpClient.Builder enableHigherTlsOnPreLollipop(OkHttpClient.Builder builder) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
|
||||||
|
try {
|
||||||
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||||
|
TrustManagerFactory.getDefaultAlgorithm());
|
||||||
|
trustManagerFactory.init((KeyStore) null);
|
||||||
|
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||||
|
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||||
|
throw new IllegalStateException("Unexpected default trust managers:"
|
||||||
|
+ Arrays.toString(trustManagers));
|
||||||
|
}
|
||||||
|
|
||||||
|
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||||
|
|
||||||
|
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||||
|
sslContext.init(null, new TrustManager[] { trustManager }, null);
|
||||||
|
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
|
||||||
|
|
||||||
|
builder.sslSocketFactory(new SSLSocketFactoryCompat(sslSocketFactory),
|
||||||
|
trustManager);
|
||||||
|
} catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) {
|
||||||
|
Log.e(TAG, "Failed enabling TLS 1.1 & 1.2. " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SSLSocketFactoryCompat extends SSLSocketFactory {
|
||||||
|
private static final String[] DESIRED_TLS_VERSIONS = { "TLSv1", "TLSv1.1", "TLSv1.2",
|
||||||
|
"TLSv1.3" };
|
||||||
|
|
||||||
|
final SSLSocketFactory delegate;
|
||||||
|
|
||||||
|
SSLSocketFactoryCompat(SSLSocketFactory base) {
|
||||||
|
this.delegate = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getDefaultCipherSuites() {
|
||||||
|
return delegate.getDefaultCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getSupportedCipherSuites() {
|
||||||
|
return delegate.getSupportedCipherSuites();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(Socket s, String host, int port, boolean autoClose)
|
||||||
|
throws IOException {
|
||||||
|
return patch(delegate.createSocket(s, host, port, autoClose));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port) throws IOException {
|
||||||
|
return patch(delegate.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
|
||||||
|
throws IOException {
|
||||||
|
return patch(delegate.createSocket(host, port, localHost, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||||
|
return patch(delegate.createSocket(host, port));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
|
||||||
|
int localPort) throws IOException {
|
||||||
|
return patch(delegate.createSocket(address, port, localAddress, localPort));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String[] getMatches(String[] wanted, String[] have) {
|
||||||
|
List<String> a = new ArrayList<>(Arrays.asList(wanted));
|
||||||
|
List<String> b = Arrays.asList(have);
|
||||||
|
a.retainAll(b);
|
||||||
|
return a.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Socket patch(Socket socket) {
|
||||||
|
if (socket instanceof SSLSocket) {
|
||||||
|
SSLSocket sslSocket = (SSLSocket) socket;
|
||||||
|
String[] protocols = getMatches(DESIRED_TLS_VERSIONS,
|
||||||
|
sslSocket.getSupportedProtocols());
|
||||||
|
sslSocket.setEnabledProtocols(protocols);
|
||||||
|
}
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user