Merge request changes

- Moved certificate-related utilities to separate class

- Added settings method to return an entire SSLSettings object; refactored
  methods using separate parameters to take single SSLSettings parameter

- Advanced Settings section on login page now hides / shows along with
  other buttons to prevent it from showing up in front of the loading
  spinner

- Fixed star imports

- Refactored applySslSettings as per code from merge request

- Fixed formatting
This commit is contained in:
Galen Abell
2018-11-08 17:43:12 -05:00
parent 2d14ef1b6f
commit 8e2d90ef50
10 changed files with 274 additions and 209 deletions

View File

@@ -2,6 +2,7 @@ package com.github.gotify;
import android.content.Context;
import android.content.SharedPreferences;
import com.github.gotify.api.CertUtils;
import com.github.gotify.client.model.User;
public class Settings {
@@ -60,12 +61,23 @@ public class Settings {
sharedPreferences.edit().putString("version", version).apply();
}
// Default to always validating SSL.
public boolean validateSSL() { return sharedPreferences.getBoolean("validateSSL", true); }
private boolean validateSSL() {
return sharedPreferences.getBoolean("validateSSL", true);
}
public void validateSSL(boolean validateSSL) { sharedPreferences.edit().putBoolean("validateSSL", validateSSL).apply(); }
public void validateSSL(boolean validateSSL) {
sharedPreferences.edit().putBoolean("validateSSL", validateSSL).apply();
}
public String cert() { return sharedPreferences.getString("cert", null); }
private String cert() {
return sharedPreferences.getString("cert", null);
}
public void cert(String cert) { sharedPreferences.edit().putString("cert", cert).apply(); }
public void cert(String cert) {
sharedPreferences.edit().putString("cert", cert).apply();
}
public CertUtils.SSLSettings sslSettings() {
return new CertUtils.SSLSettings(validateSSL(), cert());
}
}

View File

@@ -1,6 +1,5 @@
package com.github.gotify;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -15,22 +14,12 @@ import com.github.gotify.log.Log;
import com.google.android.material.snackbar.Snackbar;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import okhttp3.OkHttpClient;
import okio.Buffer;
import org.threeten.bp.OffsetDateTime;
import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.io.InputStream;
import java.util.Collection;
import java.io.InputStreamReader;
import okio.Buffer;
import org.threeten.bp.OffsetDateTime;
public class Utils {
public static void showSnackBar(Activity activity, String message) {
@@ -66,17 +55,6 @@ public class Utils {
return new ApiClient().getJSON();
}
public interface DrawableReceiver {
void loaded(Drawable drawable);
}
public static InputStream stringToInputStream(String str) {
if (str == null) return null;
return new Buffer()
.writeUtf8(str)
.inputStream();
}
public static String readFileFromStream(@NonNull InputStream inputStream) {
StringBuilder sb = new StringBuilder();
String currentLine;
@@ -92,87 +70,12 @@ public class Utils {
return sb.toString();
}
/////////////////////////
///// SSL Utilities /////
/////////////////////////
public static Certificate parseCertificate(String cert) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
return certificateFactory.generateCertificate(Utils.stringToInputStream(cert));
} catch (Exception e) {
throw new IllegalArgumentException("certificate is invalid");
}
public interface DrawableReceiver {
void loaded(Drawable drawable);
}
public static class SSLSettings {
boolean validateSSL;
String cert;
public SSLSettings(boolean validateSSL, String cert) {
this.validateSSL = validateSSL;
this.cert = cert;
}
}
// TrustManager that trusts all SSL Certs
private static final TrustManager trustAll = new X509TrustManager() {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[]{}; }
};
public static void applySslSettings(OkHttpClient.Builder builder, SSLSettings settings) {
// Modified from ApiClient.applySslSettings in the client package.
try {
TrustManager[] trustManagers = null;
HostnameVerifier hostnameVerifier = null;
if (!settings.validateSSL) {
trustManagers = new TrustManager[]{ trustAll };
hostnameVerifier = (hostname, session) -> true;
} else if (settings.cert != null) {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(stringToInputStream(settings.cert));
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
KeyStore caKeyStore = newEmptyKeyStore();
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = "ca" + Integer.toString(index++);
caKeyStore.setCertificateEntry(certificateAlias, certificate);
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(caKeyStore);
trustManagers = trustManagerFactory.getTrustManagers();
}
if (trustManagers != null && trustManagers.length > 0) {
SSLContext context = SSLContext.getInstance("TLS");
context.init(new KeyManager[]{}, trustManagers, new SecureRandom());
builder.sslSocketFactory(context.getSocketFactory(), (X509TrustManager) trustManagers[0]);
}
if (hostnameVerifier != null) builder.hostnameVerifier(hostnameVerifier);
} catch (Exception e) {
// We shouldn't have issues since the cert is verified on login.
Log.e("Failed to apply SSL settings", e);
}
}
private static KeyStore newEmptyKeyStore() throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
public static InputStream stringToInputStream(String str) {
if (str == null) return null;
return new Buffer().writeUtf8(str).inputStream();
}
}

