Browse Source

Merge branch 'handshake_test'

pull/153/head
Vavassor 9 years ago
parent
commit
b7eb5b3271
  1. 14
      app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
  2. 246
      app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
  3. 1
      app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java
  4. 2
      app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java
  5. 171
      app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java

14
app/src/main/java/com/keylesspalace/tusky/BaseActivity.java

@ -114,7 +114,11 @@ public class BaseActivity extends AppCompatActivity {
protected void createMastodonAPI() {
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() {
@Override
public Response intercept(Chain chain) throws IOException {
@ -123,7 +127,8 @@ public class BaseActivity extends AppCompatActivity {
Request.Builder builder = originalRequest.newBuilder();
String accessToken = getAccessToken();
if (accessToken != null) {
builder.header("Authorization", String.format("Bearer %s", accessToken));
builder.header("Authorization", String.format("Bearer %s",
accessToken));
}
Request newRequest = builder.build();
@ -133,10 +138,6 @@ public class BaseActivity extends AppCompatActivity {
.dispatcher(mastodonApiDispatcher)
.build();
Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getBaseUrl())
.client(okHttpClient)
@ -149,6 +150,7 @@ public class BaseActivity extends AppCompatActivity {
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);

246
app/src/main/java/com/keylesspalace/tusky/LoginActivity.java

@ -24,6 +24,7 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod;
import android.view.View;
@ -59,24 +60,88 @@ public class LoginActivity extends AppCompatActivity {
@BindView(R.id.button_login) Button button;
@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 = "&";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
if (savedInstanceState != null) {
domain = savedInstanceState.getString("domain");
clientId = savedInstanceState.getString("clientId");
clientSecret = savedInstanceState.getString("clientSecret");
} else {
domain = null;
clientId = null;
clientSecret = null;
}
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick(editText);
}
});
final Context context = this;
whatsAnInstance.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
TextView textView = (TextView) dialog.findViewById(android.R.id.message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
});
// Apply any updates needed.
int versionCode = 1;
try {
versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage());
}
if (preferences.getInt("lastUpdateVersion", 0) != versionCode) {
SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI.
* But to fix it requires forcing the app to re-authenticate with servers. So, clear
* out the stored client id/secret pairs. The only other things that are lost are
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear();
}
editor.putInt("lastUpdateVersion", versionCode);
editor.apply();
}
return s.toString();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("domain", domain);
outState.putString("clientId", clientId);
outState.putString("clientSecret", clientSecret);
super.onSaveInstanceState(outState);
}
/** Make sure the user-entered text is just a fully-qualified domain name. */
@NonNull
private static String validateDomain(String s) {
// Strip any schemes out.
s = s.replaceFirst("http://", "");
@ -95,28 +160,10 @@ public class LoginActivity extends AppCompatActivity {
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)
.client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create())
.build();
@ -144,7 +191,8 @@ public class LoginActivity extends AppCompatActivity {
} else {
Callback<AppCredentials> callback = new Callback<AppCredentials>() {
@Override
public void onResponse(Call<AppCredentials> call, Response<AppCredentials> response) {
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());
@ -168,104 +216,54 @@ public class LoginActivity extends AppCompatActivity {
};
try {
getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES,
getString(R.string.app_website)).enqueue(callback);
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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
/**
* 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();
}
setContentView(R.layout.activity_login);
ButterKnife.bind(this);
if (savedInstanceState != null) {
domain = savedInstanceState.getString("domain");
clientId = savedInstanceState.getString("clientId");
clientSecret = savedInstanceState.getString("clientSecret");
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 {
domain = null;
clientId = null;
clientSecret = null;
}
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick(editText);
}
});
final Context context = this;
whatsAnInstance.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
TextView textView = (TextView) dialog.findViewById(android.R.id.message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
});
// Apply any updates needed.
int versionCode = 1;
try {
versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage());
}
if (preferences.getInt("lastUpdate", 0) != versionCode) {
SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI.
* But to fix it requires forcing the app to re-authenticate with servers. So, clear
* out the stored client id/secret pairs. The only other things that are lost are
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear();
}
editor.putInt("lastUpdate", versionCode);
editor.apply();
editText.setError(getString(R.string.error_no_web_browser_found));
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("domain", domain);
outState.putString("clientId", clientId);
outState.putString("clientSecret", clientSecret);
super.onSaveInstanceState(outState);
}
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();
}
@Override
protected void onStop() {
super.onStop();
@ -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();
}
}

1
app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java

@ -40,6 +40,7 @@ public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build();
tuskyAPI = retrofit.create(TuskyAPI.class);

2
app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java

@ -104,7 +104,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {

171
app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java

@ -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…
Cancel
Save