View File

@@ -0,0 +1,116 @@
package com.github.gotify.api;
import android.annotation.SuppressLint;
import com.github.gotify.Utils;
import com.github.gotify.log.Log;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
public class CertUtils {
public static class SSLSettings {
boolean validateSSL;
String cert;
public SSLSettings(boolean validateSSL, String cert) {
this.validateSSL = validateSSL;
this.cert = cert;
}
}
private static final X509TrustManager trustAll =
new X509TrustManager() {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[] {};
}
};
public static Certificate parseCertificate(String cert) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
return certificateFactory.generateCertificate(Utils.stringToInputStream(cert));
} catch (Exception e) {
throw new IllegalArgumentException("certificate is invalid");
}
}
public static void applySslSettings(OkHttpClient.Builder builder, SSLSettings settings) {
// Modified from ApiClient.applySslSettings in the client package.
try {
if (!settings.validateSSL) {
SSLContext context = SSLContext.getInstance("TLS");
context.init(
new KeyManager[] {}, new TrustManager[] {trustAll}, new SecureRandom());
builder.sslSocketFactory(context.getSocketFactory(), trustAll);
builder.hostnameVerifier((a, b) -> true);
return;
}
if (settings.cert != null) {
TrustManager[] trustManagers = certToTrustManager(settings.cert);
if (trustManagers != null && trustManagers.length > 0) {
SSLContext context = SSLContext.getInstance("TLS");
context.init(new KeyManager[] {}, trustManagers, new SecureRandom());
builder.sslSocketFactory(
context.getSocketFactory(), (X509TrustManager) trustManagers[0]);
builder.hostnameVerifier((a, b) -> true);
}
}
} catch (Exception e) {
// We shouldn't have issues since the cert is verified on login.
Log.e("Failed to apply SSL settings", e);
}
}
private static TrustManager[] certToTrustManager(String cert) throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates =
certificateFactory.generateCertificates(Utils.stringToInputStream(cert));
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}
KeyStore caKeyStore = newEmptyKeyStore();
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = "ca" + Integer.toString(index++);
caKeyStore.setCertificateEntry(certificateAlias, certificate);
}
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(caKeyStore);
return trustManagerFactory.getTrustManagers();
}
private static KeyStore newEmptyKeyStore() throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -9,16 +9,17 @@ import com.github.gotify.client.auth.ApiKeyAuth;
import com.github.gotify.client.auth.HttpBasicAuth;
public class ClientFactory {
public static ApiClient unauthorized(String baseUrl, boolean validateSSL, String cert) {
public static ApiClient unauthorized(String baseUrl, CertUtils.SSLSettings sslSettings) {
ApiClient client = new ApiClient();
client.setVerifyingSsl(validateSSL);
client.setSslCaCert(Utils.stringToInputStream(cert));
client.setVerifyingSsl(sslSettings.validateSSL);
client.setSslCaCert(Utils.stringToInputStream(sslSettings.cert));
client.setBasePath(baseUrl);
return client;
}
public static ApiClient basicAuth(String baseUrl, boolean validateSSL, String cert, String username, String password) {
ApiClient client = unauthorized(baseUrl, validateSSL, cert);
public static ApiClient basicAuth(
String baseUrl, CertUtils.SSLSettings sslSettings, String username, String password) {
ApiClient client = unauthorized(baseUrl, sslSettings);
HttpBasicAuth auth = (HttpBasicAuth) client.getAuthentication("basicAuth");
auth.setUsername(username);
auth.setPassword(password);
@@ -26,18 +27,19 @@ public class ClientFactory {
return client;
}
public static ApiClient clientToken(String baseUrl, boolean validateSSL, String cert, String token) {
ApiClient client = unauthorized(baseUrl, validateSSL, cert);
public static ApiClient clientToken(
String baseUrl, CertUtils.SSLSettings sslSettings, String token) {
ApiClient client = unauthorized(baseUrl, sslSettings);
ApiKeyAuth tokenAuth = (ApiKeyAuth) client.getAuthentication("clientTokenHeader");
tokenAuth.setApiKey(token);
return client;
}
public static VersionApi versionApi(String baseUrl, boolean validateSSL, String cert) {
return new VersionApi(unauthorized(baseUrl, validateSSL, cert));
public static VersionApi versionApi(String baseUrl, CertUtils.SSLSettings sslSettings) {
return new VersionApi(unauthorized(baseUrl, sslSettings));
}
public static UserApi userApiWithToken(Settings settings) {
return new UserApi(clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token()));
return new UserApi(clientToken(settings.url(), settings.sslSettings(), settings.token()));
}
}

View File

@@ -116,7 +116,7 @@ public class InitializationActivity extends AppCompatActivity {
private void requestVersion(
final Callback.SuccessCallback<VersionInfo> callback,
final Callback.ErrorCallback errorCallback) {
VersionApi versionApi = ClientFactory.versionApi(settings.url(), settings.validateSSL(), settings.cert());
VersionApi versionApi = ClientFactory.versionApi(settings.url(), settings.sslSettings());
Api.withLogging(versionApi::getVersionAsync)
.handleInUIThread(this, callback, errorCallback);
}

View File

@@ -11,12 +11,17 @@ import android.widget.*;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import butterknife.*;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnCheckedChanged;
import butterknife.OnClick;
import butterknife.OnTextChanged;
import com.github.gotify.R;
import com.github.gotify.Settings;
import com.github.gotify.Utils;
import com.github.gotify.api.Api;
import com.github.gotify.api.Callback;
import com.github.gotify.api.CertUtils;
import com.github.gotify.api.ClientFactory;
import com.github.gotify.client.ApiClient;
import com.github.gotify.client.ApiException;
@@ -28,7 +33,6 @@ import com.github.gotify.init.InitializationActivity;
import com.github.gotify.log.Log;
import com.github.gotify.log.UncaughtExceptionHandler;
import com.squareup.okhttp.HttpUrl;
import java.io.*;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@@ -47,6 +51,9 @@ public class LoginActivity extends AppCompatActivity {
@BindView(R.id.password)
EditText passwordField;
@BindView(R.id.sslGroup)
LinearLayout sslGroup;
@BindView(R.id.showAdvanced)
Button toggleAdvanced;
@@ -108,15 +115,21 @@ public class LoginActivity extends AppCompatActivity {
checkUrlProgress.setVisibility(View.VISIBLE);
checkUrlButton.setVisibility(View.GONE);
sslGroup.setVisibility(View.GONE);
final String fixedUrl = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
Api.withLogging(ClientFactory.versionApi(fixedUrl, !disableSSLValidation, caCertContents)::getVersionAsync)
Api.withLogging(
ClientFactory.versionApi(
fixedUrl,
new CertUtils.SSLSettings(
!disableSSLValidation, caCertContents))
::getVersionAsync)
.handleInUIThread(this, onValidUrl(fixedUrl), onInvalidUrl(fixedUrl));
}
@OnClick(R.id.showAdvanced)
void doShowAdvanced() {
void toggleShowAdvanced() {
showAdvanced = !showAdvanced;
disableSSLValidationCheckBox.setVisibility(showAdvanced ? View.VISIBLE : View.GONE);
selectCACertificate.setVisibility(showAdvanced ? View.VISIBLE : View.GONE);
@@ -138,7 +151,9 @@ public class LoginActivity extends AppCompatActivity {
intent.addCategory(Intent.CATEGORY_OPENABLE);
try {
startActivityForResult(Intent.createChooser(intent, getString(R.string.select_ca_file)), FILE_SELECT_CODE);
startActivityForResult(
Intent.createChooser(intent, getString(R.string.select_ca_file)),
FILE_SELECT_CODE);
} catch (ActivityNotFoundException e) {
// case for user not having a file browser installed
Utils.showSnackBar(LoginActivity.this, getString(R.string.please_install_file_browser));
@@ -167,14 +182,15 @@ public class LoginActivity extends AppCompatActivity {
}
String contents = Utils.readFileFromStream(fileStream);
Certificate ca = Utils.parseCertificate(contents);
Certificate ca = CertUtils.parseCertificate(contents);
caFileName.setText(((X509Certificate) ca).getSubjectDN().getName());
// temporarily set the contents (don't store to settings until they decide to login)
caCertContents = contents;
}
} catch (Exception e) {
Utils.showSnackBar(LoginActivity.this, getString(R.string.select_ca_failed, e.getMessage()));
Utils.showSnackBar(
LoginActivity.this, getString(R.string.select_ca_failed, e.getMessage()));
}
}
@@ -188,6 +204,7 @@ public class LoginActivity extends AppCompatActivity {
usernameField.requestFocus();
passwordField.setVisibility(View.VISIBLE);
loginButton.setVisibility(View.VISIBLE);
sslGroup.setVisibility(View.VISIBLE);
};
}
@@ -195,6 +212,7 @@ public class LoginActivity extends AppCompatActivity {
return (exception) -> {
checkUrlProgress.setVisibility(View.GONE);
checkUrlButton.setVisibility(View.VISIBLE);
sslGroup.setVisibility(View.VISIBLE);
Utils.showSnackBar(LoginActivity.this, versionError(url, exception));
};
}
@@ -206,8 +224,14 @@ public class LoginActivity extends AppCompatActivity {
loginButton.setVisibility(View.GONE);
loginProgress.setVisibility(View.VISIBLE);
sslGroup.setVisibility(View.GONE);
ApiClient client = ClientFactory.basicAuth(settings.url(), !disableSSLValidation, caCertContents, username, password);
ApiClient client =
ClientFactory.basicAuth(
settings.url(),
new CertUtils.SSLSettings(!disableSSLValidation, caCertContents),
username,
password);
Api.withLogging(new UserApi(client)::currentUserAsync)
.handleInUIThread(this, (user) -> newClientDialog(client), this::onInvalidLogin);
}
@@ -215,6 +239,7 @@ public class LoginActivity extends AppCompatActivity {
private void onInvalidLogin(ApiException e) {
loginButton.setVisibility(View.VISIBLE);
loginProgress.setVisibility(View.GONE);
sslGroup.setVisibility(View.VISIBLE);
Utils.showSnackBar(this, getString(R.string.wronguserpw));
}
@@ -241,7 +266,6 @@ public class LoginActivity extends AppCompatActivity {
private void onCreatedClient(Client client) {
settings.token(client.getToken());
// Set the final ssl validation status and / or cert
settings.validateSSL(!disableSSLValidation);
settings.cert(caCertContents);
@@ -254,11 +278,13 @@ public class LoginActivity extends AppCompatActivity {
Utils.showSnackBar(this, getString(R.string.create_client_failed));
loginProgress.setVisibility(View.GONE);
loginButton.setVisibility(View.VISIBLE);
sslGroup.setVisibility(View.VISIBLE);
}
private void onCancelClientDialog(DialogInterface dialog, int which) {
loginProgress.setVisibility(View.GONE);
loginButton.setVisibility(View.VISIBLE);
sslGroup.setVisibility(View.VISIBLE);
}
private String versionError(String url, ApiException exception) {

View File

@@ -107,7 +107,8 @@ public class MessagesActivity extends AppCompatActivity
Log.i("Entering " + getClass().getSimpleName());
settings = new Settings(this);
client = ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token());
client =
ClientFactory.clientToken(settings.url(), settings.sslSettings(), settings.token());
appsHolder = new ApplicationHolder(this, client);
appsHolder.onUpdate(() -> onUpdateApps(appsHolder.get()));
appsHolder.request();
@@ -401,7 +402,9 @@ public class MessagesActivity extends AppCompatActivity
@Override
protected Void doInBackground(Void... ignore) {
TokenApi api =
new TokenApi(ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token()));
new TokenApi(
ClientFactory.clientToken(
settings.url(), settings.sslSettings(), settings.token()));
stopService(new Intent(MessagesActivity.this, WebSocketService.class));
try {
List<Client> clients = api.getClients();

View File

@@ -3,6 +3,7 @@ package com.github.gotify.service;
import android.os.Handler;
import com.github.gotify.Utils;
import com.github.gotify.api.Callback;
import com.github.gotify.api.CertUtils;
import com.github.gotify.client.JSON;
import com.github.gotify.client.model.Message;
import com.github.gotify.log.Log;
@@ -32,12 +33,17 @@ public class WebSocketConnection {
private Runnable onReconnected;
private boolean isClosed;
WebSocketConnection(String baseUrl, boolean validateSSL, String cert, String token) {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.pingInterval(1, TimeUnit.MINUTES)
.connectTimeout(10, TimeUnit.SECONDS);
Utils.applySslSettings(builder, new Utils.SSLSettings(validateSSL, cert));
WebSocketConnection(String baseUrl, CertUtils.SSLSettings settings, String token) {
// client = new ApiClient()
// .setVerifyingSsl(validateSSL)
// .setSslCaCert(Utils.stringToInputStream(cert))
// .getHttpClient();
OkHttpClient.Builder builder =
new OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.pingInterval(1, TimeUnit.MINUTES)
.connectTimeout(10, TimeUnit.SECONDS);
CertUtils.applySslSettings(builder, settings);
client = builder.build();

View File

@@ -42,7 +42,9 @@ public class WebSocketService extends Service {
super.onCreate();
settings = new Settings(this);
missingMessageUtil =
new MissedMessageUtil(ClientFactory.clientToken(settings.url(), settings.validateSSL(), settings.cert(), settings.token()));
new MissedMessageUtil(
ClientFactory.clientToken(
settings.url(), settings.sslSettings(), settings.token()));
Log.i("Create " + getClass().getSimpleName());
}
@@ -79,7 +81,7 @@ public class WebSocketService extends Service {
}
connection =
new WebSocketConnection(settings.url(), settings.validateSSL(), settings.cert(), settings.token())
new WebSocketConnection(settings.url(), settings.sslSettings(), settings.token())
.onOpen(this::onOpen)
.onClose(() -> foreground(getString(R.string.websocket_closed)))
.onBadRequest(this::onBadRequest)